0%

类的加载过程:

Java 类的加载器

Java 类的加载器是 Java 虚拟机(JVM)的一个重要组成部分,它负责将 Java 类文件加载到内存中,并将其转换为 Java 类对象。Java 类加载器主要有以下三种类型:

Bootstrap ClassLoader(引导类加载器):也称为根加载器,是 Java 虚拟机内置的类加载器,负责加载 Java 运行时环境所需的基础类,如 java.lang 包中的类。

Extension ClassLoader(扩展类加载器):负责加载 Java 运行时环境的扩展类,即位于 jre/lib/ext 目录下的类。

System ClassLoader(系统类加载器):也称为应用程序类加载器,负责加载应用程序类路径(Classpath)下的类。

此外,还有一种特殊的类加载器:用户自定义类加载器。这种类加载器是由开发人员自己实现的,可以用来加载一些特殊的类或者是自定义的类,例如动态加载类、热部署、插件化等。用户自定义类加载器需要继承自 java.lang.ClassLoader 类,并重写父类中的 findClass() 方法来实现类的加载。

Java 类加载器的作用是实现类的动态加载,这使得 Java 应用程序可以在运行时动态地加载一些类或者资源。通过类加载器,Java 应用程序可以实现一些特殊的功能,例如模块化编程、插件化架构、代码热替换等。同时,类加载器还能够保证类的安全性,避免因为恶意代码或者不良代码的加载而导致的安全漏洞。

java虚拟机是如何加载类

假设有一个类 MyClass,该类的源代码位于 MyClass.java 文件中。当 Java 应用程序运行时,它需要将该类加载到内存中,并创建该类的实例。

加载:Java 应用程序首先需要加载 MyClass 的二进制数据。Java 虚拟机根据类的全限定名 com.example.MyClass 来查找并加载该类的二进制数据,例如 com/example/MyClass.class 文件。

验证:Java 虚拟机会对 MyClass 类的字节码进行验证,以确保它符合 Java 虚拟机规范。

准备:在验证通过后,Java 虚拟机为 MyClass 类的静态变量分配内存,并设置默认值,例如将整型静态变量赋值为 0。

解析:Java 虚拟机将 MyClass 类的符号引用(例如类名)转换为直接引用(例如内存地址),以便于执行代码时访问类的方法和属性。

初始化:最后,Java 虚拟机执行 MyClass 类的静态初始化代码,例如赋初始值给静态变量。

在加载 MyClass 类的过程中,Java 类加载器起到了关键的作用。如果 MyClass 类位于应用程序类路径下,则由系统类加载器加载;如果该类位于扩展路径下,则由扩展类加载器加载;如果该类是 Java 运行时环境的基础类,则由引导类加载器加载。

总之,Java 应用程序的类加载过程是一个动态的过程,Java 类加载器负责将类加载到内存中,Java 虚拟机负责验证、准备、解析和初始化类,最终创建该类的实例。

什么是java虚拟机的规范

Java 虚拟机规范(Java Virtual Machine Specification)是一份由 Oracle 公司发布的文档,描述了 Java 虚拟机(JVM)的实现规范和行为规范,包括:

1
2
3
4
5
6
JVM 的内部结构和运行机制。
JVM 支持的数据类型和指令集。
Java 字节码的格式和语义。
类加载器的机制和类加载的过程。
Java 虚拟机的执行引擎和垃圾回收机制。
Java 虚拟机规范的主要作用是为 Java 语言的跨平台特性提供了支持。由于 Java 虚拟机规范定义了 Java 字节码的格式和语义,以及虚拟机的内部结构和运行机制,因此只要编译出的 Java 代码符合 Java 字节码的规范,就可以在任何支持 Java 虚拟机规范的平台上运行。

同时,Java 虚拟机规范还为 JVM 的实现提供了一些指导性的建议,使得不同的 JVM 实现可以保证在一定程度上的互操作性,也便于开发者对 JVM 进行优化和调试。

Java 虚拟机规范是 Java 平台的核心技术之一,它不仅为 Java 语言的跨平台特性提供了支持,也为 Java 生态系统的发展

RIIT

RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现和使用类型信息

Java 是如何在运行时识别对象和类信息的。主要有两种方式:

  1. “传统的” RTTI:假定我们在编译时已经知道了所有的类型;
  2. “反射” 机制:允许我们在运行时发现和使用类的信息

在 Java 中,每个对象都有一个与之关联的类,该类定义了对象的属性和方法。RTTI 允许程序在运行时确定对象的实际类型,而不是在编译时确定对象的静态类型。这意味着,即使对象的静态类型是某个父类或接口,程序仍然可以根据对象的实际类型来访问它的方法和属性。

在 Java 中,可以使用 instanceof 运算符来检查对象的类型,并根据对象的类型来执行相应的操作。例如:

code
1
2
3
4
5

if (obj instanceof MyClass) {
MyClass myObj = (MyClass) obj;
myObj.myMethod();
}

上面的代码会检查 obj 对象是否是 MyClass 类型的对象,如果是,就将 obj 强制转换为 MyClass 类型,并调用它的 myMethod 方法。

需要注意的是,如果对象的类型与 instanceof 运算符中指定的类型不兼容,会抛出 ClassCastException 异常。因此,在使用 RTTI 时,应该始终使用强制类型转换之前进行类型检查。

为什么需要 RTTI 机制

1
Class<? extends Object> objClass = obj.getClass();

这里涉及到另外一个根本的问题,为什么我们需要在运行时识别对象和类信息。
我们看一个demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Animal {
public void makeSound() {
System.out.println("The animal makes a sound");
}
}

class Dog extends Animal {
public void makeSound() {
System.out.println("The dog barks");
}
}

class Cat extends Animal {
public void makeSound() {
System.out.println("The cat meows");
}
}

public class Main {
public static void main(String[] args) {
Animal animal1 = new Animal();
Animal animal2 = new Dog();
Animal animal3 = new Cat();

animal1.makeSound();
animal2.makeSound();
animal3.makeSound();
}
}

当使用多态性时,对象的实际类型和调用方法的类型可能不同,需要在运行时识别对象和类信息,在以上的信息中

在上面的示例中,有三个 Animal 对象:animal1、animal2 和 animal3。

animal1 的实际类型是 Animal,animal2 的实际类型是 Dog,animal3 的实际类型是 Cat。
调用 makeSound() 方法时,animal1 会调用 Animal 类的 makeSound() 方法,输出 “The animal makes a sound”;animal2 会调用 Dog 类的 makeSound() 方法,输出 “The dog barks”;animal3 会调用 Cat 类的 makeSound() 方法,输出 “The cat meows”。

因为在编译时,编译器只能确定调用 makeSound() 方法的对象是 Animal 类型的,无法确定其实际类型是什么,因此需要在运行时识别对象和类信息,才能确定调用哪个方法。

通过这个例子,可以看到,运行时识别对象和类信息是多态性的关键,也是 Java 中许多其他特性的基础。

Class 对象

引用

要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。
这项工作是由称为 Class 对象的特殊对象完成的,它包含了与类有关的信息。
实际上,Class 对象就是用来创建该类所有 “常规” 对象的。
Java 使用 Class 对象来实现RTTI,即便是类型转换这样的操作都是用 Class 对象实现的。

类是程序的一部分,每个类都有一个 Class 对象。换言之,每当我们编写并且编译
了一个新类,就会产生一个 Class 对象(更恰当的说,是被保存在一个同名的 .class
文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用 “类加载器” 子系统把
这个类加载到内存中。

类加载器首先会检查这个类的 Class 对象是否已经加载,如果尚未加载,默认的类
加载器就会根据类名查找 .class 文件(如果有附加的类加载器,这时候可能就会在数
据库中或者通过其它方式获得字节码)。这个类的字节码被加载后,JVM 会对其进行验
证,确保它没有损坏,并且不包含不良的 Java 代码 (这是 Java 安全防范的一种措施)。

一旦某个类的 Class 对象被载入内存,它就可以用来创建这个类的所有对象。下
面的示范程序可以证明这点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// typeinfo/SweetShop.java
// 检查类加载器工作方式
class Cookie {
static { System.out.println("Loading Cookie"); }
}
class Gum {
static { System.out.println("Loading Gum"); }
}
class Candy {
static { System.out.println("Loading Candy"); }
}
public class SweetShop {
public static void main(String[] args) {
System.out.println("inside main");
new Candy();
System.out.println("After creating Candy");
try {
Class.forName("Gum");
} catch(ClassNotFoundException e) {
System.out.println("Couldn't find Gum");
}
System.out.println("After Class.forName(\"Gum\")");
new Cookie();
System.out.println("After creating Cookie");
}
}
输出结果:
inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie

Class 对象仅在需要的时候才会被加载,static 初始化是在类加载时进行的。
重点的是注意这行代码

1
Class.forName("Gum");

forName() 是 Class 类的一个静态方法,我
们可以使用 forName() 根据目标类的类名(String)得到该类的 Class 对象。上面的
代码忽略了 forName() 的返回值,因为那个调用是为了得到它产生的 “副作用”。
从结果可以看出,forName() 执行的副作用是如果 Gum 类没有被加载就加载它,而在加载的过程中,Gum 的 static 初始化块被执行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
interface HasBatteries {}
interface Waterproof {}
interface Shoots {}
class Toy {
// 注释下面的无参数构造器会引起 NoSuchMethodError 错误
Toy() {}
Toy(int i) {}
}
class FancyToy extends Toy
implements HasBatteries, Waterproof, Shoots {
FancyToy() { super(1); }
}
public class ToyTest {
static void printInfo(Class cc) {
System.out.println("Class name: " + cc.getName() +
" is interface? [" + cc.isInterface() + "]");
System.out.println(
"Simple name: " + cc.getSimpleName());
System.out.println(
"Canonical name : " + cc.getCanonicalName());
}
public static void main(String[] args) {
Class c = null;
try {
c = Class.forName("typeinfo.toys.FancyToy");
} catch(ClassNotFoundException e) {
System.out.println("Can't find FancyToy");
System.exit(1);
}
printInfo(c);
for(Class face : c.getInterfaces())
printInfo(face);
Class up = c.getSuperclass();
Object obj = null;
try {
// Requires no-arg constructor:
obj = up.newInstance();
} catch(InstantiationException e) {
System.out.println("Cannot instantiate");
System.exit(1);
} catch(IllegalAccessException e) {
System.out.println("Cannot access");
System.exit(1);
}
printInfo(obj.getClass());
}
}
输出结果:
Class name: typeinfo.toys.FancyToy is interface?
[false]
Simple name: FancyToy
Canonical name : typeinfo.toys.FancyToy
Class name: typeinfo.toys.HasBatteries is interface?
[true]
Simple name: HasBatteries
Canonical name : typeinfo.toys.HasBatteries
Class name: typeinfo.toys.Waterproof is interface?
[true]
Simple name: Waterproof
Canonical name : typeinfo.toys.Waterproof
Class name: typeinfo.toys.Shoots is interface? [true]
Simple name: Shoots
Canonical name : typeinfo.toys.Shoots
Class name: typeinfo.toys.Toy is interface? [false]
Simple name: Toy
Canonical name : typeinfo.toys.Toy

你还可以调用 getSuperclass() 方法来得到父类的 Class 对象,再用父类的 Class 对象调用该方法,重复多次,你就可以得到一个对象完整的类继承结构

类字面常量

当使用 .class 来创建对 Class 对象的引用时,不会自动地
初始化该 Class 对象。为了使用类而做的准备工作实际包含三个步骤:

  1. 加载,这是由类加载器执行的。该步骤将查找字节码(通常在 classpath 所指定的
    路径中查找,但这并非是必须的),并从这些字节码中创建一个 Class 对象。
  2. 链接。在链接阶段将验证类中的字节码,为 static 字段分配存储空间,并且如果需要的话,将解析这个类创建的对其他类的所有引用。
  3. 初始化。如果该类具有超类,则先初始化超类,执行 static 初始化器和 static 初始化块。

直到第一次引用一个 static 方法(构造器隐式地是 static)或者非常量的 static
字段,才会进行类初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// typeinfo/ClassInitialization.java
import java.util.*;
第十九章类型信息 11
class Initable {
static final int STATIC_FINAL = 47;
static final int STATIC_FINAL2 =
ClassInitialization.rand.nextInt(1000);
static {
System.out.println("Initializing Initable");
}
}
class Initable2 {
static int staticNonFinal = 147;
static {
System.out.println("Initializing Initable2");
}
}
class Initable3 {
static int staticNonFinal = 74;
static {
System.out.println("Initializing Initable3");
}
}
public class ClassInitialization {
public static Random rand = new Random(47);
public static void
main(String[] args) throws Exception {
Class initable = Initable.class;
System.out.println("After creating Initable ref");
// Does not trigger initialization:
System.out.println(Initable.STATIC_FINAL);
// Does trigger initialization:
System.out.println(Initable.STATIC_FINAL2);
// Does trigger initialization:
System.out.println(Initable2.staticNonFinal);
Class initable3 = Class.forName("Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}
输出结果:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74

初始化有效地实现了尽可能的 “惰性”,从对 initable 引用的创建中可以看
到,仅使用 .class 语法来获得对类对象的引用不会引发初始化。

但与此相反,使用 Class.forName() 来产生 Class 引用会立即就进行初始化,如 initable3。
如果一个 static final 值是 “编译期常量”(如 Initable.staticFinal),那么这
个值不需要对 Initable 类进行初始化就可以被读取。但是,如果只是将一个字段设置
成为 static 和 final,还不足以确保这种行为。
例如,对 Initable.staticFinal2 的访问将强制进行类的初始化,因为它不是一个编译期常量。
如果一个 static 字段不是 final 的,那么在对它访问时,总是要求在它被读取之
前,要先进行链接(为这个字段分配存储空间)和初始化(初始化该存储空间),就像
在对 Initable2.staticNonFinal 的访问中所看到的那样

Home键退出和返回键退出的区别

Home键退出,程序保留状态为后台进程;
而返回键退出,程序保留状态为空进程,空进程更容易被系统回收。Home键其实主要用于进程间切换,返回键则是真正的退出程序。

从理论上来讲,无论是哪种情况,在没有任何后台工作线程(即便应用处于后台,工作线程仍然可以执行)的前提下,被置于后台的进程都只是保留他们的运行状态,并不会占用CPU资源,所以也不耗电。只有音乐播放软件之类的应用需要在后台运行Service,而Service是需要占用CPU时间的,此时才会耗电。所以说没有带后台服务的应用是不耗电也不占用CPU时间的,没必要关闭,这种设计本身就是Android的优势之一,可以让应用下次启动时更快。然而现实是,很多应用多多少少都会有一些后台工作线程,这可能是开发人员经验不足导致(比如线程未关闭或者循环发送的Handler消息未停止),也可能是为了需求而有意为之,导致整个Android应用的生态环境并不是一片干净。

请问“强制进行GPU渲染”和“停用HW叠加层”有什么区别?

这两个还是有区别的,GPU渲染就是hwa(hard ware acceleration硬件加速)的一种,其存在的意义就是为了分担CPU的负担,其原理是通过GPU对软件图形图像的处理来减轻CPU的负担,从而使应用软件能够以更快的速度被处理,以达到提速的目的,简单来说就是一般情况下软件的2D图形图像处理是交给CPU的,3D图形图像处理才是交给GPU的,而开启这个则是强制用GPU进行2D图形图像处理从而降低CPU处理器的负担,这个主要是2D界面的图形也交给GPU处理从而更流畅点。

而HW叠加层,HW和叠加层应该分开理解,HW指的是硬件加速,而这个硬件指的是GPU处理器,而不是其它硬件或处理器硬件,至于为什么可以去参考一下硬解的相关资料,然后叠加层指的是使用CPU进行辅助运算,而不只是让GPU来进行全部的渲染工作,这个也是停用HW叠加层默认关闭的原因,汇总起来就是停用GPU上的CPU辅助计算,停用HW叠加层默认关闭就是说明HW叠加层默认是开启的,为什么会默认开启?这个就是因为就算开启了强制进行GPU渲染但并不是所有的图形处理交给了GPU,像游戏里面的一些画面、视频中的硬解(手机上不开启的话硬解是靠SQV的)就不是全部交给GPU的,而开启停用HW叠加层就是关闭HW叠加层,关闭HW叠加层就意味着图形处理任务就全部交给了GPU,这个主要是可以让游戏、视频之类一些原本不是GPU渲染的画面也强制使用GPU进行处理(像一些配置的视频显示不支持硬解,然后开启强制进行GPU渲染后就可以硬解了,因为强制使用GPU而不是手机上的另一个SQV硬解了,但因视频配置、屏幕刷新率之类的还是会有卡顿的情况,开启HW叠加层可以让部分高帧率的视频达到流畅的效果,但也受限于视频配置、屏幕刷新率等其它因素)。强制进行GPU渲染和停用HW叠加层不建议长时间开启,虽然开启这些可以让性能更好,但是也会损耗不少东西。

Android的同步屏障消息是指什么

Android 中的 Handler 用于在不同的线程之间传递消息和任务。一个常见的用例是在后台线程中执行耗时操作,然后将结果传递给主线程以更新用户界面。同步屏障消息是一种特殊的消息类型,可以用于控制消息处理的顺序。

原理

当你向 Handler 发送同步屏障消息时,它会将该消息插入到消息队列中,并等待消息队列中的所有先前的消息都被处理完毕,然后再处理同步屏障消息。这可以用于确保某些操作在其他消息处理之前执行,从而实现同步。

示例

可以在主线程上使用同步屏障消息来确保在更新用户界面之前执行某些初始化操作。这样,你可以防止在初始化操作尚未完成之前更新界面,从而提高了代码的可靠性和一致性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Handler handler = new Handler();

// 发送一个普通消息
handler.post(new Runnable() {
@Override
public void run() {
// 这是一个普通消息
// 可能会执行在其他消息之前
}
});

// 发送一个同步屏障消息
handler.postAtFrontOfQueue(new Runnable() {
@Override
public void run() {
// 这是一个同步屏障消息
// 会等待前面的消息都处理完才执行
}
});

同步屏障消息

系统一些高优先级的操作会使用到同步屏障消息

Android 是禁止App往MessageQueue插入同步屏障消息的,代码会报错

Looper.getMainLooper.postSyncBarrier

demo

例如View在绘制的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
->ViewRootImpl

@UnsupportedAppUsage
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//插入同步屏障消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障消息
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}

为了保证View的绘制过程不被主线程其它任务影响,View在绘制之前会先往MessageQueue插入同步屏障消息,然后再注册Vsync信号监听,Choreographer$FrameDisplayEventReceiver就是用来接收vsync信号回调的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
...
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
...
//
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
//1、发送异步消息
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
// 2、doFrame优先执行
doFrame(mTimestampNanos, mFrame);
}
}

收到Vsync信号回调,注释1会往主线程MessageQueue post一个异步消息,保证注释2的doFrame优先执行。
doFrame才是View真正开始绘制的地方,会调用ViewRootImpl的doTraversal、performTraversals,
而performTraversals里面会调用我们熟悉的View的onMeasure、onLayout、onDraw。
这里还可以延伸到vsync信号原理,以及为什么要等vsync信号回调才开始View的绘制流程、掉帧的原理、屏幕的双缓冲、三缓冲,由于文章篇幅关系,不是本文的重点,就不一一分析了~

虽然app无法发送同步屏障消息,但是使用异步消息是允许的

异步消息

首先,SDK中限制了App不能post异步消息到MessageQueue里去的,相关字段被加了UnsupportedAppUsage注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-> Message

@UnsupportedAppUsage
/*package*/ int flags;

/**
* Returns true if the message is asynchronous, meaning that it is not
* subject to {@link Looper} synchronization barriers.
*
* @return True if the message is asynchronous.
*
* @see #setAsynchronous(boolean)
*/
public boolean isAsynchronous() {
return (flags & FLAG_ASYNCHRONOUS) != 0;
}

不过呢,高版本的Handler的构造方法可以通过传async=true,来使用异步消息

1
2
public Handler(@Nullable Callback callback, boolean async) {}

然后在Handler发送消息的时候,都会走到 enqueueMessage 方法,如下代码块所示,每个消息都带了异步属性,有优先处理权

1
2
3
4
5
6
7
8
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) {
...
//如果mAsynchronous为true,就都设置为异步消息
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

对于低版本SDK,想要使用异步消息,可以通过反射调用Handler(@Nullable Callback callback, boolean async),参考androidx内部的一段代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
->androidx.arch.core.executor.DefaultTaskExecutor

private static Handler createAsync(@NonNull Looper looper) {
if (Build.VERSION.SDK_INT >= 28) {
return Handler.createAsync(looper);
}
if (Build.VERSION.SDK_INT >= 16) {
try {
return Handler.class.getDeclaredConstructor(Looper.class, Handler.Callback.class,
boolean.class)
.newInstance(looper, null, true);
} catch (IllegalAccessException ignored) {
} catch (InstantiationException ignored) {
} catch (NoSuchMethodException ignored) {
} catch (InvocationTargetException e) {
return new Handler(looper);
}
}
return new Handler(looper);
}


针对 Andorid 内核的管理, Google 定义了一个所谓 “AOSP 通用内核 ( AOSP common kernels,简称 ACKs)” 的概念。 ACKs 版本基于 Linux 内核 开发(术语上称之为 downstream,形象地说可以认为 Linux 内核 的发布处于上游,而 ACKs 随着 Linux 内核版本的升级而升级,就好像处在流水线的下游),ACKs 在 Linux 内核 的版本基础上包含了许多与 Android 社区相关但尚未合并到 LTS 的补丁程序,可以简单地分成以下几大类。