Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)
2016-09-13 14:51
459 查看
转自:http://blog.csdn.net/u010386612/article/details/51131642
一、关于CSDN mardown编辑器的坑
Android热补丁动态修复技术(三)这篇博文其实在4月8日的晚上已经发布了,然后紧接着写第四篇,但是我将(四)保存到草稿箱时,发现已经发布的(三)消失了,取而代之的是第四篇博文。在论坛问过版主,可能是因为我误操作导致的,第三篇博文已经无法恢复。
真是手贱!写了好几天的东西啊,不过比起误操作我更倾向认为这是csdn的bug……
markdown编辑器绝对有坑!光是写新文章时不会自动清楚缓存我认为就是一个很严重的Bug了!
二、前言
因为第三篇博文消失的原因,伴随着演示的Demo项目也修改了很多内容,我也没那么精力重新写一篇,就和第四篇博文合并在一起当做第三篇吧,这可能导致内容跨度有些大,并且不会像之前的博文这么详细,希望大家多多支持和理解。上一篇博客中,我们再Application中成功注入了patch_dex.jar到ClassLoader中。
但是伴随着CLASS_ISPREVERIFIED问题,解决方式就在在所有类的构造函数中添加一行代码
System.out.println(AntilazyLoad.class);
三、Gradle, Transfrom, Task, Plugin
我们来分析一下如何在所有类的构造函数中添加System.out.println(AntilazyLoad.class);
在源码中直接添加,这个不行。AntilazyLoad.class这个类找不到,编译不通过
绕过编译,使用javassist操作字节码,直接注入代码。
第二点是可行的,但是AndroidStudio项目是使用Gradle构建的,编译-打包-签名都是自动化。
我们在什么时候注入代码?
看过我上一篇博文推荐的文章就知道,Gradle是通过一个一个Task执行完成整个流程的,其中肯定也有将所有class打包成dex的task。
(在gradle plugin 1.5 以上和以下版本有些不同)
1.5以下,preDex这个task会将依赖的module编译后的class打包成jar,然后dex这个task则会将所有class打包成dex
1.5以上,preDex和Dex这两个task已经消失,取而代之的是TransfromClassesWithDexForDebug
3.1 Transfrom
Transfrom是Gradle 1.5以上新出的一个api,其实它也是Task,不过定义方式和Task有点区别。对于热补丁来说,Transfrom反而比原先的Task更好用。
在Transfrom这个api出来之前,想要在项目被打包成dex之前对class进行操作,必须自定义一个Task,然后插入到predex或者dex之前,在自定义的Task中可以使用javassist或者asm对class进行操作。
而Transform则更为方便,Transfrom会有他自己的执行时机,不需要我们插入到某个Task前面。Tranfrom一经注册便会自动添加到Task执行序列中,并且正好是项目被打包成dex之前。
而本文就是使用Gradle1.5以上版本,下面则是Google对Transfrom的描述文档。
http://tools.android.com/tech-docs/new-build-system/transform-api
有时候会访问不了,你可能需要一把梯子……
3.2 Task的inputs和outputs
Gradle可以看做是一个脚本,包含一系列的Task,依次执行这些task后,项目就打包成功了。而Task有一个重要的概念,那就是inputs和outputs。
Task通过inputs拿到一些东西,处理完毕之后就输出outputs,而下一个Task的inputs则是上一个Task的outputs。
例如:一个Task的作用是将java编译成class,这个Task的inputs就是java文件的保存目录,outputs这是编译后的class的输出目录,它的下一个Task的inputs就会是编译后的class的保存目录了。
3.3 Plugin
Gradle中除了Task这个重要的api,还有一个就是Plugin。Plugin的作用是什么呢,这一两句话比较难以说明。
Gralde只能算是一个构建框架,里面的那么多Task是怎么来的呢,谁定义的呢?
是Plugin,细心的网友会发现,在module下的build.gradle文件中的第一行,往往会有
apply plugin : 'com.android.application'亦或者
apply plugin : 'com.android.library'。
com.android.application:这是app module下Build.gradle的
com.android.library:这是app依赖的module中的Builde.gradle的
就是这些Plugin为项目构建提供了Task,使用不同的plugin,module的功能也就不一样。
可以简单的理解为: Gradle只是一个框架,真正起作用的是plugin。而plugin的主要作用是往Gradle脚本中添加Task。
当然,实际上这些是很复杂的东西,plugin还有其他作用这里用不上。
四、如何注册一个Transfrom
我们可以自定义一个plugin,然后使用plugin注册一个Transfrom。4.1 apply plugin
在此之前,先教大家怎么自定义一个plugin。1. 新建一个module,选择library module,module名字必须叫BuildSrc
2. 删除module下的所有文件,除了build.gradle,清空build.gradle中的内容
3. 然后新建以下目录 src-main-groovy
4. 修改build.gradle如下,同步
``` apply plugin: 'groovy' repositories { jcenter() } dependencies { compile gradleApi() compile 'com.android.tools.build:gradle:1.5.0' compile 'org.javassist:javassist:3.20.0-GA' } ```
5. 这时候就可以像普通module一样新建package和类了,不过这里的类是以groovy结尾,新建类的时候选择file,并且以.groovy作为后缀。
Register就是我自定义个Plugin(无视黑色涂块,Demo被我修改太多了,再次鄙视csdn)
代码如下
package com.aitsuki.plugin import org.gradle.api.Plugin; import org.gradle.api.Project /** * Created by hp on 2016/4/8. */ public class Register implements Plugin<Project> { @Override public void apply(Project project) { project.logger.e 4000 rror "================自定义插件成功!==========" } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
[/code]
在app module下的buiil.gradle中添apply 插件
说明:如果plugin所在的module名不叫BuildSrc,这里是无法apply包名的,会提示找不到。所以之前也说明取名一定要叫buildsrc
运行一下项目就可以看到”================自定义插件成功!==========”这句话了
和gradle有关的输出都会显示在gradle console这个窗口中。
4.2 自定义Transfrom
新建一个groovy继承Transfrom,注意这个Transfrom是要com.android.build.api.transform.Transform这个包的
要先添加依赖才能导入此包,如下
dependencies { compile gradleApi() compile 'com.android.tools.build:gradle:1.5.0' compile 'org.javassist:javassist:3.20.0-GA' }1
2
3
4
5
[/code]
javassist待会要用到,顺便添加进来了。
我们定义一个PreDexTransform,代码如下
package com.aitsuki.plugin import com.android.build.api.transform.* import com.android.build.gradle.internal.pipeline.TransformManager import org.gradle.api.Project public class PreDexTransform extends Transform { Project project // 添加构造,为了方便从plugin中拿到project对象,待会有用 public PreDexTransform(Project project) { this.project = project } // Transfrom在Task列表中的名字 // TransfromClassesWithPreDexForXXXX @Override String getName() { return "preDex" } // 指定input的类型 @Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS } // 指定Transfrom的作用范围 @Override Set<QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } @Override boolean isIncremental() { return false } @Override void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { // inputs就是输入文件的集合 // outputProvider可以获取outputs的路径 } }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
[/code]
然后再Register这个plugin的apply方法中添加一下代码,注册Transfrom
def android = project.extensions.findByType(AppExtension) android.registerTransform(new PreDexTransform(project))1
2
[/code]
再次运行项目(需要先clean项目,否则apply plugin不会重新编译)
首先,我们看到自定义的PreDexTransfrom已经运行了,但是接下来的DexTransform却报错了。
那是因为我们自定义的Transfrom的transfrom方法为空,没有将inputs输出到outputs,DexTransfrom是在PreDexTransfrom下面,获取到的inputs为空,所以就报错了。
我们只需要在Tranfrom中将inputs文件复制到ouputs目录就可以了,代码如下。
// Transfrom的inputs有两种类型,一种是目录,一种是jar包,要分开遍历 inputs.each {TransformInput input -> input.directoryInputs.each {DirectoryInput directoryInput-> //TODO 这里可以对input的文件做处理,比如代码注入! // 获取output目录 def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) // 将input的目录复制到output指定目录 FileUtils.copyDirectory(directoryInput.file, dest) } input.jarInputs.each {JarInput jarInput-> //TODO 这里可以对input的文件做处理,比如代码注入! // 重命名输出文件(同目录copyFile会冲突) def jarName = jarInput.name def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if(jarName.endsWith(".jar")) { jarName = jarName.substring(0,jarName.length()-4) } def dest = outputProvider.getContentLocation(jarName+md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR) FileUtils.copyFile(jarInput.file, dest) } }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
[/code]
加入这段代码到transform方法中再次运行就没问题了,再次说明:要先Clean项目!
上面有两个TODO注释,我们在获取inputs复制到outpus目录之前,可以在这里对class注入代码!
4.3 查看inputs和ouputs
我们先来看看Transfrom的inputs和outputs,这里有个方法:在app module下的build.gradle中添加以下代码即可。
applicationVariants.all { variant-> def dexTask = project.tasks.findByName("transformClassesWithDexForDebug") def preDexTask = project.tasks.findByName("transformClassesWithPreDexForDebug") if(preDexTask) { project.logger.error "======preDexTask======" preDexTask.inputs.files.files.each {file -> project.logger.error "inputs =$file.absolutePath" } preDexTask.outputs.files.files.each {file -> project.logger.error "outputs =$file.absolutePath" } } if(dexTask) { project.logger.error "======dexTask======" dexTask.inputs.files.files.each {file -> project.logger.error "inputs =$file.absolutePath" } dexTask.outputs.files.files.each {file -> project.logger.error "outputs =$file.absolutePath" } } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[/code]
输出如下:
glide和xutils是app依赖的jar包
hotpatch是我将application中加载dex的代码抽取成独立module后,app依赖此module的结果
其余的则是项目默认依赖的jar包。
得出一个结论,app依赖的module在dex之前会被打包成classes.jar,和其他依赖的jar包一起放到
exploded-arr这个目录。
而依赖的module会放在
exploded-arr\项目名\module名这个目录下
附上hotPatch这个将application中的代码打包好的module
然后这是
inputs =D:\aitsuki\HotPatchDemo\app\build\intermediates\exploded-aar\HotPatchDemo\hotpatch\unspecified\jars\classes.jar解压后的结果
五、使用javassist注入代码
建议先去了解下javassit的最基本使用方法,否则可能看不懂我在说什么。5.1 建立Hack Module
注入System.out.println(AntilazyLoad.class);这行代码的时候,如果javasssit找到AntilazyLoad.class这个类就会抛异常
所以创建AntilazyLoad.class,并且将AntilazyLoad.class所在的路径append到ClassPool的classpath中。
首先我们建一个hack module,如下
5.2 制作hack.jar
制作方式在上一篇博客中就有。将AntilazyLoad.class复制到同包名的文件夹下,然后运行打包命令,不重复赘述了。
然后将hack.jar放到app module中的assets文件夹中,如图
然后我们在加载patch_dex之前就要先将这个hack加载进classLoader,加载hack的方式和步骤跟加载补丁是一摸一样的,不再赘述,具体请直接看Demo,最后面有下载链接。
5.3 使用javassist注入代码
代码量稍多,我就不那么详细的解释了,这里说下最基本的两点app module编译后class文件保存在debug目录,直接遍历这个目录使用javassist注入代码就行了
app module依赖的module,编译后会被打包成jar,放在exploded-aar这个目录,需要将jar包解压–遍历注入代码–重新打包成jar
首先我们专门写一个用来操作javassist注入代码的inject类。
package com.aitsuki.plugin import javassist.ClassPool import javassist.CtClass import javassist.CtConstructor import org.apache.commons.io.FileUtils /** * Created by AItsuki on 2016/4/7. * 注入代码分为两种情况,一种是目录,需要遍历里面的class进行注入 * 另外一种是jar包,需要先解压jar包,注入代码之后重新打包成jar */ public class Inject { private static ClassPool pool= ClassPool.getDefault() /** * 添加classPath到ClassPool * @param libPath */ public static void appendClassPath(String libPath) { pool.appendClassPath(libPath) } /** * 遍历该目录下的所有class,对所有class进行代码注入。 * 其中以下class是不需要注入代码的: * --- 1. R文件相关 * --- 2. 配置文件相关(BuildConfig) * --- 3. Application * @param path 目录的路径 */ public static void injectDir(String path) { pool.appendClassPath(path) File dir = new File(path) if (dir.isDirectory()) { dir.eachFileRecurse { File file -> String filePath = file.absolutePath if (filePath.endsWith(".class") && !filePath.contains('R$') && !filePath.contains('R.class') && !filePath.contains("BuildConfig.class") // 这里是application的名字,可以通过解析清单文件获得,先写死了 && !filePath.contains("HotPatchApplication.class")) { // 这里是应用包名,也能从清单文件中获取,先写死 int index = filePath.indexOf("com\\aitsuki\\hotpatchdemo") if (index != -1) { int end = filePath.length() - 6 // .class = 6 String className = filePath.substring(index, end).replace('\\', '.').replace('/', '.') injectClass(className, path) } } } } } /** * 这里需要将jar包先解压,注入代码后再重新生成jar包 * @path jar包的绝对路径 */ public static void injectJar(String path) { if (path.endsWith(".jar")) { File jarFile = new File(path) // jar包解压后的保存路径 String jarZipDir = jarFile.getParent() + "/" + jarFile.getName().replace('.jar', '') // 解压jar包, 返回jar包中所有class的完整类名的集合(带.class后缀) List classNameList = JarZipUtil.unzipJar(path, jarZipDir) // 删除原来的jar包 jarFile.delete() // 注入代码 pool.appendClassPath(jarZipDir) for (String className : classNameList) { if (className.endsWith(".class") && !className.contains('R$') && !className.contains('R.class') && !className.contains("BuildConfig.class")) { className = className.substring(0, className.length() - 6) injectClass(className, jarZipDir) } } // 从新打包jar JarZipUtil.zipJar(jarZipDir, path) // 删除目录 FileUtils.deleteDirectory(new File(jarZipDir)) } } private static void injectClass(String className, String path) { CtClass c = pool.getCtClass(className) if (c.isFrozen()) { c.defrost() } CtConstructor[] cts = c.getDeclaredConstructors() if (cts == null || cts.length == 0) { insertNewConstructor(c) } else { cts[0].insertBeforeBody("System.out.println(com.aitsuki.hack.AntilazyLoad.class);") } c.writeFile(path) c.detach() } private static void insertNewConstructor(CtClass c) { CtConstructor constructor = new CtConstructor(new CtClass[0], c) constructor.insertBeforeBody("System.out.println(com.aitsuki.hack.AntilazyLoad.class);") c.addConstructor(constructor) } }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
[/code]
下面这是解压缩jar包的类
package com.aitsuki.plugin import java.util.jar.JarEntry import java.util.jar.JarFile import java.util.jar.JarOutputStream import java.util.zip.ZipEntry /** * Created by hp on 2016/4/13. */ public class JarZipUtil { /** * 将该jar包解压到指定目录 * @param jarPath jar包的绝对路径 * @param destDirPath jar包解压后的保存路径 * @return 返回该jar包中包含的所有class的完整类名类名集合,其中一条数据如:com.aitski.hotpatch.Xxxx.class */ public static List unzipJar(String jarPath, String destDirPath) { List list = new ArrayList() if (jarPath.endsWith('.jar')) { JarFile jarFile = new JarFile(jarPath) Enumeration<JarEntry> jarEntrys = jarFile.entries() while (jarEntrys.hasMoreElements()) { JarEntry jarEntry = jarEntrys.nextElement() if (jarEntry.directory) { continue } String entryName = jarEntry.getName() if (entryName.endsWith('.class')) { String className = entryName.replace('\\', '.').replace('/', '.') list.add(className) } String outFileName = destDirPath + "/" + entryName File outFile = new File(outFileName) outFile.getParentFile().mkdirs() InputStream inputStream = jarFile.getInputStream(jarEntry) FileOutputStream fileOutputStream = new FileOutputStream(outFile) fileOutputStream << inputStream fileOutputStream.close() inputStream.close() } jarFile.close() } return list } /** * 重新打包jar * @param packagePath 将这个目录下的所有文件打包成jar * @param destPath 打包好的jar包的绝对路径 */ public static void zipJar(String packagePath, String destPath) { File file = new File(packagePath) JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(destPath)) file.eachFileRecurse { File f -> String entryName = f.getAbsolutePath().substring(packagePath.length() + 1) outputStream.putNextEntry(new ZipEntry(entryName)) if(!f.directory) { InputStream inputStream = new FileInputStream(f) outputStream << inputStream inputStream.close() } } outputStream.close() } }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
[/code]
然后再Transfrom中这么使用,我将整个类再贴一遍好了
package com.aitsuki.plugin import com.android.build.api.transform.* import com.android.build.gradle.internal.pipeline.TransformManager import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.io.FileUtils import org.gradle.api.Project public class PreDexTransform extends Transform { Project project public PreDexTransform(Project project) { this.project = project // 获取到hack module的debug目录,也就是Antilazy.class所在的目录 def libPath = project.project(':hack').buildDir.absolutePath.concat("\\intermediates\\classes\\debug") Inject.appendClassPath(libPath) Inject.appendClassPath("D:\\Sdk\\platforms\\android-22\\android.jar") } @Override String getName() { return "preDex" } @Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS } @Override Set<QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } @Override boolean isIncremental() { return false } @Override void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { // 遍历transfrom的inputs // inputs有两种类型,一种是目录,一种是jar,需要分别遍历。 inputs.each {TransformInput input -> input.directoryInputs.each {DirectoryInput directoryInput-> //TODO 注入代码 Inject.injectDir(directoryInput.file.absolutePath) def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) // 将input的目录复制到output指定目录 FileUtils.copyDirectory(directoryInput.file, dest) } input.jarInputs.each {JarInput jarInput-> //TODO 注入代码 String jarPath = jarInput.file.absolutePath; String projectName = project.rootProject.name; if(jarPath.endsWith("classes.jar") && jarPath.contains("exploded-aar\\"+projectName) // hotpatch module是用来加载dex,无需注入代码 && !jarPath.contains("exploded-aar\\"+projectName+"\\hotpatch")) { Inject.injectJar(jarPath) } // 重命名输出文件(同目录copyFile会冲突) def jarName = jarInput.name def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if(jarName.endsWith(".jar")) { jarName = jarName.substring(0,jarName.length()-4) } def dest = outputProvider.getContentLocation(jarName+md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR) FileUtils.copyFile(jarInput.file, dest) } } } }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
[/code]
然后运行项目(最后再重复一次:记得先clean项目!),成功注入补丁!不报错了
六、Demo的GIF演示
关于SDCard:如果手机支持TF卡,那么请将补丁复制到内部存储。
还有这里是下载地址, 补丁已经放在根目录
http://download.csdn.net/detail/u010386612/9490542
七、写在后面
补充一点:在上面代码中,我们为所有的module编译后的jar注入了代码。实际上在hotpatch这个module是不需要注入代码的,因为这个module是用于加载dex的,而执行该module的时候,AntilazyLoad.class肯定没加载进来,所以注入代码毫无作用,应该排除这个module
这篇博文解决了class_ispreverified问题,并且成功使用javassist注入字节码,完成了热补丁框架的雏形。
但是还有几个需要解决的问题
1. 补丁没有签名校验,不安全,容易被恶意注入代码
2. 混淆开启的情况下,类名可能被更换,补丁打包不成功。
下一篇博文可能是关于混淆或者补丁签名
相关文章推荐
- Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)
- 使用字节码处理框架javassist动态注入代码
- 【Android开发高级技术】Android 热补丁动态修复框架分析与总结
- Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!
- 【转】Android 热补丁动态修复框架小结
- Android 热补丁动态修复框架小结
- Android 热补丁动态修复框架小结
- Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!
- Android 热补丁动态修复框架小结
- Android 热补丁动态修复框架小结
- Android 热补丁动态修复框架小结
- (4.2.32.2)android热修复之ClassLoader方式:Android 热补丁动态修复框架小结
- Android 热补丁动态修复框架小结
- Android 热补丁动态修复框架小结
- Android 热补丁动态修复框架小结
- Android 热补丁动态修复框架小结
- Android 热补丁动态修复框架小结
- Android热补丁动态修复技术(完结篇):自动生成打包带签名的补丁,重构项目
- Android AndFix热补丁动态修复框架使用教程
- Android 热补丁动态修复框架小结