ANDROID 四月 09, 2019

Android • Review

文章字数 22k 阅读约需 41 mins. 阅读次数 0

引言

本篇将介绍我在整理曾经学习 Android时的一些笔记,也算是对曾经所学知识的回顾

待完善ing……


Java • Native

Java

四大特性

封装

原则:将不需要对外提供的内容隐藏起来,属性隐藏,提供公共的访问方式。

好处:提高安全性,代码复用性

局部变量隐藏成员变量(就近原则)


继承

在程序中,继承是在描述类与类之间的所属关系,蒋多个类中的共有成员变量和成员方法抽取到一个类中(父类),让多个类去继承这个父类。

一个类继承另一个类,继承这个类以后,就继承了它所有的属性和方法

使用extends关键字继承另一个类。格式:class (子类或派生类) extends (父类、超类或基类)。

class Son extends Father {
    // ...
}

特点:

  • Java中,类只支持单继承,不允许多继承。

  • 一个类可以被多个类继承。

  • 多层继承,一个类的父类可以在去继承另一个父类。

  • 子父类的关系是相对概念。


继承中成员变量的特点:

  • 只能继承父类非私有化,成员变量名字相同就用子类自己的名字。名字不同,子类没有,就用父类。

  • 就近原则:谁离得近就用谁,如果就局部变量就优先使用局部变量,如果没有局部变量,就先看子类的成员变量 ,如果子类的成员变量也没有,就看父类的成员变量。

  • super:和this用法很像,super表示父类(this和super都是写在方法的第一行,但是他俩不能出现在同一行)。


多态

父类应用变量指向子类对象

格式:父类类型 变量名 =new 子类类型();

多态的前提:

  • 子父类继承关系(实现关系)

  • 父类引用指向子类对象

  • 方法的重写

一个父类类型的变量指向子类类型的对象,在运行时,变现出子类的特征。

多态情况下,只能直接调用父类中的方法,子类中的方法不直接调用。

出多态的原因:Java代码需要编译,在编译时,允许父类对象指向子类对象,该对象边线处父类特征,这叫编译时类型。在运行时,变现出子类特征,在运行时类型。


抽象

抽象:使用abstract关键字修饰一个类,这个类就是抽象类。

用于修饰方法和类。

  • 抽象方法:不同类的方法是相似的,但是具体内容不一样,这是我们只抽取他们的声明。没有具体的方法体,没有集体方法体的方法就叫做抽象方法

  • 抽象类:有抽象方法的类必须时抽象类。

注意:一个类继承了抽象类需要重写所有的抽象方法,否则这个类就是抽象方法。

特点:

  • 抽象方法只能在抽象类中。

  • 抽象方法和抽象类都被abstract修饰。

  • 抽象类不能创建对象。

  • 抽象类中,可以有非抽象方法。

  • 抽象类与类的关系也是继承。

  • 一个类继承了抽象类需要重写所有的抽象方法,否则这个类就是抽象类。

抽象类的成员特点:

  • 成员变量:可以有成员变量,也可以有常量。

  • 成员方法:可以有抽象方法,也可以有非抽象方法。

  • 构造方法:可以有构造方法,也可以对成员变量初始化。

  • private:私有,只有当前类中可以访问。

  • [default]:默认的,同一个包可以访问。

  • protected:受保护的,字类可以访问。

  • public:公有的,所有类中都可以访问。


== / equals()

==

通常用于比较两个变量的值是否相同,即两个对象在内存中的首地址。


equals()

对于字符串来说是比较内容的。

而对于非字符串来说是比较其指向的对象是否相同的。


String s1,s2,s3="abc",s4="abc";
        s1=new String("abc");
        s2=new String("abc");

        String s1,s2,s3="abc",s4="abc";
        s1=new String("abc");
        s2=new String("abc");

        System.out.println("s1 == s2: "+(s1==s2)); // false,两个变量的内存地址不一样,也就是说它们指向的对象不一样。
        System.out.println("s1 == s3: "+(s1==s3)); // false
        System.out.println("s3 == s4: "+(s3==s4)); // true

        System.out.println("s1.equals(s2): "+(s1.equals(s2))); // true,两个变量的所包含的内容是abc,故相等。
        System.out.println("s1.equals(s3): "+(s1.equals(s3))); // true
        System.out.println("s3.equals(s4): "+(s3.equals(s4))); // true

对于 StringBuffer

StringBuffer类中没有重新定义 equals 这个方法。

因此这个方法就来自Object类(Object类中的equals方法是用来比较 “地址” 的,所以等于 false)

StringBuffer s1=new StringBuffer("a");
        StringBuffer s2=new StringBuffer("a");

        System.out.println("s1.equals(s2): "+(s1.equals(s2))); // false

总结

  • 基本类型比较

只能用 == 来比较,不能用 equals()。

int a=3;
        int b=4;
        int c=3;

        System.out.println(a==b); // false
        System.out.println(a==c); // true

        System.out.println(a.equals(c)); // 错误,编译不能通过,不能使用 equals() 方法。

  • 基本类型的包装类型

比如Boolean、Character、Byte、Shot、Integer、Long、Float、Double 等的引用变量。

== 是比较地址的,而 equals() 是比较内容的。

Integer n1=new Integer(30);
        Integer n2=new Integer(30);
        Integer n3=new Integer(31);

        System.out.println(n1==n2); // false,两个不同的Integer对象,其地址不同。
        System.out.println(n1==n3); // false,不管是 new Integer(30),还是 new Integer(31),结果都显示 false。

        System.out.println(n1.equals(n2)); // true,根据jdk文档中的说明,n1与n2指向的对象中的内容是相等的,都是30,故equals比较后结果是true。
        System.out.println(n1.equals(n3)); // false,因对象内容不一样,一个是30一个是31。

final

final作为Java中的关键字可以用于三个地方。用于修饰类、类属性和类方法。

特征:凡是引用final关键字的地方皆不可修改。

  • 修饰:表示该类不能被继承.

  • 修饰方法:表示方法不能被重写。

  • 修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。


反射

获取

// 示例学生类。
public class Student {
    private String name;
    private String sex;

    // constructor, getter && setter ...
}

    public static void main(String[] args) {
        // (1):实例化获取
        Student student = new Student("Java", "Female");
        Class<? extends Student> aClass = student.getClass();

        // (2):名称匹配 / 地址 获取,注意抛出 ClassNotFoundException 异常
        try {
            Class<?> aClass1 = Class.forName("main.Student");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        // (3):通过类获取
        Class<Student> studentClass = Student.class;
    }

Examples

// 实体类
public class Developer {
  private final String name; // 姓名
  private final String department; // 所在部门
  private final int age; // 年龄

  @Override
  public String toString() {
    return "Developer name: " + name +
            " department: " + department +
            " age " + age;
  }

  public Developer(String name, String department, int age) {
    this.name = name;
    this.department = department;
    this.age = age;
  }

  // 创建一个让程序员开始工作的方法
  public void work() {
    System.out.println(name + " start work");
  }
}
public class Demo {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        // 通过反射获取实体类
        Class<?> developer = Class.forName("Developer");

        // 获取构造函数
        // 参数列表为实体类构造函数的参数列表类型
        Constructor<?> constructor = developer.getConstructor(String.class, String.class, int.class);

        // 接下来可通过该构造体,生成新的实例
        // 参数列表为新实例的 属性
        // 可以自行设置
        Object instance = constructor.newInstance("Peter Chen", "Dev", 22);

        // 实例创建结束,调用方法
        System.out.println(instance.toString());

        // 可以通过反射,修改实例对象的属性值
        // 修改姓名
        Field name = developer.getDeclaredField("name");
        // 公开访问
        name.setAccessible(true);
        // 修改
        name.set(instance, "zhangsan");

        // 可通过反射,调用实体类的中的方法,试试让新的 zhangsan 程序员开始工作
        Method work = developer.getMethod("work", null);
        // 反馈回刚才生成的实例
        work.invoke(instance, null);
    }
}

强引用 / 软引用 / 弱引用 / 虚引用

强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用
对象来解决内存不足的问题。

Object object=new Object();

        String string="Test";

软引用

软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。

对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。

因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。

public class SoftRef {

    public static void main(String[] args) {
        System.out.println("start");
        Obj obj = new Obj();
        SoftReference<Obj> sr = new SoftReference<Obj>(obj);
        obj = null;
        System.out.println(sr.get());
        System.out.println("end");
    }
}

class Obj {
    int[] obj;

    public Obj() {
        obj = new int[1000];
    }  

当内存足够大时可以把数组存入软引用,取数据时就可从内存里取数据,提高运行效率。

软引用在实际中有重要的应用,例如浏览器的后退按钮,这个后退时显示的网页内容可以重新进行请求或者从缓存中取出:

  • 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建。

  • 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出这时候就可以使用软引用。


弱引用

弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。

在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。所以被软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。

WeakReference<String> sr=new WeakReference<String>(new String("hello"));
        System.out.println(sr.get());

// 在使用软引用和弱引用的时候,我们可以显式地通过 System.gc() 来通知JVM进行垃圾回收。
// 但是要注意的是,虽然发出了通知,JVM不一定会立刻执行。
// 也就是说 System.gc() 是无法确保此时JVM一定会进行垃圾回收的。
        System.gc();
        System.out.println(sr.get());

弱引用还可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

Object obj=new Object(); //只要obj还指向对象就不会被回收
        WeakReference<Object> wr=new WeakReference<Object>(obj);

当要获得 WeakReference引用的object时, 首先需要判断它是否已经被回收,如果wr.get()方法为空, 那么说明 WeakReference 指向的对象已经被回收了。


虚引用

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在Java中用java.lang.ref.PhantomReference类表示。

如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动。

虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。

如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

// 虚引用必须和引用队列关联使用
ReferenceQueue<String> queue=new ReferenceQueue<String>();
        PhantomReference<String> pr=new PhantomReference<String>(new String("hello"),queue);

        System.out.println(pr.get());

总结

引用类型 被回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时
软引用 内存不足时 对象缓存 内存不足时
弱引用 jvm垃圾回收时 对象缓存 gc运行后
虚引用 未知 未知 未知

数组 / 集合

  • 数组是Java语言内置的数据类型,他是一个线性的序列,所有可以快速访问其他的元素,数组和其他语言不同,当你创建了一个数组时,他的容量是不变的,而且在生命周期也是不能改变的,还有Java数组会做边界检查,如果发现有越界现象,会报RuntimeException异常错误,当然检查边界会以效率为代价。

  • Java 还提供其他集合,List,Map,Set,他们处理对象的时候就好像这些对象没有自己的类型一样,而是直接归根于Object,这样只需要创建一个集合,把对象放进去,取出时转换成自己的类型就行了。


区别

  • 数组声明了它容纳的元素的类型,而集合不声明。

  • 数组是静态的,一个数组实例具有固定的大小,一旦创建了就无法改变容量了。

  • 集合是可以动态扩展容量,可以根据需要动态改变大小,集合提供更多的成员方法,能满足更多的需求。

  • 数组的存放的类型只能是一种(基本类型/引用类型)。

  • 集合存放的类型可以不是一种(不加泛型时添加的类型是Object)。

  • 数组是Java语言中内置的数据类型,是线性排列的,执行效率或者类型检查都是最快的。


String / StringBuffer / StringBuilder

腾讯云 • 深入理解String、StringBuffer和StringBuilder类的区别

可变与不可变

  • String:不可变类,即创建String对象后,该对象中的字符串是不可改变的,直到这个对象被销毁

  • StringBuffer:继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,是可变类。

  • StringBuilder:继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,是可变类。

由于String是可变类,适合在需要被共享的场合中使用,当一个字符串经常被修改时,最好使用StringBuffer实现。

如果用String保存一个经常被修改的字符串,该字符串每次修改时都会创建新的无用的对象,这些无用的对象会被垃圾回收器回收,会影响程序的性能,不建议这么做。


初始化方式

由于String是可变类,适合在需要被共享的场合中使用,当一个字符串经常被修改时,最好使用StringBuffer实现。如果用String保存一个经常被修改的字符串,该字符串每次修改时都会创建新的无用的对象,这些无用的对象会被垃圾回收器回收,会影响程序的性能,不建议这么做。


字符串修改方式

String字符串修改方法:

  1. 创建一个StringBuffer

  2. 调用StringBuffer的append方法

  3. 调用StringBuffer 的 toString()方法 把结果返回

示例代码如下:

// String
String str="hello";
        str+="java";

// 对String的修改等价于以下使用StringBuffer的代码:

// StringBuffer
        StringBuffer stringBuffer=new StringBuffer(str);
        stringBuffer.append("java");
        str=stringBuffer.toString();

上述String字符串的修改过程要比StringBuffer多一些额外操作,会增加一些临时的对象,从而导致程序的执行效率降低。

StringBuffer和StringBuilder在修改字符串方面比String的性能要高。


内部实现 equals 和 hashCode 方法

String 实现了 equals()方法 和 hashCode()方法。

new String("java").equals(new String("java")) // 结果为true

StringBuffer没有实现equals()方法和hashCode()方法。

new StringBuffer("java").equals(new StringBuffer("java")) // 结果为false

将StringBuffer对象存储进Java集合类中会出现问题。


线程安全

StringBuffer与StringBuilder都提供了一系列插入、追加、改变字符串里的字符序列的方法,它们的用法基本相同,只是StringBuilder是线程不安全的
StringBuffer是线程安全的

如果只是在单线程中使用字符串缓冲区,则StringBuilder的效率会高些,但是当多线程访问时,最好使用StringBuffer。


总结

综上,在执行效率方面,StringBuilder最高,StringBuffer次之,String最低,对于这种情况,一般而言,如果要操作的数量比较小,应优先使用String类;如果是在单线程下操作大量数据,应优先使用StringBuilder类;如果是在多线程下操作大量数据,应优先使用StringBuilder类。


用例:将字符串反转

// StringBuffer reverse
StringBuffer stringBuffer=new StringBuffer();
        stringBuffer.append("abcdefg");
        System.out.println(stringBuffer.reverse()); // gfedcba

// StringBuilder reverse
        StringBuilder stringBuilder=new StringBuilder();
        stringBuilder.append("abcdefg");
        System.out.println(stringBuilder.reverse()); // gfedcba

Android • Native

Android

Activity

在 Google 公开 Jetpack 后,官方提倡 单Activity,多Fragment 的开发模式。

优点:

  • Fragment 比 Activity 占用较少系统资源,使 App 能在较低端的手机上流畅运行。

  • Fragment 作为碎片,更容易控制每个场景的生命周期与状态。


生命周期

下图为Activity的生命周期示意图:

Activity Lifecycle

场景:用户点击 App图标 启动App

Activity启动 -> onCreate() -> onStart() -> onResume()

  • onCreate():设置布局资源,数据加载,图片预加载等。

  • onStart():Activity暂未处在前台状态,用户无法对 Activity 进行触摸点击或者滑动等操作。

  • onResume():该生命周期方法调用后,用户即可对Activity进行控制,也可在该方法中进行数据预加载。


场景:用户将App退居后台。

Activity 不可见 -> onPause() -> onStop()

  • onPause():表明 Activity 处于停滞状态,Activity 可见但不可被操作,Activity退居后台时被调用。

  • onStop():整个 Activity 处于后台不可见或被其他 Activity 覆盖,若系统处于内存吃紧状态时可能会被系统回收。


场景:用户从后台返回App、用户再次点击App图标使其回到前台。

再次回到 Activity 时 -> onRestart() -> onStart() -> onResume()

  • onRestart():Activity由不可见状态到可见状态时调用,常见场景为后台App重新回到前台。

场景:用户将App从后台堆栈中移除。

退出当前Activity时 -> onPause() -> onStop -> onDestroy()

  • onDestroy():最后一个生命周期方法,资源释放、内存回收。

显式Intent、隐式Intent

  • 显示Intent

对于已经在 Manifest清单文件 中定义的Activity,可使用以下代码进行显示跳转

// 模拟从欢迎页跳转到主页
startActivity(new Intent(WelcomeActivity.this,MainActivity.class));

  • 隐式Intent

相较于显式Intent,隐式Intent则含蓄了许多,它并不明确指出想要启动哪一个Activity,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并找出合适的Activity去启动。

// 模拟拨号,隐式Intent 将会判断传入的意图,并自动搜索适合的Activity进行启动
Intent telIntent=new Intent(Intent.ACTION_DIAL)

// 设置电话号码
        Uri data=Uri.parse("tel:"+telNum);
        telIntent.setData(data);

        startActivity(telIntent);

进程优先级

由高到低依次为:

前台 / 可见 / 服务 / 后台 / 空

  • 前台进程:App前台可操作。

  • 可见:App处于后台堆栈,可见但不可操作。

  • 服务:Service服务进程通常是一种可在后台执行长时间操作而不提供界面的应用组件。

  • 后台:App处于后台堆栈不可见,若手机内存不足,则该进程可能被后台回收。

  • 空:在该进程内,没有任何东西运行,该进程目的是用作缓存。以减少应用在下次启动中所需的时间。


启动模式

  • standard:普通模式,走所有生命周期方法,对内存有很大消耗。

  • singleTop:栈顶复用模式,判断 待创建的Activity 是否已位于栈顶,若 待创建Activity 已位于栈顶,则会复用 栈顶
    Activity;若没有,则还是会走所有生命周期创建一个 新的Activity。

  • singleTask:栈内复用模式,检测整个任务栈中是否存在当前需要启动的Activity,若存在,则直接置于栈顶,并将已有 栈顶Activity 全数销毁出栈,并回调
    onNewIntent() 方法

  • singleInstance:独享模式,若Activity在整个App中只有一个实例,且独占一个任务栈,则称为独享模式。


进程间通信

Android中进程间通信(IPC)方式

IPC(Inter-Process Communication)为进程间通信或跨进程通信,是指两个进程进行进程间通信的过程。

在Android中,为每一个应用程序都分配了一个独立的虚拟机,不同虚拟机在内存分配上都有不同的地址空间,互相访问数据需要借助其他手段。

  • Bundle模式

在Android中三大组件(Activity,Service,Receiver)都支持在Intent中传递Bundle数据,由于Bundle实现了Parcelable接口(一种特有的序列化方法),所以它可以很方便的在不同的进程之间进行传输。当在一个进程中启动另外一个进程的Activity,Service,Receiver时,可以在Bundle中附加需要传输给远程的进程的信息,并通过Intent发送出去。

Intent intent=new Intent(MainActivity.this,SecondActivity.class);
        Bundle bundle=new Bundle();
        bundle.putString("key","value");
        intent.putExtras(bundle);
        startActivity(intent);

  • 文件共享模式

文件共享:将对象序列化之后保存到文件中,在通过反序列,将对象从文件中读取出来。此方式对文件的格式没有具体的要求,可以是文件、XML、JSON等。

文件共享方式也存在着很大的局限性,如并发读/写问题,如读取的数据不完整或者读取的数据不是最新的。文件共享适合在对数据同步要求不高的进程间通信,并且要妥善处理并发读/写的问题。


  • Message模式

  • 使用 AIDL 的方式

  • ContentProvider

  • 广播(Broadcast)

  • Socket

Fragment

Fragment

生命周期

Fragment Lifecycle

场景:Activity 携带的 Fragment 初始化。

CREATED -> STARTED -> RESUMED

  • CREATED:onCreate() -> onCreateView() -> onViewCreated() -> onViewStateRestored()

对于 onCreteView(),可在该方法内手动填充或创建 Fragment 视图,且可在该方法内使用 getViewLifecycleOwnerLiveData() 对 LiveData
进行初始化或更新。若属兔已经创建完成,则会调用
onViewStateRestored(),在该方法内需要对已与 Fragment视图 进行关联的状态进行重建。

  • STARTED:onStart()

Fragment实体已经处于可用状态(用户暂不可操作),当前Fragment 所属的 FragmentManager 将执行相应 Fragment事务。

  • RESUMED:onResumed()

Fragment可见,其动画与事务均执行完毕,且可响应用户操作。


场景:用户选择离开当前Fragment页面、宿主Activity 退居后台、当前Fragment 退居后台不可见。

RESUMED -> STARTED -> CREATED

  • RESUMED:onResumed()

  • STARTED:onPause()

  • CREATED:onStop() -> onSaveInstanceSate() -> onDestroyView()

对于API级别28(Android 9)之前的设备,onSaveInstanceState() 函数将在 onStop() 前调用。

对于API级别28(Android 9)之后的设备,则反之。

onSaveInstanceState() 与 onStop()


场景:宿主Activity 或 当前Fragment 被回收、当前Fragment的FragmentManager被销毁。

DESTROYED

  • onDestroy()

Fragment 通信

大部分情况下,通过ViewModel在多个Fragment或与其宿主Activity之间共享数据是一个理想的选择。

普通模式:

  • Fragment中 调用Activity中 的方法 GetActivity(F -> A)

  • Activity中 调用Fragment中 的方法进行 接口回调(A -> F)

  • Fragment中 调用Fragment中 的方法 findFragmentById (F -> F)


Service

应用场景

一种可在后台执行长时间运行操作而不提供界面的应用组件,如后台处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序进行交互。

简单地说,服务是一种即使用户未与应用交互也可在后台运行的组件,因此,只有在需要服务时才应创建服务。


与 线程Thread 区别

如果必须在主线程之外执行操作,但只在用户与应用交互时执行此操作,则应创建新线程。例如,如果只是想在 Activity 运行的同时播放一些音乐,则可在 onCreate() 中创建线程,在
onStart() 中启动线程运行,然后在 onStop() 中停止线程。


生命周期

Service Lifecycle

onStartCommand() -> onBind() -> onCreate() -> onDestroy()

  • onStartCommand()

当一个组件,如Activity请求启动服务时,系统将调用该方法,在服务启动后,会在后台无限期运行,直到调用stopSelf() 或 stopService() 停止。

PS:若只需要绑定服务,则无需实现此方法。


  • onBind()

当另一个组件想要与服务绑定时,系统将调用该方法,在此方法中,必须返回IBinder提供的接口,以供客户端与服务进行通信。


  • onCreate()

首次创建服务时,系统会首先调用此方法执行一次性设置(先于onStartCommand()或onBind()),若该服务已在运行,则不会调用。


  • onDestroy()

不再使用服务并且准备将其销毁时,系统将调用该方法,服务应通过使用该方法清理并回收资源,如:线程、注册的侦听器、接受器等。


前台Service

若希望Service一直保持运行状态,则可以考虑使用前台Service,该Service会有一个一直运行在系统状态栏的图标,下拉状态栏可以看到更加详细的信息,类似通知的效果。

由于状态栏中一直有一个正在运行的图标,相当于应用的另一种前台可见状态,因此系统不会倾向于回收前台Service。


IntentService

处理异步请求,实现多线程。

若 Service中 处理过多耗时逻辑,则有可能出现 ANR(Application Not Responding)的情况。

  • 与 普通Service 区别

Service需要主动调用 stopSelf() 来结束服务,而IntentService不需要。

在 IntentService中,onHandleIntent() 方法可用来处理一些耗时逻辑。

在所有Intent被处理完后,系统会自动关闭服务。

public class MyIntentService extends IntentService {

    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        // 处理耗时逻辑
    }
}

Content Provider

使用场景:当前应用需要与其他应用共享数据,如当前应用需使用拨号,则会寻求电话应用共享数据。


Broadcast Receiver

使用场景:登录异常强制退出、用户身份认证过期需重新登陆。

  • 标准广播:是一种完全异步执行的广播,在广播发出之后,所有的BroadcastReceiver几乎会在一时刻收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高
    ,但同时也意味着它是无法被截断的。

  • 有序广播(消息队列):是一种同步执行
    的广播,在广播发出之后,同一时刻只会有一个BroadcastReceiver能够收到这条广播消息,当这个BroadcastReceiver中的逻辑执行完毕后,广播才会继续传递。所以此时的BroadcastReceiver是有先后顺序的,优先级高的BroadcastReceiver就可以先收到广播消息,并且前面的BroadcastReceiver还可以截断正在传递的广播,这样后面的BroadcastReceiver就无法收到广播消息了。


SharedPreferences / SQLite

本地持久化。


多线程编程

Android 本身不允许我们在Ui线程中进行复杂的耗时操作,这些耗时操作极易造成 ANR(Application Not Responding),因此耗时操作通常会在后台子线程中执行。

在 Android中,官方提供了诸如Handler、AsyncTask来处理后台耗时消息。

异步消息处理机制

  • Message

Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间传递数据。


  • Handler

Handler顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用Handler的sendMessage()方法、post()
方法等,而发出的消息经过一系列地辗转处理后,最终会传递到Handler的handleMessage()方法中。


  • MessageQueue

MessageQueue是消息队列的意思,它主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。


  • Looper

Looper是每个线程中的MessageQueue的管家,调用Looper的loop()
方法后,就会进入一个无限循环当中,然后每当发现MessageQueue中存在一条消息时,就会将它取出,并传递到Handler的handleMessage()
方法中。每个线程中只会有一个Looper对象


AsyncTask

首先来看一下AsyncTask的基本用法。由于AsyncTask是一个抽象类,所以如果我们想使用它,就必须创建一个子类去继承它。在继承时我们可以为AsyncTask类指定3个泛型参数,这3个参数的用途如下。

  • Params:在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。

  • Progress:在后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。

  • Result:当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型

class DownloadTask extends AsyncTask<Params, Progress, Result>(){
        ...
        }

对于AsyncTask的使用我们还需要重写以下4个方法才能完成对任务的定制。

  • onPreExecute()

这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。


  • doInBackground(Params…)

这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成,就可以通过return语句将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定的是Unit,就可以不返回任务执行结果。

注意,在这个方法中是不可以进行UI操作的。

如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用 publishProgress(Progress…)方法来完成。


  • onProgressUpdate(Progress…)

当在后台任务中调用了publishProgress(Progress…)方法后,onProgressUpdate (Progress…)
方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。

在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。


  • onPostExecute(Result)

当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。

返回的数据会作为参数传递到此方法中,可以利用返回的数据进行一些UI操作,比如说提醒任务执行的结果,以及关闭进度条对话框等。


一个模拟下载的AsyncTask例子:

public static class DownloadTask extends AsyncTask<Void, Integer, Boolean> {

    @Override
    protected void onPreExecute() {
        progressDialog.show(); // 显示进度对话框
    }

    @Override
    protected Boolean doInBackground(Void... voids) {
        try {
            while (true) {
                Integer downloadPercent = doDownload(); // 这是一个虚构的方法
                publishProgress(downloadPercent);

                if (downloadPercent >= 100) {
                    break;
                }
            }

            return true;

        } catch (Exception exception) {
            return false;
        }
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        // 在这里更新下载进度
        progressDialog.setMessage("Downloaded " + values[0] + "%");
    }

    @Override
    protected void onPostExecute(Boolean result) {
        progressDialog.dismiss();// 关闭进度对话框
        // 在这里提示下载结果
        if (result) {
            Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(context, "Download failed", Toast.LENGTH_SHORT).show();
        }

    }
}

在这个DownloadTask中,我们在doInBackground()
方法里执行具体的下载任务。这个方法里的代码都是在子线程中运行的,因而不会影响主线程的运行。注意,这里虚构了一个doDownload()
方法,用于计算当前的下载进度并返回,我们假设这个方法已经存在了。在得到了当前的下载进度后,下面就该考虑如何把它显示到界面上了,由于doInBackground()
方法是在子线程中运行的,在这里肯定不能进行UI操作,所以我们可以调用publishProgress()方法并传入当前的下载进度,这样onProgressUpdate()
方法就会很快被调用,在这里就可以进行UI操作了。

当下载完成后,doInBackground()方法会返回一个布尔型变量,这样onPostExecute()
方法就会很快被调用,这个方法也是在主线程中运行的。然后,在这里我们会根据下载的结果弹出相应的Toast提示,从而完成整个DownloadTask任务。

简单来说,使用AsyncTask的诀窍就是,在doInBackground()方法中执行具体的耗时任务,在onProgressUpdate()方法中进行UI操作,在onPostExecute()
方法中执行一些任务的收尾工作。

如果想要启动这个任务,只需编写以下代码即可:

new DownloadTask().execute();

点击事件分发机制

事件传递的顺序

Activity -> ViewGroup -> View


Android • Jetpack

Jetpack

Jetpack 是一个架构组件库,其中任何一个组件也可独立拆分使用,采用由 Google 官方推荐的 MVVM
架构(Model、View、ViewModel),旨在为开发者提供开箱即用的快速开发体验。

主要的 Jetpack 框架组件:

  • Lifecycle(生命周期)

  • LiveData(观察者模式的数据监控)

  • ViewModel(状态管理,页面通信)

  • ViewBinding or DataBinding(数据双向绑定与Ui访问控制)

  • Navigation(页面管理)

  • Paging(分页)

  • WorkManager(多线程处理)

  • ROOM(数据库接口)

组件库提供的新 Ui 组件

  • Jetpack Compose(声明式Ui,所见即所得)

  • ViewPager2


MVC / MVP / MVVM

  • MVC:Model,View,Controller

  • MVP:Model,View,Presenter

  • MVVM:Model,View,ViewModel

架构差异

  • MVC:Controller通常为Activity,将包含数据处理与逻辑业务,但该模式中Activity既充当着View与Controller角色,将导致代码极度耦合。

  • MVP:Presenter 持有 View接口,不直接操作View,并在其中执行业务操作,可将视图操作与业务逻辑解耦,使 Activity 成为 真正View层

MVVM

Model:实体模型。

View:对应于Activity和XML,负责View的绘制以及用户交互。

ViewModel:负责完成View与Model间的交互,负责业务逻辑。


Lifecycle

常见的 Lifecycle
组件用法是通过其提供的观察者模式,将生命周期的复杂操作进行封装,开发者只需在视图控制器(配合ViewBinding或DataBinding使用)中注册生命周期持有者,即可优雅完成对当前
Activity 或 Fragment 生命周期控制。


LiveData

LiveData的出现主要是为了帮助开发者遵循 “通过唯一可信源分发状态” 的标准化开发理念。

通过与Lifecycle配合使用,以观察者模式将Ui与数据动态解耦,使Ui能动态响应数据变化,LiveData仅在当前状态为 STARTED 或 RESUMED 时才会通知 Ui 进行更新。

LiveData 带来的优点:

  • 确保 Ui 与数据始终保持一致:LiveData 将时刻关注被赋予 Observer 的对象,当你的数据发生变化时,无需手动对 Ui 进行重绘,LiveData将自动通知 Ui 更新数据。

  • 减少内存泄漏:由于 Observer 对象的生命周期与当前 Activity 或 Fragment绑定,LiveData将自动对已进入 Destroyed
    状态的对象进行清理,从而减少不必要的内存泄漏。若当前 Activity 或 Fragment 在栈区中处于不活跃状态,LiveData则不会接收任何对于数据的更新直到绑定对象重新位于前台活跃状态。

  • 减少通过人为手段对生命周期的管理监控:LiveData能够自动管理其绑定对象的生命周期,并在 App 处于前/后台状态时自动更新其状态。


ViewModel


ViewBinding / DataBinding

与使用 findViewById 相比,视图绑定(ViewBinding)具有一些很显著的优点:

  • Null 安全:由于视图绑定会创建对视图的直接引用,因此不存在因视图 ID 无效而引发 Null 指针异常的风险。此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用
    @Nullable 标记。

  • 类型安全:每个绑定类中的字段均具有与它们在 XML 文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。
    这些差异意味着布局和代码之间的不兼容将会导致构建在编译时(而非运行时)失败。

与数据绑定的对比

视图绑定和数据绑定均会生成可用于直接引用视图的绑定类。但是,视图绑定旨在处理更简单的用例,与数据绑定相比,具有以下优势:

  • 更快的编译速度:视图绑定不需要处理注释,因此编译时间更短。

  • 易于使用:视图绑定不需要特别标记的 XML 布局文件,因此在应用中采用速度更快。在模块中启用视图绑定后,它会自动应用于该模块的所有布局。

反过来,与数据绑定(DataBinding)相比,视图绑定也具有以下限制:

  • 视图绑定不支持布局变量或布局表达式,因此不能用于直接在 XML 布局文件中声明动态界面内容。

  • 视图绑定不支持双向数据绑定。

考虑到这些因素,在某些情况下,最好在项目中同时使用视图绑定和数据绑定。您可以在需要高级功能的布局中使用数据绑定,而在不需要高级功能的布局中使用视图绑定。


与 IOS开发 中的故事板一样,Jetpack为导航提供Navigation组件以管控各页面间的关联跳转。使用了 Navigation组件
构建的App通常需要有一个其实目的地,在应用中,back操作pop up操作将会起到一样的作用。

导航组件提供各种其他优势,包括以下内容:

  • 处理 Fragment 事务。

  • 默认情况下,正确处理往返操作。

  • 为动画和转换提供标准化资源。

  • 实现和处理深层链接。

  • 包括导航界面模式(例如抽屉式导航栏和底部导航),用户只需完成极少的额外工作。

  • Safe Args - 可在目标之间导航和传递数据时提供类型安全的 Gradle 插件。

  • ViewModel 支持 - 您可以将 ViewModel 的范围限定为导航图,以在图表的目标之间共享与界面相关的数


WorkManager

任务管理。


ROOM

Room 持久性库在 SQLite 的基础上提供了一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制。

具体来说,ROOM具有以下优势:

  • 针对 SQL 查询的编译时验证。

  • 可最大限度减少重复和容易出错的样板代码的方便注解。

  • 简化了数据库迁移路径。

ROOM 概览

  • Entity

ROOM 使用 @Entity 注解对类进行标识,数据实体通常为数据库中的一行,如常见的 User实体,通常会包括含主键id在内的姓名,昵称,出生日期,性别等列属性。

对于主键,可使用 @PrimaryKey 对其进行标注,并使用 unique属性 指定唯一性;

对于列属性,可使用 @ColumInfo 对其进行标注,并通过 name属性 指定别名。

  • Dao

数据访问对象(Data Access Object),开发者可通过使用 Dao,对数据实体以及数据库进行交互,包括常见的 CURD操作。

常见的Dao注解有:@Insert,@Update, @Delete,@Query

  • Database

在使用数据库之前通常需要对数据库实例进行初始化,这一操作最好在 ViewModel 中进行,通过与 ViewModel 进行生命周期绑定,使其能与 整体App 生命周期统一管理。

通过继承 RoomDatabase 并进行单例操作,以创建全局唯一数据库实例。

局限性

由于数据库操作通常是耗时操作,Google Android官方不推荐将 ROOM 直接应用于主线程中,而是以异步的形式在后台进行数据处理,通常配合 AsyncTask 进行异步使用。


Paging

通过与ROOM进行配合,Paging库 能轻松对所接受数据进行分页。


Jetpack Compose

声明式 Ui 组件库。


Flutter

Flutter

优势

  • 编译为本地机器码,支持跨平台,代码简洁。

  • 与使用 JavaScript 的React-Native 对比,性能更高效且支持更多平台。


层级

  • Framework:Dart层,为 Dart 上级Api 提供支持。

  • Engine:引擎层,包含 Skia、Dart 和 Text。

  • Embedder:嵌入层,使 Flutter 能嵌入到任何平台。


为什么使用 Dart

  • DartVM 可编译为 ARM代码,直接运行在硬件上。

  • 使用isolate实现多线程,且不共享内存,可实现无锁快速分配。

  • 线性指针,保证内存线性增长。

  • 底层为 OpenGL绘制库,先调用OpenGL 后调用操作系统API,性能接近原生渲染。


混合开发

将 Flutter 集成到现有应用 • Flutter.cn


第三方库(Android)

Volley

Google官方出品的异步请求框架,2013年发布,适合数据量小,通信量大的发布。

  • 步骤1:创建 RequestQueue请求队列

请求队列通常只需要生成一个,可使用单例模式进行创建,进行高并发请求。

public class VolleyUtils {
    private static RequestQueue requestQueue;

    public static synchronized RequestQueue getInstance(Context context) {
        if (requestQueue == null) {
            requestQueue = Volley.newRequestQueue(context.getApplicationContext());
        }

        return requestQueue;
    }
}

  • 步骤2:创建 StringRequest请求对象
StringRequest request=new StringRequest(requestUrl,response->{
        // success
        },error->{
        // failure
        });

  • 步骤3:向 RequestQueue请求队列 中添加 StringRequest请求对象
VolleyUtils.getInstance(context).add(request);

源码分析

  • CacheDispatcher:内部就是一个线程,开启子线程的缓存请求队列,while时刻运行网络请求。首次操作会先从本地判断是否存在缓存,如果有则直接从缓存中获取,否则直接走NetworkDispatcher

  • NetworkDispatcher:内部也是一个线程,开启网络请求队列,while时刻运行网络请求。首次操作会先从CacheDispatcher中判断是否存在缓存,有的话则走CacheDispatcher,若无缓存或缓存过期则会新建一个请求,并将响应回传主线程。

  • ResponseDelivery:响应分发器,对响应进行分发回传。


okHttp

有关 okHttp 的源码解析,可查看我的另一篇源码解析文章 👉

开源网络请求框架。


Retrofit

有关 Retrofit 的源码解析,可查看我的另一篇源码解析文章 👉

开源网络请求框架 okHttp 简化封装。


Glide

有关 Glide 的源码解析,可查看我的另一篇源码解析文章 👉

图像解析。


LeakCanary

内存泄漏检测。


Image Compressor

Reference

高效的 Android 图片压缩框架扩展库

Reference

Luban.aar • Maven

Luban-turbo.aar • Maven


  • Compressor By zetbaitsu:Github

  • Compressor By Shouheng88:Github


第三方库(Flutter)

dio

网络请求。

Github • Dio


GetX

状态管理、依赖注入。

Github • GetX


参考资料


0%