第一行代码读书笔记 13 -- Android 中的一些高级技巧

本篇文章主要介绍以几下个知识点:

  • 获取全局 Context
  • 使用 Intent 传递对象
  • 定制日志工具
  • 创建定时任务
  • 多窗口模式编程
图片来源于网络

13.1 全局获取 Context 的技巧

  在某些情况下,获取 Context 并非是一件容易事,下面就来学习让你在项目的任何地方都能轻松获取到 Context 的一种技巧。

  Android 提供了一个 Application 类,每当应用程序启动时,系统就会自动将这个类进行初始化。我们可以定制一个自己的 Application 类,以便管理程序内一些全局的状态信息,比如全局的 Context。

  定制一个自己的 Application 并不复杂,首先需要创建一个 MyApplication 类继承自 Application,如下:

public class MyApplication extends Application {

    private static Context context;

    @Override
    public void onCreate() {
        context = getApplicationContext();
    }

    public static Context getContext(){
        return context;
    }
}

  接下来在 AndroidManifest.xml 文件的<application>标签下进行指定就可以了,如下:

 <application
     android:name=".MyApplication"
     android:allowBackup="true"
     android:icon="@mipmap/ic_launcher"
     android:label="@string/app_name"
     android:supportsRtl="true"
     android:theme="@style/AppTheme">
     . . .
</application>

  这样就实现了一种全局获取 Context 的机制,之后在项目的任何地方想要获取 Context,只需调用 MyApplication.getContext() 就可以了,如弹吐司:

Toast.makeText(MyApplication.getContext(),"提示内容",Toast.LENGTH_SHORT).show();

  任何一个项目都只能配置一个 Application,当引用第三方库如 LitePal 时要配置 LitePalApplication 就会起冲突了,这种情况就要在自己的 Application 中去调用 LitePal 的初始化方法,如下:

public class MyApplication extends Application {

    private static Context context;

    @Override
    public void onCreate() {
        context = getApplicationContext();
        // 调用 LitePal 的初始化方法
        LitePal.initialize(context);
    }

    public static Context getContext(){
        return context;
    }
}

  使用上面的写法,把 Context 对象通过参数传递给 LitePal,效果和在 AndroidManifest.xml 中配置 LitePalApplication 是一样的。

  当然,我个人是更习惯于通过获取全局类实例的方法来定制自己的 Application 类,如下:

public class MyApplication extends Application {

    private static MyApplication mInstance;
    
    @Override
    public void onCreate() {
        super.onCreate();
        mInstance = this;
        // 调用 LitePal 的初始化方法
        LitePal.initialize(this);        
    }

    /**
     * Singleton main method. Provides the global static instance of the helper class.
     * @return The MyApplication instance.
     */
    public static synchronized MyApplication getInstance() {
        return mInstance;
    }
}

13.2 使用 Intent 传递对象

  使用 Intent 时,可以在 Intent 中添加一些附加数据,以达到传值的效果,如在第一个活动中添加如下代码:

Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("string_data","hello");
intent.putExtra("int_data",100);
startActivity(intent);

  然后在第二个活动中就可以获得这些值了,如下:

getIntent().getStringExtra("string_data");
getIntent().getIntExtra("int_data",0);

  但上面的 putExtra() 方法中所支持的数据类型是有限的,若要传递一些自定义对象时就无从下手了,下面就来学习下用 Intent 来传递对象的技巧:SerializableParcelable

13.2.1 Serializable 方式

  Serializable 是序列化的意思,表示将一个对象转化成可储存或可传输的状态。序列化的对象可在网络上传输也可存储到本地。将一个类序列化只要去实现 Serializable 接口就可以了。

  比如一个 Person 类,将它序列化可以这样写:

public class Person implements Serializable{
    
    private String name;
    
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

  接下来在第一个活动中的写法非常简单:

Person person = new Person();
person.setName("Tom");
person.setAge(18);
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("person_data",person);
startActivity(intent);

  然后在第二个活动中获取对象也非常简单:

Person person = (Person) getIntent().getSerializableExtra("person_data");

  这样就实现了使用 Intent 传递对象了。

13.2.2 Parcelable 方式

  Parcelable方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是 Intent 所支持的数据类型,这样也就实现传递对象的功能了。

  Parcelable 的实现方式要稍微复杂一些,修改 Person 中的代码如下:

public class Person implements Parcelable {

    private String name;

    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }


    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        // 写出数据:将 Person 类中的字段一一写出
        dest.writeString(name);// 写出 name
        dest.writeInt(age);// 写出 age
    }

    public static final Parcelable.Creator<Person>CREATOR = new Parcelable.Creator<Person>(){

        @Override
        public Person createFromParcel(Parcel source) {
            // 读取数据:读取的顺序要和写出的顺序完全相同
            Person person = new Person();
            person.name = source.readString();//读取 name
            person.age = source.readInt();//读取 age
            return person;
        }

        @Override
        public Person[] newArray(int size) {
            return new Person[size];
        }
    };
}

  接下来在第一个活动中的写法不变,在第二个活动中获取对象稍加改动:

Person person = (Person)getIntent().getParcelableExtra("person_data");

  这样就也实现使用 Intent 传递对象了。

  对比一下,Serializable 的方式较为简单,但由于把整个对象进行序列化,效率会比 Parcelable 方式低。一般推荐使用 Parcelable 的方式来实现 Intent 传递对象的功能。

13.3 定制自己的日志工具

  开发一个项目时,定制一个自己的日志工具能够自由的控制日志的打印,当程序处于开发阶段时让日志打印出来,当程序上线后把日志屏蔽。

  新建日志工具类 LogUtils 如下:

public class LogUtils {

    public static final int VERBOSE = 1;

    public static final int DEBUG = 2;

    public static final int INFO = 3;

    public static final int WARN = 4;

    public static final int ERROR = 5;

    public static final int NOTHING = 6;

    public static int level = VERBOSE;

    public static void v(String tag,String msg){
        if (level <= VERBOSE){
            Log.v(tag,msg);
        }
    }

    public static void d(String tag,String msg){
        if (level <= DEBUG){
            Log.d(tag,msg);
        }
    }

    public static void i(String tag,String msg){
        if (level <= INFO){
            Log.i(tag,msg);
        }
    }

    public static void w(String tag,String msg){
        if (level <= WARN){
            Log.w(tag,msg);
        }
    }

    public static void e(String tag,String msg){
        if (level <= ERROR){
            Log.e(tag,msg);
        }
    }
}

  上述代码提供了5个自定义的日志方法,其内部分别调用了 Android 自带的打印日志方法,在项目里使用就像使用普通日志工具一样,如打印一行 DEBUG 级别的日志可以这样写:

LogUtils.d("TAG","debug log");

  值得注意的是,LogUtils 定义了一个静态变量 level,在开发阶段将 level 指定成 VERBOSE,当项目正式上线时将 level 指定成 NOTHING,将所有日志屏蔽。

13.4 创建定时任务

  Android 中的定时任务一般有两种实现方式,一种是使用 Java API 里提供的 Timer 类(不太适用于需要长期在后台运行的定时任务),一种是使用 Android 的 Alarm 机制(具有唤醒 CPU 功能,可以保证大多数情况下执行定时任务时 CPU 能正常工作)。

13.4.1 Alarm 机制

  Alarm 机制的用法不复杂,主要是借助 AlarmManager 类来实现的。比如想要设定一个任务在 10 秒钟后执行,可写成:

// 获取 AlarmManager 的实例
AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        
// 设置触发时间
// SystemClock.elapsedRealtime() 获取系统开机至今所经历时间的毫秒数
// SystemClock.currentTimeMillis() 获取1970年1月1日0点至今所经历时间的毫秒数
long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000;
        
// 3个参数:指定 AlarmManager 的工作类型、定时任务的触发时间、PendingIntent
// 其中AlarmManager 的工作类型有四种:
// ELAPSED_REALTIME 定时任务的触发时间从系统开机开始时算起,不会唤醒 CPU
// ELAPSED_REALTIME_WAKEUP 系统开机开始时算起,会唤醒 CPU
// RTC 从1970年1月1日0点开始算起,不会唤醒 CPU
// RTC_WAKEUP 从1970年1月1日0点开始算起,会唤醒 CPU
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pandingIntent);

  举个例子,实现一个长时间在后台定时运行的服务,首先新建一个普通的服务 LongRunningService,将触发定时任务的代码写到 onStartCommand() 方法中,如下:

public class LongRunningService extends Service {
    public LongRunningService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 在这里执行具体的逻辑操作
            }
        }).start();
        AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
        int anHour = 60 * 60 * 1000;//1小时的毫秒数
        long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
        Intent i = new Intent(this,LongRunningService.class);
        PendingIntent pi = PendingIntent.getService(this,0,i,0);
        manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pi);
        return super.onStartCommand(intent, flags, startId);
    }
}

  然后,要启动定时服务时调用如下代码即可:

Intent intent = new Intent(context,LongRunningService.class);
context.startService(intent);

  值得注意的是,从 Android 4.4开始,由于系统在耗电方面的优化,Alarm 任务的触发时间变得不准确,可能会延迟一段时间后再执行。当然,使用 AlarmManager 的 setExact() 方法来替代 set() 方法,基本上可以保证任务准时执行。

13.4.2 Doze 模式

  在 Android 6.0中,谷歌加入了一个全新的 Doze 模式,可以极大幅度地延长电池的使用寿命。下面就来了解下这个模式,掌握一些编程时注意事项。

  在 6.0 及以上系统的设备上,若未插接电源,处于静止状态(7.0中删除了这一条件),且屏幕关闭了一段时间之后,就会进入到 Doze 模式。在 Doze 模式下,系统会对 CPU、网络、Alarm 等活动进行限制,从而延长电池的使用寿命。

  当然,系统不会一直处于 Doze 模式,而是会间歇性的退出一小段时间,在这段时间应用可以去完成它们的同步操作、Alarm 任务等,其工作过程如下:

Doze 模式的工作过程

  Doze 模式下受限的功能有:
 (1)网络访问被禁止
 (2)系统忽略唤醒CPU或屏幕操作
 (3)系统不再执行WIFI扫描
 (4)系统不再执行同步任务
 (5)Alarm 任务将会在下次退出 Doze 模式时执行

  特殊需求,要 Alarm 任务在 Doze 模式下也必须正常执行,则可以调用 AlarmManager 的 setAndAllowWhileIdle() 或 setExactAndAllowWhileIdle() 方法。

13.5 多窗口模式编程

  Android 7.0中引入了一个非常有特色的功能——多窗口模式,允许在同一个屏幕中同时打开两个应用程序。

13.5.1 进入多窗口模式

  手机的导航栏上有3个按钮:左边 Back 按钮、中间 Home 按钮、右边 OverView 按钮,如下所示:

手机导航栏

  OverView 按钮的作用是打开一个最近访问过的活动或任务的列表界面,进入多窗口模式需要用到 OverView 按钮,并且有两种方式:

  • 在 OverView 列表界面长按任意一个活动的标题,将该活动拖到屏幕突出显示的区域,则可以进入多窗口模式。

  • 打开任意一个程序,长按 OverView 按钮,也可以进入多窗口模式。

  多窗口模式效果如下:

上下分屏的多窗口模式
左右分屏的多窗口模式

  可以看出多窗口模式下,应用界面缩小很多,编写程序时要多考虑使用 match_parent 属性、RecyclerView、ScrollView 等控件,适配各种不同尺寸的屏幕。

13.5.2 多窗口模式下的生命周期

  多窗口模式并不会改变活动原有的生命周期,只是会将用户最近交互过的那个活动设置为运行状态,而将多窗口模式下另外一个可见的活动设置为暂停状态。若这时用户又去和暂停的活动进行交互,那么该活动就变成运行状态,之前处于运行状态的活动变成暂停状态。

  进入多窗口模式时活动会被重新创建,若要改变这一默认行为,可以在 AndroidManifest.xml 中对活动添加如下配置:

android:configChanges="orientation|keyboardHidden|screenSize|screenLayout"

  添加这行配置后,不管是进入多窗口模式还是横竖屏切换,活动都不会被重新创建,而是会将屏幕发生变化的事件通知到 Activity 的 onConfigurationChanged() 方法中。因此,若要在屏幕发生变化时进行相应的逻辑处理,那么在活动中重写 onConfigurationChanged() 方法即可。

13.5.3 禁用多窗口模式

  禁用多窗口模式的方法很简单,只需在 AndroidManifest.xml 的<application>或<activity>标签中加入如下属性即可:

android:resizeableActivity=["true"|"false"]

  其中,true 表示支持多窗口模式,false 表示不支持,若不配置这属性默认是 true。

  虽说 android:resizeableActivity 这个属性的用法简单,但这个属性只适用于 targetSdkVersion 24 或更高的时候,若低于24则无效,可能会被告知此应用在多窗口模式下可能无法正常工作,并进入多窗口模式。

  Android 规定,若项目指定的 targetSdkVersion 低于24,并且活动是不允许横竖屏切换时是不支持多窗口模式的。因此针对上面的情况,就需要在 AndroidManifest.xml 的<activity>标签中配置如下属性:

android:screenOrientation=["portrait"|"landscape"]

  其中,portrait 表示只支持竖屏,landscape 表示只支持横屏。

  本篇文章就介绍到这。

推荐阅读更多精彩内容