#目录
#正文
因为公司的技术方案的选型的原因,想要把整个工程框架往模块化/组件化的方向的重构一次。为此我去调研了一下常见的路由框架,并且进行了一场对ARouter的基本思想到源码的浅析讲座。
说到模块化,当然想到了后面的插件化,为了尽可能的提高方案的后续兼容性,我也稍微调研了Small和RePlugin。特此写一篇文章对这三者的原理和性能进行对比。由于两者设计的内容比较庞大,限于篇幅原因。有机会再分析阿里的Altas。而DroidPlugin我是两年前以它作为插件化框架学习的例子,这里就不多讲,讨论一下最新的几个插件化框架。
想要分析插件化框架,我们首先要知道插件化是做什么的? #背景
Android应用上线流程打好包上线之后,很难对上线的应用进行更新。万一出现紧急的bug或者突然出现临时的活动,这种时候只能重新发版。比起网页前端和后端来说,灵活性十分低。为此很多人想了很多办法解决问题。因此而诞生出了热更新,插件化等技术。
多说一句,希望看本篇文章的读者,可以对Android的Activity的启动流程有一定的了解,才好跟得上接下来的思路。
在分析插件化之前,我们要思考一下,如果我们自己编写插件化究竟会遇到什么问题。
就以Android启动Activity为例子(最复杂也是Activity的启动)。假如我们想要跨越模块启动一个新的Activity。会遇到什么阻碍? 1.有点基础的Android工程师都知道。我们要启动一个Activity先要在AndroidManifest.xml中注册好对应的Activity,之后我们才能过AMS的验证,启动到对应的Activity。
问题是一旦牵扯到插件化,我们一般想要启动插件里面的Activity,我们几乎无可奈何,因为我们并没有注册把插件的Activity注册到我们的主模块或者说宿主中,也谈何启动新的Activity。
2.当我们想办法解决了Activity如何从插件中启动出来。接下来又遇到新的问题。当我们启动了启动了Activity之后,就要开始加载资源。
对于第二个问题,如果我们看过资源文件R.java之后,就知道Android实际上是把资源映射为一个id找到对应的资源。当我们拥有两个插件时候,如果我们通过取出对应的资源id的时候,往往会发现id取错了,取成了宿主的或者干脆找不到。
这一次我们的目标是解决这两个大问题。如果这两个问题解决了,实际上启动插件的Activity已经完成一大半。
在解析这些插件化框架的时候,先说说看整个插件化框架的雏形。
最后,我希望每个人看完这篇文章之后,能够知道这几个框架之间设计上和思想上的区别。最好能够有能力写属于自己的插件化框架。
#个人实现思路
实际上,这些思路都是老东西了。我一年前早就试过了一遍了。其实并不是什么厉害的东西。你会发现实际上实现思路挺巧的。实际上绝大部分插件化框架也是顺着这个思路进行下去的。
##1.Activity注册问题
先解决Activity的注册问题。我们先看看Android 7.0的源码。看看它究竟是怎么检测Activity的。
详细的可以去我的csdn看看Activity的启动流程。那是毕业那段时间写的文章,虽然写的不大好:blog.csdn.net/yujunyu12/a…
我这里摆一张时序图出来:
这里只跟踪了Activity中关键行为。这段源码我又花了一点时间再看了一遍了,从4.4一直到6.0都看了好几遍,只能说,核心的东西几乎没有什么变动,不熟悉的读者可以稍微看看我上面那个对Activity的源码解析,你或许会稍稍对这个流程有点理解。可能不是完全正确,但是至少十之八九的意思都表达出来了。
好了。源码的部分介绍的差不多。我们开始进入正题吧。
假如我们想要启动一个不存在在注册表中的Activity,那么思路很简单,我们就造一个假的Activity放在AndroidManifest.xml,用来骗过Android系统的检测。
核心思想就是我们要下个钩子赶在Activity相关的信息进入到AMS之前做一次暗度粮仓,方法明面上启动的是我们没有注册的Activity,实际上在给到AMS的时候,没有注册好的代理Actvity会把信息放到注册好的Acitivty的Intent中,骗过Android系统。
接着检测都通过之后,我们再借尸还魂,把代理的Acitivity中换成我们真正要启动的Activity。
这一次我就来hook一下Android 7.0的代码,来展示一下一年前的DoridPlugin的思路。
就算是到了7.0的代码大体上流程还是没有太多变化,到了8.0下钩子的地方稍稍出现了点变化。因为获取获取AMS的实例已经切换到了ActivityManager中。
废话不多说。先上代码。 我们先创建三个Activity,分别是RealActivity,ProxyActivity,MainActivity。RealActivity是我们真的想要从MainActivity跳转的Activity,而ProxyActivity则是作为一个代理承载RealActivity,用来欺骗Android的Activity检测。
源码分析原理
从上面的时序图我们可以知道,在ActivityManagerNative的时刻就会通过AIDL调用startActivity,跨进程到ActivityManagerService中,换句话说就是脱离了我们控制。同时也代表着ActivityManagerService之前我们可以下手脚。而到了ActivityStackSupervisor又通过scheduleLaunchActivity 跨进程回到我们的App的ActivityThread中,也就意味着我们可以在此时再做一些手脚。
而几乎所有的插件化框架都是沿用这套思路,入侵系统。
我们要骗过Android对AndroidMainfest的检测首先要知道哪里检测。
其实就在Instrumentation中:
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
Uri referrer = target != null ? target.onProvideReferrer() : null;
if (referrer != null) {
intent.putExtra(Intent.EXTRA_REFERRER, referrer);
}
if (mActivityMonitors != null) {
synchronized (mSync) {
final int N = mActivityMonitors.size();
for (int i=0; i<N; i++) {
final ActivityMonitor am = mActivityMonitors.get(i);
if (am.match(who, null, intent)) {
am.mHits++;
if (am.isBlocking()) {
return requestCode >= 0 ? am.getResult() : null;
}
break;
}
}
}
}
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
复制代码
ActivityManagerNative.getDefault()的方法就是通过ActivityManagerNative获取IActivityManager实例。这个实例实际上是一个aidl用于和ActivityManangerService跨进程交互的。接着跨进程调用.startActivity的方法。
调用完之后,调用checkStartActivityResult来检测这个Activity是否检测了。
/** @hide */
public static void checkStartActivityResult(int res, Object intent) {
if (res >= ActivityManager.START_SUCCESS) {
return;
}
switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException(
"No Activity found to handle " + intent);
case ActivityManager.START_PERMISSION_DENIED:
throw new SecurityException("Not allowed to start activity "
+ intent);
case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
throw new AndroidRuntimeException(
"FORWARD_RESULT_FLAG used while also requesting a result");
case ActivityManager.START_NOT_ACTIVITY:
throw new IllegalArgumentException(
"PendingIntent is not an activity");
case ActivityManager.START_NOT_VOICE_COMPATIBLE:
throw new SecurityException(
"Starting under voice control not allowed for: " + intent);
case ActivityManager.START_VOICE_NOT_ACTIVE_SESSION:
throw new IllegalStateException(
"Session calling startVoiceActivity does not match active session");
case ActivityManager.START_VOICE_HIDDEN_SESSION:
throw new IllegalStateException(
"Cannot start voice activity on a hidden session");
case ActivityManager.START_CANCELED:
throw new AndroidRuntimeException("Activity could not be started for "
+ intent);
default:
throw new AndroidRuntimeException("Unknown error code "
+ res + " when starting " + intent);
}
}
复制代码
换句话说。我们要赶在这个方法调用之前,做一些手脚才能骗过Android系统。
暗度粮仓第一步的原理
上面说过了,我们需要在Activity在会通过通信ActivityManagerNative来通行ActivityManagerService。那很正常可以想到。如果我可以拿到ActivityManagerNative的实例,动态代理这个实例,把startActivity的方法拦截下来,修改注入的参数。
还有其他方案,我们稍后再跟着其他框架再聊聊。如果能有其他很妙的思路的,希望可以教教我。 我们显获取ActivityManangerNative实例。看看这个实例在哪里
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};
}
复制代码
运气很好。动态代理只能代理实现了接口的类,而这个IActivityManager 恰好是一个接口。那么顺着这个思路继续往下走。
实现获取ActivityManangerNative的实例
我们反射获取gDefault的实例,获取到内部ActivityManangerNative的实例之后,把这个类给动态代理下来。并且把startActivity方法拦截下来。
public void init(Context context){
this.context = context;
try {
Class<?> amnClazz = Class.forName("android.app.ActivityManagerNative");
Field defaultField = amnClazz.getDeclaredField("gDefault");
defaultField.setAccessible(true);
Object gDefaultObj = defaultField.get(null);
Class<?> singletonClazz = Class.forName("android.util.Singleton");
Field amsField = singletonClazz.getDeclaredField("mInstance");
amsField.setAccessible(true);
Object amsObj = amsField.get(gDefaultObj);
amsObj = Proxy.newProxyInstance(context.getClass().getClassLoader(),
amsObj.getClass().getInterfaces(),new HookHandler(amsObj));
amsField.set(gDefaultObj,amsObj);
}catch (Exception e){
e.printStackTrace();
}
}
复制代码
既然要对startActivity的参数做处理,我们需要再看看我们要对那几个参数做处理才能骗过AMS(ActivityManagerService,以后用AMS代替)
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
复制代码
第一个参数是ApplicationThread也可以说是ActivityThread中用来沟通AMS的Binder接口,是一种通行桥梁。第二个参数是当前的包名,第三个参数就是我们启动时候带的intent。看到这里就ok了。我们要做暗度粮仓第一件事当然要把粮偷偷的放到哪里,骗过敌人。很简单就是把我们要启动的Activity放到intent里面。
实现暗度粮仓第一步
class HookHandler implements InvocationHandler{
private Object amsObj;
public HookHandler(Object amsObj){
this.amsObj = amsObj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.e("method",method.getName());
if(method.getName().equals("startActivity")){
// 启动Activity的方法,找到原来的Intent
Intent proxyIntent = (Intent) args[2];
// 代理的Intent
Intent realIntent = new Intent();
realIntent.setComponent(new ComponentName(context,"com.yjy.hookactivity.RealActivity"));
// 把原来的Intent绑在代理Intent上面
proxyIntent.putExtra("realIntent",realIntent);
// 让proxyIntent去骗过Android系统
args[2] = proxyIntent;
}
return method.invoke(amsObj,args);
}
}
复制代码
做好了暗度粮仓的准备。别忘了我们度过之后要取出来,借着代理在AMS中做好的ActivityRecord做一次借尸还魂。
在这里我稍微提一下在整个ActivityThread中有一个mH的Handler作为整个App的事件总线。无论是哪个组件的的哪段生命周期,都是借助这个Handler完成的。 那么正常的想法就是在执行这个Handler的msg之前,如果可以执行我们自己的处理方法不就好了吗?这里就涉及到了Handler的源码的。看过我之前对Handler的分析的话,就会对Handler的源码十分熟悉。这里直接放出dispatchMessage的方法。
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
复制代码
可以知道如果当mCallback不等于空的时候,且mCallback.handleMessage(msg)返回false的时候,将会先执行mCallback的handleMessage再执行我们常用的handleMessage。很幸运,这又给我们提供可空子进入,可以赶在nH处理msg之前处理一次我们的暗度在里面的“粮”。
public void hookLaunchActivity(){
try {
Class<?> mActivityThreadClazz = Class.forName("android.app.ActivityThread");
Field sActivityThreadField = mActivityThreadClazz.getDeclaredField("sCurrentActivityThread");
sActivityThreadField.setAccessible(true);
Object sActivityThread = sActivityThreadField.get(null);
Field mHField = mActivityThreadClazz.getDeclaredField("mH");
mHField.setAccessible(true);
Handler mH = (Handler)mHField.get(sActivityThread);
Field callback = Handler.class.getDeclaredField("mCallback");
callback.setAccessible(true);
callback.set(mH,new ActivityThreadCallBack());
}catch (Exception e){
e.printStackTrace();
}
}
class ActivityThreadCallBack implements Handler.Callback{
@Override
public boolean handleMessage(Message msg) {
if(msg.what == LAUNCH_ACTIVITY){
handleLaunchActivity(msg);
}
return false;
}
}
private void handleLaunchActivity(Message msg) {
try {
//msg.obj ActivityClientRecord
Object obj = msg.obj;
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
Intent proxy = (Intent) intentField.get(obj);
Intent orgin = proxy.getParcelableExtra("realIntent");
if(orgin != null){
intentField.set(obj,orgin);
}
}catch (Exception e){
e.printStackTrace();
}
}
复制代码
为什么我们hook ActivityClientRecord替换内部的intent起效果呢?可以去看看我上面毕业写的文章。这里稍微总结一下:我们会在ActivityStack中准备好Activity的的task,task之中的关系等等。之后我们再在performLaunchActivity中,获取ActivityRecord通过反射生成新的Activity。
这样就完成了越过AndroidMainest.xml。下面就是越过AndroidMainest.xml的控制中心方法。只要在调用startActivity前调用一下init和hookLaunchActivity方法即可。
很简单吧。但是事情还没完。因为插件化,我们往往连对方的包名+类名都完全不知道。只是第一步而已。接下来我就要通过PacketManagerService来解决这个问题。而且跨越检测也没有结束。因为在适配AppCompatActivity会出点问题。
2.由跨越检测AndroidManest.xml引出的问题。如何把插件中的类加载进主模块。
实际上这个也很简单。但是我们首先要熟悉Android源码和Java中类加载时候的双亲模型。这里我们先看看Android启动流程的源码。native层面上的启动源码我有机会和你们分析分析,这是去年学习的目标之一。实际上看了4.4到7.0这些核心东西也几乎太大变动
当我们通过Zyote进程fork(也有人叫孵化)出我们App进程的时候,会做一次类的加载以及Application的初始化。会走ActivityThread的main方法接着会调用它的attach方法。在attach中会跨进程走到AMS中的attachApplication,在里面分配pid等参数之后就会回到bindApplication走Application的onCreate方法。
关键方法是其中的getPackageInfoNoCheck又会调用getPackageInfo方法。那为什么我们选择反射getPackageInfoNoCheck而不是getPackageInfo呢?因为最大的区别getPackageInfoNoCheck是public方法,getPackageInfo是私有方法。而在编码规范中public作为暴露出来的接口变动的可能性比较小。
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
: "Loading resource-only package ") + aInfo.packageName
+ " (in " + (mBoundApplication != null
? mBoundApplication.processName : null)
+ ")");
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
if (mSystemThread && "android".equals(aInfo.packageName)) {
packageInfo.installSystemApplicationInfo(aInfo,
getSystemContext().mPackageInfo.getClassLoader());
}
if (includeCode) {
mPackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
}
}
return packageInfo;
}
}
复制代码
而返回LoadApk这个类指代的就是Apk在内存中的表示。上面的方法的意思是,假如在mPackage中找到我们要的LoadApk则直接返回,不然就新建一个新的LoadApk。
难道说我们只要给这个方法参数,反射调用这个方法,生成LoadApk就能获得插件的apk。然后加到系统的mPackage的Map中管理,欺骗系统说这个插件已经安装了。这样就能调用,实现我们的业务。思路是这样没错。
但是理想是丰满的,现实往往是骨感的。别忘了我们所有的Activity都是通过ClassLoader反射而来,宿主应用的classloader怎么加载的了插件的classloader呢?
这也就引申出了classloader的双亲委派。说穿了,也不是什么高大上的东西。实际上就是当前的ClassLoader先不去加载class,如果找不到则再去委托上层去查找class缓存,如果找到了就返回,没有则自上而下的查找有没有对应的class。
为了避免有人不太懂classloader,这里稍微提一句classloader实际上是会加载dex文件之后,从dex中查找出class文件对应的位置。插件的dex很明显和宿主的dex不同,所以无法通过classloader找到对应的class。
这里我借用网上一个挺好的示意图片
在这里要提一点,Android出了上述几种ClassLoader之外,自己也定义了一套ClassLoader。分别是BaseDexClassLoader,DexClassLoader和PathClassLoader。实际上这部分就是上面所说的自定义类加载器。
相应的PathClassLoader是用于加载已经安装好的apk的dex文件,DexClassLoader能够用于加载外部dex文件。
查找外部class的方式
那么我们可以推测出两种做法。一种是直接全权用我们的classloader直接替代掉系统的classloader。第二种就是看看能不能hook一下BaseDexClassLoader让我们做事情。这就是网上所说的,比较粗暴的方法和温柔的方法。
其实两种我都试过了。这一次,我就讲讲暴力的方法。因为温柔的方式将会在Small中体现出来。
说穿了,实际上也是十分的简单。如果对上述的图熟悉的话,就十分简单。就是自己做一个ClassLoader专门用来读取dex文件的。这样就能在类的加载的时候找到这个文件。
实现跨插件查找
不多说上代码; 1.先自定义一个classloader
public class PluginClassLoader extends DexClassLoader {
public PluginClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
}
}
复制代码
这里说一下,第一个参数是你要读入的apk文件还是dex文件还是jar文件。到最后它都会解析dex文件。第二个参数是dex优化后的文件,也就是我们常说的odex文件。第三个是native的文件夹,第四个是指定自己的上层类加载器,用于委托。
public static void loadPlugin(Context context){
try {
dirPath = context.getCacheDir().getParentFile().getAbsolutePath()
+File.separator+"Plugin"+File.separator+"data"+File.separator+"com.yjy.pluginapplication";
apk = new File(dirPath,"plugin.apk");
if(apk.exists()){
Log.e("apk","exist");
}else {
Log.e("apk","not exist");
Utils.copyFileFromAssets(context,"plugin.apk",
dirPath+ File.separator +"plugin.apk");
}
cl = new PluginClassLoader(apk.getAbsolutePath(),
context.getDir("plugin.dex", 0).getAbsolutePath(),null,context.getClassLoader().getParent());
hookPackageParser(apk);
}catch (Exception e){
e.printStackTrace();
}
}
//查找是否存在对应的class
public static Class<?> findClass(String path){
try {
Class<?> clazz = cl.loadClass(path);
return clazz;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
复制代码
好了如何跨越插件找class也做到了,只要让LoadApk里面的ClassLoader切换为我们的classloader就能找到我们类!! 我们接下来就是去下钩子加载我们的插件Activity,其实这个也不难。但是需要我们熟悉PackageManagerService.
让我们看看getPackageInfoNoCheck这个方法是怎么样的。
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
CompatibilityInfo compatInfo) {
return getPackageInfo(ai, compatInfo, null, false, true, false);
}
复制代码
也就说我们需要找到ApplicationInfo这个参数和CompatibilityInfo 这个参数。CompatibilityInfo 这个参数好说,是一个数据类。无论我们本地造一个还是反射获取都ok。
但是ApplicationInfo就没这么好获得了。因为这个信息关系到我们的整个Application的关键信息。我们必须步步为营,小心翼翼的处理。最好能通过系统里面某个方法获得是最好的。
当然如果熟悉PackageManagerService就知道PMS流程中PackageParser的类有这么一个方法generateActivityInfo,专门用来获取ActivityInfo的。这里面当然也有ApplicationInfo这个参数。为什么要用这个函数呢?因为调用的ApplicationInfo是从ActivityInfo中获得的。
public static final ActivityInfo generateActivityInfo(Activity a, int flags,
PackageUserState state, int userId) {
if (a == null) return null;
if (!checkUseInstalledOrHidden(flags, state)) {
return null;
}
if (!copyNeeded(flags, a.owner, state, a.metaData, userId)) {
return a.info;
}
// Make shallow copies so we can store the metadata safely
ActivityInfo ai = new ActivityInfo(a.info);
ai.metaData = a.metaData;
ai.applicationInfo = generateApplicationInfo(a.owner, flags, state, userId);
return ai;
}
复制代码
第一个参数Activity 是指当前的Activity。我们需要一点特殊的技巧。如果我们熟悉Android的安装流程的话,就知道我们显通过PackageParser的parsePackage解析整个apk包,解析好的对象里面存放着apk里面所有四大组件的信息。
那么我们只需要做这几件事情,解析出这个包里面的Activity信息也就是PackageParser$Activity,取出我们想要的Activity,放进来调用这个方法生成想要的ActivityInfo即可。
public Package parsePackage(File packageFile, int flags) throws PackageParserException {
if (packageFile.isDirectory()) {
return parseClusterPackage(packageFile, flags);
} else {
return parseMonolithicPackage(packageFile, flags);
}
}
复制代码
这个PackageUserState这个类是关于package是否安装等信息,由于这个插件这个时候并没有相关,我们完全可以反射直接实例化出来即可。后者userId是在ActivityThread中attach方法中绑定userid,我们这里是单进程,单App模式直接拿本App的即可。万事俱备只欠东风了。
思路整理
1.在加载整个apk包进入classloader的时候,调用Package.paresPackage(File,flag)解析整个apk包,存下解析出来的activity信息
/**
* 解析包
* @param apk
*/
public static void hookPackageParser(File apk){
try {
packageParserClass = Class.forName("android.content.pm.PackageParser");
mPackageParser = packageParserClass.newInstance();
//先解析一次整个包名
Method paresPackageMethod = packageParserClass.getDeclaredMethod("parsePackage",File.class,int.class);
//Package.paresPackage(File,flag)
Object mPackage = (Object) paresPackageMethod.invoke(mPackageParser,apk,0);
//解析完整个包,获取Activity的集合,保存起来
Field mActivitiesField = mPackage.getClass().getDeclaredField("activities");
activities = (ArrayList<Object>) mActivitiesField.get(mPackage);
Log.e("activites",activities.toString());
}catch (Exception e){
e.printStackTrace();
}
}
复制代码
这里只展示Activity的流程。当然我们也能从中获取出apk包中其他信息,现在并没有想法去解决其他地方的问题。
在上面的hook mH之后,添加一步把之前解析出来的包的信息运用起来。细分下去又是如下几步:
1.使用上面解析的信息,调用PackageParser.generateActivityInfo获取ActivityInfo
2.调用ActivityThread.getPackageInfoNoCheck获取LoadApk
3.切换LoadApk中的classloader为我们的自己ClassLoader 也就是属性mClassLoader
为什么要这么做呢?我们看看源码就明白了,看看Android是怎么是实例化Activity的
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
复制代码
这是从r. packageInfo获取classloader。而这个packageInfo又是什么呢?其实就是LoadApk。而这个r是指ActivityClientRecord,这是是在整个mH中作为obj对象作为Acitivity的启动流程在到处传递。也因为从这个packageInfo获取classloader所以我们要替换。
4.把这个LoadApk放到mPackages这个在ActivityThread中保存着安装好的apk信息。
从上方的getPackageInfo方法中。可以得知当我们从mPackages这个ArrayMap中获取到包名对应的LoadApk的时候就会直接返回LoadApk。我们要做的是在系统自己调用getPackageInfoNoCheck之前,先把我们LoadApk放入mPackages中,欺骗系统我们已经安装这个插件了,就会直接返回我们自己的LoadApk。
5.把这个ActivityInfo设置到ActivityClientRecord
当我们以为万事大吉的时候,忘记了这一步。你会发现我们并没有获取到我们自己LoadApk,为什么会这样呢?看看源码就知道了。 在ActivityThread的performLaunchActivity中有这么一个判断
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
复制代码
就算我们创建了新的LoadApk如果ActivityClientRecord中的ActivityInfo为空的化,系统自己又回创建一个新的LoadApk,这样我们之前的工作就白做了。
####实现hookGetPackageInfoNoCheck 都分析出来了直接上源码。
public static void hookGetPackageInfoNoCheck(Object mActivityClientRecordObj,Intent intent){
//获取ActivityInfo
try {
Class<?> sPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Object mPackageUserState = sPackageUserStateClass.newInstance();
Class<?> sActivityClass = Class.forName("android.content.pm.PackageParser$Activity");
Method generateActivityInfoMethod = packageParserClass.getDeclaredMethod("generateActivityInfo",sActivityClass,int.class,sPackageUserStateClass,int.class);
ComponentName name = intent.getComponent();
Log.e("ComponentName",name.getClassName());
//获取activityInfo
//已经知道我们插件中的Activity信息只有一条,就没必要筛选了。作者本人懒了
ActivityInfo activityInfo = (ActivityInfo) generateActivityInfoMethod.invoke(mPackageParser,
activities.get(0),0,mPackageUserState, getCallingUserId());
//有了activityInfo,再获取sDefaultCompatibilityInfo,调用getPackageInfoNoCheck方法
Method getPackageInfoNoCheckMethod = mActivityThreadClazz.getDeclaredMethod("getPackageInfoNoCheck",ApplicationInfo.class,
CompatibilityInfoCompat.getMyClass());
fixApplicationInfo(activityInfo,apk);
//获取到LoadApk实例
Object LoadApk = getPackageInfoNoCheckMethod.invoke(sActivityThread,activityInfo.applicationInfo,CompatibilityInfoCompat.DEFAULT_COMPATIBILITY_INFO());
//把LoadApk中的classloader切换为我们的classloader
Field mClassLoaderField = LoadApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(LoadApk,cl);
//把这个loadApk放到mPackages中
Field LoadApkMapField = mActivityThreadClazz.getDeclaredField("mPackages");
LoadApkMapField.setAccessible(true);
Map LoadApkMap = (Map)LoadApkMapField.get(sActivityThread);
//调用Map的put方法 mPackages.put(String,LoadApk)
LoadApkMap.put(activityInfo.applicationInfo.packageName,new WeakReference<Object>(LoadApk));
//设置回去
LoadApkMapField.set(sActivityThread,LoadApkMap);
Field activityInfoField = mActivityClientRecordObj.getClass().getDeclaredField("activityInfo");
activityInfoField.setAccessible(true);
activityInfoField.set(mActivityClientRecordObj,activityInfo);
Thread.currentThread().setContextClassLoader(cl);
}catch (Exception e){
e.printStackTrace();
}
}
复制代码
##3. 解决跨插件导致资源找不到或者资源冲突问题 这样就万事大吉了吗?如果你直接上上面代码你会发现资源找不到导致系统崩溃。 那你一定会骂作者,不是说好的LoadApk代表了apk在内存中的数据吗?按照道理一定能找到里面的资源,一定是你的姿势不对。
确实是这样没错。细心的你一定会发现上面有一行方法我并没有解释,那就是fixApplicationInfo。
我们看看源码activity是怎么查找资源的。这里先上个时序图。
了解整个资源是怎么查找的。我们再深入去看看源码的细节。
这个流程先放在这里,当作一个伏笔埋在这里。转个头来看看,当我们想要为Activity设置布局的时候,往往都需要调用setContentView。让我们看看setContentView的源码是怎么查找资源的。
熟知Activity的窗口绘制流程流程就能知道这段源码直接在PhoneWindow中查找。
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
复制代码
我们不管上面创建DecorView,把焦点放在
mLayoutInflater.inflate(layoutResID, mContentParent);
复制代码
实际上视图的创建就是通过LayoutInflater。这里也不讲LayoutInflater的原理,什么缓存模型,直奔inflate的方法。
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
复制代码
你们会发现实际上资源都会通过context内部绑定好的resource来获取真实的资源文件。
那么伏笔就来了。既然是从context来的resource。那么我们想到借助系统创建一个Activity中的Context,把Context里面的resources对象换成我们的资源。这个过程最好不要过多的干预系统,最好能让系统自己生成。
先关注时序图中的LoadApk的getResources方法
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
}
return mResources;
}
复制代码
发现实际上我们所有的数据都是通过getTopLevelResources去解析LoadApk中的存放好的资源目录来进行解析。
而这些LoadApk的数据是怎么来的,当然是调用getPackageInfoNoCheck生成的,也就是说我们要赶在调用这个方法之前,把apk的目录填进去就能找到资源了。
private static void fixApplicationInfo(ActivityInfo activityInfo,File mPluginFile){
ApplicationInfo applicationInfo = activityInfo.applicationInfo;
if (applicationInfo.sourceDir == null) {
applicationInfo.sourceDir = mPluginFile.getPath();
}
if (applicationInfo.publicSourceDir == null) {
applicationInfo.publicSourceDir = mPluginFile.getPath();
}
if (applicationInfo.dataDir == null) {
String dirPath = context.getCacheDir().getParentFile().getAbsolutePath()
+File.separator+"Plugin"+File.separator+"data"+File.separator+applicationInfo.packageName;
File dir = new File(dirPath);
if(!dir.exists()){
dir.mkdirs();
}
applicationInfo.dataDir = dirPath;
}
try {
Field scanDirField = applicationInfo.getClass().getDeclaredField("scanSourceDir");
scanDirField.setAccessible(true);
scanDirField.set(applicationInfo,applicationInfo.dataDir);
}catch (Exception e){
e.printStackTrace();
}
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Field PublicSourceDirField = applicationInfo.getClass().getDeclaredField("scanPublicSourceDir");
PublicSourceDirField.setAccessible(true);
PublicSourceDirField.set(applicationInfo,applicationInfo.dataDir);
}
}catch (Exception e){
e.printStackTrace();
}
try {
PackageInfo mHostPackageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
applicationInfo.uid = mHostPackageInfo.applicationInfo.uid;
}catch (Exception e){
e.printStackTrace();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (applicationInfo.splitSourceDirs == null) {
applicationInfo.splitSourceDirs = new String[]{mPluginFile.getPath()};
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (applicationInfo.splitPublicSourceDirs == null) {
applicationInfo.splitPublicSourceDirs = new String[]{mPluginFile.getPath()};
}
}
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
if (Build.VERSION.SDK_INT < 26) {
Field deviceEncryptedDirField = applicationInfo.getClass().getDeclaredField("deviceEncryptedDataDir");
deviceEncryptedDirField.setAccessible(true);
deviceEncryptedDirField.set(applicationInfo,applicationInfo.dataDir);
Field credentialEncryptedDirField = applicationInfo.getClass().getDeclaredField("credentialEncryptedDataDir");
credentialEncryptedDirField.setAccessible(true);
credentialEncryptedDirField.set(applicationInfo,applicationInfo.dataDir);
}
Field deviceProtectedDirField = applicationInfo.getClass().getDeclaredField("deviceProtectedDataDir");
deviceProtectedDirField.setAccessible(true);
deviceProtectedDirField.set(applicationInfo,applicationInfo.dataDir);
Field credentialProtectedDirField = applicationInfo.getClass().getDeclaredField("credentialProtectedDataDir");
credentialProtectedDirField.setAccessible(true);
credentialProtectedDirField.set(applicationInfo,applicationInfo.dataDir);
} catch (Exception e) {
e.printStackTrace();
}
}
if (TextUtils.isEmpty(applicationInfo.processName)) {
applicationInfo.processName = applicationInfo.packageName;
}
}catch (Exception e){
e.printStackTrace();
}
}
复制代码
这样就能骗过系统获取到资源文件。
说句老实话,插件化难度不高,只是对源码的熟悉度提上去,加上一点取巧的思想,都能写出来。
##反思 我写的demo是否有问题?问题当然是多多的。首先一点,反射代码冗余了,当然也和我想让读者能够一目了然反射是如何运作的才这么写。
第二点,LoadApk加入到了mPackages这个Map中作为弱引用包裹着。一旦出现了GC,我们的工作前功尽弃了,所以肯定需要亲自缓存下来。直接通过packagename从我们自己的缓存取出。
第三点,我这个demo没有适配Appcompat包,没有适配AppCompatActivity。这个包有点意思,通过LayoutInflater拦截view生成Appcompat对应的东西,需要单独处理。
除了这些问题之外,这个demo的设计也令人汗颜。不过这的确能够让人对这个模型一目了然。
思路总结
这里整理一个模型,来总结上述的流程
以上是基于Android7.0源码,加上DroidPlugin源码写的demo。既然都清楚了整个插件化框架的流程,对于轻量级别的插件化框架Small和RePlugin也就好分析了。