美文网首页
第一行代码(十三)

第一行代码(十三)

作者: radish520like | 来源:发表于2018-04-20 16:59 被阅读0次

第十三章主要讲了一些日常开发中常用的技巧

一、全局获取 Context 的技巧

  截止到目前,我们还没有碰见过为得不到 Context 而发愁的情况,因为我们很多的操作都是在活动中进行的,而活动本身就是一个 Context 对象,但是,当应用程序的架构逐渐复杂起来的时候,很多逻辑代码都将脱离 Activity 类,但此时你又恰恰需要使用 Context,这就伤脑筋了。
  举个例子,你写了一个 Util 类,该类的某个方法需要用到 Context,怎么办?也许你会想到通过方法传递进来,还是用代码来说明吧。

public class Util {

    public static void needContext(){
        //下面的逻辑处理需要用到 Context 对象
    }
}

  大多数人的第一反应就是通过方法的参数传递进来

public class Util {

    public static void needContext(Context context){
        //下面的逻辑处理需要用到 Context 对象
    }
}

  是的,没问题,可以这样写,但是如果调用方也获取不到 Context 怎么办?我们可以使用另一种更好的方法来解决这个问题。

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

  首先你需要新建一个类,继承自 Application

/**
 * 自定义 Application
 */

public class MyApplication extends Application {

    private static Context mContext;

    /**
     * 通过静态方法获取 Context
     */
    public static Context getContext(){
        return mContext;
    }

    /**
     * 重写 onCreate() 方法
     */
    @Override
    public void onCreate() {
        super.onCreate();
        mContext = getApplicationContext();
    }
}

  接下来我们需要告知系统,当程序启动的时候应该初始化 MyApplication 类,而不是默认的 Application 类。

    <!-- 这里注意,一定是全包名,或者前面有个" . ",否则系统找不到这个类 -->
    <application
        android:name=".MyApplication"
        ...>

        ...
    </application>

  接下来,我们就可以在 Util 类里面这样获取 Context 了。

public class Util {

    public static void needContext(){
        //下面的逻辑处理需要用到 Context 对象
        Context context = MyApplication.getContext();
    }
}

二、使用 Intent 传递对象

  我们可以借助 Intent 的 putExtra() 方法来传递数据,里面可以传递boolean、byte、short、int、long、float、double、char、String,以及前面这些数据类型所对应的数组形式的数据类型,那么传递对象呢?
  putExtra() 方法还有两个数据类型:Serializable 和 Parcelable

    1. Serializable 方式:
        Serializable 是序列化的意思,表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。至于序列化的方法也很简单,只需要让类实现 Serializable 接口即可。
/**
 * 实体类
 */

public class Fruit implements Serializable{

    ...
}

  使用 Intent 传递对象

        //发送方
        Fruit fruit = new Fruit();
        fruit.setName("苹果");
        Intent intent = new Intent(this,MainActivity.class);
        intent.putExtra("fruit_data",fruit);
        startActivity(intent);
        //接收方
        /*
            通过 getSerializable() 方法获取序列化对象,再向下转型成 Fruit 对象即可
         */
        Fruit fruit_data = (Fruit) getIntent().getSerializableExtra("fruit_data");
    1. Parcelable 方式:
        不同于 Serializable ,Parcelable 方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是 Intent 所支持的数据类型,这样就实现传递对象的功能了。
        实现 Parcelable 的方式首先也是让一个类去实现 Parcelable 接口。
/**
 * 实体类
 */

public class Fruit implements Parcelable {

    private String name;//水果的名字
    private int imageId;//水果对应资源的图片id

    public Fruit() {

    }

    public Fruit(String name, int imageId) {
        this.name = name;
        this.imageId = imageId;
    }

    protected Fruit(Parcel in) {
        /*
           这里注意,读取的顺序一定要和刚才写入的顺序一致。
        */
        name = in.readString();
        imageId = in.readInt();
    }

    /**
     * 必须重写的方法
     */
    @Override
    public int describeContents() {
        //这里直接返回 0 就可以了
        return 0;
    }

    /**
     * 必须重写的方法
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        /*
            将字段一一写出
            字符串调用 writeString(),整数调用 writeInt() 依此类对
         */
        dest.writeString(name);
        dest.writeInt(imageId);
    }

    /**
     * 定义一个常量,创建 Parcelable.Creator 接口的实现,并且指定泛型为当前类,
     */
    public static final Creator<Fruit> CREATOR = new Creator<Fruit>() {
        @Override
        public Fruit createFromParcel(Parcel in) {
            return new Fruit(in);
        }

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

    public String getName() {
        return name;
    }

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

    public int getImageId() {
        return imageId;
    }

    public void setImageId(int imageId) {
        this.imageId = imageId;
    }

    @Override
    public String toString() {
        return "Fruit{" +
                "name='" + name + '\'' +
                ", imageId=" + imageId +
                '}';
    }
}

  具体的使用方法和 Serializable 类似,只是在获取的时候,只需要把 getSerializableExtra() 方法换成 getParcelableExtra() 方法即可。

注意:Serializable 的方式较为简单,但由于会把整个对象进行序列化,因此效率会比 Parcelable 方式低一些,所以在通常情况下还是更加推荐使用 Parcelable 的方式来实现 Intent 传递对象的功能。

三、定制自己的日志工具

  理想状态下,我们的日志打印是可以控制的,当程序处于开发阶段时就让日志打印出来,当程序上线了之后就把日志屏蔽掉。

/**
 * 日志工具类
 */

public class LogUtil {

    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;
    /*
        这里通过改变 level 的值来控制打印日志的行为,比如:
        让 level 等于 VERBOSE 就是打印所有日志,让 level 等于
        WARN 就是只打印警告以上级别的日志,让 level 等于
        NOTHING 就是把所有日志都屏蔽掉。
     */
    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.v(tag, msg);
        }
    }

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

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

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

四、创建定时任务

  Android 中定时任务一般有两种实现方式,一种是使用 Java API 里提供的 Timer 类,一种是使用 Android 的 Alarm 机制。在大多数情况下,这两种方式都能实现类似的效果,但是 Timer 有一个明显的短板,它并不太适用于那么些需要长期在后台运行的定时任务,为了让电池更加耐用,每种手机都会有自己的休眠策略,Android 手机会在长时间不操作的情况下自动让 CPU 进入休眠功状态,这就可能导致 Timer 中的定时任务无法正常运行。而 Alarm 则具有唤醒 CPU 的功能,它可以保证在大多数情况下需要执行定时任务的时候 CPU 都能正常工作,需要注意,这里唤醒 CPU 唤醒屏幕完全不是一个概念。

Alarm机制

  主要是借助了 AlarmManager 类来实现,这个类 NotificationManager 有点类似。

        /*
            SystemClock.elapsedRealtime() 方法可以获取到系统开机至今所经历时间的毫秒数
            System.currentTimeMillis() 方法获取到的是1970年1月1日0点至今所经时间的毫秒数
         */
        long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000;
        AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        /*
            参1:用于指定AlarmManager的工作类型,有四种值可选:
                1.ELAPSED_REALTIME          :表示让定时任务的触发时间从系统开机开始算起,但不会唤醒CPU
                2.ELAPSED_REALTIME_WAKEUP   :表示让定时任务的触发时间从系统开机开始算起,但会唤醒CPU
                3.RTC                       :表示让定时任务的触发时间从1970年1月1日0时开始算起,但不会唤醒CPU
                4.RTC_WAKEUP                :表示让定时任务的触发时间从1970年1月1日0时开始算起,但会唤醒CPU
         */
        manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pendingIntent);

  如果我们需要实现一个长时间在后台定时运行的服务要怎么做?

public class LongRunningService extends Service {

    public LongRunningService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //处理具体的逻辑
            }
        }).start();
        AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        int anHour = 60 * 60 * 1000;//一小时的毫秒数
        //设置时间为一小时后执行
        long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
        Intent i = new Intent(this,LongRunningService.class);
        PendingIntent pendingIntent = PendingIntent.getService(this,0,i,0);
        //执行定时任务,这样会使得 LongRunningService 的onStartCommand() 方法每隔一小时就执行一次
        alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pendingIntent);
        return super.onStartCommand(intent, flags, startId);
    }
}

注意:从Android 4.4 系统开始, Alarm 任务的触发时间将变得不准确,有可能会延迟一段时间后任务才能得到执行。这不是 bug,而是系统在耗电性方面进行的优化。系统会自动检测目前有多少 Alarm 任务存在,然后将触发时间相近的几个任务放在一起执行,这样可以大幅度减少 CPU 被唤醒的次数,从而有效延长电池的使用时间。
  如果你要求 Alarm 任务的执行事件必须准确无误,就要使用 AlarmManager 的 setExact() 方法代替 set() 方法。

Doze 模式

  Doze模式是Android6.0系统中新加入的模式,从而可以极大幅度地延长电池的使用寿命。
  Doze模式就是当设备是 Android 6.0 及以上系统时,如果该设备未插接电源,处于静止状态(Android 7.0 中删除了这一条件),且屏幕关闭了一段时间后,就会进入到 Doze 模式,在 Doze 模式下,系统会对 CPU、网络、Alarm 等活动进行限制,从而延长电池的使用寿命。
  当然,系统并不会一直处于 Doze 模式,而是会间歇性地退出 Doze 模式一小段时间,在这段时间中,应用就可以去完成他们的同步操作、Alarm任务

注意:随着设备进入 Doze 模式的时间越长,间歇性地退出 Doze 模式的时间间隔也会越长,因为如果设备长时间不使用的话,是没有必要频繁退出 Doze 模式来执行同步等操作的。其中 Doze 模式下会受到限制的功能:
1.网络访问被禁止
2.系统忽略唤醒CPU或屏幕操作
3.系统不在执行WIFI扫描
4.系统不再执行同步服务
5.Alarm任务将会在下次退出Doze模式的时候执行
  注意最后一条,如果在 Doze 模式下,我们的任务将会变得不准时

  如果你要求Alarm任务即使在Doze模式下也必须正常执行,Android还提供了解决方案。调用 AlarmManager 的setAndAllowWhileIdle() 或者 setExactAndAllowWhileIdle() 方法就能让定时任务即使在 Doze 模式下也能正常执行了,这两个方法的区别和 set()与setEsact()方法之间的区别是一样的。

五、多窗口模式编程

  Android 7.0 系统中引入的特色功能,允许我们在同一个屏幕中同时打开两个应用程序。
  如何进入多窗口模式?一般手机底部按键有三个,左边的是返回键,右边的是HOME键,右边的是Overview键,点一下Overview键,会打开一个最近访问过的活动或任务的列表界面。


image.png

  那么我们可以通过两种方式进入多窗口模式:

  • 1.在 Overview 列表界面长按任意一个活动的标题,将该活动拖动到屏幕突出显示的区域,则可以进入多窗口模式。
  • 2.打开任意一个程序,长按 Overview 按钮,也可以进入多窗口模式(没有试出来)


    image.png

      另外,我们还可以旋转,使上下分屏变成左右分屏


    image.png
      如果想要退出多窗口模式,只要将屏幕中央的分割线向屏幕任意方向拖动到底即可。

多窗口模式下的生命周期

  多窗口模式不会改变活动原有的生命周期,只会将正和用户交互的那个活动设置为运行状态,将另一个活动设置为暂停状态。
  我们新建两个项目,并且将这两个项目的启动 Activity 的生命周期都进行打印。
  先运行第一个项目


image.png

  在运行第二个项目


image.png
  接下来我们进入到多窗口模式
image.png
  可以看到,第一个项目先销毁然后重新创建,因为进入多窗口模式后活动的大小发生了比较大的变化,默认是会重新创建活动的。由于我进入多窗口模式后又让该项目获取了焦点,所以多执行了一个onResume(),接下来将第二个项目从 Overview 列表中选中。
image.png

  这时候只要我们点击任意一个项目,就会切换这两个项目的暂停还是活动的状态。


image.png
  一个onPause,另一个onResume

注意:在多窗口模式下我们需要考虑一些关键性的点,比如说,在多窗口模式下,用户仍然可以看到处于暂停状态的应用,如果是播放器之类的应用,此时就应该能继续播放视频才对。因此,我们最好不要在活动的 onPause() 方法中去处理视频播放器的暂停逻辑,应该在 onStop() 方法中处理,并且在 onStart() 方法中恢复视频播放。

注意:如果想改变进入多窗口模式时活动会被重新创建这一默认行为,可以在清单文件中进行如下配置,加入了如下配置后,不管是进入多窗口模式,还是横竖屏切换,活动都不会被重新创建,而是会将屏幕发生变化的事件通知到 Activity 的 onConfigurationChanged() 方法当中

        <activity android:name=".MainActivity"
            android:configChanges="orientation|keyboardHidden|screenSize|screenLayout">
            
        </activity>

禁用多窗口模式

  如果你不希望应用能够在多窗口模式下运行,就可以将这个功能关闭掉,禁用多窗口模式的方法很简单,只需要在清单文件中的 <application> 或 <activity> 标签中加入如下属性即可:
android:resizeableActivity="false",其中如果传 true 表示支持多窗口模式,也是默认的情况,如果传入 false 表示不支持多窗口模式。

注意:虽然说 android:resizeableActivity 属性的用法很简单,但是他有一个很严重的问题,就是这个属性只有当项目的 targetSdkVersion 指定成 24 或更高的时候才会有用,否则这个属性是无效的。
  解决方案:Android 规定,如果项目指定 targetSdkVersion 低于 24,并且活动是不允许横竖屏切换的,那么该应用也将不支持多窗口模式。
  那么让应用不允许横竖屏切换,只需要在清单文件的 <activity> 标签中加入如下配置即可:android:screenOrientation="portrait" 或 android:screenOrientation="landscape",其中 portrait 表示活动只支持竖屏,landscape 表示活动只支持横屏。

六、Lambda表达式

  Java 8 中引入了一些非常有特色的功能,如 Lambda 表达式、stream API、接口默认实现等等。其中,stream API 和接口默认实现等特性都只支持 Android 7.0 及以上的系统,而 Lambda 表达式最低兼容到 Android 2.3 系统,所以我们来学习一下 Lambda 表达式。

Lambda 表达式本质上是一个匿名方法,它既没有方法名,也没有访问修饰符和返回值类型,使用它来编写代码将会更加简洁,更加易读。

  如果要在 Android 项目中使用 Labmda 表达式或者 Java 8中的其他新特性,需要在 app/build.gradle 中添加如下配置:

android {
    ...
    defaultConfig {
        ...
        jackOptions.enabled = true;
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

注意:如果你的项目中有 Android Library 模块,那么上述方式是不能引用成功的,我们首先要在最外层的 project build.gradle 中加入这句话classpath 'me.tatarka:gradle-retrolambda:3.2.0'
然后在要使用 lambda 表达式的 Module 的 build.gradle 中加入如下代码

apply plugin: 'me.tatarka.retrolambda'
android {
    ...

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

  写个简单的例子:

        //开启线程传统写法
        new Thread(new Runnable() {
            @Override
            public void run() {
                //具体处理逻辑
            }
        }).start();

        //开启线程 Lambda 表达式
        new Thread(() -> {
            //具体处理偶偶机
        }).start();

  能这样写是因为 Thread 类的构造函数接收的参数是一个 Runnable 接口,并且改接口中只有一个待实现的方法。

注意:凡是这种只有一个待实现方法的接口,都可以使用 Lambda 表达式的写法

        //传统写法
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                //具体处理逻辑
            }
        };

        //Lambda 写法
        Runnable runnable1 = () -> {
            //具体处理逻辑
        };

  接下来,我们自定义一个接口,再使用 Lambda 表达式的方式进行实现。

/**
 * 定义一个接口
 */
public interface MyListener {
    //注意,该方法有返回值,有参数
    String doSomething(String a,int b);
}
        //Lambda 表达式写法
        MyListener myListneer2 = (String a,int b) -> {
            return a + b;
        };

         /*
            还可以精简成这样
            这里,Java 会自动判断出 a 和 b 的数据类型
         */
        MyListener myListener2 = (a, b) -> {
            return a + b;
        };

  假如有一个方法,需要传入 MyListener 接口

    public void hello(MyListener mListener) {
        String a = "Hello Lambda";
        int b = 1024;
        String result = mListener.doSomething(a, b);
        Log.d(TAG, "hello: " + result);
    }
        //传统写法
        hello(new MyListener() {
            @Override
            public String doSomething(String a, int b) {
                return a + b;
            }
        });

        //Lambda 表达式
        hello((a, b) -> {
            return a + b;
        });

  Lambda表达式的基本用法已经掌握了,接下来看一看在 Android 中有哪些常用的功能是可以使用 Lambda 表达式替换的。
  其实,只要符合接口中只有一个待实现方法这个规则的功能,都是可以使用 Lambda 表达式来编写的,除了开启子线程之外,还有点击事件之类的功能。

        //传统写法
        findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //处理点击事件
            }
        });

        //Lambda 表达式
        findViewById(R.id.tv).setOnClickListener((v) -> {
            //处理点击事件
        });

注意:另外,当接口的待实现方法有且只有一个参数的时候,我们还可以进一步简化,将参数外面的括号去掉

        //Lambda 表达式进一步简化
        findViewById(R.id.tv).setOnClickListener(v -> {
            //处理点击事件
        });

下一篇文章:https://www.jianshu.com/p/3487e4dd8db4

相关文章

网友评论

      本文标题:第一行代码(十三)

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