横向浅析Small,RePlugin两个插件化框架(一) 原理解析

·  阅读 59

#目录

#正文

因为公司的技术方案的选型的原因,想要把整个工程框架往模块化/组件化的方向的重构一次。为此我去调研了一下常见的路由框架,并且进行了一场对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的启动流程.jpg

这里只跟踪了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。

这里我借用网上一个挺好的示意图片 classloader.jpg

在这里要提一点,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是怎么查找资源的。这里先上个时序图。 Framework层的资源查找与context绑定.png

了解整个资源是怎么查找的。我们再深入去看看源码的细节。

这个流程先放在这里,当作一个伏笔埋在这里。转个头来看看,当我们想要为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的设计也令人汗颜。不过这的确能够让人对这个模型一目了然。

思路总结

这里整理一个模型,来总结上述的流程 插件化框架基础模型.png

以上是基于Android7.0源码,加上DroidPlugin源码写的demo。既然都清楚了整个插件化框架的流程,对于轻量级别的插件化框架Small和RePlugin也就好分析了。

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改