Android插件化原理(Small)

插件化原理(small)

ClassLoader

DexClassLoader 和 PathClassLoader

android 中的calssloader,区别在于DexClassLoader多了一个optimize的优化目录,其可以加载外部的dex,zip,so等包,而pathclassloader只能加载内部的dex,apk等包

而两个都是继承自BaseDexClassLoader ,而BaseDexClassLoader的主要工作是交给DexPathList是做,接下来让我们看看这个DexPathList的构造方法


  public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }
        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
        }
        if (optimizedDirectory != null) {
            if (!optimizedDirectory.exists())  {
                throw new IllegalArgumentException(
                        "optimizedDirectory doesn't exist: "
                        + optimizedDirectory);
            }
            if (!(optimizedDirectory.canRead()
                            && optimizedDirectory.canWrite())) {
                throw new IllegalArgumentException(
                        "optimizedDirectory not readable/writable: "
                        + optimizedDirectory);
            }
        }
        this.definingContext = definingContext;
        this.dexElements =
            makeDexElements(splitDexPath(dexPath), optimizedDirectory);
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }

dexPath就是我们需要加载插件的路径,可以看到主要是由makeDexElements这个方法实现

    private static Element[] makeDexElements(ArrayList<File> files,
            File optimizedDirectory) {
        ArrayList<Element> elements = new ArrayList<Element>();
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
            ZipFile zip = null;
            DexFile dex = null;
            String name = file.getName();
            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                try {
                    zip = new ZipFile(file);
                } catch (IOException ex) {
                    /*
                     * Note: ZipException (a subclass of IOException)
                     * might get thrown by the ZipFile constructor
                     * (e.g. if the file isn't actually a zip/jar
                     * file).
                     */
                    System.logE("Unable to open zip file: " + file, ex);
                }
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ignored) {
                    /*
                     * IOException might get thrown "legitimately" by
                     * the DexFile constructor if the zip file turns
                     * out to be resource-only (that is, no
                     * classes.dex file in it). Safe to just ignore
                     * the exception here, and let dex == null.
                     */
                }
            } else {
                System.logW("Unknown file type for: " + file);
            }
            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }

该方法返回的是一个element的数组。
看到这里,我们先不用深入理解makeDexElements内部的逻辑实现,先思考一个问题,何为插件化?
我们做插件的目的是有很多种,例如:减少包体积,热更新 。。。
插件化意味着宿主和插件之间能够进行通信,宿主可以调用插件里的对象,宿主可以访问插件里的资源等等。

所以每个BaseDexClassLoader构造完之后都会有一个dexElements,这就说明宿主的classloader有一个,我们插件内部自己的classloader也会有一个,说到这里已经说明插件化类访问的原理了。其核心就是分为以下步骤:

    1. 宿主的classloader通过反射拿到内部的dexPathList数组
    1. 构造一个我们插件的DexClassLoader(而不是PathClassLoader),然后通过反射拿到其中的dexPathList数组
    1. 将两个数组进行合并,然后通过反射设置会宿主的classloader中

事实上,Android官方的multidex就是这个原理。完成这些步骤以后,我们在宿主中就可以调用插件的类了,但是工作还没完,资源如何访问?

Resources

设想一个问题,我们将两个dexPathList进行了合并,此时宿主可以调用插件,但是假设插件内部根据一个id查找一个资源,会报ResourcesNotFind的异常,为什么呢?我们来看看源码,假设当前处在插件中的某个activity,根据id获取获取某个drawable并设置进
imageview中

        imageview.setImageDrawable(getResources().getDrawable(R.drawable.ic_launcher_background))

最终回到ContextThemeWrapper中:

    @Override
    public Resources getResources() {
        return getResourcesInternal();
    }

    private Resources getResourcesInternal() {
        //mResouces为空
        if (mResources == null) {
            if (mOverrideConfiguration == null) {
                    //将会调用super.getResources()
                mResources = super.getResources();
            } else {
                final Context resContext = createConfigurationContext(mOverrideConfiguration);
                mResources = resContext.getResources();
            }
        }
        return mResources;
    }

而super.getReources最终实现是ContextImpl.getResources()中
而ContextImpl是在ActivityThread中由系统执行各个步骤时创建的,我们插件化的activity根本不会走这样一套流程(如果走这套流程的话,插件化就毫无意义啦~~)
所以,拿到的ContextImpl则是宿主的。而这个ComtextImpl在Application到Activity的各个阶段都会有所区别

具体在于,Application的mBase成员是通过ContextImpl.createAppContext经过attachBaseContext后创建的,而Activity的mBase成员是通过ContextImpl.createActivityContext创建的,两者的区别有兴趣可以阅读下源码

无论以哪种方式,最终都会来到ResourcesManager.getOrCreateResources()方法创建资源对象,
而经过层层判断之后,又会来到createResourcesImpl()方法,
而createResourcesImpl()内部

    private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {

        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }

        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);


        return impl;
    }

而createAssetManager()方法:

 protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        AssetManager assets = new AssetManager();

        // resDir can be null if the 'android' package is creating a new Resources object.
        // This is fine, since each AssetManager automatically loads the 'android' package
        // already.
        if (key.mResDir != null) {
        
                //看到这里大概猜到为什么插件无法访问资源了
            if (assets.addAssetPath(key.mResDir) == 0) {
                    ...
                return null;
            }
        }
            
        ...
        ...

        return assets;
    }

原来assets.addAssetPath()方法是把key.mResDir加进去assetmanager中,这样就可以访问到资源,mResDir就是res文件
至此我们终于知道为啥在插件访问不到资源了。

看到这里有两个实现方法

  1. 在插件的Activity重写getResources方法,然后根据重新创建一个AssetManager,这样插件内的资源可能通过自己的AssetManager进行资源
  2. 通过反射拿到宿主的AssetManager,然后调用内部addAssetPath()将当前插件的路径传进去,相当于进行资源的合并。

这两种方法都可行,第一种会导致资源爆炸,宿主一份,插件一份,而且这里面的资源无法公用。第二种则会导致资源id冲突,但是可以通过某些手段进行控制(比如控制分配id的段达到防止资源id冲突)

而small用的是第二种,并且配合gradle介入资源id段(PP)的分配情况
具体原理则是:在gradle执行到mergeAndroidResources这个task时,将R.java,R.txt替换为small extention中配置的packageId字段,并且替换完成后,重写整个resources.arsc文件,将原来的arsc文件里面的索引的id替换成配置后的id。如原来生成的id为0x7F010001 替换成自定义 0x21010001

替换资源id这个方法,除了上述这个之外,还可以手动修改AAPT的源码,然后重新编译一个aapt工具

至此,资源也可以访问了。

四大组件

类和资源都可以访问了,我们都知道四大组件要在宿主的AndroidManifest.xml中注册才可以使用,否则会提示找不到该component。以activity为例,如果将插件中的activity在宿主AndroidManifest中注册,那插件化将毫无意义,因为每次有新activity都需要更新宿主,插件的思想也就无从谈起。

有什么办法可以做到不在宿主中注册也可以调用呢?

Small使用hook,主要是hook住Instrumentation,ActivityThread和mH这几个类。

App创建过程 说过 Instrumentation最初的目的是为了给UI测试预留的接口,没想到可以被插件化玩出花样来,可能谷歌一开始也没想到。

步骤如下:

  • Step 1
public static void hookInstrumentation() {
        
        try {
            Class at = Class.forName("android.app.ActivityThread");
            Method atMethod = at.getDeclaredMethod("currentActivityThread",null);
            atMethod.setAccessible(true);
            Object activityThread = atMethod.invoke(null,null
            );
            
            Field instruFiled = at.getDeclaredField("mInstrumentation");
            instruFiled.setAccessible(true);
            Instrumentation instrumentation = (Instrumentation) instruFiled.get(activityThread);
            
            TestInstrumentationWrapper wrapper = new TestInstrumentationWrapper(instrumentation);
            instruFiled.set(activityThread, wrapper);
            Log.d(TAG,"hook init success");
        } catch (Throwable e) {
            e.printStackTrace();
        }
        
    }
    
    private static class TestInstrumentationWrapper extends Instrumentation {
        
        private Instrumentation mBase;
        
        public TestInstrumentationWrapper(Instrumentation base) {
            mBase = base;
        }
        
        public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent
                intent,
                int requestCode, Bundle options) {
            Log.d(TAG, "TestInstrumentationWrapper hook 1");
            //step1
            return realExecStartActivity1(who, contextThread, token, target, intent, requestCode, options);
        }
        
        public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, String target, Intent intent,
                int requestCode, Bundle options) {
            
            Log.d(TAG, "TestInstrumentationWrapper hook 2");
            
            return realExecStartActivity2(who, contextThread, token, target, intent, requestCode, options);
        }
        
        public ActivityResult execStartActivity(
                Context who, IBinder contextThread, IBinder token, String resultWho,
                Intent intent, int requestCode, Bundle options, UserHandle user) {
            
            Log.d(TAG, "TestInstrumentationWrapper hook 3");
            
            return realExecStartActivity3(who,contextThread,token,resultWho,intent,requestCode,options,user);
        }
        
        @SuppressWarnings("NewApi")
        private ActivityResult realExecStartActivity3(Context who, IBinder contextThread, IBinder token, String resultWho,
                Intent intent, int requestCode, Bundle options, UserHandle user) {
            ActivityResult activityResult = null;
            try {
                Class c = mBase.getClass();
                Method execStartActivity = c.getDeclaredMethod("execStartActivity",
                        Context.class,
                        IBinder.class,
                        IBinder.class,
                        String.class,
                        Intent.class,
                        int.class,
                        Bundle.class,
                        UserHandle.class
                );
                
                activityResult = (ActivityResult) execStartActivity.invoke(mBase,
                        who,
                        contextThread,
                        token,
                        resultWho,
                        intent,
                        requestCode,
                        options,
                        user
                );
                
            } catch (Exception e) {
                e.printStackTrace();
            }
            
            return activityResult;
            
        }
        
        
        private ActivityResult realExecStartActivity2(Context who, IBinder contextThread, IBinder token, String target,
                Intent intent, int requestCode, Bundle options) {
            ActivityResult activityResult = null;
            try {
                Class c = mBase.getClass();
                Method execStartActivity = c.getDeclaredMethod("execStartActivity",
                        Context.class,
                        IBinder.class,
                        IBinder.class,
                        String.class,
                        Intent.class,
                        int.class,
                        Bundle.class
                );
                
                activityResult = (ActivityResult) execStartActivity.invoke(mBase,
                        who,
                        contextThread,
                        token,
                        target,
                        intent,
                        requestCode,
                        options
                );
                
            } catch (Exception e) {
                e.printStackTrace();
            }
            
            return activityResult;
            
        }
        
        private ActivityResult realExecStartActivity1(Context who, IBinder contextThread, IBinder token, Activity target,
                Intent intent, int requestCode, Bundle options) {
            ActivityResult activityResult = null;
            try {
                Class c = mBase.getClass();
                Method execStartActivity = c.getDeclaredMethod("execStartActivity",
                        Context.class,
                        IBinder.class,
                        IBinder.class,
                        Activity.class,
                        Intent.class,
                        int.class,
                        Bundle.class
                );
                
                activityResult = (ActivityResult) execStartActivity.invoke(mBase,
                        who,
                        contextThread,
                        token,
                        target,
                        intent,
                        requestCode,
                        options
                );
                
            } catch (Exception e) {
                e.printStackTrace();
            }
            
            return activityResult;
            
        }
        
        @Override
        public Activity newActivity(ClassLoader cl, String className, Intent intent)
                throws InstantiationException, IllegalAccessException, ClassNotFoundException {
            //step2
            //在这里实例化插件的activity
            return super.newActivity(cl, className, intent);
        }


    // on Applicaiton
    
class App : Application(){

    override fun onCreate() {
        super.onCreate()
        HookUtil.hookInstrumentation()
    }
    
}

在MainActivity中通过intent启动一个TestActivity,运行结果:

2019-03-23 15:36:09.820 6537-6537/com.example.simpleapp D/Hook: hook init success
2019-03-23 15:36:13.068 6537-6537/com.example.simpleapp D/Hook: TestInstrumentationWrapper hook 1

此时我们已经hook住了startActivity过程,那么我可以在宿主中占坑一个ProxyActivity,在启动插件activity的过程中,重定向至ProxyActivity达到偷梁换柱的目的。

step2:
有去有回,经过上面已经可以做到将插件的activity换了个皮变成宿主中的ProxyActivity,但是怎么将这个ProxyActivity换回来呢?

这涉及app启动流程,主要是本地app进程(ActivityThread)和系统SystemServer进程(ActivityManagerService)进行binder通信的过程,有兴趣的看下之前写过的 一篇文章 分析app创建流程

现在我们只需要知道,Context.startActivity()最终会来到
ActivityStackSupervisor.realStartActivityLocked()


  final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
            boolean andResume, boolean checkConfig) throws RemoteException {
    
    ...
     app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
                        System.identityHashCode(r), r.info,
                        // TODO: Have this take the merged configuration instead of separate global
                        // and override configs.
                        mergedConfiguration.getGlobalConfiguration(),
                        mergedConfiguration.getOverrideConfiguration(), r.compat,
                        r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,
                        r.persistentState, results, newIntents, !andResume,
                        mService.isNextTransitionForward(), profilerInfo);           
 
 
 }

而app.thread是ApplicationThread,它是一个ActivityThread的内部类,可以理解为在ActivityManagerService这一侧的ActivityThread代理对象,主要是通过binder与远端(app进程)进行调用,接着我们分析

  public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
                ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
                CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
                int procState, Bundle state, PersistableBundle persistentState,
                List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
                boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {

            updateProcessState(procState, false);

            ActivityClientRecord r = new ActivityClientRecord();

            r.token = token;
            r.ident = ident;
            r.intent = intent;
            r.referrer = referrer;
            r.voiceInteractor = voiceInteractor;
            r.activityInfo = info;
            r.compatInfo = compatInfo;
            r.state = state;
            r.persistentState = persistentState;

            r.pendingResults = pendingResults;
            r.pendingIntents = pendingNewIntents;

            r.startsNotResumed = notResumed;
            r.isForward = isForward;

            r.profilerInfo = profilerInfo;

            r.overrideConfig = overrideConfig;
            updatePendingConfiguration(curConfig);

            sendMessage(H.LAUNCH_ACTIVITY, r);
        }

主要是通过mH这个handler发送消息然后进行处理,最终又会来到performLaunchActivity这个方法里面:


 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ...
        ...
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.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);
            }
        }

        ...
        ...


        return activity;
    }


就是利用Instrumentation.newActivity()方法通过反射调用实例化我们插件中的activity,从而将插件中的activity交给系统托管。

而Small正是利用了这一点,核心原理大概讲完了.而其余组件的

总结

当然这里只是对核心原理进行了一下简略的描述,要想达到生产需求还要许多工作要做。

例如:正确区分宿主的activity和插件的activity,当某个activity处在宿主中且已注册时,直接跳过插件化的步骤,交给系统处理即可。

再例如,当我们的需求需要在start多个相同的launchMode的activity时,需要在宿主占坑多少个这样的proxy activity?

像上文提到的mH这个handler,我们其实可以hook住这个mH然后所有的分发事件。插件化的实现有很多种,但无非都是在App创建流程中在ActivityThread,Instrumentation,ActivityManagerNative(AMS的本地代理对象)做文章,所以理解App创建流程对于插件化思想至关重要,说不定可以找到某个新奇的突破点进行插件化。

值得一提的是Android9开始对反射进行限制,像反射调用ActivityThread里的currentActivityThread(),mH都被标为浅灰名单。
可能在日后的版本中插件化思想将不能使用了。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341

推荐阅读更多精彩内容