热修复/插件化/组件化-Andfix/Tinker源码简单解读及
一、知识详解模块
1.dex/class深入讲解
2.jvm/dvm/art三个虚拟机的深入讲解
3.class Loader类加载器的深入讲解
二、热修复应用模块
1.热修复原理深入讲解
2.如何合理的接入开源的热修复框架
3.开源框架的原理及版本发布控制
三、插件化应用模块
1.插件化原理以及组件化的区别
2.如何将应用插件化
3.插件化能为我们带来那些好处
一、知识详解模块
1.Class文件结构深入解析(生成、执行)
2.dex文件结构深入解析(生成、执行)
3.Class与Dex的对比
Class文件是什么?如何生成、作用以及文件格式详解
其是能够被JVM虚拟机识别加载并执行的文件格式
(1)IDE编译器--build生成
(2)javac命令生成
(3)通过java命令去执行class文件 javac -target 1.6 -source 1.6 Hello.java
记录一个类文件的所有信息
Class文件结构解析:
(1)一种8字节的二进制流文件
(2)各个数据按顺序紧密的排列、无间隙
(3)每一个类或接口都独自占据一个class文件
http://blog.csdn.net/linzhuowei0775/article/details/49556621 Class类文件的结构
u4 magic; 它的唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件
u2 minor_version; 次版本号
u2 major_version; 主版本号(java版本)
u2 constant_pool_count; 常量池的数量(这个容量计数是从1而不是从0开始)
cp_info constant_pool[constant_pool_count-1]; 表类型数据集合,即常量池中每一项常量都是一个表,共有11种结构各不相同的表结构数据
u2 access_flags; ----------------------------作用域
u2 this_class; ------------------------------JVM生成的本类对象
u2 super_class; -----------------------------JVM生成的父类对象
u2 interfaces_count; ------------------------接口
u2 interfaces[interfaces_count];
u2 fields_count; ----------------------------变量
field_info fields[fields_count];
u2 methods_count; ---------------------------方法
method_info methods[methods_count];
u2 attributes_count; ------------------------属性
attribute_info attributes[attributes_count];
Class文件的弊端:
(1)内存占用大,不适合移动端
(2)JVM采用堆栈加载模式,加载速度慢
(3)文件IO操作多,类查找慢
Dex文件是什么?如何生成、作用、文件格式详解
其是一个能够被DVM识别,加载并执行的文件格式
(1)通过IDE自动--build生成
(2)通过DX命令生成dex文件 dx --dex --output Hello.dex Hello.class
(3)手动运行dex文件到手机 dalvikvm -cp /scard/Hello.dex Hello
记录整个工程中所有类文件的信息
Dex文件结构解析:
(1)一个8字节二进制流文件
(2)各个数据按照顺序紧密排列无缝隙
(3)整个应用中的所有Java源文件都放在一个Dex文件中
http://blog.csdn.net/feglass/article/details/51761902 Android Dex文件结构解析
主要分为三部分:文件头、索引区、数据区
header---文件头
索引区
string_ids--字符串的索引
type_ids ---类型索引
proto_ids --方法原型的索引
field_ids ----域的索引
method_ids ---方法的索引
数据区
class_def -----类的定义区
data -----------数据区
link_data -------链接数据区
Class与Dex的区别
(1)本质上两者是一样的,dex是从class文件演变过来的 dx --dex --output xx.dex xx.class
(2)class文件存在很多的冗余信息,dex会去除冗余并合并
(3)结构不一样
第二章
1.JVM虚拟机结构解析
2.Dalvik与JVM的不同
3.ART与Dalvik相比有哪些优势
1.JVM虚拟机结构解析
(1)JVM整体结构讲解
(2)Java代码的编译和执行过程
(3)内存管理和垃圾回收
JAVA虚拟机、Dalvik虚拟机和ART虚拟机简要对比:
JVM:Java的跨平台性是由JVM来实现的,就是把平台无关的.class字节码翻译成平台相关的机器码,来实现的跨平台;
JVM在把描述类的数据从class文件加载到内存时,需要对数据进行校验、转换解析和初始化,最终才形成可以被虚拟机直接使用的JAVA类型,因为大量的冗余信息,会严重影响虚拟机解析文件的效率
DVM:为了减小执行文件的体积,安卓使用Dalvik虚拟机,SDK中有个dx工具负责将JAVA字节码转换为Dalvik字节码,dx工具对JAVA类文件重新排列,将所有JAVA类文件中的常量池分解,消除其中的冗余信息,重新组合形成一个常量池,所有的类文件共享同一个常量池,使得相同的字符串、常量在DEX文件中只出现一次,从而减小了文件的体积.
Dalvik执行的是dex字节码,依靠JIT编译器去解释执行,运行时动态地将执行频率很高的dex字节码翻译成本地机器码,然后在执行,但是将dex字节码翻译成本地机器码是发生在应用程序的运行过程中,并且应用程序每一次重新运行的时候,都要重新做这个翻译工作,因此,及时采用了JIT,Dalvik虚拟机的总体性能还是不能与直接执行本地机器码的ART虚拟机相比
ART:
Dalvik虚拟机执行的是dex字节码,ART虚拟机执行的是本地机器码
安卓运行时从Dalvik虚拟机替换成ART虚拟机,并不要求开发者重新将自己的应用直接编译成目标机器码,也就是说,应用程序仍然是一个包含dex字节码的apk文件。所以在安装应用的时候,dex中的字节码将被编译成本地机器码,之后每次打开应用,执行的都是本地机器码
JIT与AOT两种编译模式
JIT:即时编译技术,JIT会在运行时分析应用程序的代码,识别哪些方法可以归类为热方法,这些方法会被JIT编译器编译成对应的汇编代码,然后存储到代码缓存中,以后调用这些方法时就不用解释执行了,可以直接使用代码缓存中已编译好的汇编代码。这能显著提升应用程序的执行效率
AOT:
ART优点:
①系统性能显著提升
②应用启动更快、运行更快、体验更流畅、触感反馈更及时
③续航能力提升
④支持更低的硬件
ART缺点
①更大的存储空间占用,可能增加10%-20%
②更长的应用安装时间
总的来说ART就是“空间换时间”
JVM与DVM的对比:
(1)JAVA虚拟机运行的是JAVA字节码,Dalvik虚拟机运行的是Dalvik字节码
(2)Dalvik可执行文件体积更小
(3)JVM基于栈,DVM基于寄存器
注意:
在DVM虚拟机中,总是在运行时通过JIT把字节码文件编译成机器码,因此程序跑起来就比较慢,所以在ART模式(4.0引入,5.0设置为默认解决方案)上,就改为AOT预编译,也就是在安装应用或者OTA系统升级时提前把字节码编译成机器码,这样就可以直接运行了,提高了运行的效率。但是AOT的缺点,每次执行的时间都太长,且ROM空间占用有比较大,所以在Android N上google做了混合编译,即同时支持JIT+AOT。
简单来讲:安装的时候不做编译,而是JIT解释字节码,以便能够快速启动,在运行的时候分析运行过程中的代码以及区分“热代码”,这样就可以在机器空闲的时候对通过dex2aot这部分热代码采用AOT进行编译存储为base.art文件然后在下次启动的时候一次性把app image加载进来到缓存,预先加载代替用时查找以提升应用的性能,并且同一个应用可以编译数次,以找到“热”的代码路径或者对已经编译的代码进行新的优化。
JVM结构:
虚拟机内存区域分为:运行时数据区+(执行引擎+本地库接口+本地方法库)
运行时数据区:方法区、Java栈、Java堆、本地方法栈、程序计数器
Class文件----(通过类加载器子系统)-----加载到内存空间(方法区、JAVA堆、JAVA栈、本地方法栈)-------垃圾收集器(GC)
内存优化方案
JVM内存管理
1.JAVA栈(虚拟机栈):存储JAVA方法执行时所有的数据;存放基本型,以及对象引用。线程私有区域
其由栈帧组成,一个栈帧代表一个方法的执行,每个方法在执行的同时都会创建一个栈帧
JAVA栈帧--每个方法从调用到执行完成就对应一个栈帧在虚拟机中入栈到出栈
JAVA栈帧存储:局部变量、栈操作数、动态链接、方法出口.
2.JAVA堆:所有通过new创建的对象产生的内存都在堆中分配存放对象实例和数组,此区域也是垃圾回收器主要作用的区域。
特点:
是虚拟机中最大的一块内存,是GC要回收的部分
堆区内存:
Young generation(新创建的存储区)
Old generation(暂时用不到的,不可达的对象存储区)若是Young与Old都存储满了 就会爆出OOM异常
Permanent generation
分成两个的原因:有利于动态调整两个区域的大小
即时通信服务时调用MSG消息比较多,就可以把Young调整的大一些,这样便于内存的分配,加快对象的创建
3.本地方法栈:是专门为native方法提供服务的 虚拟机中的JIT即时编译单元负责本机系统中比如C语言或者C++语言和Java程序间的互相调用,
这个Native Method Stack就是用来存放与本线程互相调用的本机代码
4.方法区:存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的数据,是所有线程的共享区域。
也称为永久代(Permanent Generation)但随着Java8的到来,已放弃永久代改为采用Native Memory来实现方法区的规划。
此区域回收目标主要是针对常量池的回收和对类型的卸载
5.程序计数器:可看做是当前线程所执行的字节码的行号指示器;如果线程在执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;
如果执行的是Native方法,这个计数器的值为空(Undefined)
垃圾回收:
垃圾收集算法(垃圾确认)
1.引用计数算法(JDk1.2之前提供)
优点:实现简单,判定效率高
缺点:两个不可达的对象相互引用,导致内存无法回收。
2.JDK1.2之后采用可达性算法(根搜索算法)---采用离散数学原理
从GC Roots为根节点一直向下遍历,找到下一个节点。。。。,这样找不到的就是不可达的,那这些就是垃圾,被垃圾回收器回收
引用的类型:
强引用(弱引用内存不足也不会回收,除非OOM,它是内存泄漏的主要原因之一)通常new产生的对象就是强引用
软引用(SoftReference内存充足时,保持引用,内存不足时,进行回收)
弱引用(WeakReference不管JVM的内存空间是否足够,总会回收该对象占用的内存)
虚引用
垃圾回收算发:(垃圾回收)
1.标记清除算法--
好处:不需要对象的移动,并且仅对不存活的对象进行处理
在存活对象比较多的情况下极为高效,效率问题,标记和清除两个过程的效率都不高
坏处:一般是使用单线程操作,直接回收不存活对象,会造成大量的不连续的内存碎片,从而不利于后续对一些对象的分配
2.复制算法
好处:在存活对象比较少时极为的高效,实现简单,运行高效;每次都是对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等情况,只要移动堆顶指针,按顺序分配内存即可
坏处:需要一块内存空间作为交换空间
3.标记--整理算法--标记清除算法基础之上的一个算法,解决了内存碎片的问题,主要针对对象存活率高的老年代
4.分代收集算法---根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、
没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收
垃圾回收的触发机制:(垃圾回收的时机)
java虚拟机无法再为新的对象分配内存空间了
手动调用System.gc()方法
低优先级的GC线程,被JVM启动时就会执行GC
Dalvik VM 与JVM的不同
1.执行的文件不同,一个是class的一个是dex的
2.类加载的系统与JVM区别较大
3.可以同时存在多个dvm
4.Dalvik是基于寄存器的,而JVM是基于堆栈的
Dalvik VM与ART的不同之处
DVM使用JIT(即时编译每次运行都会执行编译)来将节码转换成机器码,效率低
ART采用了AOT预编译技术,执行速度更快(JDK 1.9 代码的分段缓存技术,执行速度更快)
ART会占用更多的应用安装时间和存储空间
第三章:
Java中的ClassLoader回顾
Android中的ClassLoader的作用详解
Android中的动态加载比一般Java程序复杂在哪里
Android ClassLoader概述
Android中ClassLoader的种类
Android中ClassLoader的特点
ClassLoader源码讲解
Java中的ClassLoader回顾
BootstrapClassLoader: Load JRE\lib\rt.jar或者Xbootclasspath选项指定的jar包
ExtensionClassLoader: Load JRE\lib\ext\*.jar或者Djava.ext.dirs指定目录下的Jar包
AppClassLoader: Load CLASSPATH或者Djava.class.path所指定的目录下的类和Jar包
CustomClassLoader:通过java.lang.ClassLoader的子类自定义加载Class
Android中ClassLoader的种类
BootClassLoader:Load JRE\lib\rt.jar或者Xbootclasspath选项指定的jar包及Framework层的一些Class字节码文件
PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点。
BaseDexClassLoader:是PathClassLoader与DexClassLoader的父类
一个应用程序最少需要
BootClassLoader、PathClassLoader两个类加载器
特点及作用
双亲代理模型的特点
(先判断类是否是被当前的ClassLoader加载过,加载过则返回,没有加载过在去看其父类ClassLoader是否加载过,加载过返回,否的话,最终由其子类ClassLoader去加载)
这个特点也就意味着一个类被位于事务中的任意一个ClassLoader加载过,那么在今后的整个系统的生命周期中,该类就不会再重新被加载了,大大提高了类加载的一个效率。
类加载的共享功能(Framework里面的一些类一旦被顶层的ClassLoader加载过,就会保存在缓存里面,以后就不要重新加载了)
类加载的隔离功能(不同继承路径上的ClassLoader加载的类肯定不是同一个类,可以防止用户数据被篡改,防止自定义类冒充核心类库,以访问核心类库中的成员变量)
双亲委派机制的描述:
当某个特定的类加载器在接到类加载的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类的加载任务,就成功返回;
只有父类加载器无法完成加载任务时,自己才去加载。
意义:防止内存中出现多份同样的字节码;例如:
比如两个类A和类B都要加载System类:
如果不用委托而是自己加载自己的,那么类A就会加载一份System字节码,然后类B又会加载一份System字节码,这样内存中就出现了两份System字节码。
如果使用委托机制,会递归的向父类查找,也就是首选用Bootstrap尝试加载,如果找不到再向下。这里的System就能在Bootstrap中找到然后加载,
如果此时类B也要加载System,也从Bootstrap开始,此时Bootstrap发现已经加载过了System那么直接返回内存中的System即可而不需要重新加载,这样内存中就只有一份System的字节码了
双亲委派机制也就是热修复在技术上可以实现的根本依据,即:多个dex文件组成一个数组Element[],按照顺序加载,对于一个已经加载的Class是不会再次加载的。
同一个ClassLoader加载过的类可以称之为同一个类(还要有相同的包名、类名)
Android ClassLoader加载顺序:
ClassLoader.java中 loadClass()方法,在该方法中判断是否是被加载过,若没有主要用到了findClass()方法,而在ClassLoader.java中,findClass()是一个空实现,于是到PathClassLoader,DexClassLoader中查看
发现两者都是共同父类BaseDexClassLoader中实现的,此时调用到了DexPathList对象的findClass()方法,而DexPathList对象实在BaseDexClassLoader的构造方法中通过new实现的,于是找到
DexPathList类的构造方法中找到makeDexElements()方法,该方法返回一个Element[]数组,在该方法makeDexElements()中遍历所有的Dex文件,调用loaddexFile()方法,在该方法中
返回了DexFile对象,并最终存到Element[]数组中。此时,再来看DexPathList对象的findClass()方法中遍历Element[]数组,对数组中的每一个Element元素(DexFile)调用DexFile对象的
loadClassBinaryName()方法返回一个Class对象,而loadClassBinaryName()最终调用的是Native方法defineClassNative()该方法是C与C++实现的。
Android动态加载的难点:
1.有许多组件类需要注册之后才能使用
2.资源的动态加载很复杂
3.Android程序运行时需要一个上下文环境
第四章热修复详解:
热修复的基本概念讲解
当前市面上比较流行的几种热修复技术
方案对比及技术选型
什么是热修复?
动态修复、动态更新
热修复有哪些好处
热修复的流行技术:
QQ空间的超级补丁方案
微信的Tinker
阿里的AndFix,dexposed(最新版本Sophix 3.2.0)
美团的Robust,饿了吗的migo,百度的hotfix
第五章 本章概述
AndFix的基本介绍
AndFix执行流程及核心原理
使用AndFix完成线上bug修复
AndFix源码讲解
https://www.cnblogs.com/wetest/p/7595958.html
https://segmentfault.com/a/1190000011365008
http://blog.csdn.net/lostinai/article/details/54694959
AndFix修复即时生效的原理:
腾讯系的方案为什么不能及时运行?
腾讯系的方案是基于类加载机制采用全量替换的原理完成的,由于类加载的双亲代理特点,已加载到内存中的Class是不会再次加载的。
因此,采用腾旭系方案,不会及时生效,不重启则原来的类还在虚拟机中,就无法加载新类,当再次启动的时候,去加载新合成的dex文件,以完成增量更新。
这样Tinker将新旧两个DEx的差异放在补丁包中,下发到移动端,在本地采用反射的原理修改dexElements数组,合成完整的dex文件。
但是全量替换会造成补丁包体积过大,因此,采用了Dexdiff算法,大大优化下发差异包的大小。
在Android N中完全废弃掉PathClassloader,而采用一个新建Classloader来加载后续的所有类,即可达到将cache无用化的效果。
AndFix的方案采用底层替换方案,即:通过修改一个方法包括方法入口地址在内的每一项数据,使之指向一个新的方法。
具体来讲就是:
通过反射机制得到所要替换的Method对象所对应的object,进而找到这个方法底层Java函数所对应ArtMethod的真实地址,把旧函数的所有变量都替换为新函数的
因为,每一个java方法在DVM/ART中都对应着一个底层Java函数,在Art模式中是:ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括所属类、访问权限、代码执行地址等等。
通过env->FromReflectedMethod,可以由Method对象得到这个方法对应的ArtMethod的真正起始地址。然后就可以把它强转为ArtMethod指针,从而对其所有成员进行修改。
这样全部替换完之后就完成了热修复逻辑。以后调用这个方法时就会直接走到新方法的实现中了
AndFix兼容性的根源所在:
采用的Native替换方案:都是写死了ArtMethod结构体(Andfix是把底层结构强转为了art::mirror::ArtMethod),AndFix里面的ArtMethod是遵照android虚拟机art源码里面的ArtMethod构建的。
由于Android是开源的,各个手机厂商都可以对代码进行改造,而Andfix里ArtMethod的结构是根据公开的Android源码中的结构写死的,若是厂商对开源的ArtMethod结构体进行了修改,
和源码里面的结构体不一致,替换则出现了问题。
优化策略:
直接替换ArtMethod而不是直接替换,以解决兼容性问题,
即时生效带来的限制:只能是方法级别的替换,不能添加新的字段,不能增加方法,以防止在计算sizeOf()时出现差错。
AndFix支持从2.3到7.0的Android版本,包括ARM和X86架构,Dalvik和ART运行时,32位和64位
AndFix的实现原理是方法的替换,使得有Bug的方法永远都不会被执行到。(是阿里的热修复开源版本)当前最新的
热修复版本是阿里HotFix 3.0(Sophix 3.2.0)
https://help.aliyun.com/document_detail/61082.html?spm=a2c4g.11186623.6.547.3I5XDy Sophix 3.2.0的稳健接入
dexposed和andfix是alibaba的开源项目,都是apk增量更新的实现框架,目前dexposed的兼容性较差,只有2.3,4.0~4.4兼容,
其他Android版本不兼容或未测试,详细可以去dexposed的github项目主页查看,而andfix则兼容2.3~6.0,所以就拿这个项目来实现增量更新吧。
AndFix的集成阶段
Gradle添加依赖 compile 'com.alipay.euler:andfix:0.5.0@aar'
1.https://github.com/hanfengzqh/AndFix AndFix的GitHub地址
1.AndFix的 PatchManager 的初始化
mPatchManager = new PatchManager(context);
mPatchManager.init(Utils.getVersionName(context));//
//进行热修复之后,为了将SortedSet mPatchs中的Patch每一个文件取出调用mAndFixManager.fix();
mPatchManager.loadPatch();
2.加载我们的patch文件
if (mPatchManager != null) {
//网络有热修复文件(.apatch)时调用,将.apatch补丁文件,复制到包内/apatch/文件夹下,并将
//包内文件夹下的.apatch文件Patch文件,转换为调用loadPatch(Patch)方法
mPatchManager.addPatch(path);
}
AndFix的准备阶段
1.build一个有bug的old apk并安装到手机上
2.分析问题解决bug后,build一个new apk
补丁会生成一个以.apatch结尾的文件
AndFix bug修复的常用两条指令:
usage: apkpatch -f -t -o -k -p <***> -a -e <***> 新建
-a,--alias keystore entry alias.
-e,--epassword <***> keystore entry password.
-f,--from new Apk file path.
-k,--keystore keystore path.
-n,--name patch name.
-o,--out output dir.
-p,--kpassword <***> keystore password.
-t,--to old Apk file path.
usage: apkpatch -m -o -k -p <***> -a -e <***> 合并
-a,--alias keystore entry alias.
-e,--epassword <***> keystore entry password.
-k,--keystore keystore path.
-m,--merge path of .apatch files.
-n,--name patch name.
-o,--out output dir.
-p,--kpassword <***> keystore password.
Patch安装阶段:
1.将apatch 文件通过adb push 到手机上
2.使用户已经安装的应用load我们的apatch文件
3.load成功后验证我们的bug是否被修复
AndFix优劣:
1.原理简单,集成简单,使用简单,即时生效(Andfix采用的方法是,在已经加载了的类中直接在native层替换掉原有方法,是在原来类的基础上进行修改的)
2.只能修复方法级别的bug,极大限制了使用场景
http://blog.csdn.net/cocoooooa/article/details/51096613 Android 热修补方案(AndFix)源码解析 涉及到JNI层
https://yq.aliyun.com/articles/74598?spm=a2c4g.11186623.2.33.DpP3QL Android热修复升级探索——追寻极致的代码热替换
AndFix劣势补充:https://www.cnblogs.com/aimqqroad-13/p/5965683.html AndFix的限制因素
1.不支持YunOS在线推送。
2.不支持添加新的类和新的字段,(官网)不支持构造方法、参数数目大于8或者参数包括long,double,float基本类型的方法。
3.需要使用加固前的apk制作补丁,但是补丁文件很容易被反编译,也就是修改过的类源码很容易暴露
4.使用有些加固平台可能会使热补丁功能失效(亲测:360加固不存在该问题,是在dex外侧加一层壳)
5.andfix不支持布局资源等的修改(即:在热修复时新旧apk的版本号不可更改,只能一致,否则“.apatch”补丁包 无法生成)
6.潜在问题:加载一次补丁之后,out.apatch文件会copy到getFileDir目录下的/apatch文件夹中,
在下次补丁更新时,会检测补丁是否已经添加在apatch文件夹下,若是存在就不会复制加载sdcard的out.apatch
7.AndFix并没有提供多次修复的解决方案,需要自己封装
(1)https://www.jianshu.com/p/58fc4c2cb65a andfix 多次修改同一个方法报错的解决 Gradle引入方式
(2)https://yq.aliyun.com/articles/63774?spm=5176.10695662.1996646101.searchclickresult.49db6001ZX2cgM android 热修补之andfix实践(多次修复) Module
(2.1)有新补丁.apatch文件,调用mPatchManager.addPatch(patchPath)之后,将SD卡下下载的.apatch补丁文件删除掉,以避免启动都会复制加载一次。
(2.2)在addPatch()内部判断包内apatch/文件夹下存在的.apatch文件删除掉,以便放入新的.patch补丁文件
8.无安全机制
AndFix源码解析:
1.PatchManager是一个典型的外观模式,把api封装在其内部,只留下初始化init()/loadPatch()/addPatch()等方法
PatcthManager内部几个重要的成员变量:
AndFixManager 主要的修复工具类(譬如:removeOptFile()移除文件)
SortedSet mPatchs;里面放置了所有的patch补丁文件
在PatchManager构造方法里面就是各主要成员变量的初始化操作,以及创建 apatch与apatch_opt文件夹
2.init(appVersion)初始化操作过程中,完成对Patch文件的添加与删除操作集合mPatchs。
(1)若是版本号不同(即:版本升级了)就会把包内目录下的apatch与apatch_opt文件夹下的.apatch文件删掉
(2)若是版本号相同就把所有的.apatch文件转化为Patch文件添加到mPatchs里面,以便loadPatch()加载使用。
3.loadPatch()方法加载,调用loadPatch(Patch)最终调用mAndFixManager.fix()方法,
在该方法(存在于AndFixManager类里面)里面验证签名,若是签名验证通过则将Patch中的File转化为DexFile,
在while循环里面遍历DexFile文件,通过dexFile.loadClass()方法查找我们真正需要的Class
然后再调用fixClass()方法,在该方法内部通过反射获取有 MethodReplace.class 注解标记的方法
在调用 replaceMethod()完成方法替换,在该方法中调用AndFix.addReplaceMethod(src, dest);
最终调用native 方法 replaceMethod(),最终完成方法替换。
@AndFix/src/com/alipay/euler/andfix/AndFix.java
private static native void replaceMethod(Method src, Method dest);
src 需要被替换的原有方法;
dest 对应的就是新方法
4.addPatch(patchPath)最终也是调用了loadPatch(Patch)也是调用mAndFixManager.fix()方法
JNI层:
@AndFix/src/com/alipay/euler/andfix/AndFix.java
private static native void replaceMethod(Method src, Method dest);
@AndFix/jni/andfix.cpp
static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,
jobject dest) {
if (isArt) {
art_replaceMethod(env, src, dest);
} else {
dalvik_replaceMethod(env, src, dest);
}
}
最终根据Method对象找到底层Java函数对应的ArtMethod的真实地址,完成方法的替换
必知必会:
AndFix的原理及执行流程
AndFix的集成及基本用法
AndFix组件化的思路和代码实现
第六章 Tinker的相关学习
1.Tinker的基本介绍
2.Tinker的执行原理及流程
3.使用Tinker完成线上bud修复
4.Tinker源码讲解
1.Tinker基本介绍:
开源项目,微信官方的Android热补丁解决方案,支持动态下发代码、so、资源,在不需要重新安装的情况下实现更新。
支持2.x-7.x版本
Tinker主要包含以下三个部分:
1.gradle编译插件:tinker-patch-gradle-plugin
2.核心SDK:tinker-android-lib
3.非gradle编译用户的命令行版本:tinker-patch-cli-1.9.1.jar
Tinker的核心原理
基于Android原生的ClassLoader,开发了自己的ClassLoader
基于Android原生的AAPT,开发了自己的AAPT
微信团队基于Dex文件格式,研发了自己的DexDiff算法
Tinker的优劣:
(1)Tinker不支持修改 AndroidManifest.xml,Tinker不支持新增四大组件
(2)由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码
(3)在Android N上,补丁对应用的启动时间有轻微的影响
(4)不支持部分三星android-21机型
(5)对于资源替换,不支持修改 remoteView。例如transition动画,notification icon以及桌面图标。
https://github.com/Tencent/tinker/wiki Tinker -- 微信Android热补丁方案
若出现资源变更,我们需要使用applyResourceMapping方式编译,这样不仅可以减少补丁包大小,同时防止remote view id变更造成的异常情况
Tinker集成阶段:
Gradle中添加Tinker依赖
在代码中完成对Tinker的初始化
因为Tinker需要监听Application的生命周期,在ApplicationLike委托对象里面可以完成Tinker对Application生命周期的监听。
通过ApplicationLike完成Tinker的初始化操作
准备阶段
1.build一个old.apk并安装到手机
2.修改一些功能之后,build一个new.apk
patch文件的生成:
1.命令行的方式完成patch的生成
各个文件的作用及命令参数讲解
通过使用tinker-patch命令生成patch文件
2.利用Gradle插件的方式完成patch包的生成
1.patch命令行方式:
java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out output_path
(1)修改apk前后不一样的apk,修改资源文件
(2)tinker-config.xml中更改loader标签为ApplicationLike类中注解 @DefaultLifeCycle中的自定义application全路径
(3)修改加密文件、密码及别名及密码
(4)
android:value="tinker_id_b168b32"/>
2.Gradle插件配置生成
(1)gradle中正确配置tinker参数(非常重要)
(2)在android studio中直接生成patch文件
在ApplicationLike自定义类中的onBaseContextAttached()方法中
TinkerInstaller.install(ApplicationLike);//完成tinker初始化
调用
if (Tinker.isTinkerInstalled()) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), patch);
}
完成patch补丁文件的加载//TinkerInstaller.onReceiveUpgradePatch();
几个重要的部分:
安全校验:无论是补丁合成还是补丁加载,我们都要进行必要的安全校验
版本管理:Tinker支持补丁升级,甚至是多个补丁不停的切换。这里要保证所有进程版本的一致性
补丁加载:通过反射加载我们合成的dex,so,资源文件等
补丁合成:这些都是在单独的patch进程中完成的,这里包括dex,so,资源,主要完成补丁包的合成与升级。
监听回调:在合成与加载的过程中,出现问题及时回调。
1.https://www.jianshu.com/p/6e09f0766af3 微信热补丁Tinker -- 补丁流程
2.http://blog.csdn.net/tyk9999tyk/article/details/53391519 Tinker 常见问题
Tinker库中有什么类是不能修改的?
Tinker库中不能修改的类一共有25个,即com.tencent.tinker.loader.*类。加上你的Appliction类,只有25个类是无法通过Tinker来修改的
Tinker多补丁版本发布:
Tinker支持对同一基准版本做多次补丁修复,在生成补丁时,oldApk依然是已经发布出去的那个版本。
即补丁版本二的oldApk不能是补丁版本一,它应该依然是用户手机上已经安装的基准版本。
Tinker的高级功能:
1.Tinker如何支持多渠道打包
2.如何自定义Tinker行为
3.tinker使用过程中需要注意到的哪些问题
1.多渠道打包
(1)命令行方式智能一个渠道一个渠道的打tpatch
(2)gradle方式则只需简单的修改一下gradle脚本即可
步骤:
(1).在AndroidManifest.xml中添加
android:name="MY_CHANNEL"
android:value="${MY_CHANNEL}" />
或者其他途径
(2).在build.gradle中添加productFlavor
productFlavors {//多渠道脚本支持
xiaomi {
manifestPlaceholders = [MY_CHANNEL: "xiaomi"]
buildConfigField "String", "AUTO_TYPE", "\"1\""
}
baidu {
manifestPlaceholders = [MY_CHANNEL: "baidu"]
buildConfigField "String", "AUTO_TYPE", "\"2\""
}
_360 {
manifestPlaceholders = [MY_CHANNEL: "_360"]
buildConfigField "String", "AUTO_TYPE", "\"3\""
}
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [MY_CHANNEL: name]
}
}
(3).修改tinker脚本添加多渠道的目录tinkerBuildFlavorDirectory = "${bakPath}/tinkergradle-0218-12-22-06"
最重要的是拷贝目录,完成多渠道目录的拼凑
2.Tinker的自定义行为
(1)自定义PatchListener监听patch receiver事件 ,继承于 DefaultPatchListener
作用:DefaultPatchListener implements PatchListener
而PatchListener中只有onPatchReceived(String path)方法,因此自定义的必须得实现
在该方法中调用int returnCode = patchCheck(path,patchMd5);当returnCode==0校验通过,就会调用
TinkerPatchService.runPatchService(context, path);开启一个新的后台进程进行补丁合成
校验patch文件是否合法;启动service去安装patch文件
(2)自定义TinkerResultService改变patch安装成功后的行为,继承于DefaultTinkerResultService extends AbstractResultService extends IntentService
作用:决定在patch安装完成之后的后续操作,默认实现是杀死进程
重写onPatchResult(PatchResult result)方法,删除杀死进程的方法,提高用户体验
Tinker自定义初始化个参数:
LoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext());//patch加载阶段各种异常情况的监听回调
1.回调运行在加载的进程,它有可能是各个不一样的进程。我们可以通过tinker.isMainProcess或者tinker.isPatchProcess知道当前是否是主进程,patch补丁合成进程。
2.回调发生的时机是我们调用installTinker之后,某些进程可能并不需要installTinker
PatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext());//patch文件合成阶段各种异常情况的监听回调
isUpgrade:区分补丁合成的类型。是由于文件丢失而发起的RepariPatch, 还是收到新的补丁而发起的UpgradePatch
patchListener = new CustomPatchListener(getApplicationContext());//反馈
PatchListener类是用来过滤Tinker收到的补丁包的修复、升级请求,也就是决定我们是不是真的要唤起:patch进程去尝试补丁合成。我们为你提供了默认实现DefaultPatchListener.java。
一般来说, 你可以继承DefaultPatchListener并且加上自己的检查逻辑,例如SamplePatchListener.java。
若检查成功,我们会调用TinkerPatchService.runPatchService唤起:patch进程,去尝试完成补丁合成操作。反之,会回调检验失败的接口。事实上,你只需要复写patchCheck函数即可。
若检查失败,会在LoadReporter的onLoadPatchListenerReceiveFail中回调。
public int patchCheck(String path, boolean isUpgrade)
AbstractPatch abstractPatch = new UpgradePatch();
TinkerInstaller.install(mAppLike,
loadReporter,patchReporter,
patchListener,CustomResultService.class,
abstractPatch);//完成tinker初始化
http://blog.csdn.net/tyk9999tyk/article/details/53391525 Tinker 自定义扩展
Tinker的源码分析:
Tinker初始化过程中:
使用到了外观模式TinkerInstaller.install()、构建者模式Tinker tinker = new Tinker.Builder()、单例模式public static Tinker with(Context context)
Tinker的patch补丁文件的加载合成过程
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), patch);
最核心的方法就是
TinkerPatchService.runPatchService(context, path);开启一个新的后台进程进行补丁合成
TinkerPatchService extends IntentService
最终是在 onHandleIntent(Intent intent)方法中完成patch文件的加载与合成
调用public abstract boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult);方法
在UpgradePatch extends AbstractPatch的tryPatch()方法中分别调用
if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
return false;
}
if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");
return false;
}
if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
return false;
}
完成dex,res,lib加载与合并
必知必会
Tinker两种集成使用方式
使用Tinker完成对应用的动态更新
如何自定义patch加载及加载完成后的行为
第七章 代码及版本发布管理
1.加入动态更新之后如何管理我们的代码分支
2.加入动态更新之后如何管理我们的发版节奏
分支管理(普通)
master分支
dev分支--开发分支
引入动态更新之后的分支管理
除了master分支和dev分支,引入hot_fix分支
hot_fix分支专门用来管理动态更新迭代
sourceTree git代码界面化管理软件
第八章 插件化讲解
1.插件化相关知识介绍
2.插件化原理与实践
1.插件化相关知识介绍
插件化产生的背景:
(1)app的体积原来越庞大,功能模块越来越多
(2)模块耦合度高,协同开发沟通成本极大
(3)方法数可能会超过65535,占用内存过大
(4)为了平行高效开发。
插件化的优点:
1)模块解耦,应用程序扩展性强
2)解除单个dex函数不能超过 65535的限制
3)动态升级,下载更新节省流量
4)高效开发(编译速度更快)
基于以上问题 如何解决?
1.将一个大的APK按照业务分割成多个小的APK
2.每一个小的APK即可以独立运行又可以作为插件运行
基于Android动态加载技术以上是可以实现的
插件化带来的优势:
业务模块基本完全解耦
高效并行开发(编译速度更快)
按需加载,内存占用更低等等
插件化的相关概念
宿主:主APP,可以加载插件,也称之为Host
插件:插件APP,被宿主加载的App,可以是与普通app一样的apk
插件化:将一个应用按照宿主插件的方式改造就叫做插件化
相关概念对比:
(与组件化对比)
组件化是一种编程思想,而插件化是一种技术
组件化是为了代码的高度复用性而出现的
插件化是为了解决应用越来越大而出现的
(与动态更新对比)
与动态更新一样,都是动态加载技术的应用
动态更新是为了解决线上bug或者小功能的更新而出现
插件化是为了解决应用越来越庞大而出现的
https://www.jianshu.com/p/1c5afe686d75 Android组件化框架设计与实践
https://segmentfault.com/a/1190000009577849 使用ARouter实现组件化
https://www.jianshu.com/p/c0eecbbf1481 谈谈App的统一跳转和ARouter
https://www.jianshu.com/p/7cb2cc9b726a [Alibaba-ARouter] 简单好用的Android页面路由框架
http://blog.csdn.net/zhaoyanjun6/article/details/76165252 Android 路由框架ARouter最佳实践
组件化产生的背景:
1.代码量膨胀,不利于维护,不利于新功能的开发及测试
2.项目工程的构建速度慢
3.代码之间的耦合严重
4.做不到按需加载打包
组件化的优势:
代码简洁,冗余量少,维护方便,易扩展新功能。
提高编译速度,从而提高并行开发效率。
避免模块之间的交叉依赖,做到低耦合、高内聚。
引用的第三方库代码统一管理,避免版本统一,减少引入冗余库。
定制项目可按需加载,组件之间可以灵活组建,快速生成不同类型的定制产品。
制定相应的组件开发规范,可促成代码风格规范,写法统一。
系统级的控制力度细化到组件级的控制力度,复杂系统构建变成组件构建。
每个组件有自己独立的版本,可以独立编译、测试、打包和部署。
组件化与模块化的区别?
1.模块化关注点是功能,组件化的关注点是复用性,更加注重分离。
2.通常一个模块化包含几个组件
3.两者划分的粒度不一样,组件化粒度更小,组件化更加侧重于单一功能的内聚,偏向于解耦
组件化思路:首先就是解耦,形式就是每一个Module自己能够运行
组件分为两类:技术组件和业务组件
技术组件:合理封装,避免技术组件库过大;
业务组件:选择性依赖技术组件可以使得业务组件单独运行
组件化实施过程:
1.对于技术组件需要合理封装,减少之后可能存在的替换成本;
2.同时注意,将技术组件分为常用和非常用,可以选择自己需要的技术组件,避免一个统一的技术组件库过大;
3.将业务代码根据模块进行剥离,剥离成一个个的小模块;
4.单独的业务模块加上必要的技术组件,支撑在开发阶段的独立运行;
组件化难点:
1.技术组件的整理、抽离
2.一定要有的DisPatcher:提供隐式的跳转和模块间方法的调用能力;
3.组件的划分
4.调试及集成方式
组件化实战:构建中间层,只让组件对中间层单方面耦合,中间层不对其他模块发声耦合。
组件之间的通信问题:
通过接口+实现的结构进行组件间的通信。即:每个组件声明自己提供服务的 Service API,这些Service都是一些接口,
组件负责将这些Service 接口实现并注册到一个统一的路由Router中去。因此,如果要使用某个组件的功能,只需要向Router请求这个Service的实现。
在组件化架构设计图中 Common 组件就包含了路由服务组件,里面包括了每个组件的路由入口和跳转。
组件化的概念就类似于Binder通信
组件化下的UI跳转问题:
UI跳转也是组件通信的一种,一般通过短链的方式进行,跳转到具体的Activity。每个组件注册自己所能处理的短链Scheme和Host,并定义传输数据的格式,
然后注册到统一的 UIRouter 中,UIRouter 通过 Scheme 和 Host 的匹配关系负责分发路由。但目前比较主流的做法是通过在每个 Activity 上添加注解,
然后通过 APT 形成具体的逻辑代码。目前方式是引用阿里的 ARouter 框架,通过注解方式进行页面跳转。
动态加载技术:
动态更新/热更新/热修复
插件化
封装、设计模式----组件化
插件化相关原理讲解
相关知识:
android ClassLoader加载class文件原理
Java反射原理
android资源加载原理
四大组件加载原理
Gradle打包原理
一、插件化对Manifest清单文件的处理
宿主 ManiFest/Aar Manifest/Bundle Manifest....------(合并Merge APK Manifest)---->(解析Bundle BundleInfoList)
流程:
1.构建期进行全量Merge操作
2.Bundle的依赖单独Merge,生成Bundle的Merge manifest
3.解析各个Bundle的Merge Manifest,得到整包的BundleInfoList
二、插件化框架对插件类的加载
插件化框架为插件类提供其加载所需的ClassLoader,用来完成插件类的加载
插件化框架提供ClassLoader进行插件类加载,涉及到两个问题:
1.如何定义ClassLoader加载类文件,继承DexClassLoader
2.如何调用插件APK文件中的类
即:创建了ClassLoader并且能够加载插件中的类,才能算得上是有用的ClassLoader
三、资源加载
1.若资源只有文件名File Name,则需要AssetManager通过文件名直接加载对应的资源。
2.若资源有对应的Resource ID(譬如:图片、动画、字符串等是会生成Resource ID的)因此需要使用到
Resources这个类完成对应资源文件的加载,加载完之后再使用AssetManager完成对资源文件的读写
以上可以得知:要想完成资源的加载需要用到Resources与AssetManager两个类
而安装到系统中的APK通过Context就可以完成对这两个类的直接引用,从而完成对资源的加载
而插件类的资源文件,因为插件并没有安装,因此这些插件类就没有自己的资源相关的Resources与AssetManager,因此这两个类就需要插件化框架帮我们完成动态的去创建
当前流行的插件化框架加载资源文件,是为每一个Bundle(插件)分别创建一个AssetManager完成对应插件的加载。
在插件被调用的时候将插件的Res,So等的资源文件目录路径,通过反射的方式加入到AssetManager,这样我们的AssetManager就知道了插件res,so资源文件存放路径,因此
插件化对资源加载的核心原理是:
想办法为每一个插件创建对应的AssetManager,加载插件中的资源路径
插件化的核心技术:
1.处理所有插件Apk文件中的Manifest文件(Bundle Manifest 合并到宿主Manifest)
2.管理宿主apk中所有的插件apk信息(因为不止有一个插件)
3.为每一个插件apk创建对应的类加载器,资源管理器,以完成对插件资源的加载
第九章 插件化框架实战
市面上现有的插件化框架介绍
具体使用一种框架完成插件化改造
市面上插件化框架
360手机助手的DroidPlugin框架
百度的dynamic-load-apk框架
个人开发者林光亮的Small框架
alibaba开源的Atlas框架
使用Small对项目进行改造
集成:
1.按照规则创建对应的project
2.在创建好的project build.gradle中集成编译插件gradle-Small
文件末尾引用gradle-small插件:apply plugin: 'net.wequick.small'
用gradlew small 验证集成是否正确>
3.在工程的宿主module中初始化Smal在自定义Application中 Small.preSetUp(this);
插件创建阶段
一、以指定的规范来创建插件
Module name 以 app.* 命名的模块将被 Small 在 编译时 识别为应用插件模块。
Package name 以 app* 结尾的插件将被 Small 在 运行时 识别为应用插件
二、编译创建好的插件
(1)先编译公共库 gradlew buildlib -q (宿主是一个最基础的公共库)
(2)再编译app.main插件 gradlew buildBundle -q Dbundle.arch=arm
(3)查看编译完成的具体情况 gradlew small
至此,我们已经完成插件编译并将之内置到宿主包中去了
三、通过宿主应用启动插件应用
(1)新建assets目录,在该文件夹下创建路由配置文件 bundle.json;
(2)修改bundle.json,添加路由:
{
"version": "1.0.0",
"bundles": [
{
"uri": "main",
"pkg": "com.example.appmain"
}
]
}
这里的:
version,是 bundle.json 文件格式版本,目前始终为 1.0.0
bundles,插件数组
uri,插件唯一ID
pkg,插件包名
通过这个配置,main 将被路由向 com.example.mysmall.appmain#MainActivity
(3)回到宿主的 app > java > com.example.mysmall > MainActivity,在 onStart 方法中我们通过上述配置的 uri 来启动 app.main 插件:
@Override
protected void onStart() {
super.onStart();
Small.setUp(this, new Small.OnCompleteListener() {
@Override
public void onComplete() {
Small.openUri("main", MainActivity.this);
}
});
}
(4)运行宿主
需要掌握的small集成此基础:
通过 build.gradle 集成 Small
通过 自定义Application 初始化 Small
通过 buildLib,buildBundle 编译 Small 插件
通过 bundle.json 配置插件路由
通过 Small.openUri 启动插件
业务类插件/公共库插件创建
(1)创建公共库插件模块
Lib.style
Module name: lib.style
Package name: com.example.libstyle
(2)添加公共库引用
修改 app.main/build.gradle,增加对 lib.style 的依赖:
dependencies {
...
compile project(':lib.style')
}
(3)添加插件路由
修改 app/assets/bundle.json:
{
"pkg": "com.example.libstyle"
}
(4)重新编译插件
清除公共库:
./gradlew cleanLib -q
编译公共库:
./gradlew buildLib -q -Dbundle.arch=x86
编译业务单元:
./gradlew buildBundle -q -Dbundle.arch=x86
(5)重新运行程序
Small进阶教程
公共库插件模块
公共库插件模块在 开发时 可以通过 compile project(':插件模块名')来被 应用插件模块 所引用。
同时在 编译时 (buildLib) 会被打包成为一个可独立更新的插件。
定义公共库插件模块有两种方式:
指定 Module name 为 lib.*
在 Small DSL 中显式指明 bundles lib your_module_name
要正确读取到打包的公共库插件也有两种方式:
指定 Package name 为 **.lib.* 或 **.lib*
在 bundle.json 中添加描述 "type": "lib"
应用插件模块
应用插件模块在 开发时 可以独立运行。
同时在 编译时 (buildBundle 或 :模块:aR ) 会被打包成一个可独立更新的插件。
定义应用插件模块有两种方式:
指定 Module name 为 app.*
在 Small DSL 中显式指明 bundles app your_module_name
要正确读取到打包的公共库插件也有两种方式:
指定 Package name 为 **.app.* 或 **.app*
在 bundle.json 中添加描述 "type": "app"
自定义资源ID分段
在整合插件资源的过程,为避免资源ID冲突,需要为每个插件分配一个ID段。
我们知道默认程序的ID段为 0x7f。由于系统使用了 0x00,0x01,0x02。因此插件允许的范围在 [0x03, 0x7e] 之间。
但是有些手机厂商占用了一些分段,黑名单如下:
ID 厂商
0x03 HTC
0x10 小米
为此,Small 根据哈希算法将插件的模块名散列到 [0x11, 0x7e] 区间,作为自动分配的插件资源ID段,比如:
模块名 ID
app.main 0x77
lib.style 0x79
但是,这个算法生成的ID有可能是重复的,所以必要时你可以通过以下方法自定义插件的资源ID。
修改 build.gradle
在插件模块所在的 build.gradle 脚本里,增加配置:
ext {
packageId = 0x12
}
这里的 0x12 是16进制整型值
将把当前插件的资源ID段分配为 0x12。
或修改 gradle.properties
在插件模块所在的 gradle.properties 配置里,增加配置:
packageId=3f
properties只接收字符串值
将把当前插件的资源ID段分配为 0x3f。
http://code.wequick.net/Small/cn/home Small开发文档
必知必会
Samll的基本用法:集成、插件化生成、宿主配置
将已有项目改造成插件化架构
Samll的进阶知识,做到对Samll有一个全面的了解
Atlas框架讲解
重量级框架 非常复杂
Atlas的基本概念和作用
了解Atlas的整体结构和原理
Alibaba独立开发并开源的一种插件化技术方案,也叫动态组件化(Dynamic Bundle)框架
目前手机淘宝和优酷使用的是这种技术方案
功能强大但是使用特别复杂,适用于门户型的App,它主要提供了解耦化、组件化、动态性的支持
核心原理与Samll是完全一样的
与插件化框架不同的是,Atlas是一个组件框架,Atlas不是一个多进程的框架,他主要完成的就是在运行环境中按需地去完成各个bundle的安装,加载类和资源
Atlas的整体结构和原理
https://alibaba.github.io/atlas/principle-intro/Runtime_principle.html
框架层次:
包含四个层次
1.最下面的一层:hack工具层,主要是进行hack工具类的初始化和校验工作
2.Bundle Framework 负责bundle的安装更新以及管理整个bundle的生命周期
3.runtime层:主要包括清单管理、版本管理、系统代理三大模块,基于不同的触发点
按需执行bundle的安装和加载。从delegate层可以看到,最核心的两个代理点:
一个是DelegateClasssLoader,负责路由由class加载到各个bundle内部,
一个是DelegateResource:负责资源查找时能够找到bundle内的资源,这是bundle能够真正运行起来的的根本点。
4.对外接入层:AtlasBridgeApplication是atlas框架下apk的真正Application,在基于Atlas框架构建的过程中会替换原有manifest中的application,AtlasBridgeApplication里面除了完成了Atlas的初始化功能,同时内置了multidex的功能,这样做的原因有两个:
很多大型的app不合理的初始化导致用multidex分包逻辑拆分的时候主dex的代码就有可能方法数超过65536,AtlasBridgeApplication与业务代码完全解耦,所以拆分上面只要保证atlas框架在主dex,其他代码无论怎么拆分都不会有问题;
如果不替换Application,那么atlas的初始化就会在application里面,由于基于Atlas的动态部署实际上是类替换的机制,那么这种机制就会必然存在包括Application及其import的class等部分代码在dalvik不支持部署的情况,这个在使用过程中造成一定成本,需要小心的使用以避免dalivk内部class resolve机制导致部分class没成功,替换以后该问题得到最好的解决,除atlas本身以外,所有业务代码均可以动态部署
Bundle的生命周期:
Installed bundle被安装到storage目录
Resolved classloader被创建,assetpatch注入DelegateResoucces
Active bundle的安全校验通过;bundle的dex检测已经成功dexopt(or dex2oat),resource已经成功注入
Started bundle开始运行,bundle application的onCreate方法被调用
每一层都为上一层次提供服务
类加载机制
Atlas里面通常会创建了两种classLoader,第一个是DelegateClassLoader,他作为类查找的一个路由器而存在,本身并不负责真正类的加载;DelegateClassLoader启动时被atlas注入LoadedApk中,替换原有的PathClassLoader;第二个是BundleClassLoader,参考OSGI的实现,每个bundle resolve时会分配一个BundleClassLoader,负责该bundle的类加载。关系如下图所示: DelegateClassLoader以PathClassLoader为parent,同时在路由过程中能够找到所有bundle的classloader;
BundleClassLoader以BootClassLoader为parent,同时引用PathClassLoader,BundleClassLoader自身findClass的顺序为
1. findOwn: 查找bundle dex 自身内部的class
2. findDependency: 查找bundle依赖的bundle内的class
3. findPath: 查找主apk中的class
Bundle的Activity启动的类加载过程来帮助理解:
ActivityThread从LoadedApk中获取classloader去load Activity Class;
根据上面的classloader关系,先去parent里面加载class;
由于class在bundle里面,所以pathclassloader内查找失败,接着delegateclassloader根据bundleinfo信息查找到classloader在bundle中(假设为bundleA);
从bundleA中加载class,并且创建class;
后面在Activity起来后,如果bundleA对bundleB有依赖关系,那么如果用到了bundleB的class,又会根据bundlA的bundleClassloader的dependency去获取bundleB的classloader去加载;
资源加载机制:
类似ClassLoader,LoadedApk中的Resources被替换成Atlas内部的DelegateResources,同时在每个Bundle安装的过程中,每个bundle的assetspath会被更新到DelegateResources的AssetsManager中;每个bundle的资源特征如图可知:
bundle构建过程中,每个bundle会被独立进行分区,packageId保证全局唯一,packageID在host的构建工程内会有个packageIdFile.properties进行统一分配;
虽然每个bundle的manifest都声明了自己的packagename,但是在aapt过程中,arsc文件里面所有bundle的packagename均被统一为hostApk的package,比如在手淘内就都是com.taobao.taobao;这样改的目的是为了解决在资源查找中一些兼容性问题;
名词解释:
Bundle
awb
host
remote bundle
动态部署
Bundle: 类似OSGI规范里面bundle(组件)的概念,每个bundle有自己的classloader,与其他bundle相隔离,同时Atlas框架下bundle有自身的资源段(PackageID,打包时AAPT指定);另外与原有OSGI所定义的service格式不同之处是Atlas里面Bundle透出所有定义在Manifest里面的component,随着service,activity的触发执行bundle的安装,运行。
awb: android wireless bundle的缩写,实际上同AAR类似,是最终构建整包前的中间产物。每个awb最终会打成一个bundle。awb与aar的唯一不同之处是awb与之对应有个packageId的定义。
host: 宿主的概念,所有的bundle可以直接调用host内的代码和资源,所以host常常集合了公共的中间件,UI资源等
基于Atlas构建后大致工程的结构:
首先有个构建整体APK工程Apk_builder,里面管理着所有的依赖(包括atlas)及其版本,Apk_builder本身可能不包含任何代码,只负责构建使用
host内部包含独立的中间件,以及一个Base的工程,里面可能包含应用的Application,应用icon等基础性内容(如果足够独立,application也可以直接放在apk_builder内);
业务层基本上以bundle为边界自上而下与host发生调用,同时bundle之间允许存在依赖关系;相对业务独立的bundle如果存在接口耦合建议封装成aidl service的方式保证自身封装性;同时某些中间件如果只存在若干bundle使用的也可以封装bundle的方式提供出来,以保证host内容精简
remote bundle: 远程bundle,远程bundle只是apk构建时并未打到apk内部,而是单独放在了云端;同时远程bundle的限制条件是第一次被触发的前提是bundle内的Activity需要被start,此时基于Atlas内的ClassNotFoundInterceptorCallback可以进行跳转的重定向,提示用户下载具体bundle,待用户确定后进行异步下载同时完成后再跳转到目标bundle(此部分代码由于涉及下载及UI展示等内容并未包含在开源仓库中,有需要可以根据ClassNotFoundInterceptorCallback自行实现)
动态部署: 基于Atlas的installorUpdate和atlas-update库及构建插件,可以生成与之前发布的apk diff生成的差异文件,在更新时拉取同时静默更新到设备上,在用户下次启动之后生效新代码,具体原理可以参考动态部署章节的解析
APK结构:
基于Atlas构建后的APK结构如下图,host与普通Apk无异,但是Manifest和assets会添加一些额外的内容,同时在armeabi目录中会增加若干个bundle构建的产物,取名为String.format(lib%s.so,packagename.replace(".","_"));packagename为bundle的AndroidManifest中的packagename,这些so都是正常的apk结构,改为so放入lib目录只是为了安装时借用系统的能力从apk中解压出来,方便后续安装
assets/bundleinfo-version.json
构建完的apk在host的assets目录下,会有个bundleinfo-verison.json的文件,其中version为manifest中的versinonname,里面记录了每个bundle大小,版本,名字以及里面所有的component信息,这些内容在构建的时候生成,基于这些信息每个bundle可以在component被触发的时候去按需的进行安装,整个过程对开发者透明(从中也可以看到默认情况下bundle对外暴露的只是基于Android原生的Activity,service,receiver等component)
AndroidManifest
运行期文件结构
/data/data/pkgname/files/bundlelisting
之前打包构建时记录的bundleinfo信息(发生动态部署收文件会进行更新)
/data/data/pkgname/files/baselineinfo
存放动态部署后的版本变化内容,以及每次部署发生更新的bundle的版本,依赖等信息
/data/data/pkgname/files/storage
storage目录是bundle安装的目录,每个bundle的安装目录以bundle的packagename为文件夹名,首次启动后会安装到version.1目录下,目录中可能含有bundle的zip文件,dex文件以及native so等内容。如果bundle发生更新,则可能会有version.2、version.3 等目录,每次加载bundle的时候选取最高可用版本进行载入。考虑bundle的回滚功能和对空间占用的影响,目前容器内最多保留两个最近可用版本。