笔记,参考7.2中所列参考文章所写,DEMO地址在
1.综述
2015年是Android插件化技术突飞猛进的一年,随着业务的发展各大厂商都碰到了Android Native平台的瓶颈。 从技术上讲,业务逻辑的复杂导致代码量急剧膨胀,各大厂商陆续出到65535方法数的天花板;同时,运营为王的时代对于模块热更新提出了更高的要求。 在业务层面上,功能模块的解耦以及维护团队的分离也是大势所趋;各个团队维护着同一个App的不同模块,如果每个模块升级新功能都需要对整个app进行升级,那么发布流程不仅复杂而且效率低下;在讲究小步快跑和持续迭代的移动互联网必将遭到淘汰。 H5和Hybird可以解决这些问题,但是始终比不上native的用户体验;于是,国外的FaceBook推出了react-native;而国内各大厂商几乎都选择纯native的插件化技术。可以说,Android的未来必将是react-native和插件化的天下。 react-native资料很多,但是讲述插件化的却凤毛菱角;
插件化技术听起来高深莫测,实际上要解决的就是两个问题:
- 1.代码加载
- 2.资源加载
- 参考
1.1 需要解决的问题
-
1.代码的加载
类的加载可以使用Java的ClassLoader机制,但是对于Android来说,并不是说类加载进来就可以用了,很多组件都是有“生命”的;因此对于这些有血有肉的类,必须给它们注入活力,也就是所谓的组件生命周期管理;
另外,如何管理加载进来的类也是一个问题。假设多个插件依赖了相同的类,是抽取公共依赖进行管理还是插件单独依赖?这就是ClassLoader的管理问题;
-
2.资源的加载
资源加载方案大家使用的原理都差不多,都是用AssetManager的隐藏方法addAssetPath;但是,不同插件的资源如何管理?是公用一套资源还是插件独立资源?共用资源如何避免资源冲突?对于资源加载,有的方案共用一套资源并采用资源分段机制解决冲突(要么修改aapt要么添加编译插件);有的方案选择独立资源,不同插件管理自己的资源。
1.2 Android中加载类 和 Java加载类的区别
Android中许多组件类(如Activity、Service等)是需要在Manifest文件里面注册后才能工作的(系统会检查该组件有没有注册),所以即使动态加载了一个新的组件类进来,没有注册的话还是无法工作;
Res资源是Android开发中经常用到的,而Android是把这些资源用对应的R.id注册好,运行时通过这些ID从Resource实例中获取对应的资源。如果是运行时动态加载进来的新类,那类里面用到R.id的地方将会抛出找不到资源或者用错资源的异常,因为新类的资源ID根本和现有的Resource实例中保存的资源ID对不上;
2.实现思路
目前国内开源的较成熟的插件方案有DL和DroidPlugin;但是DL方案仅仅对Framework的表层做了处理,严重依赖that语法,编写插件代码和主程序代码需单独区分;而DroidPlugin通过Hook增强了Framework层的很多系统服务,开发插件就跟开发独立app差不多;就拿Activity生命周期的管理来说,DL的代理方式就像是牵线木偶,插件只不过是操纵傀儡而已;而DroidPlugin则是借尸还魂,插件是有血有肉的系统管理的真正组件;DroidPlugin Hook了系统几乎所有的Sevice,欺骗了大部分的系统API;掌握这个Hook过程需要掌握很多系统原理,因此学习DroidPlugin对于整个Android FrameWork层大有裨益。
以DroidPlugin为例的方式
-
1.构造DexClassLoader : 既然我们知道如果想启动插件apk就需一个Classloader,那么我们换一种想法,能不能我们将我们的插件apk的信息告诉系统的这个Classloader,然后让系统的Classloader来帮我们加载及创建呢?答案是肯定之前我们说过讲过android中的Classloader主要分析PathClassLoader和DexClassLoader,系统通过PathClassLoader来加载系统类和主dex中的类。而DexClassLoader则用于加载其他dex文件中的类。他们都是继承自BaseDexClassLoader。(如果没有看过的建议先看看 插件化知识详细分解及原理 之ClassLoader及dex加载过程)
-
2.拿到宿主apk里ClassLoader中的pathList对象和我们Classloader的pathList,进行合并
- 2.1 通过PathClassLoader拿到宿主应用的dexPathList,通过DexClassLoader拿到插件的dexPathList。
- 2.2 拿到宿主和插件的dex列表
- 2.3 将宿主和插件的dexPathList合并
-
3.hook住startActivity方法,使用占坑的方式,也就是说我们可以提前在AndroidManifest中固定写死一个Activity,这个Activity只不过是一个傀儡,我们在启动我们插件apk的时候使用它去系统层校检合法性,然后等真正创建Activity的时候再通过hook思想拦截Activity的创建方法,提前将信息更换回来创建真正的插件apk。
- 3.1 通过反射,拿到AMS的代理对象
- 3.2 自定义InvocationHandler拦截startActivty方法
- 3.3 带系统检查完Activity合法性后重新执行原Activity
3.准备工作
3.1 理解应用的启动过程
- 参考
下面是大致的流程,详细内容可以看Activity的笔记或者罗升阳的《Android源码分析》
应用程序是由Launcher启动起来的,其实Launcher本身也是一个应用程序。
启动一个应用时会调用 startActivitySafely
-> startActivity
-> startActivityForResult
最终会调用Instrumentation
的execStartActivity
方法。
在execStartActivity
中会调用ActivityManagerNative.getDefault().startActivity
,实际上就是调用了AMS的startActivity方法。
在execStartActivity
中还会调用checkStartActivityResult
方法对start的结果进行检查。
AMS的startActivity
->startActivityAsUser
->mActivityStarter.startActivityMayWait
,最终调用 ActivityStackSupervisor
的realStartActivityLocked
方法。
在realStartActivityLocked
方法中,会调用ActivityThread
的scheduleLaunchActivity
方法。
在 scheduleLaunchActivity
方法中 有一行代码 sendMessage(H.LAUNCH_ACTIVITY, r);
在ActivityThread
内部的H
类中调用
public void handleMessage(Message msg) { if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what)); switch (msg.what) { case LAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); final ActivityClientRecord r = (ActivityClientRecord) msg.obj; r.packageInfo = getPackageInfoNoCheck( r.activityInfo.applicationInfo, r.compatInfo); handleLaunchActivity(r, null); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } break; }复制代码
在handleLaunchActivity
->performLaunchActivity
在 performLaunchActivity
中通过Instrumentation.newActivity
创建Activity,并创建Application和Context
3.2 理解Activity和Service的启动过程
在performLaunchActivity
方法中
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);复制代码
实际是通过r.packageInfo这个LoadedApk类型的对象获得。
LoadedApk保存了Apk文件的相关信息。
在H
类里通过getPackageInfoNoCheck
方法获得LoadedApk,针对代码的信息会存到mPackages
表里
apk被安装之后,apk的文件代码以及资源会被系统存放在固定的目录比如/data/app/package_name/base.apk)中,系统在进行类加载的时候,会自动去这一个或者几个特定的路径来寻找这个类。(可以在Android Studio的Device File Explorer去查看)
在startActivityLocked方法内部进行了一系列重要的检查:比如权限检查,Activity的exported属性检查等等;我们上文所述的,启动没有在Manifestfest中显示声明的Activity抛异常也是这里发生的:
3.3 理解DexClassLoader 和 PathClassLoader
3.3.1 DexClassLoader
构造函数:
public DexClassLoader (String dexPath, String dexOutputDir, String libPath, ClassLoader parent)复制代码
dexPath:dex文件路径列表,多个路径使用”:”分隔 dexOutputDir:经过优化的dex文件(odex)文件输出目录 libPath:动态库路径(将被添加到app动态库搜索路径列表中) parent:这是一个ClassLoader,这个参数的主要作用是保留java中ClassLoader的委托机制(优先父类加载器加载classes,由上而下的加载机制,防止重复加载类字节码)复制代码
DexClassLoader是一个可以从包含classes.dex实体的.jar或.apk文件中加载classes的类加载器。可以用于实现dex的动态加载、代码热更新等等。这个类加载器必须要一个app的私有、可写目录来缓存经过优化的classes(odex文件)
3.3.2 PathClassLoader
PathClassLoader提供两个常用构造方法
public PathClassLoader (String path, ClassLoader parent)1public PathClassLoader (String path, String libPath, ClassLoader parent)复制代码
path:文件或者目录的列表 libPath:包含lib库的目录列表 parent:父类加载器复制代码
3.3.3区别
- DexClassLoader:能够加载未安装的jar\apk dex。
- PathClassLoader:Android系统通过PathClassLoader来加载系统类和主dex中的类。
参考:
3.4 理解反射和Hook
首先我们得找到被Hook的对象,我称之为Hook点;什么样的对象比较好Hook呢?自然是容易找到的对象。什么样的对象容易找到?静态变量和单例;在一个进程之内,静态变量和单例变量是相对不容易发生变化的,因此非常容易定位,而普通的对象则要么无法标志,要么容易改变。我们根据这个原则找到所谓的Hook点。
-
1.寻找Hook点,原则是静态变量或者单例对象,尽量Hook pulic的对象和方法,非public不保证每个版本都一样,需要适配。
-
2.选择合适的代理方式,如果是接口可以用动态代理;如果是类可以手动写代理也可以使用cglib。
-
3.偷梁换柱——用代理对象替换原始对象
4.其他
4.1 热门热修复框架
目前比较有名的插件化框架: 任玉刚的:dynamic-load-apk,这个项目使用的是一种代理的方式去实现 https://github.com/singwhatiwanna/dynamic-load-apk
360的:DroidPlugin,这个项目是通过hook系统类来实现 https://github.com/Qihoo360/DroidPlugin
目前比较火的热修复框架: 阿里的:andfix,用于对方法的修复,可以立即生效,不支持资源及类替换 https://github.com/alibaba/AndFix
腾讯的:tinker,除了不支持立即生效,全部支持 https://github.com/Tencent/tinker
美团的:robust,不开源
7.2 参考文章
总述
Hook
生命周期管理
插件的加载
下面这几篇文章是参考上面的文章所写,主要参考上面的几篇文章,下面的文章作为补充