Android 热修复方案Tinker(一) Application改造
基于Tinker V1.7.5
Android 热修复方案Tinker(一) Application改造
Android 热修复方案Tinker(二) 补丁加载流程
Android 热修复方案Tinker(三) Dex补丁加载
Android 热修复方案Tinker(四) 资源补丁加载
Android 热修复方案Tinker(五) SO补丁加载
Android 热修复方案Tinker(六) Gradle插件实现
Android 热修复方案Tinker(七) 插桩实现
带注释的源码
这篇文章主要分析一下Tinker隔离Application.至于为什么要隔离Application?可以参考上一篇 Android 热修复方案分析文章中说到的Qzone方案,要给除了Application子类所有的类注入一个独立dex中的类引用,来避免class被打上CLASS_ISPREVERIFIED标记.而这个独立的dex是在Application启动之后加载的,所以Application子类就不能插入独立dex的引用也就不能进行修复.不仅如此,由于Application子类被打上CLASS_ISPREVERIFIED标记,那么Application子类直接引用的对象就无法打补丁包,会抛出pre-verified异常.
在这种前提下还是想修复尽可能多的类怎么办?对于Qzone方案的解决方法就是把Application子类中的逻辑都抽离到一个独立的类例如ApplicationProxy中,Application只引用这个ApplicationProxy类从而减小影响范围.Tinker使用的是全量更新,补丁也是在Application中加载的,所以Tinker的Application也是不能修改的.并且由于Tinker的全量更新不能兼容加固方案,如果app使用了加固的话可以使用usePreGeneratedPatchDex模式回退到Qzone方案上,这样的话就需要面临同样的问题.
同时还因为Android N混合编译与对热补丁影响解析,这会造成要修复的class被缓存在App image中,App image中的class会插入PathClassLoader中,而PathClassLoader 加载补丁的时候不会替换缓存的class,最终会导致在全量更新的情况下部分类是从base.apk中加载,部分类是从patch.dex中加载,抛出IllegalAccessError.Tinker的解决方案是在运行时改写PathClassLoader来加载类,让App image中的缓存失效.而Application这个入口类还是只能用系统的PathClassLoader来加载,从这个角度来说也需要Application解耦出来.
Application 隔离
Tinker提供了两种方式来隔离Application,分别是DefaultLifeCycle注解和继承TinkerApplication,DefaultApplicationLike.推荐使用DefaultLifeCycle注解来隔离Application,这种方式会编译自动生成Application,减少一些误操作.而继承TinkerApplication,DefaultApplicationLike其实就是自己写出注解生成的代码,所以这篇文章就只介绍注解的方式.
@DefaultLifeCycle(
application = "com.test.MyApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false
)
public class ApplicationLike extends DefaultApplicationLike {
public MyApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent,
Resources[] resources, ClassLoader[] classLoader, AssetManager[] assetManager) {
super(application, tinkerFlags, tinkerLoadVerifyFlag,
applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent,
resources, classLoader, assetManager);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
在编译的过程时注解执行完毕之后就会在设定的包路径下生成出真实的Application java文件,默认继承TinkerApplication.将注解中配置的数据传递给构造函数的参数.然后被编译成.class打包到dex文件中.
public class MyApplication extends TinkerApplication {
public MyApplication() {
super(7, "cn.jesse.patchersample.ApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
}
}
1
2
3
4
5
6
7
DefaultLifeCycle注解包含四个属性,分别是真实的application路径(application), 补丁loader的路径(loaderClass), 补丁支持不同文件格式flag(flags), 是否每次都校验补丁包MD5的flag(loadVerifyFlag).
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@Inherited
public @interface DefaultLifeCycle {
/**
* 真实的Application
*/
String application();
/**
* patch loader
*/
String loaderClass() default "com.tencent.tinker.loader.TinkerLoader";
/**
* 支持的文件类型
* ShareConstants.TINKERDISABLE:不支持任何类型的文件
* ShareConstants.TINKERDEXONLY:只支持dex文件
* ShareConstants.TINKERLIBRARYONLY:只支持library文件
* ShareConstants.TINKERDEXANDLIBRARY:只支持dex与res的修改
* ShareConstants.TINKERENABLEALL:支持任何类型的文件,也是我们通常的设置的模式
*/
int flags();
/**
* 是否每次都校验补丁包的MD5
*/
boolean loadVerifyFlag() default false;
}
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
在注解处理器AnnotationProcessor中,根据注解的参数和真实的Application模板,生成真正的Application.
在Android studio中 annotation module必须是java library否则在处理anno的时候会找不到AbstractProcessor.如果需要调试这部分代码的话可以通过配置gradle --deamon和加断点来debug.
DefaultLifeCycle ca = e.getAnnotation(DefaultLifeCycle.class);
//拿到代理类的名称和包名
String lifeCycleClassName = ((TypeElement) e).getQualifiedName().toString();
String lifeCyclePackageName = lifeCycleClassName.substring(0, lifeCycleClassName.lastIndexOf('.'));
lifeCycleClassName = lifeCycleClassName.substring(lifeCycleClassName.lastIndexOf('.') + 1);
//拼装出真实的Application类名称
String applicationClassName = ca.application();
if (applicationClassName.startsWith(".")) {
applicationClassName = lifeCyclePackageName + applicationClassName;
}
//拆分出真实的Application类名称和包名
String applicationPackageName = applicationClassName.substring(0, applicationClassName.lastIndexOf('.'));
applicationClassName = applicationClassName.substring(applicationClassName.lastIndexOf('.') + 1);
//拿到patch loader的名称
String loaderClassName = ca.loaderClass();
if (loaderClassName.startsWith(".")) {
loaderClassName = lifeCyclePackageName + loaderClassName;
}
//读取Application模板文件,将模板中的%KEY%占位符全部替换成真实的数据
final InputStream is = AnnotationProcessor.class.getResourceAsStream(APPLICATION_TEMPLATE_PATH);
final Scanner scanner = new Scanner(is);
final String template = scanner.useDelimiter("\\A").next();
final String fileContent = template
.replaceAll("%PACKAGE%", applicationPackageName)
.replaceAll("%APPLICATION%", applicationClassName)
.replaceAll("%APPLICATION_LIFE_CYCLE%", lifeCyclePackageName + "." + lifeCycleClassName)
.replaceAll("%TINKER_FLAGS%", "" + ca.flags())
.replaceAll("%TINKER_LOADER_CLASS%", "" + loaderClassName)
.replaceAll("%TINKER_LOAD_VERIFY_FLAG%", "" + ca.loadVerifyFlag());
//将完整的Application代码写入到跟代理Application的相同路径下的文件中
// 至此注解生成真实Application的工作就完成了
try {
JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(applicationPackageName + "." + applicationClassName);
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Creating " + fileObject.toUri());
Writer writer = fileObject.openWriter();
try {
PrintWriter pw = new PrintWriter(writer);
pw.print(fileContent);
pw.flush();
} finally {
writer.close();
}
} catch (IOException x) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, x.toString());
}
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
Application模板是把一些变化的属性通过%KEY%的形式代替其中包含文件包名,类名,构造函数名,tinker_flag, 代理Application, patcher loader, 和补丁校验位.在注解编译时就可以根据key对包名,Application名和构造函数的参数进行填充.
package %PACKAGE%;
import com.tencent.tinker.loader.app.TinkerApplication;
/**
*
* Generated application for tinker life cycle
*
*/
public class %APPLICATION% extends TinkerApplication {
public %APPLICATION%() {
super(%TINKER_FLAGS%, "%APPLICATION_LIFE_CYCLE%", "%TINKER_LOADER_CLASS%", %TINKER_LOAD_VERIFY_FLAG%);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Application 相关依赖
用了DefaultLifeCycle注解之后就把真实的Application和代理Application隔离开了.至于两者之间是如何同步声明周期,如何建立依赖关系,以及他们的职责是什么?可以看一下这部分的类图.
真实Application
在最早能获取context的方法attachBaseContext处做相关的初始化工作.像时间统计,利用反射将TinkerLoader对象构建出来并调用tryLoad方法(校验环境后加载补丁),反射创建出代理Application对象,同步Application生命周期和重置safe mode计数.
//记录系统启动时间和App启动时刻
applicationStartElapsedTime = SystemClock.elapsedRealtime();
applicationStartMillisTime = System.currentTimeMillis();
//初始化patch loader, 并且调用tryLoad方法
loadTinker();
//确保代理Application对象已经被创建出来了
ensureDelegate();
//反射调用代理Application的`onBaseContextAttached`方法同步生命周期
try {
Method method = ShareReflectUtil.findMethod(delegate, "onBaseContextAttached", Context.class);
method.invoke(delegate, base);
} catch (Throwable t) {
throw new TinkerRuntimeException("onBaseContextAttached method not found", t);
}
//重置safe mode计数, 当safe mode计数不小于三次时PatcherResultIntent会记录patch失败
if (useSafeMode) {
String processName = ShareTinkerInternals.getProcessName(this);
String preferName = ShareConstants.TINKER_OWN_PREFERENCE_CONFIG + processName;
SharedPreferences sp = getSharedPreferences(preferName, Context.MODE_PRIVATE);
sp.edit().putInt(ShareConstants.TINKER_SAFE_MODE_COUNT, 0).commit();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
代理Application
因为要用ApplicationLike代理真实的Application,所以ApplicationLike就要持有Application, resources, classLoader, assetManager的引用.其中tinkerResultIntent属性中记录着TinkerLoader启动和加载patch的状态.
//Application,resource,assetManager,classLoader的引用
private final Application application;
private Resources[] resources;
private ClassLoader[] classLoader;
private AssetManager[] assetManager;
//用于记录启动加载patcher loader的状态
private final Intent tinkerResultIntent;
//系统的存活时间和app启动时刻
private final long applicationStartElapsedTime;
private final long applicationStartMillisTime;
//注解中的两个flags
private final int tinkerFlags;
private final boolean tinkerLoadVerifyFlag;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
同时还要暴露出常用的Application共有方法,供外部使用.
void onCreate();
void onLowMemory();
void onTrimMemory(int level);
void onTerminate();
void onConfigurationChanged(Configuration newConfig);
void onBaseContextAttached(Context base);
至此Application改造的分析就完成了,上面提到了在Application的attachBaseContext中通过反射调起了TinkerLoader的tryLoad方法.tryLoad方法是加载最终补丁包的入口,下篇文章会沿着这条线继续分析下去.
https://blog.csdn.net/l2show/article/details/53187548
Android 热修复方案Tinker(一) Application改造的更多相关文章
- Android 热修复方案Tinker
转自:http://blog.csdn.net/l2show/article/details/53925543 Android 热修复方案Tinker(一) Application改造 Android ...
- Android热修复方案比较
热修复的特点:无需重新发版,实时高效热修复:用户无感知修复,无需下载新的应用,代价小: 修复成功率高,把损失降到最低. 一.热修复开源方案和使用情况 方案名称 方案开发公司 开发时间 Github星评 ...
- Android 热修复使用Gradle Plugin1.5改造Nuwa插件
随着谷歌的Gradle插件版本号的不断升级,Gradle插件如今最新的已经到了2.1.0-beta1,相应的依赖为com.android.tools.build:gradle:2.0.0-beta6, ...
- Android热修复之微信Tinker使用初探
文章地址:Android热修复之微信Tinker使用初探 前几天,万众期待的微信团队的Android热修复框架tinker终于在GitHub上开源了. 地址:https://github.com/ ...
- 深入探索Android热修复技术原理读书笔记 —— 代码热修复技术
在前一篇文章 深入探索Android热修复技术原理读书笔记 -- 热修复技术介绍中,对热修复技术进行了介绍,下面将详细介绍其中的代码修复技术. 1 底层热替换原理 在各种 Android 热修复方案中 ...
- Android 热修复Nuwa的原理及Gradle插件源码解析
现在,热修复的具体实现方案开源的也有很多,原理也大同小异,本篇文章以Nuwa为例,深入剖析. Nuwa的github地址 https://github.com/jasonross/Nuwa 以及用于 ...
- Android热修复技术选型(不在市场发布新版本的情况下,直接更新app)
2015年以来,Android开发领域里对热修复技术的讨论和分享越来越多,同时也出现了一些不同的解决方案,如QQ空间补丁方案.阿里AndFix以及微信Tinker,它们在原理各有不同,适用场景各异,到 ...
- Android热修复技术选型——三大流派解析
声明,本文转载自微信公众号文章 2015年以来,Android开发领域里对热修复技术的讨论和分享越来越多,同时也出现了一些不同的解决方案,如QQ空间补丁方案.阿里AndFix以及微信Tinker,它们 ...
- Android热修复技术总结
https://blog.csdn.net/xiangzhihong8/article/details/77718004 插件化和热修复技术是Android开发中比较高级的知识点,是中级开发人员通向高 ...
随机推荐
- robots.txt、humans.txt、.editorconfig、.gitignore、LICENSE.txt、README.md、CHANGLOG.md
robots.txt搜索引擎查看的时候会查看这个文件,告诉搜索引擎哪些文件可以查看,哪些文件不能查看 当搜索引擎搜索网站的时候,会看有这个文件没,如果有,会通过里面的文件来确定哪些文件能看,哪些文件不 ...
- bert中的分词
直接把自己的工作文档导入的,由于是在外企工作,所以都是英文写的 chinese and english tokens result input: "我爱中国",tokens:[&q ...
- ExtJs常用布局--layout详解(含实例)
序言: 笔者用的ExtJs版本:ext-3.2.0 ExtJs常见的布局方式有:border.form.absolute.column.accordion.table.fit.card.anchor ...
- 判断上学和放假的demo
var today = new Date(); var xq = today.getDay(); var Now = today.getHours(); if (xq >= 1 &&am ...
- java基础篇---HTTP协议
java基础篇---HTTP协议 HTTP协议一直是自己的薄弱点,也没抽太多时间去看这方面的内容,今天兴致来了就在网上搜了下关于http协议,发现有园友写了一篇非常好的博文,博文地址:(http: ...
- 在Centos中安装aria2c
# 安装aria2c 1 安装epel源 rpm -ivh http://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarch.rpm ...
- 最近关于mysql的造型,binlog使用,以及阿里云上线数据处理错误导致被处罚的思考
因团队中成员,上线代码时,不小心将数据表中吃掉物理的数据清空,导致被单位处罚,痛定思痛,我们应该如何上线,还需要准备哪些技能? 1.上线时,必须关闭服务,不能一边上线,一边让用户可以继续操作,一边产生 ...
- 解决asp.net 报错 无法获取所需的权限错误
asp.net 报错 无法获取所需的权限 无法获取所需的权限.说明: 执行当前 Web 请求期间,出现未处理的异常.请检查堆栈跟踪信息,以了解有关该错误以及代码中导致错误的出处的详细信息. 无法获取所 ...
- BZOJ2325 [ZJOI2011]道馆之战 树链剖分 线段树
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - BZOJ2325 题意概括 给你一棵N个点的树,树上的每个节点有A,B两块区域,且每种区域有两种状态:可以走的 ...
- BZOJ1177 [Apio2009]Oil 二维前缀和 二维前缀最值
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - BZOJ1177 题意概括 在一个n*m的矩阵中,每一个位置一个数字. 现在让你选出3个k*k的矩阵,它们互不 ...