0%

关于编译时注解(APT)由浅入深有三部分,分别是:

1. 自定义注解处理器
例如 ButterKnife、Room 根据注解生成新的类。

2. 利用JcTree在编译时修改代码
像 Lombok 自动往类中新增 getter/setter 方法、往方法中插入代码行等。这种方式不推荐使用,因为只对 Java 代码有效,对 Kotlin 代码无效。

3. 自定义 Gradle 插件在编译时修改代码 (本文)例如一些代码插桩框架、日志框架、方法耗时统计框架等。


1. 环境搭建及Gradle配置

1.1 配置 Project 级的 build.gradle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
buildscript {
// 省略...

dependencies {
// Gradle相关api
classpath "com.android.tools.build:gradle:3.3.2"

// 注解处理器 相关
classpath "com.neenbedankt.gradle.plugins:android-apt:1.8"

// 支持 Kotlin
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.31"

// javassist
classpath "org.javassist:javassist:3.21.0-GA"
}
}

1.2 配置实现插件的 Module

创建一个 Java Library 或者 Android Library。

1.2.1 修改 module 的 build.gradle

修改 build.gradle 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apply plugin: 'java'
apply plugin: 'kotlin'

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])

// 引入 Gradle 的 SDK
implementation gradleApi()

// 引入 Transform 相关API
implementation "com.android.tools.build:gradle:" + Gradle_Version

// Javassist
implementation 'org.javassist:javassist:3.21.0-GA'
}

repositories {
google()
jcenter()
mavenLocal()
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

1.2.2 创建 META-INF

创建 resources 文件夹,整体结构如下:
文件结构

  1. 其中文件 your.gradle.plugin.name.properties 表明当前 module 下有一个gradle插件,插件的名称是 your.gradle.plugin.name

  2. 一个 module 可以定义多个插件,每一个插件都需要在 gradle-plugins 文件夹下注册一个 xxx.properties 文件。

1.2.3 编辑 .properties 文件

注册文件的内容只有一行,用于关联该插件的具体实现类:

1
implementation-class=your.plugin.implement.Class

你需要把 your.plugin.implement.Class 替换为你自己的实现类全类名。


2. 实现插件

下文中的 Plugin 类指的是 org.gradle.api.Plugin 类,
Transform 类指的是 com.android.build.api.transform.Transform 类。

大致步骤:

  • 实现一个 Plugin 的子类 和 一个 Transform 的子类。

  • 在子类的 apply 方法中,注册一个 Transform 实例。

我们在用到这个插件的 Module 的 build.gradle 中,添加 apply plugin: '插件名称' 实际上就是在调用 Plugin 实例的 apply 方法。

Transform 是什么?
Tansform是一个抽象类。每个 Transform 对象都是在打包过程中,从 .class 文件 生成 .dex 文件 期间,要执行的操作。我们可以用 Transform 处理注解信息,修改已存在的类和方法等。

2.1 实现 Plugin 子类

这一步简单,继承 Plugin 实现一个自定义的插件类,然后在 apply 方法中注册一个 Transformer 即可。
注意:该类的全类名需要和在xxx.properties文件中注册的全类名一样。

1
2
3
4
5
6
7
8
9
10
11
12
class CustomPlugin : Plugin<Project> {

override fun apply(project: Project) {
val hasAppPlugin = project.plugins.hasPlugin(AppPlugin::class.java)
if (!hasAppPlugin) {
return
}
val appExtension = project.extensions.findByType(AppExtension::class.java) ?: return
appExtension.registerTransform(CustomTransform(project, appExtension))
}

}

CustomPlugin 可以替换为其他名称,只要和xxx.properties中注册的一致就行。
CustomTransform 也可以替换为你自己需要的名称。

2.2 实现 Transform 子类

2.2.1 Tansform 的重要方法

transform:

1
2
3
4
5
/**
* Tansform 的实现类最重要的方法,用于做具体的数据转换。
* 可以通过参数 transformInvocation 得到所有的 .class 等输入。
*/
fun transform(transformInvocation: TransformInvocation?)

getInputTypes:

1
2
3
4
5
6
7
8
/**
* 返回当前 Transform 需要的输入的类型。
* ContentType 常用的类型有:
* CLASSES: 编译好的.class文件
* RESOURCES: 原始的Java文件
* NATIVE_LIBS: C/C++库
*/
fun getInputTypes(): MutableSet\<ContentType\>

getScopes:

1
2
3
4
5
6
/**
* 返回当前 Transform 应用的范围。
* Scope 常用的类型 PROJECT、SUB_PROJECT、EXTERNAL_LIBRARIES 等。
* 通常返回常量集合 SCOPE_FULL_PROJECT 即可。
*/
fun getScopes(): MutableSet\<Scope\>

getName:

1
2
3
4
/**
* 返回当前 Transform 唯一的名称。
*/
fun getName(): String**

isIncremental

1
2
3
4
/**
* 当前 Transform 是否支持增量编译。
*/
fun isIncremental(): Boolean

2.2.2 实现 transform 方法

假设我们有这么一个需求:修改添加了 @DemoAnnotation 注解的方法,使得该方法在执行原始代码块之前和之后都打印一句话。例如将方法:

1
2
3
void function() {
System.out.println("这是方法的原始内容")
}

修改为:

1
2
3
4
5
void function() {
{ Log.i("自定义插件", "function方法开始执行了!") }
System.out.println("这是方法的原始内容")
{ Log.i("自定义插件", "function方法执行完毕了!") }
}

transform 方法要做的步骤:

  • 获取所有的输入 => inputs;

  • 创建 ClassPool 对象 => classPool;

  • 把系统类路径、inputs 包含的路径加入到 classPool 备用;

  • 遍历 inputs:

    • 获取每一个 input 的文件夹:
      • 递归遍历文件夹,处理每一个类
      • 获取当前文件夹的输出路径
      • 将 input 文件夹复制到 输出文件夹
    • 获取每一个 input 的 Jar:
      • 同文件夹的处理方式

具体实现:

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
67
68
private var mProject: Project? = null
private var mAppExtension: AppExtension? = null
private val mClassPool = ClassPool()

constructor(project: Project, appExtension: AppExtension) {
mProject = project
mAppExtension = appExtension
}

override fun transform(transformInvocation: TransformInvocation?) {
super.transform(transformInvocation)

// 获取所有输入
val inputs : Collection<TransformInput> = transformInvocation?.inputs ?: return

// 获取 OutputProvider
val outputProvider = transformInvocation.outputProvider

// 将系统的类加入到搜索路径中
mClassPool.appendSystemPath()
val bootClasses = mAppExtension?.bootClasspath
bootClasses?.forEach { file ->
mClassPool.appendClassPath(file.absolutePath)
}

// 把所有需要打包到 apk 中的类都加入到搜索路径中
inputs.forEach { input ->
input.jarInputs.forEach { mClassPool.appendClassPath(it.file.absolutePath) }
input.directoryInputs.forEach { mClassPool.appendClassPath(it.file.absolutePath) }
}

// 遍历每一个输入
inputs.forEach { input ->

// 遍历每一个文件夹
input.directoryInputs.forEach { directory ->

// 遍历当前文件夹下的所有类,并逐一处理
FileScanner.scan(directory.file) { file -> handleClass(directory.file, file) }

// 获取输出路径
val output = outputProvider.getContentLocation(
directory.name,
directory.contentTypes,
directory.scopes,
Format.DIRECTORY
)

// 将修改后的文件夹复制到输出路径
FileUtils.copyDirectory(directory.file, output)
}

// 遍历每一个 Jar
input.jarInputs.forEach { jar ->

// 获取输出路径
val output = outputProvider.getContentLocation(
jar.file.absolutePath,
jar.contentTypes,
jar.scopes,
Format.JAR
)

// 虽然不处理 Jar 中的类,但也需要复制到输出目录
FileUtils.copyFile(jar.file, output)
}
}
}

2.2.3 实现 handleClass 方法

上面的 transform 方法是通用的流程,我们再来看怎么具体处理每一个文件。因为输入的都是 .class 文件,所以每个文件就有且只有一个类。

主要步骤:

  • 根据文件读取到 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
private fun handleClass(directory: File, file: File) {

if (!file.path.endsWith(".class")) {
return
}

if (file.path.endsWith("/R.class")) {
return
}

// 获取文件流
val inputStream = file.inputStream()

// 读取类信息
val reader = ClassReader(inputStream)
val className = reader.className.replace('/', '.')
val tempCls: CtClass = mClassPool.get(className)

// 可以通过下面这些方法获取这个类的更多信息:
// val annotations = tempCls.annotations
// val methods = tempCls.methods;
// val nestedClasses = tempCls.nestedClasses
// val constructors = tempCls.constructors
// val declaredClasses = tempCls.declaredClasses
// val declaringClass = tempCls.declaringClass

var hasModified = false

// 遍历这个类定义的所有方法
tempCls.declaredMethods.forEach { method ->

// 根据自己的需求,判断是否需要修改这个方法
val needModify = tempCls.hasAnnotation("your.annotation.ClassName")
if (needModify) {
// 修改这个方法
modifyMethod(method, tempCls)
hasModified = true
}
}

inputStream.close()

// 如果这个类有修改,把新的类写入文件
if (hasModified) {
tempCls.writeFile(directory.absolutePath)
}
return false
}

2.2.4 实现 modifyMethod 方法

到这一步,我们可以根据自己的需求,对这个方法进行修改了。作为示例,我们为这个方法的执行前后都加上一句日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private fun modifyMethod(method: CtMethod, clazz: CtClass) {
try {
if (clazz.isFrozen) {
clazz.defrost()
}

// 拼接代码
method.insertBefore("android.util.Log.i(\"TAG\", \"${clazz.name} 类的 ${method.name} 方法开始执行\");")
method.insertAfter ("android.util.Log.i(\"TAG\", \"${clazz.name} 类的 ${method.name} 方法执行结束\");")

} catch (e: Exception) {
e.printStackTrace()
}
}

insertAfter 方法会自动在所有 return 的地方都加上代码,开发者不用考虑提前return的问题。

结合上一篇 使用 APT 生成代码 的方法,可以在 modifyMethod 中插入自动生成的代码实现更强的功能。

关于编译时注解(APT)由浅入深有三部分,分别是:

1. 自定义注解处理器
例如 ButterKnife、Room 根据注解生成新的类。

2. 利用JcTree在编译时修改代码
像 Lombok 自动往类中新增 getter/setter 方法、往方法中插入代码行等。这种方式不推荐使用,因为只对 Java 代码有效,对 Kotlin 代码无效。

3. 自定义 Gradle 插件在编译时修改代码
例如一些代码插桩框架、日志框架、方法耗时统计框架等。

这篇文章以 demo 的形式,介绍如何从零开始创建一个自定义的注解处理器,并生成一个新的类。这个类中有一个静态方法,方法返回添加了自定义注解的所有类。 看懂这篇文章,你就能写出自己的 ButterKnife 啦~

本文中的源代码可以在这里查看: https://github.com/hipoom/APT-Source-Code


1. 环境搭建和 Gradle 配置

1.1 创建注解 module
我们在工程中新建一个 Java Library,module 名称定义为 annotation。 再定义一个自定义的注解类:

1
2
3
@Target(ElementType.TYPE)
public @interface DemoAnnotation {
}

第一步就完啦~ (如果不清楚元注解的使用,可以搜索其它文章了解)

1.2 创建注解处理器 Module
在工程中再创建一个 Java Library,名称定义为 annotation-processor,并在 build.gradle 中加入如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.gradle.internal.jvm.Jvm

apply plugin: 'java-library'

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])

// 刚才定义的 Annotation 模块
implementation project(":annotation")

// 谷歌的 AutoService 可以让我们的注解处理器自动注册上
implementation 'com.google.auto.service:auto-service:1.0-rc4'

// 用于生成新的类、函数
implementation "com.squareup:javapoet:1.9.0"

// 谷歌的一个工具类库
implementation "com.google.guava:guava:24.1-jre"

implementation files(Jvm.current().toolsJar)
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

1.3 配置项目级的 build.gradle
再在项目级的 build.gradle 中增加 android-apt 的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
buildscript {

repositories { ... }

dependencies {
...
classpath "com.neenbedankt.gradle.plugins:android-apt:1.8"
}

...
}


2. 实现自定义注解处理器

所有的自定义注解处理器都应该继承自 AbstractProcessor 类。
我们也定义一个处理器,并实现几个模板方法:

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
@AutoService(Processor.class)
public class DemoProcessor extends AbstractProcessor {

/* ======================================================= */
/* Fields */
/* ======================================================= */

/**
* 用于将创建的类写入到文件
*/
private Filer mFiler;


/* ======================================================= */
/* Override/Implements Methods */
/* ======================================================= */

@Override
public synchronized void init(ProcessingEnvironment environment) {
super.init(environment);
mFiler = environment.getFiler();
}

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 这个方法是注解处理器的核心,稍后单独分析这个方法如何实现
return false;
}

@Override
public Set<String> getSupportedAnnotationTypes() {
// 这个方法返回当前处理器 能处理哪些注解,这里我们只返回 DemoAnnotation
return Collections.singleton(DemoAnnotation.class.getCanonicalName());
}

@Override
public SourceVersion getSupportedSourceVersion() {
// 这个方法返回当前处理器 支持的代码版本
return SourceVersion.latestSupported();
}
}

2.1 process() 方法详解
我们的需求是生成一个新的类,类中有一个静态方法,方法返回添加了 @Annotation 注解的所有类。
这些操作都需要我们在 process() 方法中去实现。步骤:
(1) 获取所有添加了注解的元素;
(2) 生成一个方法,方法的代码块是返回(1)中获取到的列表。
(3) 生成一个类,类中加入(2)中生成的方法;
(4) 将(3)中生成的类写入文件。

所以我们得到这个方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment environment) {

// 获取所有被 @DemoAnnotation 注解的类
Set<? extends Element> elements = environment.getElementsAnnotatedWith(DemoAnnotation.class);

// 创建一个方法,返回 Set<Class>
MethodSpec method = createMethodWithElements(elements);

// 创建一个类
TypeSpec clazz = createClassWithMethod(method);

// 将这个类写入文件
writeClassToFile(clazz);

return false;
}

接下来就让我们看看这三个关键的方法分别是怎么实现的:

2.2 如何创建新的方法

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
/**
* 创建一个方法,这个方法返回 elements 中的所有类信息。
*/
private MethodSpec createMethodWithElements(Set<? extends Element> elements) {

// "getAllClasses" 是生成的方法的名称
MethodSpec.Builder builder = MethodSpec.methodBuilder("getAllClasses");

// 为这个方法加上 "public static" 的修饰符
builder.addModifiers(Modifier.PUBLIC, Modifier.STATIC);

// 定义返回值类型为 Set<Class>
ParameterizedTypeName returnType = ParameterizedTypeName.get(
ClassName.get(Set.class),
ClassName.get(Class.class)
);
builder.returns(returnType);

// 经过上面的步骤,
// 我们得到了 public static Set<Class> getAllClasses() {} 这个方法,
// 接下来我们实现它的方法体:

// 方法中的第一行: Set<Class> set = new HashSet<>();
builder.addStatement("$T<$T> set = new $T<>();", Set.class, Class.class, HashSet.class);

// 上面的 "$T" 是占位符,代表一个类型,可以自动 import 包。其它占位符:
// $L: 字符(Literals)、 $S: 字符串(String)、 $N: 命名(Names)

// 遍历 elements, 添加代码行
for (Element element : elements) {

// 因为 @Annotation 只能添加在类上,所以这里直接强转为 ClassType
ClassType type = (ClassType) element.asType();

// 在我们创建的方法中,新增一行代码: set.add(XXX.class);
builder.addStatement("set.add($T.class)", type);
}

// 经过上面的 for 循环,我们就把所有添加了注解的类加入到 set 变量中了,
// 最后,只需要把这个 set 作为返回值 return 就好了:
builder.addStatement("return set");

return builder.build();
}

2.3 如何创建新的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 创建一个类,并把参数中的方法加入到这个类中
*/
private TypeSpec createClassWithMethod(MethodSpec method) {
// 定义一个名字叫 OurClass 的类
TypeSpec.Builder ourClass = TypeSpec.classBuilder("OurClass");

// 声明为 public
ourClass.addModifiers(Modifier.PUBLIC);

// 为这个类加入一段注释
ourClass.addJavadoc("这个类是自动创建的哦~\n\n @author ZhengHaiPeng");

// 为这个类新增一个方法
ourClass.addMethod(method);

return ourClass.build();
}

2.4 如何将创建的类写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 将一个创建好的类写入到文件中参与编译
*/
private void writeClassToFile(TypeSpec clazz) {
// 声明一个文件在 "me.moolv.apt" 下
JavaFile file = JavaFile.builder("me.moolv.apt", clazz).build();

// 写入文件
try {
file.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}

3. 使用自定义注解处理器

在要使用的 module 中,例如 app,的 build.gradle 中加入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
apply plugin: 'com.android.application'

android {
...
}

dependencies {
...
annotationProcessor project(":annotation-processor")
implementation project(path: ':annotation')
}

执行 Android Studio 的 Build > Make Project, 就能在 app module 的 build/source/apt 路径下找到生成的类文件了:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 这个类是自动创建的哦~
*
* @author ZhengHaiPeng
*/
public class OurClass {
public static Set<Class> getAllClasses() {
Set<Class> set = new HashSet<>();
set.add(MainActivity.class);
return set;
}
}

这样我们就实现了 自定义注解处理器,并生成代码啦,有疑问留言就好~


4. 如何为注解处理器传递参数?

APT 中的 processor 可能会用到一些参数,这些参数可以在 gradle 中配置。

设置参数

1
2
3
4
5
6
7
8
9
10
11
12
13
android {
...
defaultConfig {
...
javaCompileOptions {

annotationProcessorOptions {
// 下面定义要传递的参数
argument "key1", "value1"
argument "key2", "value2"
}
}
}

获取参数
在 processor 的 init 方法中可以获取参数:

1
2
3
4
5
6
7
8
9
10
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);

...

String value1 = env.getOptions().get("key1");

...
}

源码:https://github.com/hipoom/APT-Source-Code

Android 的安装包签名方案到目前有3个版本,分别是:

  • 最初签名方案V1;
  • 为了提高验证速度和覆盖度在 7.0 引入的 V2;
  • 以及为了实现密钥轮转在 9.0 引入的 V3。

让我们分别了解一下这些签名的原理:


一、 V1 签名方案

1. 签名相关的文件

apk 本质是个 zip 文件,解压缩后,在 META-INFO 文件夹中可以看到有 MANIFEST.MFCERT.SFCERT.RSA 三个文件。这三个文件在签名时创建,在安装时用于验证签名。下面让我们看一下这三个文件各自的作用:

1.1 MANIFEST.MF文件

文件的作用:
记录 apk 中每一个文件对应的摘要信息,防止某个文件被篡改。

文件的内容:
打开 MANIFEST.MF 文件可以看到文件内容是这种格式:

1
2
3
4
5
6
7
8
9
10
11
Manifest-Version: 1.0
Built-By: Generated-by-ADT
Created-By: Android Gradle 2.3.1

Name: res/drawable-hdpi-v4/tracepoint_tip.png
SHA1-Digest: UqNwQcd9oLGpVfILjkVOtNQmySA=

Name: res/layout/activity_new_base_layout.xml
SHA1-Digest: Uw3jXiCR9Msf9C6P0Mjcmh2/A/E=

...

前三行记录了基础信息,后面每一块都对应了 apk 中一个原始文件的数据摘要,摘要算法是 SHA-1。在 MANIFEST.MF 文件没被篡改的情况下,可以用于保证 apk 中的其他文件不被篡改。那怎么保证 MANIFEST.MF 文件本身不被篡改呢? 就是靠下面的 CERT.SF 文件了:


1.2 CERT.SF文件

文件的作用:
记录 MANIFEST.MF 文件的摘要,以及 MANIFEST.MF 中,每个数据块的摘要。防止 MANIFEST.MF 被篡改。

文件的内容:
CERT.SF 的文件内容如下:

1
2
3
4
5
6
7
8
9
10
Signature-Version: 1.0
SHA1-Digest-Manifest: m4hofJv2im9b2HQo/h6VPKRnzqE=
Created-By: 1.0 (Android)

Name: res/drawable-hdpi-v4/tracepoint_tip.png
SHA1-Digest: UqNwQcd9oLGpVfILjkVOtNQmySA=

Name: res/layout/activity_new_base_layout.xml
SHA1-Digest: Uw3jXiCR9Msf9C6P0Mjcmh2/A/E=
...

第一行 Signature-Version 记录了签名版本;第二行 SHA1-Digest-Manifest 记录了整个 MANIFEST.MF 文件的摘要;

后面每一块都是 MANIFEST.MF 中对应数据块的摘要;例如,CERT.SF 中对应的这一段:

1
2
Name: res/drawable-hdpi-v4/tracepoint_tip.png
SHA1-Digest: UqNwQcd9oLGpVfILjkVOtNQmySA=

其中「UqNwQcd9oLGpVfILjkVOtNQmySA=」就是 MANIFEST.MF 中这一段的摘要(包含换行符):

1
2
3
Name: res/drawable-hdpi-v4/tracepoint_tip.png
SHA1-Digest: UqNwQcd9oLGpVfILjkVOtNQmySA=
\r\n

CERT.SF 如果没被篡改,就能用于验证清单文件 MANIFEST.MF 是否被篡改。但又怎么验证 CERT.SF 是否被篡改呢? 靠的就是签名文件 CERT.RSA 了:


1.3 CERT.RSA文件

文件的作用:
这个文件是为了验证 CERT.SF 文件有没有被篡改。

文件的内容:
它包含了 「对 CERT.SF 文件的签名」以及「包含公钥的开发者证书」。

如果不了解 证书和签名 是如何用于数据验证的,可以看《摘要、签名与数字证书都是什么?》


2. V1的签名机制

V1签名的文件生成过程

签名的流程如下:

  1. 计算每一个原始文件的 SHA-1 摘要,写入到 MANIFEST.MF 中;

  2. 计算整个 MANIFEST.MF 文件的 SHA-1 摘要,写入到 CERT.SF 中;

  3. 计算 MANIFEST.MF 中,每一块的 SHA-1 摘要,写入到 CERT.SF 中;

  4. 计算整个 CERT.SF 文件的摘要,使用开发者私钥计算出摘要的签名;

  5. 将签名和开发者证书(X.509)写入 CERT.RSA


3. V1签名是怎么校验的?

校验的流程如下:

  1. 取出 CERT.RSA 中包含的开发者证书;

  2. 通过系统的根证书(CA证书)验证这个开发者证书是否可信;

  3. 如果开发者证书可信,用证书中的公钥解密 CERT.RSA 中包含的签名。

  4. 计算 CERT.SF 的签名;

  5. 对比 (3) 和 (4) 的签名是否一致;

  6. 如果一致,用 CERT.SF 去校验 MANIFEST.MF 是否被修改;

  7. 如果没有被修改,再用 MANIFEST.MF 中的每一块数据去校验每一个文件是否被修改。


4. V1签名如何防止篡改

假如攻击者修改了其中某一个文件,那么他必须修改 MANIFEST.MF 中对应文件的摘要,否则这个文件校验不通过;接着还得修改 CERT.SF 中的摘要,否则摘要校验不过;还得重新计算 CERT.SF 的签名,否则签名校验不通过;但是计算签名需要私钥,私钥在开发者手中,攻击者没有私钥,所以无法签名。


5. V1签名存在的问题

校验速度慢:需要对 apk 中的每个文件都计算摘要并验证,如果文件很多,校验时间会很长。完整性不够:V1 签名只会校验 Zip 文件中的部分文件,例如 META-INFO 文件夹就不会参与校验。


二、 V2 签名方案

V2 签名是在 Android7.0 之后引入的,它解决了 V1 签名校验时速度慢的问题,同时对完整性的校验扩展到整个安装包。

了解 V2 签名原理之前,我们先了解一下 Zip 文件:

1. Zip 文件

1.1 Zip 文件的格式和解析过程

Zip文件格式

  1. 先从文件尾部查找 0x06054b50,确定 End Of Central Directory Record 区域的起始位置;

  2. 解析 EoCD 区域,并获得中央目录的起始位置;

  3. 根据起始位置,逐个解析文件。

从解析过程可以看出,如果在 「文件信息部分」 和 「中央目录部分」之间插入了其他数据,是不会影响 Zip 文件的解压缩的。


2. V2 签名数据块的格式

V2 签名时,会将 签名信息块 插入到 Zip 文件的「文件信息」和「中央目录」之间,如图:
apk V2 签名前后对比

「Apk Signing Block」的具体结构:

V2签名数据块的结构

签名块的前8个字节记录了所有键值对数据块的大小。其后紧接着键值对数据块,数据块由一个个的键值对块组成。每个键值对块的开始8字节记录了「键值对的ID」和「键值对的Value」的大小。接下来4字节是键值对的ID,后面跟着对应的值。
ID = 0x7109871a 的键值对块就是保存签名信息的地方。
键值对数据块的后面还有8个字节,也是用于记录「键值对块」的大小,它的值和最开始的8字节相同。签名块的末尾是一个魔数,也就是‘APK Sig Block 42’的 ASCII 码。

下一篇文章我们会讲到如何在这里动态插入其他信息,例如渠道信息等。

签名信息的具体结构:
签名信息的结构


3. V2 摘要计算方式

V2 签名摘要的计算就不是按照文件计算的了,而是按照 1MB 为单位计算:

apk按照1MB大小计算摘要

步骤:

  1. 对原始apk文件的 文件信息部分、中央目录部分、EoCD部分,按照 1MB 大小分割为多个小块(Chunks);

  2. 分别对每一个小块计算其摘要,类似于 V1 签名中的 MANIFEST.MF 文件;

  3. 对(2)中所有摘要计算其摘要,类似于 V1 签名中的 CERT.SF 文件;

4. V2 签名的校验

Android 7.0 及以上在校验时,会先判断是否具有 V2 签名,如果有 V2 签名,会走 V2 签名的校验流程,不再验证V1签名了。

如何判断是否有V2签名?根据Zip文件格式的规则,我们可以找到中央目录区的起始位置。读取从起始位置开始往回的16个字节,判断这16个字节的值是否为 “Apk Sig Block 42”,如果是,则对应上了魔数,说明有 V2 签名。后续就是解析 V2 签名块的流程了。

用公钥和签名对蓝色区域验证,验证通过后,用 「APK数据摘要集」对APK每一块做验证。


三、 V3 签名方案

V3 签名方案的签名块格式和V2完全一样,只是 V2 的签名块信息存放在 ID = 0x7109871a 的数据块中,而 V3 的签名信息存放在 ID = 0xf05368c0 的数据块中。

在这个新的数据块中,记录了旧的签名信息和新的签名信息,以密钥转轮的方案,做签名的替换和升级。这意味着我们可以更改 APK 的签名。

V3 签名块的大小必须是 4096 的整数倍,否则在安装时会出现如下异常:

1
2
3
adb: failed to install xxx.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES:
Failed to collect certificates from /data/app/xxxx.apk using APK Signature Scheme v3:
Size of APK Signing Block is not a multiple of 4096: xxxx]

1. 消息摘要(Digest)

什么是消息摘要?

对一份数据,进行一个单向的 Hash 函数,生成一个固定长度的 Hash 值,这个值就是这份数据的摘要,也称为指纹。

摘要算法

常见的摘要算法有 MD5、SHA-1、SHA-256 等。他们都有这些特点:

  • 对于同一个摘要算法,无论输入的数据是什么,输出都是相同长度的值。例如 MD5,无论数据有多大,输出总是128位的散列值。

  • 摘要算法是单向的,只能根据原始数据计算出它的摘要值,但是不能根据摘要值反算出原始数据。

  • 越优秀的摘要算法越难找到Hash碰撞。虽然长内容生成短摘要是必定会产生碰撞的,但一个优秀的摘要算法很难主动构造出两条数据,使得他们的摘要值相同。

  • 消息摘要的用途:可以用于校验数据的完整性。例如我们在下载文件时,数据源会提供一个文件的MD5。文件下载好之后,我们本地计算出文件的MD5,和数据源提供的MD5做对比,如果相同则文件是完整的。但独立使用消息摘要时,无法确保数据没有被篡改,因为无法保证从数据源获取的MD5有没有被中途篡改。

  • 相比加密算法,摘要算法速度都相对较快。


2. 数字签名(Signature)

在了解签名之前,需要了解什么是公开密钥体系:

公开密钥密码体系:
基于大整数的因数分解可以生成一对公钥和私钥。公钥和私钥是一一对应关系,一把私钥有着和它唯一对应的公钥,反之亦然。用公钥加密的数据,只能用和它对应的私钥解密,用私钥加密也只能同与之对应的公钥解密。密钥对的生成很快速,但根据公钥反推私钥是极其困难的事。

根据公开密钥密码体系,我们有了非对称加密。常见的非对称加密是 RSA 加密。

如果用「公钥」对数据加密,用「私钥」去解密,这是「加密」;反之用「私钥」对数据加密,用「公钥」去解密,这是「签名」。

简单地看,似乎没有区别,只是换了个名字。但实际上,两者的用途完全不一样。由于所有人都持有公钥,所以「签名」并不能保证数据的安全性,因为所有人都可以用公钥去解密。但「签名」却能用于保证消息的准确性和不可否认性。因为公钥和私钥是一一对应的,所以当一个公钥能解密某个密文时,说明这个密文一定来自于私钥持有者

我们来看一下具体签名和验证的过程

  1. 消息发送者持有 私钥 和 加密算法,称为信源;信源用私钥和加密算法对明文数据进行加密,得到密文数据,称为签体;

  2. 接着把明文数据和密文数据同时给到消息接收者;

  3. 消息接收者收到后,先取出密文数据,用公钥对密文解密,得到一份明文数据;

  4. 再将这份明文数据和收到的明文数据做对比,如果相同则数据完整且可信。

签名验证过程

即使他人截获并篡改了「明文数据」,由于「私钥」是保密的,篡改者也无法生成正确的「签体」。所以签名能保证消息的准确性。但在单独使用非对称加密的数字签名方案时,要对所有明文消息进行加密,效率很低。怎么提高效率呢?

更高效的数字签名方案: 将摘要算法和非对称加密结合使用。如何签名:先用摘要算法计算明文数据的摘要值,再对这个摘要值用私钥加密。这样就能较快速地得到了原始信息的签名;如何验证:先用相同的摘要算法计算原始信息的摘要值,再用公钥对签名解密,得到收到的摘要值,最后对比这两个摘要值判断是否相等。如果不相等说明数据不可信。

数字签名方案的问题:
数据接收者如何获取正确的公钥呢?如果公钥本身都被篡改了,这个签名方案就不正确了。所以需要有某种方式确保公钥的正确性,这就是数字证书。


3. 数字证书(Certificate)

数字证书的作用:
确保数据接收者的公钥是没有被篡改过的。

数字证书通常包含以下内容:
(1) 证书所有人的公钥;
(2) 证书发行者对证书的数字签名
(3) 证书所用的签名算法;
(4) 证书发布机构、有效期、所有者的信息等其他信息。

数字证书的验证过程需要用到 CA根证书业务相关证书根证书 是预装在操作系统中的。

在理解数字证书工作原理之前,我们需要先理解这两种证书是怎么生成的:

1. CA根证书的生成
CA数字证书的生成过程

步骤:

  1. 权威机构利用RSA等算法,生成一对 公钥PK1 / 私钥SK1;

  2. 将 公钥PK1 和 证书发布机构、有效期等信息组成一份原始的证书内容,设为 C1;

  3. 利用某种摘要算法,计算原始内容 C1 的数字摘要,设为 H1;

  4. 用第一步生成的私钥SK1,对摘要H1签名,得到签名内容S1;

  5. 将原始内容C1 和 签名内容S1 合在一起,就得到了证书。

根证书安装在操作系统中,我们认为根证书是一定正确的。

2. 业务相关证书的生成
企业申请证书的过程

步骤:

  1. 企业利用RSA等算法,生成一对 公钥PK2 / 私钥SK2;

  2. 将 公钥PK2 和 证书其他内容 组成原始证书内容,设为C2,给到权威机构;

  3. 权威机构拿到 C2 后,利用摘要算法,生成摘要信息 H2;

  4. 权威机构用自己的私钥SK1 (这是关键点),对摘要信息H2 签名,得到签名内容S2;

  5. 将 原始内容C2 和 签名内容S2 合并到一起,得到证书,交给企业。

区别点在于:业务申请的证书,在签名时用的私钥是CA机构的私钥。这个私钥是和根证书中的公钥对应的。

3. 数字证书的真伪验证
有了根证书,我们就能校验其他证书的真伪了:
证书真伪的验证过程.png

用根证书的公钥,可以验证其他证书的签名是否正确。如果签名正确,则证书是可信的、没有被篡改的。后续就可以使用这个被信任证书中包含的公钥,去验证收到的消息是否可信了。

用CA证书去证明另外一个证书是否可信,我们可以称之为 证书的递归验证。类似地,我们也可以用一个受信任的证书,去验证其他证书是否可信。

在多线程中使用 notify-wait 时,如果 等待线程A 在调用 wait() 之前,唤醒线程B 已经调用了 notify() 方法,会导致 等待线程A 永远得不到唤醒,一直等待下去,这就是 Signal Before Wait 问题:
示意图

例如下面的代码:

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
int main() {
// 定义互斥量和条件变量
std::mutex mutex;
std::condition_variable condition;

// 唤醒线程
auto signalThread = new std::thread([&]() {
log("即将唤醒");
condition.notify_all();
log("唤醒完毕");
});
signalThread->detach();

// sleep 1秒,模拟耗时操作
sleep(1);

// 等待线程
auto waitThread = new std::thread([&]() {
std::unique_lock lock(mutex);
log("即将等待");
condition.wait(lock);
log("等待完毕");
});
waitThread->detach();

// 主线程5分钟后释放
sleep(60 * 5);
delete signalThread;
delete waitThread;
return 0;
}

为了解决这个问题,我们引入标记位 isReady 来解决( 第7、12、26行 ):

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
int main() {
// 定义互斥量和条件变量
std::mutex mutex;
std::condition_variable condition;

// 新增标记位,用于标记是否已经准备好了
bool isReady = false;

// 唤醒线程
auto signalThread = new std::thread([&]() {
log("即将唤醒");
isReady = true;
condition.notify_all();
log("唤醒完毕");
});
signalThread->detach();

// sleep 1秒,模拟耗时操作
sleep(1);

// 等待线程
auto waitThread = new std::thread([&]() {
std::unique_lock lock(mutex);
log("即将等待");
// 只有在没有准备好时才需要等待
while (!isReady) {
condition.wait(lock);
}
log("等待完毕");
});
waitThread->detach();

// 主线程5分钟后释放
sleep(60 * 5);
delete signalThread;
delete waitThread;
return 0;
}

实际上,只添加这个标记位,是没有解决问题的,如果在 while -> wait 之间,isReady 被修改为 true,依然会导致等待线程陷入等待状态:

因此,我们需要把 while-wait 和 isReady更新 都要放到临界区中。由于 unique_lock 在构造方法中自动调用了 mutex.lock() 方法,且 condition.wait() 在移到等待队列前会自动调用 mutex.unlock(),所以 等待线程A 的 while-wait 本来就在临界区中了,只需修改唤醒线程的代码:

1
2
3
4
5
6
7
8
9
10
// 唤醒线程
auto signalThread = new std::thread([&]() {
log("即将唤醒");
mutex.lock();
isReady = true;
condition.notify_all();
mutex.unlock();
log("唤醒完毕");
});
signalThread->detach();

这里有个细节是 notify() 要不要放在临界区中。如果放到临界区中,可能存在多余的线程上下文切换:

如果 notify() 在 unlock() 之前调用,那么 等待线程A 在收到唤醒时,会尝试 lock(),但 唤醒线程B 还没有 unlock(),所以 等待线程A 会再次进入休眠。这就多了2次线程的上下文切换。那如果 notify() 在 unlock() 之后会不会有问题呢?当然也可能有问题的,例如有三个线程时:这会导致虚假唤醒。如果业务更复杂,可能会导致意料之外的其他错误。如果对性能要求非常严格,且 unlock - notify 之间不会有意料之外的逻辑,那可以把 unlock 放到 notify 之前;如果不在意这种细微的性能损失,就把 notify 放到 unlock 之前。这样能避免很多隐秘的BUG。


源码:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
void log(const std::string& msg) {
std::cout << msg << std::endl;
}
int main() {
// 定义互斥量和条件变量
std::mutex mutex;
std::condition_variable condition;
// 新增标记位,用于标记是否已经准备好了
bool isReady = false;
// 唤醒线程
auto signalThread = new std::thread([&]() {
log("即将唤醒");
isReady = true;
condition.notify_all();
log("唤醒完毕");
});
signalThread->detach();
// sleep 1秒,模拟耗时操作
sleep(1);
// 等待线程
auto waitThread = new std::thread([&]() {
std::unique_lock lock(mutex);
log("即将等待");
// 只有在没有准备好时才需要等待
while (!isReady) {
condition.wait(lock);
}
log("等待完毕");
});
waitThread->detach();
// 主线程5分钟后释放
sleep(60 * 5);
delete signalThread;
delete waitThread;
return 0;
}
void core1() {
std::mutex mutex;
std::condition_variable condition;
bool isReady = false;
auto signalThread = new std::thread([&condition, &isReady, &mutex]() {
// ....
sleep(2);
mutex.lock();
isReady = true;
condition.notify_all(); // ①
mutex.unlock();
});
signalThread->detach();
// while ---> notify ---> wait
auto anotherWaitThread1 = new std::thread([&condition, &isReady, &mutex]() {
// unique_lock 的构造函数会调用传入 mutex 的 lock 函数。
std::unique_lock lock(mutex);
while(!isReady) {
// condition 的 wait 函数会释放锁,并且会阻塞当前线程;当被唤醒后,会再次抢占锁。
condition.wait(lock);
}
});
anotherWaitThread1->detach();
sleep(1000);
}
void core() {
std::mutex mutex;
std::condition_variable condition;
bool isReady = false;
auto subThread = new std::thread([&condition, &isReady, &mutex]() {
sleep(1);
std::cout << "update" << std::endl;
isReady = true; // ②
sleep(1);
mutex.lock();
std::cout << "notify..." << std::endl;
condition.notify_all(); // ①
mutex.unlock();
// 关于 ① ② 的顺序:
// 如果先 notify,再更新标记位,可能出现: 等待方进入了while -> notify -> update flag -> wait 的情况。
// 如果先更新标记位,再 notify,可能出现: 等待方进入了while -> update flag -> notify -> wait 的情况。
// 都依然会导致 signal before wait。
// 所以,while-wait 和 update-notify 都需要处于临界区中。
// 准确地说, while 和 wait 之间,不能有 notify。
// while -> wait -> notify -> update 没问题
// while -> wait -> update -> notify 没问题
// while -> notify -> wait -> update 有问题
// while -> update -> wait -> notify 没问题
// while -> notify -> update -> wait 有问题
// while -> update -> notify -> wait 有问题
});
subThread->detach();
std::unique_lock lock(mutex);
mutex.lock();
std::cout << "while..." << std::endl;
while (true) {
if (isReady) {
break;
}
std::cout << "in while" << std::endl;
sleep(2);
std::cout << "wait" << std::endl;
condition.wait(lock);
}
mutex.unlock();
std::cout << "...while" << std::endl;
}
int main2() {
core1();
return 0;
}

核心就是 ViewGroup 的 dispatchTouchEvent 方法:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public boolean dispatchTouchEvent(MotionEvent event) {
// 省略...
final boolean isIntercept;

// 如果当前是 DOWN 事件, 或者 mFirstTouchTarget 不是 null
// 其中,mFirstTouchTarget 是指本轮触摸事件序列的上一个事件给到谁捕获了
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 如果不允许拦截,标记为不拦截
if (disallowIntercept) {
isIntercept = false;
} else {
isIntercept = onInterceptTouchEvent(event);
}
}
// 否则,表示这个事件不是 DOWN 事件,并且 mFirstTouchTarget == null
// 意味着,这轮触摸事件序列,没有已有的目标可以处理,并且也不是第一次的 DOWN 事件,
// 所及交给当前 ViewGroup 自己处理.
else {
isIntercept = true;
}

// 省略...

// 如果没有被取消,且没有被拦截
if (!isCanceled && !isIntercept) {
// 如果事件是 DOWN 事件,或者是其他条件(不用关心)
if (actionMasked == MotionEvent.ACTION_DOWN
|| ...) {
// 省略一些判断逻辑,比如 childrenCount > 0 ...

// 注意这里是倒序遍历的
for (int i = childrenCount - 1; i >=0; i--) {
// 计算要获取哪个 View
final View child = ...;

// 判断 child 是否可以接收动画,如果在动画中,是不会接收点击事件的
// 或者,点击事件是否在 child 的范围内
// 任一条件不满足,都不会分发给这个 child.
if (!child.canViewReceivePointerEvent()
|| !isTransformedTouchPointInView(event)) {
continue;
}

// 分发给 child
// dispatchTransformedTouchEvent 会调用 child.dispatchTouchEvent
if (dispatchTransformedTouchEvent(event, .., child)) {
// 省略...

// 这里 addTouchTarget 内部会对 mFirstTouchTarget 赋值
newTouchTarget = addTouchTarget(child, ..);
alreadyDispatchedToNewTouchEventTarget = true;
break;
}
}
}
}

// 如果 mFirstTouchTarget 是 null, 说明没有子 view 捕获该事件,交给自己处理
if (mFirstTouchTarget == null) {
// 传入的 child 参数是 null,会在内部间接调用到 onTouchEvent 方法;
handled = dispatchTransformedTouchEvent(event, canceled, child = null, .);
} else {
TouchTarget target = mFirstTouchTarget;
while(target != null) {
target = target.next;
// 如果这个事件已经交给 target 处理过了,不再重复处理
if (alreadyDispatchedToNewTouchEventTarget && target == newTouchTarget) {
handled = true;
}
// 否则,交给子view处理
else {
// 省略 ...
if (dispatchTransformedTouchEvent(event, target, ...)) {
handled = true;
}
}
}
}

return handled;
}

后续的 MOVE、UP 等事件的分发交给谁,取决于它们的起始事件 Down 是由谁捕获的。

什么时候会出发 CANCEL 事件?
当父视图的 onInterceptTouchEvent 先返回 false,然后在子 View 的 dispatchTouchEvent 中返回 true(表示子 View 捕获事件),关键步骤就是在接下来的 MOVE 的过程中,父视图的 onInterceptTouchEvent 又返回 true,intercepted 被重新置为 true,此时上述逻辑就会被触发,子控件就会收到 ACTION_CANCEL 的 touch 事件。

如何强制父布局不拦截点击事件?
使用 requestDisallowInterceptTouchEvent(boolean disallow) 方法。

dispatchTouchEvent 的简化版本:

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
public boolean dispatchTouchEvent(MotionEvent event) {
// 省略...
final boolean isIntercept;

if (disallowIntercept) {
isIntercept = false;
} else {
isIntercept = onInterceptTouchEvent(event);
}

if (!isIntercept) {
for (int i = childrenCount - 1; i >= 0; i++) {
View child = ...;
boolean consumed = dispatchTransformedTouchEvent(child);
if (consumed) {
newTouchTarget = addTouchTarget(newTouchTarget);
alreadyDispatched = true;
break;
}
}
}


boolean handled;
if (newTouchTarget == null) {
handled = dispatchTransformedTouchEvent(null);
} else {
TouchTarget next = mFirstTouchTarget;
while(next != null) {
next = next.next;
handled = dispatchTransformedTouchEvent(next);
}
}
return handled;
}

含义
单一职责 一个类、接口、函数,只负责一件事情,只有一个原因引起变化。
里氏替换 所有声明为父类的地方,都可以使用子类的实例。
依赖倒置 上层模块,不应该依赖于下层模块的具体实现,而是依赖下层模块抽象出来的接口;
接口隔离 要建立功能单一的接口 ,而不是臃肿的接口,减少对外暴露。
迪米特法则 一个对象,对其他对象知道得越少越好,减少对其它类具体实现的依赖。
开闭原则 对扩展开放、对修改封闭

接口,负责定义 public 方法;
抽象类,负责实现公共部分;
实现类,准确实现业务逻辑。

一、 单一职责原则

1. 含义

一个类、接口、函数,只负责一件事情,只有一个原因引起变化。

2. 好处

(1) 复杂性降低;
(2) 可读性提高;
(3) 可维护性提高;
(4) 变更引起的风险降低了,更容易做单元测试。

3. 实践难度

对 “职责” 和 “变化原因” 难易度量。


二、 里氏替换

1. 含义

所有声明为父类的地方,都可以使用子类的实例。


三、 依赖倒置

1. 含义

依赖倒置,是面向接口编程的具体定义,包含 3 点:
(1) 上层模块,不应该依赖于下层模块的具体实现,而是依赖下层模块抽象出来的接口;
(2) 抽象的接口,不应该依赖于具体的实现类;
(3) 实现类,需要满足抽象的接口。

2. 好处

(1) 减少类之间的耦合;
(2) 提高系统的稳定性,减少并行开发的风险;
(3) 提高代码可读性、可维护性;

四、 接口隔离

1. 含义

类之间的依赖关系,应该建立在最小的接口上。也就是说,要建立功能单一的接口 ,而不是臃肿的接口,减少对外暴露。保证接口纯洁性。


五、 迪米特法则

1. 含义

一个对象,对其他对象知道得越少越好,减少对其它类具体实现的依赖。


六、 开闭原则

1. 含义

对扩展开放、对修改封闭。

一、 设计模式分类

23 种设计模式分为 创建型、 结构型、 行为型 3 类.

分别包含:

  1. 创建型(5种)
    工厂方法、 抽象工厂、 单例模式、 建造者模式、 原型模式。

  2. 结构型(7种)
    适配器、 装饰器、 代理模式、 外观模式、 桥接模式、 组合模式、 享元模式(?)。

  3. 行为型(11种)
    策略模式、 模板方法、 观察者、 迭代器、 责任链、 命令模式、 备忘录模式、 状态机模式、 访问者模式、 中介者模式、 解释器模式。


二、 创建型

1. 工厂模式

1.1 简单工厂

角色:
(1) 一个工厂类,带有一个静态方法,传入一个类型,返回一个对象;
(2) 一个产品接口类;
(3) 多个产品实现类。

示意图

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface IFruit {
void name();
}

public class Apple extends IFruit {
void name() {
return "苹果";
}
}

public class Banana extends IFruit {
void name() {
return "香蕉";
}
}

public class FruitFactory {
IFruit create(String type) {
case "apple": return new Apple();
case "banana": return new Banana();
}
return null;
}

1.2 工厂方法

角色:

三、 结构型

四、 行为型

|-- Android .   |-- Android 基础 .   .   |-- 四大组件 .   .   |-- UI 界面 .   .   .   |-- 常用控件 .   .   .   .   |-- SurfaceView .   .   .   .   |-- TextureView .   .   .   .   |-- RecyclerView .   .   .   .   |-- ConstraintLayout .   .   .   .   `-- CoordinatorLayout .   .   .   |-- 属性动画 .   .   .   |-- 点击事件分发 .   .   .   |-- Window 与 Surface .   .   .   |-- View 的工作机制 .   .   .   `-- 大图加载 .   .   |-- Jetpack .   .   |-- Android 特有的数据结构 .   .   .   |-- ArrayMap .   .   |-- 消息机制 .   .   .   `-- epoll 机制 .   .   |-- 跨进程通信 .   .   .   |-- Binder 机制 .   .   .   `-- AIDL .   .   |-- 多媒体 .   .   .   |-- 录音 .   .   .   |-- 音频播放 .   .   .   `-- 相机 .   |-- Android 源码 .   .   `-- Activity 启动流程 .   |-- Android 专项技术 .   .   |-- 性能优化 .   .   .   |-- 启动耗时优化 .   .   .   .   |-- 定位耗时代码的方案 .   .   .   .   .   |-- TraceView和Systrace .   .   .   .   .   |-- Perfetto .   .   .   .   .   `-- 插桩统计函数耗时 .   .   .   .   |-- 其他优化方案 .   .   .   .   .   |-- 启动任务的有向无环图 .   .   .   .   .   |-- 在 Application 中预加载 .   .   .   .   .   `-- 首屏视图尽可能使用ViewStub .   .   .   |-- CPU 占用率 .   .   .   |-- 线程数量 .   .   .   .   |-- 线程数量监控 .   .   .   .   `-- 线程数量优化 .   .   .   |-- FPS和卡顿优化 .   .   .   .   |-- 卡顿监控 .   .   .   .   `-- 主线程 IO 检测 .   .   .   |-- 内存优化 .   .   .   .   |-- 内存泄露 .   .   .   .   |-- 内存抖动 .   .   .   .   `-- 内存占用 .   .   .   .   .   |-- C++ 内存占用分析 .   .   .   .   .   `-- Java 内存占用分析 .   .   |-- 编译流程 .   .   |-- Gradle .   .   |-- 插件化 .   .   |-- APT .   .   |-- 代码插桩 .   .   `-- 安全性 .   .   .   |-- 代码混淆 .   .   .   |-- 签名 .   .   .   .   |-- 摘要、签名与数字证书都是什么? .   .   .   `-- 加固 .   |-- 开源库 .   .   |-- EventBus .   .   |-- RxJava .   .   |-- Okhttp .   .   |-- Retrofit .   .   `-- Glide |-- Java .   |-- 线程安全 .   `-- 数据结构 |-- Kotlin `-- 架构设计 .   |-- 六种设计原则 .   |-- 23种设计模式 .   `-- 架构 .   .   |-- MVC .   .   |-- MVP .   .   |-- MVVM .   .   |-- VIPER .   .   `-- Clean