碎片笔记


  • 首页

  • 归档

Android编译期修改Class字节码

发表于 2019-01-11

Android 打包过程

先放一张官网的图和描述

  1. 编译器将源代码转换成DEX文件,将所有其他内容转换成编译资源。
  2. APK打包器将DEX文件和已编译资源合并成单个APK。
  3. APK打包器使用DEBUG或者RELEASE的签名文件签署APK。
  4. 在生成最终APK之前,打包器会使用zipalign工具对APK进行优化,减少设备运行时的内存占用。

再放一张较为详细的图和描述

  1. 通过AAPT打包resource资源文件,编译成R.java、resource.arsc等资源相关文件。
  2. 通过aidl工具处理源码中的.aidl文件,生成对用的java文件。
  3. 通过Java Compiler(javac)工具编译.java文件生成.class文件。
  4. 通过dx工具,将第三方jar、.class文件以及上一步生成的.class文件以及R.class文件打包成dex文件(Android可执行文件)。
  5. 通过apkbuilder工具将dex以及第一步的资源文件打包成apk。
  6. 通过apksigner工具,使用DEBUG或者RELEASE签名文件,签署apk文件。
  7. 通过zipalign工具,对其优化apk。

再放一张更为详细的图和描述

从上面的这些图来看,如果想要在编译期修改.class文件就必须需要在dx工具将.class文件打包成dex文件之前去修改才行,我们发现最详细的那张图中,在生成dex文件之前,经过了proguard混淆加密的过程,它就是一个修改.class文件的过程,我们可以参考produard是如何实现的。

Gradle插件

Proguard是通过Transfrom Api实现的,而Transform是依附于Gradle插件之上的,所以先介绍自定义Gradle插件。AndroidStudio中实现自定义Gradle插件有三种方式,一种是.gradle脚本文件,一种buildSrc模块,最后一种是独立工程。实现插件可以选择使用groovy、java以及kotlin语言。Groovy语言ide高亮支持的不是很好,没有引入的类库不会标红,难受,所以我写插件一般选择java或者kotlin。

.gradle脚本

gradle文件,构建脚本内,不过这种只能在文件内使用。

单独Module的插件

可以随意一个工程去引用,前提是需要发布到本地仓库或者maven私服。

buildSrc

可以在自己的工程内随意使用,但是其他工程就无法使用。这种模式和单独Module的插件的唯一区别就是单独Module插件需要上传,工程中无法直接引用。单独Module的插件中的build.gradle文件需要有上传的脚本代码。这里看一下buildSrc形式插件的具体结构目录:

  • build.gradle
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    apply plugin: 'java' // groovy: apply plugin: 'groovy'

    dependencies {
    implementation gradleApi() // gradle sdk
    }

    repositories {
    google()
    jcenter()
    }

如果是单独module插件形式就多了上传的代码。

  • TestEntryPlugin.java
    插件的入口文件。

    1
    2
    3
    4
    5
    6
    public class TestEntryPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
    System.out.println("custom plugin"); // 打印
    }
    }
  • resources下的.properties
    指明插件的入口文件。

    1
    implementation-class=me.jiahuan.plugin.TestEntryPlugin

以上就是最基本的插件的实现,实现完成之后就需要应用当前插件,只需要在app 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
23
24
25
26
27
apply plugin: 'me.jiahuan.plugin'
```
添加完成后,当点击sync project with grale files按钮就可在build panel中看到代码中的打印信息:
![](https://ws4.sinaimg.cn/large/006tNc79ly1fz1mtij6uhj30gg07kjsl.jpg)

简单说一下Gradle Project的概念,在Gradle中一个build.gradle就代表一个project。可以通过org.gradle.api.Project对象的Project.getExtensions().create(name,class)方法获取build.gradle中写的DSL。

Gradle插件在Android开发中担负中比较重要的角色,最熟悉的插件就是`com.android.application`,这是开发Android的基础。像一些比较成熟的第三方框架,也会开会插件去让开发者更方便的接入,也有一些组件化的方案会通过插件去解耦等等。


## Gradle Transform Api
Transform Api是Android Gradle Plugin 1.5版本 之后引入的概念。第三方可以构建Gradle插件来使用Transform Api去修改编译之后的.class文件。Android中的proguard就是以此实现的,翻阅[TaskManager](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/TaskManager.java)发现Android Gradle Plugin中还有很多的transform,如jacoco、proguard、multi-dex以及InstantRun等等。

### Transform基本原理
Transform可以有很多个,他们形成责任链模式。Transform有输入和输出,输入是上一个transfrom的输入。因此我们可以在自定义的transfrom中修改.class文件,输出给下一个transform。第一个transfrom的输入就是编译打包过程中通过Java Compiler(javac)工具编译.java文件生成.class文件以及R.class文件。
![](https://ws3.sinaimg.cn/large/006tNc79ly1fz1n0dikrjj30mb03owei.jpg)

说完Transform的传递,来简单说一下transform输入数据的过滤。transform从两个维度过滤数据。
- ContentType
```java
CONTENT_CLASS
CONTENT_JARS
CONTENT_RESOURCES
CONTENT_NATIVE_LIBS
CONTENT_DEX
CONTENT_DEX_WITH_RESOURCES
DATA_BINDING_BASE_CLASS_LOG_ARTIFACT

有些ContentType只能被Android Plugin使用。

  • Scope
    1
    2
    3
    4
    5
    6
    7
    8
    PROJECT_ONLY
    SCOPE_FULL_PROJECT
    SCOPE_FULL_WITH_IR_FOR_DEXING
    SCOPE_FULL_WITH_FEATURES
    SCOPE_FULL_WITH_IR_AND_FEATURES
    SCOPE_FEATURES
    SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS
    SCOPE_IR_FOR_SLICING

同样的,我们只能用某些Scope。

Transform简单实现

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

public class TestTransform extends Transform {

private Project mProject;

public TestTransform(Project project) {
mProject = project;
}

@Override
public String getName() {
return "TestTransform";
}

@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS; // ContentType 过滤
}

@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT; // Scope 过滤
}

@Override
public boolean isIncremental() {
return false;
}

@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
// 当前是否为增量编译
boolean isIncremental = transformInvocation.isIncremental();
// 消费型输入,可以从中获取jar包和class文件夹路径。需要输出给下一个任务
Collection<TransformInput> inputs = transformInvocation.getInputs();
// OutputProvider管理输出路径,如果消费型输入为空,OutputProvider = null
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

//
if (!isIncremental) {
outputProvider.deleteAll();
}

// 获取输入
for (TransformInput input : inputs) {
// 获取输入 .jar文件
for (JarInput jarInput : input.getJarInputs()) {
// 这里可以修改文件达到目的
...
// 设置输出
File dest = outputProvider.getContentLocation(
jarInput.getFile().getAbsolutePath(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
FileUtils.copyFile(jarInput.getFile(), dest);
}

// 获取输入 .class文件
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
// 这里可以修改文件达到目的
...
// 设置输出
File dest = outputProvider.getContentLocation(
directoryInput.getName(),
directoryInput.getContentTypes(),
directoryInput.getScopes(),
Format.DIRECTORY);
FileUtils.copyDirectory(directoryInput.getFile(), dest);
}
}
}
}
  • TransformInput
    上一个transform的输出文件,分为JarInput和DirectoryInput。输入输出的.class文件都可以在build/intermediates/transforms中找到:

  • TransformOutputProvider
    transform输出,用来获取输出.class文件路径。

以上展示的transform代码,对.class文件没有任何修改的输出。Transform还有更高级的增量修改,可以看看这篇博客

Java字节码

理论就不说了。我们要掌握的是如何查看.java .kt文件字节码,然后参照源码和字节码一一对应。就像学习kotlin语言,你写好java版本的,然后用工具转换成kotlin版本参照学习。在AndroidStudio中居然没有什么比较简单的方式查看.java文件的字节码,插件也不是很好用,我们可以利用javap命令来查看:

1
javap -c XXX.class

kotlin文件到是在AndroidStuido中集成了工具,查看就会比较方便,如下:


很好用,鼠标点到哪里,就对应高亮哪一块对应的字节码。PS:如果在kotlin代码中使用协程,在decomile的时候,as会死掉。应该是什么bug。

ASM 字节码控制框架

正常来说修改.class文件就是文件的读写,你可以在任意位置插入任意字节码,但是想要在特定的地方插入特定的字节码还是相当复杂的。市面上有很多字节码的修改框架,本篇使用的是ASM。举个小栗子,上一部分的hello函数中的CostCalculator.startCal(“hello”),在.class文件中插入相同含义的字节码:

  1. 拿到代码对应的字节码

    1
    2
    3
    GETSTATIC me/jiahuan/gradle_plugin_sample/CostCalculator.INSTANCE : Lme/jiahuan/gradle_plugin_sample/CostCalculator;
    LDC "hello"
    INVOKEVIRTUAL me/jiahuan/gradle_plugin_sample/CostCalculator.startCal (Ljava/lang/String;)V
  2. 一一对应写ASM代码

    1
    2
    3
    mv.visitFieldInsn(GETSTATIC, "me/jiahuan/gradle_plugin_sample/CostCalculator", "INSTANCE", "Lme/jiahuan/gradle_plugin_sample/CostCalculator;");
    mv.visitLdcInsn("hello");
    mv.visitMethodInsn(INVOKEVIRTUAL, "me/jiahuan/gradle_plugin_sample/CostCalculator", "startCal", "(Ljava/lang/String;)V", false);

一条对一条很简单。这里具体不讲解如何使用的,因为好难说清楚:),直接看下面的具体实际例子,看看代码。PS:asm插入的字节码不会影响原来代码的行数,所以不用担心报错的时候堆栈行数不一致。

实例:统计每个函数的耗时

有这样一个需求,需要统计每个函数消耗的时间,找出一些比较耗时的函数进行优化。如果是传统的怎么做?每个函数进入的时候记录个时间,然后最后return的时候记录个时间,相减一下。如果是个小项目,可能动手每个函数加一加没什么问题,但是大项目要这么做就有点过份了。那了解了编译器修改.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
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
public class TestDirectoryInputHandler {
public static void handle(File dir) {
System.out.println("class dir path = " + dir.getAbsolutePath());
if (dir.isDirectory() && dir.exists()) {
// 递归遍历
for (File file : dir.listFiles()) {
handle(file);
}
} else {
handleFile(dir);
}
}

private static void handleFile(File file) {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
if (file.getName().equals("MainActivity.class")) {
System.out.println("find!!!!!!!!!!!");
inputStream = new FileInputStream(file);
ClassReader classReader = new ClassReader(inputStream);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// 自定义ClassVisitor
CallClassVisitor callClassVisitor = new CallClassVisitor(classWriter, file.getName());
classReader.accept(callClassVisitor, 0);
// 输出临时class文件路径
String tempFilePath = file.getParentFile().getAbsolutePath() + File.separator + file.getName() + ".opt";
File tempFile = new File(tempFilePath);
outputStream = new FileOutputStream(tempFilePath);
outputStream.write(classWriter.toByteArray());
if (tempFile.exists()) {
tempFile.renameTo(file);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}

if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}


static class CallClassVisitor extends ClassVisitor implements Opcodes {

private String mClassName;

public CallClassVisitor(ClassVisitor cv, String className) {
super(ASM6, cv);
mClassName = className;
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("method name = " + name + ", signature = " + signature + ", desc = " + desc);
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
return mv == null ? null : new CallMethodVisitor(mv, mClassName, name, desc);
}
}

static class CallMethodVisitor extends MethodVisitor implements Opcodes {

private String mName;
private String mDesc;
private String mClassName;

public CallMethodVisitor(MethodVisitor mv, String className, String name, String desc) {
super(ASM6, mv);
mClassName = className;
mName = name;
mDesc = desc;
}

@Override
public void visitCode() {
if (mName.startsWith("hello")) {
// GETSTATIC me/jiahuan/gradle_plugin_sample/CostCalculator.INSTANCE : Lme/jiahuan/gradle_plugin_sample/CostCalculator;
mv.visitFieldInsn(GETSTATIC, "me/jiahuan/gradle_plugin_sample/CostCalculator", "INSTANCE", "Lme/jiahuan/gradle_plugin_sample/CostCalculator;");
// LDC "haha"
mv.visitLdcInsn(mClassName + mName + mDesc);
// INVOKEVIRTUAL me/jiahuan/gradle_plugin_sample/CostCalculator.startCal (Ljava/lang/String;)V
mv.visitMethodInsn(INVOKEVIRTUAL, "me/jiahuan/gradle_plugin_sample/CostCalculator", "startCal", "(Ljava/lang/String;)V", false);
}
}

@Override
public void visitInsn(int opcode) {
if (mName.startsWith("hello")) {
if (opcode == RETURN) {
// GETSTATIC me/jiahuan/gradle_plugin_sample/CostCalculator.INSTANCE : Lme/jiahuan/gradle_plugin_sample/CostCalculator;
mv.visitFieldInsn(GETSTATIC, "me/jiahuan/gradle_plugin_sample/CostCalculator", "INSTANCE", "Lme/jiahuan/gradle_plugin_sample/CostCalculator;");
// LDC "haha"
mv.visitLdcInsn(mClassName + mName + mDesc);
// INVOKEVIRTUAL me/jiahuan/gradle_plugin_sample/CostCalculator.endCal (Ljava/lang/String;)V
mv.visitMethodInsn(INVOKEVIRTUAL, "me/jiahuan/gradle_plugin_sample/CostCalculator", "endCal", "(Ljava/lang/String;)V", false);
}
}
super.visitInsn(opcode);
}
}
}

以上实现的是查找MainActivity.class中hello开头的函数,在函数的头和尾插入的统计耗时。以下是原MainActivity.kt以及编译经过transform之后.class文件的逆向:

Sample完整源码

总结

此项技术可以应用于比较多的实用需求,比如埋点的插桩实现方式、方法性能检测以及日志监测等等需求。当然也可以直接生成某些类,实现和注解生成器一样的效果。

使用Kotlin语言编写Andriod Gradle脚本

发表于 2018-11-13

概述

当我们通过AS新建一个新的项目的时候,工程的结构目录如下图所示

其中,以gradle结尾的文件(图中红框圈出),是我们在Android开发中经常需要修改的gradle脚本文件。通常gradle脚本是由Groovy语言编写的,例如:

这种写法称为DSL,指的是用于一个特定领域的语言。Groovy本身不是DSL语言,它是一种通用语言,但是因为Groovy的语言的特性对DSL提供了很好的支持,Kotlin语言也是如此。Groovy以及Kotlin语言有一个这样的特性:如果一个函数的最后的一个参数是一个lamda表达式,则可以写在()之外,如果这个函数只有一个参数并且就是lamda表达式,那么()也可以省略。因此,以上图中显示的脚本的{}就可以理解为一个函数。

根目录下的 build.gradle => build.gradle.kts

Kotlin编写gradle脚本需要将脚本文件的后缀添加kts(kotlin-script)。

  • build.gradle

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    buildscript {
    repositories {
    google()
    jcenter()
    }
    dependencies {
    classpath 'com.android.tools.build:gradle:3.2.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
    }
  • build.gradle.kts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    buildscript {
    repositories {
    google()
    jcenter()
    }
    dependencies {
    classpath("com.android.tools.build:gradle:3.2.0")
    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
    }
    }

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

settings.gradle => settings.gradle.kts

既然根目录的gradle脚本修改了名字,那么就要告知gradle编译工具寻找修改名字之后的文件,因此要在settings.gradle.kts文件中指定。

  • setting.gradle
    1
    include ':app'

-setting.gradle.kts

1
2
include("app")
rootProject.buildFileName = "build.gradle.kts"

Module中的 builde.gradle => build.gradle.kts

- build.grale

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
apply plugin: 'com.android.application'

android {
compileSdkVersion 28
defaultConfig {
applicationId "me.jiuahuan.gradle"
minSdkVersion 18
targetSdkVersion 28
versionCode 1
versionName "1.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation 'com.android.support:appcompat-v7:28.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

  • build.gradle.kts
    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
    plugins {
    id("com.android.application")
    }

    android {
    compileSdkVersion(28)
    defaultConfig {
    applicationId = "me.jiahuan.gradle"
    minSdkVersion(18)
    targetSdkVersion(28)
    versionCode = 1
    versionName = "1.0.0"

    testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
    getByName("release") {
    isMinifyEnabled = false
    proguardFiles("proguard-rules.pro")
    }
    }
    }

    dependencies {
    implementation("com.android.support:appcompat-v7:28.0.0")
    testImplementation("junit:junit:4.12")
    androidTestImplementation("com.android.support.test:runner:1.0.2")
    androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.2")
    }

统一第三方依赖版本管理

通常在开发过程我们会统一规划第三方依赖库的版本或者统一编译版本,我们会在根目录的build.gradle中编写,如:

1
2
3
4
5
6
7
8
9
10
ext {
config = [
compileSdkVersion: 27,
minSdkVersion : 19,
targetSdkVersion : 27,
versionCode : 1,
versionName : "0.1.0",
supportVersion : "27.1.1" // support依赖库的版本
]
}

然后在module的build.gradle中可以这样编写

1
2
implementation 'com.android.support:appcompat-v7:' + config.supportVersion
implementation 'com.android.support:design:' + config.supportVersion

但是在Koltin写法中并不是这样的,官方推荐的是新建一个叫buildSrcModule(这个也是实现gradle插件的方式之一),然后可以新建一个Kotlin配置文件来指定统一的版本号,如:

  • Config.kt
    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
    private const val kotlinVersion = "1.2.21"
    private const val androidGradleVersion = "3.0.1"

    // Compile dependencies
    private const val supportVersion = "27.0.2"
    private const val ankoVersion = "0.10.4"
    private const val daggerVersion = "2.14.1"
    private const val retrofitVersion = "2.3.0"
    private const val okhttpVersion = "3.9.1"
    private const val eventBusVersion = "2.4.1"
    private const val picassoVersion = "2.5.2"
    private const val priorityJobQueueVersion = "2.0.1"

    // Unit tests
    private const val mockitoVersion = "2.13.0"

    object Config {
    object BuildPlugins {
    val androidGradle = "com.android.tools.build:gradle:$androidGradleVersion"
    val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
    }

    object Android {
    val buildToolsVersion = "27.0.3"
    val minSdkVersion = 19
    val targetSdkVersion = 27
    val compileSdkVersion = 27
    val applicationId = "com.antonioleiva.bandhookkotlin"
    val versionCode = 1
    val versionName = "0.1"
    }

    object Libs {
    val kotlin_std = "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
    ...
    }

    object TestLibs {
    val junit = "junit:junit:4.12"
    ...
    }
    }

然后在需要的依赖的build.gradle脚本中这么写,

1
2
3
4
5
dependencies {
implementation(Config.Libs.kotlin_std)
...
androidTestImplementation(Config.TestLibs.mockito)
}

其他

关于Kotlin DSL写法的参考地址 https://github.com/gradle/kotlin-dsl


Android Jetpack Components 之 Architecture Lifecycles

发表于 2018-11-13

管理应用Activity以及Fragment的生命周期(Manage your activity and fragment lifecycles),组件位于android.arch.lifecycle包下。

Lifecycles 解决了什么的问题?

通常在一个MVP架构的应用中,Presenter会或多或少的关注Activity或者Fragment的生命周期,如

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 MainActivity : AppCompatActivity() {
private lateinit var mPresenter: MainPresenter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mPresenter = MainPresenter()
mPresenter.onCreate()
}

override fun onResume() {
super.onResume()
mPresenter.onResume()
...
}


override fun onPause() {
super.onPause()
mPresenter.onPause()
...
}

override fun onDestroy() {
super.onDestroy()
mPresenter.onDestroy()
...
}
}

在实际生产环境中,代码会非常复杂,会导致生命周期方法非常臃肿。Lifecycles组件以观察者模式的设计实现对被观察者生命周期的感知,通过注解的方式注册对应的生命周期。一般这个组件会与其他jetpack组件一起组合使用,比如ViewModel,LiveData。

Lifecycles 使用

我们使用Lifecycles将上面的例子改造一下

  • 观察者LifecyclerObserver

当前例子中,观察者是Presenter,这里定义一个IPresenter接口,让所有的Presenter实现此接口。观察者需要实现LifecycleObserver接口,然后使用@OnLifecycleEvent注解的方式注册监听被观察者的生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface IPresenter : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate()

@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart()

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume()

@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause()

@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop()

@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy()

@OnLifecycleEvent(Lifecycle.Event.ON_ANY)
fun onLifecycleChange()
}
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
open class BasePresenter : IPresenter {
companion object {
const val TAG = "BasePresenter"
}

override fun onResume() {
Log.d(TAG, "${this.javaClass.name} onResume")
}

override fun onPause() {
Log.d(TAG, "${this.javaClass.name} onPause")
}

override fun onStop() {
Log.d(TAG, "${this.javaClass.name} onStop")
}

override fun onStart() {
Log.d(TAG, "${this.javaClass.name} onStart")
}

override fun onCreate() {
Log.d(TAG, "${this.javaClass.name} onCreate")
}

override fun onDestroy() {
Log.d(TAG, "${this.javaClass.name} onDestroy")
}

override fun onLifecycleChange() {
Log.d(TAG, "${this.javaClass.name} onChange")
}
}
  • 被观察者

当前例子中,activity是被观察者,需要实现LifecycleOwner接口,接着维护一个LifecycleRegistry成员属性,在被观察者不同的生命周期函数中通过LifecycleRegistry成员属性通知生命周期的变化,调用addObserver方法添加观察者。

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
class MyActivity : AppCompatActivity(), LifecycleOwner {

private lateinit var mLifecycleRegistry: LifecycleRegistry
private lateinit var mPresenter: MainPresenter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mPresenter = MainPresenter()
mLifecycleRegistry = LifecycleRegistry(this)
mLifecycleRegistry.markState(Lifecycle.State.CREATED)
mLifecycleRegistry.addObserver(mPresenter)
}

public override fun onStart() {
super.onStart()
mLifecycleRegistry.markState(Lifecycle.State.STARTED)
// 或者
// mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
}

// LifecycleOwner需要重写的方法
override fun getLifecycle(): Lifecycle {
return mLifecycleRegistry
}

// 其他生命周期方法
...
}

如果正在使用26.1.0及其之后版本的Support Library,AppCompatActivity已经实现LifecycleOwner接口,可以查看SupportActivity的源码,相关代码如下

android.support.v4.app.SupportActivity

1
2
3
4
public class SupportActivity extends Activity implements LifecycleOwner, Component {
private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
...
}

MainActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
}

private lateinit var mPresenter: MainPresenter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mPresenter = MainPresenter()
lifecycle.addObserver(mPresenter)
}
}

Lifecycle

Lifecycle是一个类,它包含关于组件生命周期状态(比如Activity,Fragment)的信息,并允许其他对象观察该状态。

Lifecycle使用两个枚举类来跟踪相关组件的生命周期状态。

  • Event(源码位于Lifecycle类中)从LifecycleOwner对象中发出这些事件,由LifecycleObserver
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
public enum Event {
/**
* Constant for onCreate event of the {@link LifecycleOwner}.
*/
ON_CREATE,
/**
* Constant for onStart event of the {@link LifecycleOwner}.
*/
ON_START,
/**
* Constant for onResume event of the {@link LifecycleOwner}.
*/
ON_RESUME,
/**
* Constant for onPause event of the {@link LifecycleOwner}.
*/
ON_PAUSE,
/**
* Constant for onStop event of the {@link LifecycleOwner}.
*/
ON_STOP,
/**
* Constant for onDestroy event of the {@link LifecycleOwner}.
*/
ON_DESTROY,
/**
* An {@link Event Event} constant that can be used to match all events.
*/
ON_ANY
}
  • State(源码位于Lifecycle类中)当前的组件的状态

以上图可以看出某个Event对应的State

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
public enum State {
/**
* Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch
* any more events. For instance, for an {@link android.app.Activity}, this state is reached
* <b>right before</b> Activity's {@link android.app.Activity#onDestroy() onDestroy} call.
*/
DESTROYED,

/**
* Initialized state for a LifecycleOwner. For an {@link android.app.Activity}, this is
* the state when it is constructed but has not received
* {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} yet.
*/
INITIALIZED,

/**
* Created state for a LifecycleOwner. For an {@link android.app.Activity}, this state
* is reached in two cases:
* <ul>
* <li>after {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} call;
* <li><b>right before</b> {@link android.app.Activity#onStop() onStop} call.
* </ul>
*/
CREATED,

/**
* Started state for a LifecycleOwner. For an {@link android.app.Activity}, this state
* is reached in two cases:
* <ul>
* <li>after {@link android.app.Activity#onStart() onStart} call;
* <li><b>right before</b> {@link android.app.Activity#onPause() onPause} call.
* </ul>
*/
STARTED,

/**
* Resumed state for a LifecycleOwner. For an {@link android.app.Activity}, this state
* is reached after {@link android.app.Activity#onResume() onResume} is called.
*/
RESUMED;

/**
* Compares if this State is greater or equal to the given {@code state}.
*
* @param state State to compare with
* @return true if this State is greater or equal to the given {@code state}
*/
public boolean isAtLeast(@NonNull State state) {
return compareTo(state) >= 0;
}
}

这里关于State的isAtLeast方法用法举个例子,通常Presenter会持有View对象,在一定的时候调用View提供的回调方法来做相应的操作,假设这里有个需求,由一个弹框提示至少在当前Activity Resume的时候才弹出来。观察State那张图,当Activity属于Resume状态时,State为RESUMED。

1
2
3
4
5
6
7
8
9
10
11
class MainActivity : AppCompatActivity(), MainViewer {
...

override fun onShowTip() {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
AlertDialog ....
}
}

...
}

LifecycleOwner

LifecycleOwner是一个接口,它仅有一个方法getLifecycle,必须由子类实现,任何类都可以实现此接口,构建此类的生命周期感知。实现LifecycleObserver的类和实现LifecyclerOwner的类就组合成了经典的观察者和被观察者,任何被观察者都可以提供一个生命周期让观察者注册观察。

Lifecycles UML

参考

Lifecycles组件官方地址

MAC环境下源码编译Android平台FFMpeg

发表于 2018-09-13

FFMpeg

FFmpeg是一个自由软件,可以运行音频和视频多种格式的录影、转换、流功能,包含了用于多个项目中音频和视频的解码器库libavcodec,以及音频与视频格式转换库libavformat。

指令集

CPU执行任务时都需要遵从一定的规范,程序在被执行前都需要先翻译为CPU可以理解的语言,这种规范或语言就是指令集(ISA,Instruction Set Architecture)。常见的指令集有x86、x86_64、arm、mips。

交叉编译

  • 本地编译

    本地编译,意思就是在当前平台生成出当前平台的可执行的代码,只能在当前平台中执行。比如在x86平台上编译x86平台可执行的程序。

  • 交叉编译

    交叉编译,意思就是在当前平台上生成另外一个平台的可执行代码,当前平台不可执行,目标平台可执行。比如在x86平台上编译出在arm平台上可执行的程序。

  • 交叉编译工具链

    交叉编译工具链是为了跨平台编译而提供的一整套编译工,包含预处理、编译、链接、汇编等等。

NDK

NDK是Android提供的Native开发工具集,包括交叉编译工具链,可以快速开发C、C++动态库并打包进apk。在SDK Manager中可以非常方便下载NDK,如图所示:

(这里插个讲解,上图中的CMake是AS2.2之后实行的编译Native代码的方式,LLDB是调试Native代码用的)下载的NDK可以在sdk目录下找到,如图所示:

MAC下编译Android平台可用的FFMpeg链接库

  • 下载源代码,本文直接从github仓库拉下来的,版本是3.4

    FFMpeg官方网址
    FFMpegGithub地址

  • 编写编译shell脚本

    在根目录下创建build_android.sh脚本文件,键入(路径确认改)

    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
    #!/bin/bash

    # 编译临时位置,先自己创建好
    TEMPDIR=/Users/abbott/WorkSpace/jiahuan/ffmpeg/temp

    # NDK工具目录
    NDK=/Users/abbott/WorkSpace/Tools/Android/sdk/ndk-bundle

    # 设置编译平台,一般是APK最低支持的版本
    SYSROOT=$NDK/platforms/android-21/arch-arm

    # Include头文件位置 (这个跟NDK的版本有关,如果在编译的过程中发生头文件找不到的问题,记得这里,或者看参考里的第一篇文章)
    ISYSROOT=$NDK/sysroot
    ASM=$ISYSROOT/usr/include/arm-linux-androideabi

    # 交叉编译工具链,如果不是在其他环境中编译可以选择相应的环境
    TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64

    # 编译后的文件输出
    CPU=armv7-a
    PREFIX=$(pwd)/android/$CPU
    ADDI_CFLAGS="-marm -D__ANDROID_API__=21"

    # 配置有很多选项,这里不详细讲解
    ./configure \
    --enable-cross-compile \
    --enable-shared \
    --disable-static \
    --disable-doc \
    --disable-ffmpeg \
    --disable-ffplay \
    --disable-ffprobe \
    --disable-avdevice \
    --disable-symver \
    --prefix=$PREFIX \
    --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \ # 这里会找到目录下的gcc编译工具
    --target-os=android \
    --arch=arm \
    --sysroot=$SYSROOT \
    --extra-cflags="-I$ASM -isysroot $ISYSROOT -Os -fpic $ADDI_CFLAGS"

    $ADDITIONAL_CONFIGURE_FLAG

    make -j4 #这个可以指定编译线程数,加快编译 make -j4
    make install

以上脚本,其实就是非常经典的源码编译三部曲configure、make、make install。运行此脚本(记得改脚本权限+x),等待一定的时间后,可以在设定的输出文件夹中找到编译好的多个动态链接库文件,如图所示:

其中,include文件夹是开发中需要包含的一些头文件,share文件夹里面是一些example。到这里就可以在java层加载动态链接库,jni层引入头文件做相关开发了。

将生成的多个库文件打包成一个动态链接库

原先打包出来的库有多个,在开发时觉得文件太多,很是不爽,而且生成的库的名字不能让开发者一眼就看出是哪一个三方库,所以想编译成一个文件,并且取个代表性的名字。首先修改脚本,将原先脚本configure里的

1
2
--enable-shared \  
--disable-static \

修改成

1
2
--disable-shared \  
--enable-static \

这里不再去编译动态库了,而是去生成静态库,接着在原脚本最后加上链接成一个动态库的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$TOOLCHAIN/bin/arm-linux-androideabi-ld \
-rpath-link=$SYSROOT/usr/lib \
-L$SYSROOT/usr/lib \
-L$PREFIX/lib \
-soname libffmpeg.so \
-shared -nostdlib \
-Bsymbolic \
--whole-archive --no-undefined \
-o $PREFIX/libffmpeg.so \
$PREFIX/lib/libavcodec.a \
$PREFIX/lib/libavfilter.a \
$PREFIX/lib/libswresample.a \
$PREFIX/lib/libavformat.a \
$PREFIX/lib/libavutil.a \
$PREFIX/lib/libswscale.a \
-lc -lm -lz -ldl -llog \
$TOOLCHAIN/lib/gcc/arm-linux-androideabi/4.9.x/libgcc.a

重新运行脚本之后,等待片刻,如图所示:

编译过程中可能遇到的问题

大部分都是跟NDK的版本有关

  • 头文件找不到,脚本中-I$ASM -isysroot $ISYSROOT指定头文件路径。
  • 最后一步链接的时候undefined reference to 'stderr',参考References中第三篇文章,也可以降级NDK。
  • 其他问题,Google。

References

ffmpeg使用NDK编译时遇到的一些坑编译Android平台使用的FFmpeg库
NDK Unified Headers

JVM类加载机制

发表于 2018-08-15

JVM内存模型

JVM内存模型分为5个部分

  • PC寄存器
  • Java虚拟机栈/Java栈
  • Java堆
  • 方法区
  • 本地方法栈

PC寄存器

每一条Java虚拟机线程都有自己的pc寄存器

Java虚拟机栈

每一条Java虚拟机线程都有自己的私有Java虚拟机栈,这个栈与线程同时创建,用于存储栈帧。

Java堆

在Java虚拟机中,堆是可供各个线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域,GC主要针对区域。Java堆在虚拟机启动时创建。

方法区

在Java虚拟机中,方法区是可供各个线程共享运行时内存区域。方法区存储了每一个类的结构信息,例如,运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容。方法区在虚拟机启动时创建。

本地方法栈

Java虚拟机实现可能会用到传统的栈来支持native方法的执行,这个栈就是本地方法栈,在线程创建时分配。

Java GC

GC(垃圾回收)机制的计算算法是分代收集。GC主要针对的JVM内存模型中的堆区。堆区分为两大部分,新生代和老年代。新生代又分为三个区域,一个eden区,两个survival区。新生代使用复制算法,老年代使用清除标记算法。

JVM类加载

JVM加载机制分为三大部分,加载、连接、初始化

加载阶段

在堆中生成一个代表这个类的java.lang.class对象,在方法区中存储类的信息。

连接阶段

连接又可以分为验证、准备、解析

  • 验证
    验证阶段用于确保类的二进制表示结构上是正确的。

  • 准备
    准备阶段的任务是为类的静态字段分配空间,采用默认值初始化这些字段。

  • 解析
    解析就是把代码中的符号引用替换为直接引用。例如某个类继承了java.lang.Object,原来的符号引用记录的是“java.lang.Object”,并不是java.lang.Object对象,直接引用就是找出对应的java.lang.Object对应的内存地址,建立直接引用关系。

初始化

初始化的过程包括执行类构造器方法,static变量赋值语句,static{}代码块,如果是一个子类进行初始化会先对其父类进行初始化,保证其父类在子类之前进行初始化。

以下几种情况不会执行类初始化:

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化。
  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  • 通过类名获取Class对象,不会触发类的初始化。
  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

类加载器

加载器分类

在JVM中有三中类加载器,BootStrap Classloader、Extension Classloader、APP Classloader。

BootStrap ClassLoader主要加载JVM自身需要的类,这个加载器由C++编写是虚拟机的一部分,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的。

Extension Classloader是sun.misc.Launcher中的内部类ExtClassLoader,负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。

APP ClassLoader是sun.misc.Launcher中的内部类AppClassLoader,负责加载用户路径上的类库。

用户也可以通过继承ClassLoader实现自己的类加载器。

双亲委派模式

当一个类加载器接收到类加载的任务时,会首先交给其父加载器去加载,只有当父加载器无法加载时,才会自己加载。其好处是可以避免一个类被重复加载。

RecyclerView源码分析缓存机制

发表于 2018-07-27

简介

RecyclerView是Support V7包中的列表控件,它能比较方便的实现各种复杂的列表布局。RecyclerView高度解耦,通过设置不同的LayoutManager、ItemDecoration、ItemAnimator来实现不同的效果。这也是为什么去实现一个简单的列表,RecyclerView会比ListView代码量多的原因。ListView以及RecyclerView的特点就是其View复用缓存机制,来减少内存的消耗。本文主要通过源码去了解RecyclerView的缓存机制。源码基于android platform 27

一些基本概念

  • View中的detach和remove
  1. detach 在ViewGroup中的实现很简单,只是将当前View从ParentView的ChildView数组中移除,讲当前View的mParent设置为null, 可以理解为轻量级的临时remove。

  2. remove 代表真正的移除,不光从ChildView数组中移除,其他和View树各项联系也会被彻底斩断。

  • RecyclerView中的Scrap View
  1. Scrap View指的是在RecyclerView中,经历了detach操作的缓存。RecyclerView源码中部分代码注释的detach其实指代的是remove,此类缓存是通过position匹配的,不需要重新bindView。

  2. Recycled View指代的就是真正的移除操作remove后的缓存,取出时需重新bindView使用。

Recycler

首先来看Recycler类,它是RecyclerView的一个内部类,根据源码注释,这个类是用来管理scrapped和detached (removed)View的复用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* A Recycler is responsible for managing scrapped or detached item views for reuse.
*
* <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but
* that has been marked for removal or reuse.</p>
*
* <p>Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for
* an adapter's data set representing the data at a given position or item ID.
* If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
* If not, the view can be quickly reused by the LayoutManager with no further work.
* Clean views that have not {@link android.view.View#isLayoutRequested() requested layout}
* may be repositioned by a LayoutManager without remeasurement.</p>
*/

它有如下几个成员变量

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

final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>(); // 屏幕内,在LayoutManager布局的时候以及滚动的时候会讲屏幕上显示的Item存放在这
ArrayList<ViewHolder> mChangedScrap = null;

final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>(); // 视图外的item缓存,默认大小为2,根据position位置查找

private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;

RecycledViewPool mRecyclerPool; // 根据itemType缓存的ViewHolder List缓存

private ViewCacheExtension mViewCacheExtension; // 用户自定义的缓存

static final int DEFAULT_CACHE_SIZE = 2; // 默认大小为2

先简单说明一下这些属性

  1. mAttachedScrap和mChangedScrap就是概念里提到的Scrap View,mCachedViews、mViewCacheExtension、mRecyclerPool都是概念中的recycled view。

  2. mCachedViews默认大小为2,开发者可以通过mRecyclerView.setItemViewCacheSize()设置mCachedViews缓存的大小。RecyclerView会从这个列表中根据position取出ViewHolder,因此这个取出的ViewHolder是不需要重新调用onBindViewHolder方法的。

  3. mViewCacheExtension是开发者自定义缓存,开发者可以通过setViewCacheExtension设置。

  4. mRecyclerPool是根据ItemType缓存的列表,从这个列表中取出的ViewHolder是需要重新经过onBindViewHolder方法的

RecycledViewPool

上面说到mRecyclerPool缓存,我们来看看这个类,以下展示了RecycledViewPool的几个比较重要的方法和属性

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
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5; // 默认每个viewType的最大值

static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();

// 设置不同viewType对应的缓存最大值,mRecyclerView.getRecycledViewPool().setMaxRecycledViews();
public void setMaxRecycledViews(int viewType, int max) {
ScrapData scrapData = getScrapDataForType(viewType);
scrapData.mMaxScrap = max;
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
while (scrapHeap.size() > max) {
scrapHeap.remove(scrapHeap.size() - 1);
}
}

// 通过viewType获取列表最后一个ViewHolder
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}

// 缓存ViewHolder
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return; // 达到最大值后不缓存
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}

// 通过viewType获取对应的缓存列表
private ScrapData getScrapDataForType(int viewType) {
ScrapData scrapData = mScrap.get(viewType);
if (scrapData == null) {
scrapData = new ScrapData();
mScrap.put(viewType, scrapData);
}
return scrapData;
}

}

mScrap其实就是HashMap<Integer, ScrapData>,Integer指代的就是ViewType,因此可以把mRecyclerPool就理解成是HashMap<Integer, List<ViewHolder>>。每个ItemType对应列表大小是5,开发者可以通过mRecyclerView.getRecycledViewPool().setMaxRecycledViews()设置此类缓存大小。

缓存操作

在自定义LayoutManger的时候写缓存复用的时候会使用到这么几个API,

  1. detachAndScrapAttachedViews
  2. getViewForPosition
  3. removeAndRecycleView

首先来看detachAndScrapAttachedViews

1
LayoutManager.detachAndScrapAttachedViews(Recycler)
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
public void detachAndScrapAttachedViews(Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v); // scrap 或者 recycle
}
}

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index); // 从ViewGoup中移除View 可以联系概念中提到的
recycler.recycleViewHolderInternal(viewHolder); // 放进缓存中
} else {
detachViewAt(index); // 会调用ViewGroup的detachViewFromParent,ViewHolder的Flag.addFlags(ViewHolder.FLAG_TMP_DETACHED)
recycler.scrapView(view); // 可以联系概念中提到的
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
1
Recycler.scrapView(View)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool." + exceptionLabel());
}
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}

detachAndScrapAttachedViews的时候会把所有的ChildView根据一定的条件,存入到mAttachedScrap,这个mAttachedScrap其实就是滚动以及二次布局的时候快速复用用的。

再来看getViewForPosition,内部会调用tryGetViewHolderForPositionByDeadline

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
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount()
+ exceptionLabel());
}
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache // 一级
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount() + exceptionLabel());
}

final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder"
+ exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view." + exceptionLabel());
}
}
}
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}

long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
}
}
}

// This is very ugly but the only place we can grab this information
// before the View is rebound and returned to the LayoutManager for post layout ops.
// We don't need this in pre-layout since the VH is not updated by the LM.
if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (mState.mRunSimpleAnimations) {
int changeFlags = ItemAnimator
.buildAdapterChangeFlagsForAnimations(holder);
changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
holder, changeFlags, holder.getUnmodifiedPayloads());
recordAnimationInfoIfBouncedHiddenView(holder, info);
}
}

boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}

final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
  1. 从mChangedScrap中寻找

  2. 从mAttachedScrp中寻找

  3. 从mCachedViews中通过position寻找

  4. 从mViewCacheExtension中通过position和ViewType寻找

  5. 从共享的mRecycledPool中通过ViewType寻找

  6. 以上查找都没结果的话,则通过onCreateViewHolder创建一个新的ViewHolder实例。

新建的ViewHolder实例、mViewCacheExtension和mRecycledPool中取出的ViewHolder需要重新经过onBindViewHolder,而mAttachedScrp、mChangedScrap、mCachedViews中取出的的可以直接使用。

最后看removeAndRecycleView,内部会调用recycleViewHolderInternal,这是存缓存的方法

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
void recycleViewHolderInternal(ViewHolder holder) {
// 省略大部分代码
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size(); // 判断当前mCachedView缓存的大小是否达到设定的最大值
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0); // 从mCacheView中移除缓存加入到之中mRecyclerPool
cachedViewSize--;
}

int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
// 省略大部分代码
}

void recycleCachedViewAt(int cachedViewIndex) {
if (DEBUG) {
Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
}
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
if (DEBUG) {
Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
}
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}

void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
}
if (dispatchRecycled) {
dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}

从中可以看到,缓存的push是先判断mCachedViews列表的大小,如果大于了设定的最大值,就从mCachedViews中取第一个元素放入到mRecyclerPool缓存中。

LayoutManager缓存复用示例

https://github.com/jiahuanyu/android-example-code

参考

https://blog.csdn.net/jieqiang3/article/details/68922821
https://blog.csdn.net/fyfcauc/article/details/54342303

LruCache源码分析

发表于 2018-07-26

简介

Lru(Least recently used),中文为最近最少使用。Lru算法的基本原则是,如果一个数据在最近的一段时间内没有被访问,那么这个数据将被访问的可能性也会比较低。在一定的空间中,如果空间被占用满,那么这个一段时间内没有被访问的数据讲会被弃用、抛弃。LruCache就是基于此思想的缓存管理类。Lrucache位于android.util包中,本文基于android platform 27源码进行分析。

LinkedHashMap

在分析LruCache源代码之前,先来了解一下LinkedHashMap。LinkedHashMap中使用双向列表来维护元素的顺序,这个顺序可以是插入顺序也可以是访问顺序。 LinkedHashMap有这样一个构造方法

1
2
3
4
5
6
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder; // the ordering mode - <tt>true</tt> for access-order, <tt>false</tt> for insertion-order
}

accessOrder的参数意义为,为true时,顺序为访问顺序,fasle时为插入顺序,所谓访问顺序就是最近访问的元素会被放到列表的尾部,源代码如下

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
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}

void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}

accessOrder为true时候的访问顺序就非常符合Lru的原则。

LruCache

  1. LruCache的源代码不多,非常好理解。先看唯一的构造方法
    1
    2
    3
    4
    5
    6
    7
    8
    public LruCache(int maxSize) {
    if (maxSize <= 0) {
    throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true); // initialCapacity = 0, loadFactory = 0.75, accessOrder = true
    // initialCapacity为0时,会分配初始大小为1的hashmap
    }

构造方法中传入缓存的大小,然后初始化一个LinkedHashMap实例,第三个参数为true。

  1. put方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public final V put(K key, V value) {
    if (key == null || value == null) {
    throw new NullPointerException("key == null || value == null");
    }

    V previous;
    synchronized (this) {
    putCount++;
    size += safeSizeOf(key, value); // 获取一个value暂用的空间的大小,计算目前的缓存大小
    previous = map.put(key, value); // 如果key的hashcode相同,则会覆盖原先的,并且返回原先的value
    if (previous != null) {
    size -= safeSizeOf(key, previous); // 缓存大小减去原先的 protected int sizeOf(K key, V value) { return 1; }
    }
    }

    if (previous != null) {
    entryRemoved(false, key, previous, value); // 通知方法,在是用LruCache是可以重写。通知原先的value被替换
    }

    trimToSize(maxSize); // 调整大小,如果现在的大小大于了设定的maxSize,就要根据lru原则剔除数据
    return previous;
    }

其中调用safeSizeOf 来获取一个value暂用的空间大小。最终会调用 sizeOf方法,默认返回1,一般在使用LruCache时,都会重写这个方法,返回一个数据正确的占用空间。

  1. trimToSize方法
    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 trimToSize(int maxSize) {
    while (true) {
    K key;
    V value;
    synchronized (this) {
    if (size < 0 || (map.isEmpty() && size != 0)) {
    throw new IllegalStateException(getClass().getName()
    + ".sizeOf() is reporting inconsistent results!");
    }

    if (size <= maxSize) { // 如果当前大小小于等于了最大值,则跳出循环
    break;
    }

    Map.Entry<K, V> toEvict = map.eldest(); // 获取LinkedHashMap链表头的元素, public Map.Entry<K, V> eldest() { return head;}
    if (toEvict == null) {
    break;
    }

    key = toEvict.getKey();
    value = toEvict.getValue();
    map.remove(key);
    size -= safeSizeOf(key, value);
    evictionCount++;
    }

    entryRemoved(true, key, value, null); // 通知数据被剔除
    }
    }

如果当前的缓存大小大于了最大值,则需要调用eldest方法获取最近最少的使用的元素,也就是链表头的数据,并且删除,直到当前大小小于等于最大值或者没有元素

LruCache的基本使用

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
public class LruCacheActivity extends BaseActivity {
private static final String TAG = "LruCacheActivity";

private LruCache<String, String> mLruCache = new LruCache<String, String>(10) { // 10个字节

@Override
protected int sizeOf(String key, String value) {
return value.getBytes().length;
}

@Override
protected void entryRemoved(boolean evicted, String key, String oldValue, String newValue) {
Logger.d("剔除或者替换,key = %s, oldValue = %s, newValue = %s", key, oldValue, newValue);
}
};

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initializeActivity(R.layout.module_function_layout_lru_activity);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}

public void addItem(View v) {
mLruCache.put(Math.random() + "", "test");
}
}

示例代码

https://github.com/jiahuanyu/android-example-code

Android版本与API/SDK Level对照

发表于 2018-05-13
Android版本 API/SDK Level VERSION_CODE
Android 8.1 27 O_MR1
Android 8.0 26 O
Android 7.1 25 N_MR1
Android 7.0 24 N
Android 6.0 23 M
Android 5.1 22 LOLLIPOP_MR1
Android 5.0 21 LOLLIPOP
Android 4.4 19 KITKAT

HTTP 1.0、HTTP 1.1、HTTP 2.0区别

发表于 2018-04-28

HTTP 0.9

HTTP 是基于 TCP/IP 协议的应用层协议。它不涉及数据包(packet)传输,主要规定了客户端和服务器之间的通信格式,默认使用80端口。
最早版本是1991年发布的0.9版。该版本极其简单,只有一个命令GET。

1
GET /index.html

上面命令表示,TCP 连接(connection)建立后,客户端向服务器请求(request)网页index.html。
协议规定,服务器只能回应HTML格式的字符串,不能回应别的格式。

1
2
3
<html>
<body>Hello World</body>
</html>

  • 客户端请求以及服务端响应都是ASCII码
  • 客户端请求由一个回车换行结尾(CRLF)
  • 服务器响应的是一种超文本语言(HTML)
  • 连接在文档输出完毕之后自动断开

HTTP 1.0

  • 新增POST,HEAD方法

HTTP/1.0版的主要缺点是,每个TCP连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接。TCP连接的新建成本很高,因为需要客户端和服务器三次握手,并且开始时发送速率较慢(slow start)。所以,HTTP1.0版本的性能比较差。随着网页加载的外部资源越来越多,这个问题就愈发突出了。

HTTP 1.1

  • 默认持久链接
  • 新增多种方法

HTTP 2.0

HTTP 1.1与HTTP 2.0的性能对比
HTTP/2: the Future of the Internet

  • 二进制协议
    所有的信息,包括头信息,主体信息都是二进制

  • 头信息压缩

  • 多工

  • 服务器推送

参考

http://www.ruanyifeng.com/blog/2016/08/http.html

AndroidStudio 3.X 自定义Lint代码检查

发表于 2018-04-26

创建Java Module,配置其gradle

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


sourceCompatibility = "1.7"
targetCompatibility = "1.7"


jar {
manifest {
attributes("Lint-Registry-v2": "me.jiahuan.androidlearn.clint.CIssueRegistry")
}
}

dependencies {
compileOnly "com.android.tools.lint:lint-api:26.1.2"
compileOnly "com.android.tools.lint:lint-checks:26.1.2"
}

创建Android Library Module,配置其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: 'com.android.library'

android {
compileSdkVersion common.compileSdkVersion

defaultConfig {
minSdkVersion common.minSdkVersion
targetSdkVersion common.targetSdkVersion
versionCode common.versionCode
versionName common.versionName
}

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

}

dependencies {
lintChecks project(':clint')
}

参考

https://www.jianshu.com/p/a297714bb99b
美团外卖https://zhuanlan.zhihu.com/p/35608859
https://medium.com/@vanniktech/writing-your-first-lint-check-39ad0e90b9e6

12
JiaHuan

JiaHuan

12 日志
GitHub
© 2016 - 2019 JiaHuan
由 Hexo 强力驱动
主题 - NexT.Mist