第一行代码读书笔记 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 表示只支持横屏。

  本篇文章就介绍到这。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容