最近准备详细研究一下android 插件化技术,所以自己通过自己的理解开始写一个插件框架出来,目前只是写简单的demo,摸清楚基本原理。后面会写详细教程,先把核心内容写出来,然后再把一些别的细节写出来,可能这个周期比较长,也是为了自己可以长期坚持写博客。
插件化最核心的2点:
1:代码调用
2:资源调用
1:代码调用原理:
不管怎么样,java里面对象就是class, 只要我们能new出对象,我们就可以进行调用,如果插件里面只是普通的类,我们可以直接new,然后调用具体的函数。但Activity,Service ,等等都有对应的Context, 说直白点就是,Activity 等创建有很多属性传进来,就跟我们模拟网络请求一样,我们需要带对应的参数才能获取对应的数据,所以我们可以看到开源的插件化都是在构建参数写了大量代码,为了尽可能把参数改成正确。我们要构建合适的参数,我们需要大量的hook,去修改成我们需要的参数,hook点不一样,构建参数数量就不一样。
2:资源调用
资源调用算是参数构建,其实我这样分类不是合适。
所以总结来看:我们就是一直构建合适参数给系统,要系统能够跑起来我们的插件。
1:代码加载
android 里面用DexClassLoader 加载APk,来构建Classloader,然后我们采用合适的方式替换原来的classloader。每个框架好像思路稍微不一样,我写的demo时候直接替换ActivityThread 里面的mPackages 的Classloader(具体细节这里不说),用这种方式很直接,不用考虑很多东西,原理也容易明白。这样子ActivityThread mH 处理创建Activity 就是已经创建了(具体为什么,其实要分析ActivityThread的代码的,因为网上已经有好多人讲解,就不写了)。但这种创Activity 不能含有任何资源,因为我们只是最简单的方式创建Activity,所有的信息都是用宿主的,那么资源全部都是指向宿主,那么插件里面就找不到对应的资源了。
Activity activity = null;
try {
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);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
我们只是把宿主的ClassLoader 替换了,变成可以兼容插件的。其他的信息都没有修改,所以资源都是指向宿主。
2:资源处理
网上写的简单的思路替换对应的Activity的Resource,大多数的教程都是入侵式,所以我不喜欢。我们hook ActivityThread 里面的mInstrumentation,因为 mInstrumentation 基本管理Activity 各个时期。
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
主要是callActivityOnCreate 这个时期感觉比较好,这里我直接替换资源。
void fakePluginActivityRes(Resources pluginRes, Activity activity){
try {
activity.getResources();
//这里肯定版本不兼容的
// 替换了 android.view.ContextThemeWrapper resource 这样就不用兼容性代码了
Log.d(TAG, "fakePluginActivityRes: " + Activity.class.getSuperclass().getName());
Class activityClass = Class.forName(Activity.class.getSuperclass().getName());
Field mResourcesField = activityClass.getDeclaredField("mResources");
mResourcesField.setAccessible(true);
mResourcesField.set(activity, pluginRes);
//activity.getResources();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
我直接替换了掉资源,这里面代码跟网上稍微不一样,这里可以修改Context 资源,但我只是验证思路,就hook比较浅的层,可能会有兼容性问题。这里跟网上的插件重新getResources思路几乎一样,只是这样我不用入侵式代码。重点来了,这里我测试有问题,启动插件会崩溃,分析好久icon 找不到,这个我分析好久才分析出来,因为开始不熟悉,网上都没有说这一点,我后面分析代码与分析错误信息。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWindow.setDefaultIcon(mActivityInfo.getIconResource());
会发现这里崩溃,错误日志表明一个资源找不到,我通过反编译宿主的R,找到值就是宿主的Icon,但我们已经替换资源,如果icon 资源id 不一样的话,就肯定找不到。所以一直会报错。我后面直接把宿主的android:icon直接去掉就可以了。后面为了验证解决问题思路。我反射替换mActivityInfo 里面icon替换成插件对应的icon也可以。
在这里基本思路走通了,但自己写的时候就发现修改越晚,需要构建的参数越多。那么可能出现漏掉的参数也会越多,插件框架稳定性越差,这里为什么360 droidplugin 为什么自己重新构建loadApk,因为可以用直接插件的APK 全部参数来构建loadedApk,那么效果比较好,可能还有更合适的hook点,貌似360 商业的droidplugin hook少了很多,估计是找新的hook更合适hook点了。
写本篇文章完全是为了记录解决问题思路,花了一天事情才解决问题。读者可能看的云里雾里,因为缺少大量上下文。后续写一些列详细文章,希望自己能做到吧。