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也会有一个,说到这里已经说明插件化类访问的原理了。其核心就是分为以下步骤:

宿主的classloader通过反射拿到内部的dexPathList数组
构造一个我们插件的DexClassLoader(而不是PathClassLoader),然后通过反射拿到其中的dexPathList数组
将两个数组进行合并,然后通过反射设置会宿主的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文件
至此我们终于知道为啥在插件访问不到资源了。

看到这里有两个实现方法

在插件的Activity重写getResources方法,然后根据重新创建一个AssetManager,这样插件内的资源可能通过自己的AssetManager进行资源
通过反射拿到宿主的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都被标为浅灰名单。
可能在日后的版本中插件化思想将不能使用了。。。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安卓兼职framework应用工程师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值