最近工作中考虑平台化APP的开发,优秀考虑的是Android插件化开发技术,在网络上学习了一些知识点,个人研究了部分资料和源码,拿出一部分作为个人知识点记录,也作为公司内部互相学习的分享。希望有Android开发需求或者对Android有兴趣的同学(互相学习称为同学),多多关注,多多回复。
一、一些常见概念分区分
由于APP的内容越来越大,方法数有可能超过Dex最大的方法数65535的上限(因为是int的最大存储),因而变有了插件化的概念,将一个APP划分为多个插件。
常见的解决方式包括:Google Multidex(个人建议不要去碰,有在研究的赶快脱身,这个限制很多,而且有各种漏洞和补丁互补),H5页面(越来越多开始使用,个人觉得很有学习和使用的必要),付费版的Proguard等等。
插件化有许多优点:模块解耦;动态升级;高效并行开发;按需加载,等等。
许多人把插件化的概念容易和组件化和动态加载混淆,这里进行区分:
Android插件化——指将一个程序划分为不同的部分
Android组件化——和插件化概念差不多,组件指通用和复用性更高的构件
Android动态加载——更高层次的概念,也叫热加载或者Android动态部署,指APP在运行状态下动态加载某个模块,增加或改变某个部分
二、插件化的原理
2.1 原理
插件化的原理是使用Java ClassLoader。无论是JVM还是Dalvik都是通过ClassLoader去加载所需要的类,而ClassLoader加载类的方式称为双亲委托。
protected Class> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class> clazz = findLoadedClass(className);
if (clazz == null) {
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
// Don't want to see this.
}
if (clazz == null) {
clazz = findClass(className);
}
}
return clazz;
}
loadClass会先看这个类是否已被loaded过,没有的话则去parent找,如此递归,称为双亲委托。
2.2 动态加载jar
URL[] urls = new URL[] {new URL("file:libs/jar1.jar")};
URLClassLoader loader = new URLClassLoader(urls, parentLoader);
2.3 ClassLoader隔离问题
一个运行程序中可能会存在两个包名和类名完全一致的类,JVM和Dalvik对类的唯一标识是ClassLoader id +PackageName+ClassNmae,如果这两个“类”不是由一个ClassLoader加载,是无法将一个类的对象强转为另一个类,这是ClassLoader隔离。
例如Android碰到的异常:
java.lang.ClassCastException: android.support.v4.view.ViewPager can not be cast to android.support.v4.view.ViewPager
当碰到这种问题时可以通过 instance.getClass().getClassLoader(); 得到 ClassLoader,看 ClassLoader 是否一样。
2.4加载不同Jar包中的公共类
Host 工程包含了 common.jar, jar1.jar, jar2.jar,并且 jar1.jar 和 jar2.jar 都包含了 common.jar,我们通过 ClassLoader 将 jar1, jar2 动态加载进来,这样在 Host 中实际是存在三份 common.jar,如下图:
我们怎么保证 common.jar 只有一份而不会造成上面3中提到的 ClassLoader 隔离的问题呢,其实很简单,有三种方式:
第一种:我们只要让加载 jar1 和 jar2 的 ClassLoader 的 parent 为同一个 ClassLoader,并且该 ClassLoader 加载过 common.jar,通过上面 1 中我们知道根据双亲委托,最后都会首先被 parentClassLoader加载。
第二种:我们重写 jar1 和 jar2 的 ClassLoader,在 loadClass 函数中我们先去某个含有 common.jar 的 ClassLoader 中 load 即可,其实就是把上面的 parentClassLoader 换掉了而已。
第三种:在生成 jar1 和 jar2 时把 common.jar 去掉,只保留 host 中一份,以 host ClassLoader 为 parentClassLoader 即可。
2.5 Android的插件化原理
Android有自己的ClassLoader,分别DexClassLoader和PathClassLoader,区别是PathClassLoader不能直接从zip包中得到dex,只支持直接操作dex文件或者已经安装过的apk。而DexClassLoader可以加载外部的apk、jar或dex文件,并且会在指定的outpath路径存放其dex文件。
三、360的开源插件DroidPlugin
DroidPlugin 是360手机助手在 Android 系统上实现了一种新的插件机制,
被戏称为360的黑科技。
它可以在无需安装、修改的情况下运行APK文件,此机制对改进大型APP的架构,实现多团队协作开发具有一定的好处。
HOST程序:插件的宿主。
插件:免安装运行的APK
限制和缺陷:
1.无法在插件中发送具有自定义资源的Notification,例如: a. 带自定义RemoteLayout的Notification b. 图标通过R.drawable.XXX指定的通知(插件系统会自动将其转化为Bitmap)
2.无法在插件中注册一些具有特殊Intent Filter的Service、Activity、BroadcastReceiver、ContentProvider等组件以供Android系统、已经安装的其他APP调用。
3.对Activity的LaunchMode支持不够好,Activity Stack管理存在一定缺陷。Activity的onNewIntent函数可能不会被触发。 (此为BUG,未来会修复)
4.缺乏对Native层的Hook,对某些带native代码的apk支持不好,可能无法运行。比如一部分游戏无法当作插件运行。
特点:
1.支持Androd 2.3以上系统
2.插件APK完全不需做任何修改,可以独立安装运行、也可以做插件运行。要以插件模式运行某个APK,你无需重新编译、无需知道其源码。(
插件APP
没有进行加固的情况下 )
3.插件的四大组件完全不需要在Host程序中注册,支持Service、Activity、BroadcastReceiver、ContentProvider四大组件
4.插件之间、Host程序与插件之间会互相认为对方已经"安装"在系统上了。
5.API低侵入性:极少的API。HOST程序只是需要一行代码即可集成Droid Plugin
6.超强隔离:插件之间、插件与Host之间完全的代码级别的隔离:不能互相调用对方的代码。通讯只能使用Android系统级别的通讯方法。
7.支持所有系统API
8.资源完全隔离:插件之间、与Host之间实现了资源完全隔离,不会出现资源窜用的情况。
9.实现了进程管理,插件的空进程会被及时回收,占用内存低。
10.插件的静态广播会被当作动态处理,如果插件没有运行(即没有插件进程运行),其静态广播也永远不回被触发。
实现原理:完全模拟android的运行环境,在运行过程中就是依赖android运行APK的过程,只是我们在运行过程当中实时插入一些我们的逻辑,在运行过程中完全依赖android 自己的虚拟机,依赖android自己的运行流程或者完全模拟这样流程。插件有独立的classloader,与主程序与插件间是完全隔离,A插件无法访问B插件的代码,也没办法访问宿主的代码,在代码上实现完全隔离。插件实现资源隔离,资源ID不会冲突,每一个插件都用了独立的资源管理器,互相独立管理资源。插件框架模拟了一个android的运行环境,相当于对每一个插件分配了一个独立的数据目录,插件间没办法访问对方的数据,也没办法访问宿主的数据。插件运行过程中完全模拟了proccess的框架,插件APK本身有两进程,在运行当中,框架会模拟两进程,一般情况下插件运行在独立的进程中,崩溃不会影响宿主程序的。
Droid Plugin的架构图
Droid Plugin大约1W多行代码,核心之一Hook模块,能够保证插件在运行当中,框架能够实时的插入自己的逻辑。反射机制是改了apach的一个反射框架,加了缓存,方便提升性能。APK解析,借用了系统的API,但是做了机型的适配,在运行APK的过程当中,需要把manifest、dex文件或者资源进行解析并且缓存在内存当中,在插件运行的过程当中会调用,增加启动速度。Manifest先占坑,占完坑就有进程管理的服务,就是模拟的android自己的activity manager server对这些坑位进行管理或者分配。包管理服务是对已经安装的插件进行一个简单的管理。核心原理三个:进程共享、占坑、Hook。进程共享是android提供了一个这样的机制,两个APK他们的签名一致,userId一致并且他们的组件声明的进程是同一个名字时,这两个APK是可以运行在同一个进程里面的,在android系统的应用中会经常这么用。 占坑,占坑机制和黄牛党差不多,先去排队,去把系统的一些资源,包括进程、acitivty、server,contentProvider等等这些先去通过占坑的机制把这些资源先占下来,然后在通过之前提到的activity manager server 我们自己模拟的服务,对它进行管理。坑就是黄牛党买的票,activity manager server就是对各个插件进行分配。代码运行就是Hook,占坑后代码运行起来,我们需要对系统级的服务activity manager等做hook,插入我们的逻辑,实现欺上瞒下的功能。AppOns在android4.x加入,在android 6.0正式开放的动态权限管理,框架对所有的服务都做了hook,就是为了适应动态权限管理。
上图为开启一个activity的过程,app process发送一个start activity的明路,system server检测当前APK有没有在运行,如果没有运行,会发送Fork命令告诉Zygote,它会Fork一个新的进程出来,新进程出来后会立刻连接system server ,system server启动activity,并且回调handleLauncheActivity。activity实例化等操作归App Porcess管理,但是activity生命周期归system server管理。
框架在startAcitivy时hook住,实现了欺上;在handleLauncheActivty时hook住,实现了瞒下。系统要告诉我们的进程要启动坑位activity,但是在我们hook住后,换成我们插件acitivty,这样实现activity 的启动。
广播在插件中全部在解析过程中变为动态注册,contentprovider类似;server比较特殊,因为在启动信息回来的时候有些信息是拿不到的,server实现了一拖多,插件有N个服务,但是在系统看来只有1个服务,使用一个代理服务代理了N个服务的运行过程。
通过动态代理实现Hook,在Android的框架实现过程,大部分通过接口、对象去做的,我们通过动态代理和反射基本可以实现hook系统的大部分服务,一小部分通过bundle的代理,基本可以拦截掉App Process和System Server间的通讯99%。
程序能够运行起来,主要是对PackageManager 和AcitivityManager进行hook。对PackageManager hook住后,插件就会认为自己已经被安装了。AcitivityManager的hook做一个封装和资源调度。为了解决AppOps的问题,也hook了其他服务。
对ActivityThread类,AtivityThread.main函数进行一些hook.
DroidPlugin与其他插件化框架的区别 :