美文网首页Android UIAndroid 开发技术分享手机移动程序开发
第一行代码读书笔记 13 -- Android 中的一些高级技巧

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

作者: 开心wonderful | 来源:发表于2017-01-30 19:29 被阅读587次

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

    • 获取全局 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 表示只支持横屏。

      本篇文章就介绍到这。

    相关文章

      网友评论

      本文标题:第一行代码读书笔记 13 -- Android 中的一些高级技巧

      本文链接:https://www.haomeiwen.com/subject/phfhittx.html