0%

一、ARouter 中 Path 和 Group 的关系

ARouter 里面这二者的关系比较简单,但是单纯看代码可能会比较蒙。其实很简单,用最简单的图示就能说清楚:

Group和Path的关系

  • Group:每个 Module 作为一个 Group 组

  • Path:每个 Module 中所有的 Activity (还可以拓展 Fragment)都放在 Path 里面

体现在代码里面就是生成的2种类(Group 和 Path):

1
2
3
4
5
6
7
8
public class ARouter$$Group$$personal implements ARouterGroup {
@Overrideag-0-1h3qlcbapag-1-1h3qlcbap
public Map<String, Class<? extends ARouterPath>> getGroupMap() {
Map<String, Class<? extends ARouterPath>> groupMap = new HashMap<>();
groupMap.put("personal", ARouter$$Path$$personal.class);
return groupMap;
}
}
1
2
3
4
5
6
7
8
9
public class ARouter$$Path$$personal implements ARouterPath {
@Override
public Map<String, RouterBean> getPathMap() {
Map<String, RouterBean> pathMap = new HashMap<>();
pathMap.put("/personal/Personal_Main2Activity", RouterBean.create();
pathMap.put("/personal/Personal_MainActivity", RouterBean.create());
return pathMap;
}
}

可以看到,在Group 类中 Map 是添加了 Path 的:

groupMap.put(“personal”, ARouter$$Path$$personal.class);

然后Path 里面的 pathMap 存放的key 是 path (如:”/personal/Personal_MainActivity”),value 是 Mainactivity.class (只不过被封装成 RouterBean 了)。

思考下,Path 类和 Group 类哪个先生成?肯定是 Path ,因为 Group 需要依赖 Path

使用JavaPoet 的时候,生成类型的时候,可以用 $T 来代替这个类型,再后面写上类型,类似于Android 中的 %1$s 的写法,比如构建 Map<String, Class<? extends ARouterPath>> groupMap = new HashMap<>(); 这行代码的时候:

1
2
3
4
5
6
7
8
9
methodBuidler.addStatement("$T<$T, $T> $N = new $T<>()",
ClassName.get(Map.class),
ClassName.get(String.class),

// Class<? extends ARouterPath> 难度
ParameterizedTypeName.get(ClassName.get(Class.class),
WildcardTypeName.subtypeOf(ClassName.get(pathType))), // ? extends ARouterPath
ProcessorConfig.GROUP_VAR1,
ClassName.get(HashMap.class));

看了代码,感觉太复杂的情况还不如直接不使用 Javapoet ,使用字符串去做还好点。

在自动生成 Group 和 Path 相关的类之后,我们可以使用如下方式去实现页面跳转了:

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
public void jumpPersonal(View view) {
// 以前是这样跳转
/*Intent intent = new Intent(this, Personal_MainActivity.class);
intent.putExtra("name", "derry");
startActivity(intent);*/

// 现在是这样跳转 目前还要写这么多代码,是不是非常累

// TODO 最终的成效:用户 一行代码搞定,同时还可以传递参数,同时还可以懒加载

ARouter$$Group$$personal group$$personal = new ARouter$$Group$$personal();
Map<String, Class<? extends ARouterPath>> groupMap = group$$personal.getGroupMap();
Class<? extends ARouterPath> myClass = groupMap.get("personal");

try {
ARouter$$Path$$personal path = (ARouter$$Path$$personal) myClass.newInstance();
Map<String, RouterBean> pathMap = path.getPathMap();
RouterBean bean = pathMap.get("/personal/Personal_MainActivity");

if (bean != null) {
Intent intent = new Intent(this, bean.getMyClass());
startActivity(intent);
}

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

}

我们先 new 出特定的 Group 对象,再获取其 Map 对象(这是缓存仓库,存储了其所有的 Path),然后根据 path 的name 获取到 Path 类的 Class 文件,之后使用这个 class 文件就能 newInstance 得出 path 对象,从path 对象中再得到其 Map 对象,最后,根据路径,从这个 Map 对象获得 RouterBean (也就是目标Activity 的Class )。

可见,到目前这一步还是很麻烦的,所以ARouter 再生成的代码上再一次生成代码,进一步封装。

注:

JavaPoet API:

https://github.com/square/javapoet

一、前情回顾

早期的 XUtil 框架是使用运行期注解,采用的是反射,后续都比较倾向于编译器框架 APT 了,因为运行期反射还是有点消耗性能,目前采用编译期注解的框架有:Dagger2、Room、ARouter、Buterkife、DataBinding 等。

如果想学 APT 的使用,怎么自己生成代码,可以去看EventBus 中的注解处理器

二、使用 JavaPoet

但是上述EventBus 那种生成代码的方式有点 Low ,需要一行一行地写,从 import 写到最后一行,手抖写错一个标点符号就崩了。因此,我们可以看 ARouter 的,它里面使用了 JavaPoet 框架。但是传统方式看起来更直观,JavaPoet 看起来比较难。

2.1 如何去使用APT

新建 Android工程,在其中 new 一个 Lib 出来,用于存放所有的注解代码,再new 一个Lib ,作为注解处理器,项目结构如下所示:

APT使用时工程目录

这样划分是为了能够隔离项目代码和 APT 代码。需要把compiler 变为一个服务,监听 项目中关注的注解,所以就需要google 的 autoservice 以及生成代码需要用 JavaPoet ,所以 compiler 这个 Module 的 build.gradle 的文件如下:

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

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

// 背后的服务 能够监听 你是否在编译中.....
// AS3.4.1 + Gradle 5.1.1 + auto-service:1.0-rc4
compileOnly'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'

// 帮助我们通过类调用的形式来生成Java代码 [JavaPoet]
implementation "com.squareup:javapoet:1.9.0"

// 依赖注解
implementation project(":arouter-annotations")



}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

我们的项目也需要依赖我们的注解工程,所以需要在其 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
apply plugin: 'com.android.application'

android {
compileSdkVersion 30
buildToolsVersion "30.0.1"

defaultConfig {
applicationId "com.derry.new_modular_javapoet"
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

// 传递参数
javaCompileOptions {
annotationProcessorOptions {
arguments = [student: 'hello ni hao student javapoet']
}
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation project(path: ':arouter-annotations')
implementation project(path: ':arouter-annotations')
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

// 依赖注解
implementation project(":arouter-annotations")

// 依赖注解处理器 注解处理器才能工作
annotationProcessor project(":compiler")
}

接下来就是写compiler 这个 module 中的代码里,它是真正的处理器:

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package com.derry.compiler;

import com.derry.arouter_annotations.ARouter;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;

@AutoService(Processor.class) // 启用google提供的 autoservice 服务
@SupportedAnnotationTypes({"com.derry.arouter_annotations.ARouter"}) // 你自己写的注解位置,包名+类名
@SupportedSourceVersion(SourceVersion.RELEASE_7) // 环境的版本


// 接收 安卓工程传递过来的参数
@SupportedOptions("student")

public class ARouterProcessor extends AbstractProcessor {

// 操作Element的工具类(类,函数,属性,其实都是Element)
private Elements elementTool;

// type(类信息)的工具类,包含用于操作TypeMirror的工具方法
private Types typeTool;

// Message用来打印 日志相关信息,在编译期间不能使用Android的Log工具
private Messager messager;

// 文件生成器, 类 资源 等,就是最终要生成的文件 是需要Filer来完成的,此时还不能使用Java的File
private Filer filer;

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);

elementTool = processingEnvironment.getElementUtils();
messager = processingEnvironment.getMessager();
filer = processingEnvironment.getFiler();

String value = processingEnvironment.getOptions().get("student");
// 这个代码已经下毒了
// 如果我想在注解处理器里面抛出异常 可以使用Diagnostic.Kind.ERROR
messager.printMessage(Diagnostic.Kind.NOTE, ">>>>>>>>>>>>>>>>>>>>>>"+value);
}

// 服务:在编译的时候干活
// 坑:如果没有在任何地方使用,次函数是不会工作的
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 这个代码已经下毒了
messager.printMessage(Diagnostic.Kind.NOTE, ">>>>>>> Derry run...");

if (set.isEmpty()) {
return false; // 不干活
}

// 循环?
// 获取被 ARouter注解的 "类节点信息"
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ARouter.class);
for (Element element : elements) { // for 3 // 1 element == MainActivity 2 element == MainActivity2

/**
模块一
package com.example.helloworld;

public final class HelloWorld {

public static void main(String[] args) {
System.out.println("Hello, JavaPoet!");
}
}

*/
// Java 万物皆对象
// C 万物皆指针
/*// 1.方法
MethodSpec mainMethod = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")

// 增加main方法里面的内容
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")

.build();

// 2.类
TypeSpec testClass = TypeSpec.classBuilder("DerryTest")
.addMethod(mainMethod)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.build();

// 3.包
JavaFile packagef = JavaFile.builder("com.xiangxue.test", testClass).build();

// 生成文件
try {
packagef.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
messager.printMessage(Diagnostic.Kind.NOTE, "生成Test文件时失败,异常:" + e.getMessage());
}*/

// 包信息
String packageName = elementTool.getPackageOf(element).getQualifiedName().toString();

// 获取简单类名,例如:MainActivity MainActivity2 MainActivity3
String className = element.getSimpleName().toString();
messager.printMessage(Diagnostic.Kind.NOTE, "被@ARetuer注解的类有:" + className);

// String className = element.getSimpleName().toString();

// 目标:要生成的文件名称 MainActivity$$$$$$$$$ARouter
String finalClassName = className + "$$$$$$$$$ARouter";

/**
模板:
public class MainActivity3$$$$$$$$$ARouter {

public static Class findTargetClass(String path) {
return path.equals("/app/MainActivity3") ? MainActivity3.class : null;
}

}
*/

ARouter aRouter = element.getAnnotation(ARouter.class);

// 1.方法
MethodSpec findTargetClass = MethodSpec.methodBuilder("findTargetClass")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(Class.class)
.addParameter(String.class, "path")
// 方法里面的内容 return path.equals("/app/MainActivity3") ? MainActivity3.class : null;

// 需要JavaPoet包装转型
.addStatement("return path.equals($S) ? $T.class : null",
aRouter.path(),
ClassName.get((TypeElement) element))
.build();

// 2.类
TypeSpec myClass = TypeSpec.classBuilder(finalClassName)
.addMethod(findTargetClass)
.addModifiers(Modifier.PUBLIC)
.build();

// 3.包
JavaFile packagef = JavaFile.builder(packageName, myClass).build();

// 开始生成
try {
packagef.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
messager.printMessage(Diagnostic.Kind.NOTE, "生成" + finalClassName + "文件时失败,异常:" + e.getMessage());
}
}

return true; // false不干活了 true干完了
}
}

在编译的时候,如果要从 Android 端传入参数到 APT 中来的话,怎么操作呢?因为这 2 者肯定是不能直接通信的,我们可以从 gradle 中来中转,在应用module 的 build.gradle 中采用 javaCompileOptions 来解决:

1
2
3
4
5
6
// 传递参数
javaCompileOptions {
annotationProcessorOptions {
arguments = [student: 'hello ni hao student javapoet']
}
}

在 APT 中接收这个参数,就在 APT 代码上面加上注解:

1
2
// 接收 安卓工程传递过来的参数
@SupportedOptions("student")

三、JavaPoet高级用法

最后,如果要在 Android 中使用这个注解处理器,还需要在 Android 工程中的 build.gradle 中依赖它,加上之前加入的依赖注解代码,就是这样的:

1
2
3
4
5
// 依赖注解
implementation project(":arouter-annotations")

// 依赖注解处理器 注解处理器才能工作
annotationProcessor project(":compiler")

所以,这个 annotationProcessor 声明 是要注意的。

javapoet 主要需要注意的是步骤:

  1. 生成方法

  2. 生成类

  3. 生成包

老师说这个可以去讲一下:EventBus 使用的是普通的写的方式,所以很困难,整个类就是一个字符串,写得非常难。但是 ARouter 使用 javapoet 使用就很简单。

四、路由分组设计

很多开源框架的注解设计是这种思想:

  • 在 compiler 这个 Module 中是正儿八经的注解处理器

  • 在 xx_annotation 这个目录下写我们的注解(比如 ARouter 中这里就写 ARouter 这个注解类,并标明这个注解是修饰类,编译时注解)

  • xx_api 这个目录写的是注解相关的逻辑(用于与业务逻辑区分,比如 ARouter 中是ARouterGroup、ARouterPath 类,以及将 Activity 等放入相应的 Map 等逻辑)

一、起因

早期的时候,一个项目都在一个一起,通过包名来控制不同的模块和功能。这种方式的缺点:

  • 层次混乱:无论怎么做分包,随着项目增大,就会失去层次感,接手的人扑街
  • 耦合度高,低内聚:。包名约束太弱,稍不注意就不同业务包直接相互调用
  • 不易于版本管理,容易代码冲突
  • 难以重用

所以,很容易想到组件化的好处:

  • 不相互依赖
  • 可以互相交互
  • 高度解耦
  • 自由拆卸组合
  • 重复利用

二、组件化环境

各个模块组件都能单独打包,对于测试是很友好的。在正式上线的时候,所有模块都需要App 壳才能运行。

2.1 Gradle

gradle 的根在哪里? 就是 settings.gradle 这个文件!然后,我们整个项目有个 gradle ,这个 project 就在项目根目录下 build.gradle 。 build 的步骤就是:

  1. settings.gradle

  2. Project 级别的 build.gradle

  3. 壳工程的 build.gradle

  4. library 中的 build.gradle

app 和 各个 module 中都有 gradle 文件,里面可能会包含相同代码,比如 编译工具版本、最小支持版本。我们可以在project 下面新建 gradle ,比如命名为 derry.gradle,在里面写上 ext 扩展块,代码如下:

1
2
3
4
5
6
ext {
compileSdkVersion 30
defaultConfit {
minSdkVersion 16
}
}

不过此时还不能被系统所认识,只能知道这是 key-value 的形式,我们只能将其引入到project 所属的gradle 中才能实现,那么,在 Project 的 build.gradle 中可以写如下代码:

1
apply from: 'derry.gradle'

所以,在各个模块中,可以使用这个公共的gradle 文件了:

1
2
3
4
def ext = rootProject.ext
android {
compileSdkVersion ext.compileSdkVersion
}

这里不去定义 def ext 也是可以的,但是为什么要这么做呢?这是为了性能考虑,因为这样定义一下就相当于局部变量了,能提高运行速度(老师说这个可以在面试时候去说的,说明真的玩过gradle)。

最后,放上App 壳和各个模块之间的配置,可以做到:

  • 正式环境和测试环境的部署

  • 当测试环境时,各个模块可以单独运行和打包,正式环境的时候必须依赖App壳才能运行

  • 所有的公共配置都放在app.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//公共的依赖 derry.gradle 文件

// 扩展块
ext {
// 正式环境 和 测试环境
isRelease = false

// 正式环境 和 测试环境 服务器 URL 配置
url = [
"debug" : "https://192.188.22.99/debug",
"release": "https://192.188.22.99/release"
]

// 建立Map存储, key 和 value 都是自定义的
androidID = [
compileSdkVersion : 30,
buildToolsVersion : "30.0.1",

applicationId : "com.derry.derry",
minSdkVersion : 16,
targetSdkVersion : 30,
versionCode : 1,
versionName : "1.0",

testInstrumentationRunner: "androidx.test.runner.AndroidJUnitRunner"
]

// 建立Map存储, key 和 value 都是自定义的
appID = [
app: "com.derry.modularproject",
login: "com.derry.login",
register: "com.derry.register"
]

// 300 行 MAP key value
dependenciesID = [
"appcompat" : "androidx.appcompat:appcompat:1.2.0",
"constraintlayout": "androidx.constraintlayout:constraintlayout:2.0.1",
"material" : "com.google.android.material:material:1.1.0",
]

}
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
//project 根目录下的 build.gradle
// 根目录下的build.gradle 引入 公共的一份 引入过来
apply from : 'derry.gradle'

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

allprojects {
repositories {
google()
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}
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
//某个library
apply plugin: 'com.android.library'

println "Derry ---> lib Student hao 2"

android {
compileSdkVersion androidID.compileSdkVersion
buildToolsVersion androidID.buildToolsVersion

defaultConfig {
minSdkVersion androidID.minSdkVersion
targetSdkVersion androidID.targetSdkVersion
versionCode androidID.versionCode
versionName androidID.versionName

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

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

/*implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.1"
implementation "com.google.android.material:material:1.1.0"
implementation "androidx.vectordrawable:vectordrawable:1.1.0"
implementation "androidx.navigation:navigation-fragment:2.2.2"
implementation "androidx.navigation:navigation-ui:2.2.2"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"*/

// 一行搞定300行 循环搞定
dependenciesID.each {k,v -> implementation v}

testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

}
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
//login这个 module 的 build.gradle

// apply plugin: 'com.android.application'
if (isRelease) { // 如果是发布版本时,各个模块都不能独立运行
apply plugin: 'com.android.library' // 正式环境 library不能独立运行
} else {
apply plugin: 'com.android.application' // 测试环境 application独立运行
}

android {
compileSdkVersion 30
buildToolsVersion "30.0.1"

defaultConfig {
// applicationId "" // 有appid 能够独立运行

if (!isRelease) { // 能够独立运行 必须要有appID
applicationId appID.login // 组件化模式能独立运行才能有applicationId
}

minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

sourceSets {
main {
if (!isRelease) {
// 如果是组件化模式,需要单独运行时 Debug
manifest.srcFile 'src/main/debug/AndroidManifest.xml' // 生效
} else { // 正式环境下
// 集成化模式,整个项目打包apk
manifest.srcFile 'src/main/AndroidManifest.xml' // 让我们之前 默认的路径下的清单文件再次生效

java {
// 减小包大小,release 时 debug 目录下文件不需要合并到主工程
exclude "**/debug/**"
}
}
}
}
}

dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

}

上面代码可能有一些重复的,每个 gradle 文件可能侧重某一个点,需要注意甄别。

三、组件之间的通信方式

如果订单模块想要访问个人模块的信息,那我们必须要有个什么注册表,我们将这些功能注册到这个注册表中,需要调用的时候,就通过这个注册表。

组件化几种可行的通信方式:

  • EventBus:缺点是EventBean 的维护成本太高,不好管理

  • 广播:不好管理,都统一发送出去了,并且后续Android版本广播都需要动态注册了(这点存疑,需要验证)

  • 使用隐式意图:这个就更麻烦了,要求每个 Activity 都必须有自己唯一的action 名字

  • 类加载方式:容易写错包名的类,相对而言缺点较少,可以尝试

  • 使用全局 Map :因为所有的 module 都需要依赖公共基础库,所以可以在公共基础库中添加一个 Map ,注册所有的Activity, 需要注册很多对象,相对而言缺点少,可以尝试

其中类加载和 全局 Map 的方式使用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void jumpPersonal(View view) {
// todo 方式一 类加载
// 类加载跳转,可以成功。维护成本较高且容易出现人为失误
try {
Class targetClass = Class.forName("com.xiangxue.personal.Personal_MainActivity");
Intent intent = new Intent(this, targetClass);
intent.putExtra("name", "derry");
startActivity(intent);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

// personal/Personal_MainActivity getMap
// todo 方式二 全局Map
Class<?> targetActivity =
RecordPathManager.startTargetActivity("personal", "Personal_MainActivity");
startActivity(new Intent(this, targetActivity));
}

需要注意的一点是,使用类加载方式,我们使用的是 Class.forName 的方式,这个是反射吗?这并不是反射,这只是类加载!反射我们是指反射属性,方法等,我们也不会看到导入的包里面有 Reflect 等反射的包名

当然,使用全局 Map 的方式必须还要在 Common 公共依赖里面有管理类:

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
/**
* 全局路径记录器(根据子模块进行分组)
*
* 组名:app,order,personal
* 详情order=[Order_MainActivity,Order_MainActivity2,Order_MainActivity3]
*
*/
public class RecordPathManager {

/**
* 先理解成 仓库
* group: app,order,personal
*
* order:
* OrderMainActivity1
* OrderMainActivity2
* OrderMainActivity3
*/
private static Map<String, List<PathBean>> maps = new HashMap<>();

/**
* 将路径信息加入全局Map
*
* @param groupName 组名,如:"personal"
* @param pathName 路劲名,如:"Personal_MainActivity"
* @param clazz 类对象,如:Personal_MainActivity.class
*/
public static void addGroupInfo(String groupName, String pathName, Class<?> clazz) {
List<PathBean> list = maps.get(groupName);

if (null == list) {
list = new ArrayList<>();
list.add(new PathBean(pathName, clazz));
// 存入仓库
maps.put(groupName, list);
} else {
// 存入仓库
maps.put(groupName, list);
}
}

/**
* 只需要告诉我,组名 ,路径名, 就能返回 "要跳转的Class"
* @param groupName 组名 oder
* @param pathName 路径名 OrderMainActivity1
* @return 跳转目标的class类对象
*/
public static Class<?> startTargetActivity(String groupName, String pathName) {
List<PathBean> list = maps.get(groupName);
if (list == null) {
Log.d(Config.TAG, "startTargetActivity 此组名得到的信息,并没有注册进来哦...");
return null;
}
// 遍历 寻找 去匹配 “PathBean”对象
for (PathBean pathBean : list) {
if (pathName.equalsIgnoreCase(pathBean.getPath())) {
return pathBean.getClazz();
}
}
return null;
}
}

然后,我们可以在各个 Module 里面需要注册当前 Module 所拥有的全部 Activity (当然,这个并不优雅,开发者可能会忘记,可以采用注解或者其他的方式去做,这里只是简单实现):

1
2
3
4
5
6
7
8
9
10
11
12
public class AppApplication extends BaseApplication {

@Override
public void onCreate() {
super.onCreate();

// 如果项目有100个Activity,这种加法会不会太那个? 缺点
RecordPathManager.addGroupInfo("app", "MainActivity", MainActivity.class);
RecordPathManager.addGroupInfo("order", "Order_MainActivity", Order_MainActivity.class);
RecordPathManager.addGroupInfo("personal", "Personal_MainActivity", Personal_MainActivity.class);
}
}

所以,全局Map 的方案跳转的时候,只需要标明你想跳转哪个module ,以及module 中的哪个 Activity。但是上述方式还是有点麻烦,这时候,阿里开源的 Arouter就应运而生了

组件化通信框架很多,但是目前最优秀的是 ARouter 。

组件化:模块之间没有依赖,便于重用

插件化:侧重动态化加载某些功能,主要问题是兼容性问题,支付宝都放弃了,因为你兼容了 5.0 ,能兼容 11.0 吗

模块化:模块化是业务层面的拆分,组件化是功能层次的划分

Google 表态说是 FrameWork 仍然还是 Java ,不会改成 Kotlin

一、策略模式

都是基于同一个目的,采用不同的方式去实现。

比如: 想去西藏旅游,可以骑自行车、坐火车、坐飞机,这是不同的策略,最终都是去西藏旅游。

所以,Retrofit 中生成 call 采用 Adapter 的策略方式,这样就能适配各种情况,比如有人采用 RXJava ,这样就能设计自己Adapter ,返回的 call 不是 Okhttp 里面的 Call ,这样,避免太过于僵化。

1.1 设计模式

设计模式怎么学习:

  1. 什么是设计模式

  2. 分析源码采用,为什么用,有什么好处

  3. 思考自己的项目,写对应的代码

二、动态代理

为什么我使用 Retrofit 的时候,只需要定义一个 接口,然后就能直接使用这个接口了,接口也不能创建对象啊。其实就是动态代理的设计模式实现的。来看看他们这个的使用方法:

1
GitHubService service = retrofit.create(GitHubService.class);

使用动态代理,虚拟机就会自动生成一个类,实现了这个接口的一个类。

静态代理相当于经纪人,可以代理特定的人;动态代理相当于经济公司,动态生成代理人,能够适应各种各样的被代理人需求,拍电影的、唱歌的、跳舞的都能代理,因为是动态生成的。

三、适配器设计模式

适配器的缺点:过多的适配器容易增加阅读的复杂度。

注意:Retrofit 的源码是基于 2.0.1

一、切面编程

Okhttp 作为一个网络库是非常好的了,因为它采用了AOP切面思想(那些 Interceptors),也实现高内聚低耦合,但是在实际使用的时候有一些问题:

  • 用户网络请求的接口配置繁琐,尤其需要配置复杂请求body 、请求头 的时候

  • 数据解析过程中需要用户手动拿到 responseBody 进行解析,(请求参数)不能复用

  • 无法适配自动进行线程切换

  • 存在嵌套的网络请求(如:UserId 获取到之后再获取用户详细信息,详细信息获取后再下单),就会陷入“回调地狱”

基于此,Retrofit 就诞生了,它主要解决 2 个问题:

  • 请求前:统一配置网络的请求头,一致适配请求 request

  • 请求后:数据适配(解析json数据)、线程切换

Retrofit 总共 16个类,却采用了 8 种设计模式。

二、Retrofit 类的本身设计思想

2.1 Retrofit 的整体使用

总体分为4个步骤:

  1. 构建一个Retrofit

  2. 基于访问接口创建一个 接口类型的对象

  3. 基于接口类型的对象以及参数构建一个 Okhttp Call

  4. 将请求 Call 添加到 Okhttp 的请求队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//第一步
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();

//第二步
GitHubService service = retrofit.create(GitHubService.class);

//第三步
Call<List<Repo>> repos = service.listRepos("octocat");

//第四步
repos.enqueue(new Callable<List<Repo>>(){
public void onResponse(XX xx, YY yy) {

}
public void onFailure(M m ,N n) {

}
});

其中,接口 GitHubService.java 的代码可能是这样的 :

1
2
3
4
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}

根据上面的代码,我们可以有几个疑问:

  • 看第二步,貌似我们的接口被直接使用了?根据我们学习的 Java 知识,接口不能被直接使用啊,没有接口实例啊

  • 第三步传入的 参数 “octocat” 到底去哪里了

  • url 到底怎么形成的?因为我们传入的只有一个主域名 “https://api.github.com/"

  • 为什么所有的请求都是同样的方式

当你一行行看代码的时候就完蛋了,就会陷入细节,应该先看框架,看大体,思路流程好了之后再看细节。

参数大于 5 个 && 带有可选参数,我们就可以使用构建者模式,这是使用 Builder 的一个原则。

三、Retrofit 设计的 AOP 思想

从代码:

GitHubService service = retrofit.create(GitHubService.class);

可以看出,retrofit 肯定创建了接口子类对象了, 不然不可能给接口变量赋值。在 Retrofit 类中这个 create 方法的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public <T> T create(final Class<T> service) {
validateServiceInterface(service);
return (T)
Proxy.newProxyInstance(
service.getClassLoader(),
new Class<?>[] {service},
new InvocationHandler() {
private final Object[] emptyArgs = new Object[0];

@Override
public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
args = args != null ? args : emptyArgs;
Platform platform = Platform.get();
return platform.isDefaultMethod(method)
? platform.invokeDefaultMethod(method, service, proxy, args)
: loadServiceMethod(method).invoke(args);
}
});
}

从上面可以看出来,这是动态代理实现的。它会生成一个

四、是否可以作为单例

Retrofit 在使用的时候,能不能封装成单例呢?很多同学在 build 出来 Retrofit 对象后,将其作为单例保存,但其实不建议这么做。老师说的是可能带来内存泄漏,在 build 的时候,会有很多变量都会保存起来在 Retrofit 中,都会一直占内存。(对于这个论据不太认可,网络是会贯穿整个 App 使用周期的,所以网络的对象持续整个 App 生命周期感觉问题也不大)

如果网络请求非常非常频繁,比如股票,那我们就可以考虑 netty 。普通 App 的请求使用完就可以销毁。

五、ServiceMethod 的存在价值

注解的使用:

  • apt 技术:生成代码

  • 注解 + 反射 运用

ServiceMethod 主要作用就是解析注解,解析请求返回值

将接口转变为 Okhttp 的 Call ,把注解和传入的参数一起封装在里面。所以,一个请求接口对应一个 ServiceMethod ,当一个 interface 中有多个 方法时,就会对应多个 ServiceMethod 。

默认的话,获取到的 Call 是 ExecutorCallbackCall 这个类型。第四步中的 enqueue 操作,最终会调用到 OkhttpCall 的 enqueue ,会里面会创建一个真正的 OkhttpCall 。

ServiceMethod 中封装了所有的变量,所以 toRequest 方法可以利用这些变量(包括 post/get 等)封装出一个 OKhttp 的 Request 对象。

缓存和生命周期是 Glide 的精华。

一、资源封装

Glide 总共有三级缓存,分别是:

  • 活动缓存

  • LRU 内存缓存

  • 磁盘缓存

整个资源的获取流程是比较复杂的,一张图来说就是这样:

Glide缓存加载过程

具体而言就是:

  1. 加载图片的时候,首先从活动缓存中获取,如果没有,则去LRU 内存缓存中获取

  2. 如果LRU内存缓存中有的话,则将图片剪切(图片从内存缓存中移动到活动缓存中,LRU内存缓存自己就没有了)到活动缓存并且使用

  3. 如果 LRU 内存中没有的话,则会请求网络,请求成功后保存到磁盘缓存中,并且复制一份到活动缓存供马上使用

  4. 当监听到生命周期执行 onDestroy (非 Application 生命周期情况下利用空白 Fragment 监听),则会将活动缓存中的图片移入LRU内存缓存中

注意,LRU 内存缓存和 磁盘缓存都采用了 LRU 算法存储图片

资源封装

Glide 的资源封装,是将图片 url 作为 key(需要处理下,比如是用sh256处理) ,Bitmap 作为 Value

活动缓存(ActiveCache.java)

提到缓存,肯定就涉及到:容器、put 、get 这三个

活动缓存的容器就使用一个 普通的 HashMap 即可,因为只需要存储目前正在使用的图片,其中key 为 处理过的 url ,value 为封装的 Bitmap 即可。同时各个元素添加进来的时候实现接口 callback ,方便在生命周期变化的时候回收和移动封装的 Bitmap 。

内存缓存(MemoryCache.java)

LRUCache 的实现中,sizeOf 默认是返回 1 ,意味着,默认情况下每个元素的大小是 1 不是实际的图片大小。所以在 MemoryCache.java (继承了 LRUCache 类)需要重写其 sizeOf 方法。

Android 获取 Bitmap 的大小

在最开始的时候,使用 Bitmap.getRowBytes ,这个方法最终是在 native 层实现的。到了 API 12 也就是 3.0 的时候,开始更改方法,变成了 Bitmap.getByteCount() ,它是在 Java 层实现的。到了 API 19 也就是 4.4 的时候,又变成了 Bitmap.getAllocationByteCount 了,又放到 Native 层实现了。

说一下,为什么我们设计的时候,需要将 url 进行处理后才能作为key ?这是因为我们磁盘存储文件的时候,命名不能包含斜杠和冒号等内容,会直接报错的的

Matrix 在每个方法的开始和结束的时候,添加了 埋点,那么这是怎么实现的呢?字节码插桩技术,是在编译的时候将代码放进去的。

内部类访问外部类的私有属性的原理:

1
2
3
4
5
6
7
8
9
10
public class OutClass {
int i ;
private int j;

class InnerClass {
public InnerClass(){
int k = i + j;
}
}
}

为什么内部类 InnerClass 能直接使用 OutClass 中的 i 和 j ,尤其 j 还是 private 的。这是因为编译后,外部类会生成有一个静态方法:

1
2
3
static int access$000(com.demo.OutClass clazz) {
return clazz.j;
}

所以,最后内部类是通过静态方法获取到 j 的值。传入了 OutClass 的对象。而 i 是怎么拿到的呢?其实是通过 getfield 指令。

6.0 以后,Android 权限分为正常权限和危险权限,危险权限有以下几种:

  • calendar
  • Camera
  • Location
  • Phone-拨打电话
  • SMS -短信相关
  • Storage -读取存储相关的权限

权限是一组一组的,如果你申请了 读取 存储的权限,那么写存储的权限可以不用,因为它们是同一组的。

权限申请整体代码过程如下图所示:

权限申请代码流程

在 Activity 的 requestPermissions 的方法里面,最终会调用 startActivityForResult 启动授权 Activity ,这也是为什么我们能收到 onRequestPermissionsResult 的原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
if (requestCode < 0) {
throw new IllegalArgumentException("requestCode should be >= 0");
}
if (mHasCurrentPermissionsRequest) {
Log.w(TAG, "Can request only one set of permissions at a time");
// Dispatch the callback with empty arrays which means a cancellation.
onRequestPermissionsResult(requestCode, new String[0], new int[0]);
return;
}
Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
mHasCurrentPermissionsRequest = true;
}

这里面有个小的知识点,Intent 是通过 build 创建出来的,这里是启动隐式的 App:

1
2
3
4
5
6
7
8
9
public Intent buildRequestPermissionsIntent(@NonNull String[] permissions) {
if (ArrayUtils.isEmpty(permissions)) {
throw new IllegalArgumentException("permission cannot be null or empty");
}
Intent intent = new Intent(ACTION_REQUEST_PERMISSIONS);
intent.putExtra(EXTRA_REQUEST_PERMISSIONS_NAMES, permissions);
intent.setPackage(getPermissionControllerPackageName());
return intent;
}

直接使用 new Intent(ACTION_REQUEST_PERMISSIONS) 的方式,这是通过隐式意图来激活某个 Activity ,根据常量 ACTION_REQUEST_PERMISSIONS 可以在源码找到(肯定是在 AndroidManifest.xml 中)这个 Activity 就是 GrantPermissionsActivity 。这种隐藏的 App (比如apk安装器)我们直接启动是启动不了的。

用户点击确认授权,会通过进程间通信交给PermissionManagerService 去处理,当然,过程还需要 PKMS 去查询这个 App 。

当授权通过后,会通过 Settings.writeRuntimePermissionxxx 方法将这个记录保留到 xml (system/users/0/runtime-permissions.xml 文件)中永久保存,此后会一直存在,如果卸载 App ,就将这个记录删除掉。

权限申请源码流程总结:

第一步:MainActivity 调用 requestPermissions 进行动态权限申请;
第二步:requestPermissions函数通过隐式意图,激活PackageInstaller的GrantPermissionsActivity界面,让用户选择是否授权;
第三步:经过PKMS把相关信息传递给PermissionManagerService处理;
第四步:PermissionManagerService处理结束后回调给—->PKMS中的onPermissionGranted方法把处理结果返回;
第五步:PKMS通知过程中权限变化,并调用writeRuntimePermissionsForUserLPr函数让PackageManager的settings记录下相关授权信息;

手写无侵入式框架

为什么要写?我短短一个 Activity 代码,申请权限却要写很长很长的权限申请代码。

权限申请

权限被取消

权限被取消,还勾选不再提示

可以用到 AspectJ ,这个后台用到,可以控制注解方法的执行不执行,满足条件执行 A 方法,不满足不执行。这个框架很复杂,是 javac 的二次封装。

这个框架的设计图如下:

无侵入框架设计图

以后的框架,应该都需要是 ,无侵入式的(由框架劫持用户的行为) AspectJ 劫持函数的执行

AspectJ 为什么可以无侵入式的 监听+劫持 我们的任何注解
javac Test.java Test.class (JVM只认识class) (我们看不懂字节码,JVM看得懂)
AspectJ(Javac) Test.java 注入代码 Test.class (JVM只认识class) (我们看不懂字节码,JVM看得懂)

空白的Activity(申请权限 申请成功 申请失败 用户拒绝申请 回调给外界 告诉AspectJ)

任何一个框架,都有三种方式实现:
1.无侵入式的 由框架监听用户 劫持用户的行为,用户是没有能力调用框架的(依赖AspectJ)
2.APT 注解处理器 侵入式的框架,编译期 Dagger2 Room ARouter DataBinding
3.传统 xUtils 反射

无侵入式:用户没有能力调用我们的框架,它连看都看不到我们的框架,
是由我们的框架,全局监听用户的行为,劫持用户,控制用户,执行用户

我们框架特点:
我只需要使用三个注解就行了

用户没有能力调用框架的API

用户也不需要传递this

由我们的框架 来 监听用户的 注解的

面试题

动态申请权限的流程?

有 15 步,应用进程跨系统服务进程 PKMS 通过 PermissionManagerService 检查权限情况,回调回来给 PKMS ,并且通过 Settings 通过 io 操作将权限结果写入 xml 文件中永久保存,除非卸载,才删除这个记录

看源码最重要的 第一步,只管主线,简单走通,不要管支线

面试题:项目中已经大量使用 Glide ,但加载图片还是偶尔会出现内存溢出问题,说明大概原因。

答:可能在 Glide.with 的时候,传入了 Application 的作用域,或者从子线程使用了 Glide 也会导致变为 Application 作用域。在这种作用域下,不会创建空白 Fragment 对绑定页面进行生命周期管理。就会造成内存回收不及时的问题。

into 方法的时候,RequestBuilder 中会根据 ImageView 的 ScaleType 来生成不同的 ScaleType 的对象:

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
public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) {

if (!requestOptions.isTransformationSet() && requestOptions.isTransformationAllowed() && view.getScaleType() != null) {

switch (view.getScaleType()) {
case CENTER_CROP:
requestOptions = requestOptions.clone().optionalCenterCrop();
break;
case CENTER_INSIDE:
requestOptions = requestOptions.clone().optionalCenterInside();
break;
case FIT_CENTER:
case FIT_START:
case FIT_END:
requestOptions = requestOptions.clone().optionalFitCenter();
break;
case FIT_XY:
requestOptions = requestOptions.clone().optionalCenterInside();
break;
case CENTER:
case MATRIX:
default:
// Do nothing.
}
}

return into(
glideContext.buildImageViewTarget(view, transcodeClass),
/*targetListener=*/ null,
requestOptions,
Executors.mainThreadExecutor());
}

总结一下,Glide 中最重要的 3 个分段 with、load、into 的三个作用:

  • with:返回 RequestManager ,里面决定是哪种作用域

    1
    2
    3
    4
    5
    6
    7
    public RequestManager get(FragemntActivity activity) {
    if(Util.isOnBackgroundThread()) {
    //子线程,Application
    } else {
    //Activity/Fragment 作用域
    }
    }
  • load:最终返回 RequestBuilder

  • into : 最终返回 ImageViewTarget

根据宽高、签名、等等一系列信息,作为某个图片缓存的 key ,在内存缓存和磁盘中获取这些缓存的图片。

关于缓存

Glide 里面有活动缓存(ActiveResources)和内存缓存(Cache,也叫二级缓存,也叫LRU缓存),他们都在内存里面。那为什么要在内存里面设置 2 级缓存?Glide 将正在显示的图片都放在活动缓存里面(活动缓存里面都是使用 WeakReference 来引用图片),然后其他的图片都放在内存缓存里面。这又是什么讲究?内存缓存是 LRUCache 实现的,它可以存储很多图片,假如没有活动缓存这一级,而是直接使用内存缓存的话,那么在缓存的图片数量或者大小超限的时候,正在使用的图片就可能被清除掉,导致崩溃 Bug(比如RecyclerView里面使用,划过来可以,再划回去重新加载的时候被回收了就崩溃了) ,这也是为什么需要设计 2 级缓存。

当应用需要获取图片的时候,首先从活动缓存中获取,如果没有,则去内存缓存中获取,如果命中了,则将 内存缓存中的图片添加到活动缓存中,并且将图片资源从内存缓存中删除。当引用的页面(Activity/Fragment)关掉之后,活动缓存的图片又可以放入内存缓存中去,如果页面再次打开,就又可能从内存缓存中加载进入活动缓存。活动缓存和内存缓存中只能存在一份缓存,不可能同时在 内存缓存和活动缓存中都存在。活动缓存里面会做引用计数,如果计数为 0 的时候,会将图片放回到内存缓存里面。

面试官:Glide 源码中到处都是接口,我们应该怎么阅读?

答: 我们要找里面的伏笔。比如,我们 getRequest 的时候,要一直追踪下去,看看到底这个 getRequest 返回的是 Request 的哪个子类。不然很难知道具体是哪个类实现。

面试官:使用 Glide 为什么需要加入网络权限?

答: Glide 中执行图片请求的时候 有等待队列和运行队列2个队列,并且有 活动缓存和内存缓存2级缓存,如果这2级缓存都没有命中的话,需要通过网络去获取资源。并且,还可以通过 job.get 去判断目前任务是否完成,最终使用 UrlConnection 去完成最终的网络请求。

我们平时使用 Glide 一般都是传入 String 类型的 url ,然后会返回 InputStream 这种流,decode 的作用就是将流转成 Bitmap 。

面试官:使用 Glide 的时候,如果 with 函数在子线程调用,会有什么问题?

答:子线程不会去添加生命周期机制,主线程才会添加空白Fragment 监听 Activity/Fragment 的生命周期变化。

面试官:with 函数传入 Application ,会怎么样?

答:如果传入的是Activity 或者 Fragment ,当它们销毁的时候,Glide 会回收当前页面加载的图片任务和资源,但是如果传入的是 Application ,那么只有当应用结束的时候资源才会跟随销毁了。

如果ImageView 很小,但是图片是个很大的图片,Glide 会给做优化,只会给目标大小的图片就可以了。

最后,以一张Glide 的简化流程图结束:

简化的Glide流程图

一、为何监听Activity/Fragment 生命周期

Glide 的使用很简单:

1
Glide.with(this).load().into(imageView)

其中的职责很简单:

  • with : 生命周期管理。传入 Activity 或者 Fragment 就会创建一个空白 Fragment 来监听生命周期,传入 Application Context 则不会

  • load:构建出 RequestBuilder 对象

  • into : 做了几件事情:1)运行队列、等待队列 2)活动缓存 3)内存缓存 4)网络模型

面试题:我们使用了 Glide.with(this).load().into 加载图片之后,在 Activity 的 onDestroy 中是不是要调用:Glide.with(this).clear(imageView) 来清理掉不用的 View 呢?

很多同学可能会回复这是必须的,但是事实上这不是必需的,因为 Glide 内部会有监听机制,在 Activity 的onDestroy 中会自动 clear 掉。

二、生命周期作用域

那么 Glide 是怎么做到自动 clear 呢?原理在于它会创建一个空白的 Fragment 来监控 Activity/Fragment 的生命周期变化

你可以发送很多的加载图片的request ,这些统一由 RequestManager 去管理。

如果在子线程使用 Glide 加载图片,即时你传入 Activity ,也是在 Application 作用域,这时候不会给你搞那个空白的 Fragment 。总结一下各种情况下根据 with 传入的参数而产生的生命周期作用域:

  • 在子线程:作用域为 Application

  • (在主线程)传入 ServiceContext/Application Context: 作用域为 Application

  • (在主线程)传入View:作用域为 Fragment 或者 Activity

  • (在主线程)传入 Fragment: 作用域为 Fragment

  • (在主线程)传入 Activity: 作用域为 Activity

所以也可以总结说 Application 作用域和非Application 作用域,这是根据是否创建空白 Fragment 监听生命周期这个动作来区分的

老师说学习开源框架最好的方式就是:先大概看一遍,然后仿照它的 API 开始自己写,把它所有的实现都简化:比如它是工厂模式创建对象,那我就直接 new 出来;假如它有各种判空,直接干掉,假如有复杂的条件判断,先干掉。实现最简单的,这样才能快速理解整个流程。

RequestManager 中,生命周期 onStart 的时候: 运行队列-全部开始执行;等待队列-全部清空;onStop 的时候: 运行队列-全部停止,所有任务添加到等待队列;

Glide 中设计很巧妙的一个点就是:Glide 需要保证一个 Activity 只能有一个空白 Fragment 来监听它的生命周期,那么在 RequestManagerRetriever 这个类中,就会写出看起来匪夷所思的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private RequestManagerFragment getRequestManagerFragment(FragmentManager fm, Fragment parentHint, boolean isParentVisible) {
RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
if (current == null) {
current = pendingRequestManagerFragments.get(fm);
if (current == null) {
current = new RequestManagerFragment();
current.setParentFragmentHint(parentHint);
if (isParentVisible) {
current.getGlideLifecycle().onStart();
}
pendingRequestManagerFragments.put(fm, current);
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
}
}
return current;
}

创建了这个 fragment 需要将其放入map 中先保存起来,之后再 commit 这个 fragment ;再然后就通过 Handler 移除这个 fragment ,为什么这么做?

这是因为 fragment 做 commit 操作的时候,是通过主线程的 Handler 执行的,最后会体现在handler 往主线程发送了一个 Message,如果这个Message 还没执行,此时来了第二个请求,那么它通过 getTag 去获取这个 Fragment 是获取不到的,那么就会再创建一个,所以这里需要缓存下;那为什么在 commit 之后可以通过(主线程的) Handler 移除缓存的Fragment 呢?那么是因为 ,由于 commit 操作先执行,那么 commit 这个 Message 肯定在后续的移除 fragment 的这个 Message 之后,由于 Handler 是顺序执行这些 Message 的,所以执行移除操作的时候,commit 的那个 message 肯定已经执行过了,所以可以执行!

JetPack 的 Lifecycle 就是 模仿的 Glide 。Glide 的源码太庞大了。

有两个空白 Fragment ,一个是 Androidx 的 Fragment ,一个是 android.app 的 Fragment 。我们要注意区分。

三、生命周期回调

Fresco 看起来非常舒服,很容易看懂

但是 Glide 可能你看了一周源码,还是找不到网络请求的地方,看懂 Glide 的源码之后,再去看 Fresco 和 Picaso 都是很简单的事情。老师说是要研究半年才能给大家说这个事情。

老师说 RxJava 和 Okhttp 在 Glide 面前是小弟。这个也确实比较复杂