美文网首页技术进阶
从零开始实现一个插件化框架(三)

从零开始实现一个插件化框架(三)

作者: PanGeng | 来源:发表于2020-12-02 10:29 被阅读0次

    往期回顾

    Activity的启动流程

    上两篇文章讲了插件apk中的类加载和Activity跳转,那么面试中经常问到的Activity的启动流程是不是就有了答案?

    Activity的启动过程,我们可以从Context的startActivity说起,其实现是ContextImpl的startActivity,然后内部会通过Instrumentation来尝试启动Activity,它会调用ams的startActivity方法,这是一个跨进程过程,当ams校验完activity的合法性后,会通过ApplicationThread回调到我们的进程,这也是一次跨进程过程,而applicationThread就是一个binder,回调逻辑是在binder线程池中完成的,所以需要通过Handler H将其切换到ui线程,在handleMessage中接收到跳转的消息,最终调用handleLaunchActivity,在这个方法里完成了Activity的创建和启动。

    这里还有最后要解决的两个问题:

    1. 加载插件中的资源
    2. 各个API的版本适配

    插件资源加载

    我们知道,android中的资源加载是通过Resources加载的, 但是Resources只能加载宿主的资源,对于插件的资源是不能直接加载的。

    实际上,Resources 类也是通过 AssetManager 类来访问那些被编译过的应用程序资源文件的,不过在访问之前,它会先根据资源 ID 查找得到对应的资源文件名。 而 AssetManager 对象既可以通过文件名访问那些被编译过的,也可以访问没有被编译过的应用程序资源文件。

    Android中资源加载流程

    还是从Activity的启动流程开始看起,我们先来看ActvityThread中的handleLaunchActivity:

    //ActvityThread.java
    @Override
        public Activity handleLaunchActivity(ActivityClientRecord r,
                PendingTransactionActions pendingActions, Intent customIntent) {
           
            // ... 这里是一些判断,无关紧要
            final Activity a = performLaunchActivity(r, customIntent);
            // ...
            return a;
        }
    

    发现它调用了performLaunchActivity:

    // ActvityThread 
    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
            // ... 其他逻辑
            ContextImpl appContext = createBaseContextForActivity(r);
            Activity activity = null;
            try {
                java.lang.ClassLoader cl = appContext.getClassLoader();
                activity = mInstrumentation.newActivity(
                        cl, component.getClassName(), r.intent);
                // ...
            } catch (Exception e) {
                // ...
            }
    
            try {
                Application app = r.packageInfo.makeApplication(false, mInstrumentation);
                if (activity != null) {
                    // ... 其他逻辑
                    
                    // 调用activity中的attach方法,传入ContextImpl,其实就是连接Context
                    activity.attach(appContext, this, getInstrumentation(), r.token,
                            r.ident, app, r.intent, r.activityInfo, title, r.parent,
                            r.embeddedID, r.lastNonConfigurationInstances, config,
                            r.referrer, r.voiceInteractor, window, r.configCallback);
    
                    // ...
                }
                r.setState(ON_CREATE);
    
                mActivities.put(r.token, r);
    
            } catch (SuperNotCalledException e) {
                throw e;
    
            } catch (Exception e) {
                // ...
            }
    
            return activity;
        }
    

    上面代码可以知道, 通过createBaseContextForActivity方法创建了一个ContextImpl对象,并把它传入了Activity中的attach方法。这个稍后再来看,我们先看一下ContextImpl到底是什么:

    // Activity.java
    final void attach(Context context // .....) {
            attachBaseContext(context);
    
            // ...
    }
                      
    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        if (newBase != null) {
            newBase.setAutofillClient(this);
        }
    }
    
    

    其实就是在这里创建了一个Context,看一下super是怎么实现的,一路往上点:

    // ContextWrapper.java
    Context mBase;
    
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }
    

    到这里是不是就很熟悉了,所有的资源加载都是通过ContextWrapper中的mBase来进行加载的,这个mBase就是在ActivityThread中传入进来的ContextImpl,那好,我们继续返回去,看看ContextImpl是怎么创建的:

    // ActivityThread.java
    private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
            final int displayId;
           // ... 其他逻辑
            ContextImpl appContext = ContextImpl.createActivityContext(
                    this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
            // ... 其他逻辑
            return appContext;
        }
    

    直接通过ContextImpl静态方法创建了自己,继续往下走:

    // ContextImpl.java
    static ContextImpl createActivityContext(ActivityThread mainThread,
                LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId, Configuration overrideConfiguration) {
            // ... 其他逻辑
    
            // 直接创建了一个ContextImpl对象
            ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName, activityToken, null, 0, classLoader);
            // 。。。
            final ResourcesManager resourcesManager = ResourcesManager.getInstance();
            // 这里将通过 resourcesManager.createBaseActivityResources
            // 把resources放入context中
            context.setResources(resourcesManager.createBaseActivityResources(activityToken,
                    packageInfo.getResDir(),
                    splitDirs,
                    packageInfo.getOverlayDirs(),
                    packageInfo.getApplicationInfo().sharedLibraryFiles,
                    displayId,
                    overrideConfiguration,
                    compatInfo,
                    classLoader));
            context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
                    context.getResources());
            return context;
        }
    

    我们发现通过resourcesManager.createBaseActivityResources方法创建了一个Resources,并放入了context中,这个方法中 的参数packageInfo.getResDir()就是资源路径,到这里,是不是就明白了为什么插件的资源无法加载?因为Resources路径是我们宿主的路径~!,那如果加载插件资源就有了思路,可以自己创建一个插件的Resources,来替换掉Context中的Resources

    好,继续来看Resources是如何创建的:

    // ResourcesManager.java
    public @Nullable Resources createBaseActivityResources(@NonNull IBinder activityToken,
                @Nullable String resDir,
                @Nullable String[] splitResDirs,
                @Nullable String[] overlayDirs,
                @Nullable String[] libDirs,
                int displayId,
                @Nullable Configuration overrideConfig,
                @NonNull CompatibilityInfo compatInfo,
                @Nullable ClassLoader classLoader) {
            try {
                Trace.traceBegin(Trace.TRACE_TAG_RESOURCES,
                        "ResourcesManager#createBaseActivityResources");
                final ResourcesKey key = new ResourcesKey(
                        resDir,
                        splitResDirs,
                        overlayDirs,
                        libDirs,
                        displayId,
                        overrideConfig != null ? new Configuration(overrideConfig) : null,
                        compatInfo);
       
    
                // ... 无关代码
                return getOrCreateResources(activityToken, key, classLoader);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            }
        }
    

    在这个方法中,创建了一个ResourcesKey对象,这个对象其实就是资源路径,最后通过getOrCreateResources方法返回,看方法名就可以才出来,如果有Resources直接返回,否则创建返回,看一下它的实现:

    // ResourcesManager.java
    private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
                @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
            synchronized (this) {
                // ...
    
                if (activityToken != null) {
                    // ...
                    if (key.hasOverrideConfiguration()
                            && !activityResources.overrideConfig.equals(Configuration.EMPTY))                 {
                        // ...
                    } else {
                        // ...
                    }
    
                    ResourcesImpl resourcesImpl = createResourcesImpl(key);
                    if (resourcesImpl == null) {
                        return null;
                    }
                    final Resources resources;
                    if (activityToken != null) {
                        resources = getOrCreateResourcesForActivityLocked(activityToken,                            classLoader,resourcesImpl, key.mCompatInfo);
                    } else {
                        resources = getOrCreateResourcesLocked(classLoader, resourcesImpl,                          key.mCompatInfo);
                    }
                    return resources;
            }
        }
    

    这里有创建了一个ResourcesImpl对象,在创建resources的时候传入了,那ResourcesImpl是什么呢?

    // ResourcesManager.java
    private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
            final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
            daj.setCompatibilityInfo(key.mCompatInfo);
    
            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);
    
            if (DEBUG) {
                Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
            }
            return impl;
        }
    

    AssetManager是在这里创建的!它的功能不用多说了吧,可以加载任意路径下的资源,那么我们就自己创建一个AssetManager来替换掉原来的,怎么创建呢,继续看它怎么创建的

    protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
            final AssetManager.Builder builder = new AssetManager.Builder();
    
            // 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) {
                try {
                    // 添加 ApkAssets 对象,加载apk资源
                    builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,
                            false /*overlay*/));
                } catch (IOException e) {
                    Log.e(TAG, "failed to add asset path " + key.mResDir);
                    return null;
                }
            }
            // 。。。
    
            return builder.build();
        }
    

    可以看到,AssetManager在创建的时候,添加了一个ApkAssets对象,这里好像无从下手。怎么办?其实看过API26的同学都知道,AssetManager中有一个addAssetPath方法,在API26以前,都是通过这个方法创建的,虽然这个方法 现在被标记为过时了,但还是可以反射到的。

    插件资源加载

    下面我们就来撸码,替换掉插件的资源加载器:

     fun loadAsset(context: Context): Resources? {
            try {
                // 创建AssetManager对象
                val assetManager = AssetManager::class.java.newInstance()
                // 执行addAssetPath方法,添加资源加载路径
                val addAssetPathMethod =
                    assetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
                addAssetPathMethod.isAccessible = true
                addAssetPathMethod.invoke(assetManager, "sdcard/plugin-debug.apk")
                // 创建Resources
                return Resources(
                    assetManager,
                    context.resources.displayMetrics,
                    context.resources.configuration
                )
            } catch (e: Exception) {
                e.printStackTrace()
            }
            return null
        }
    

    在这里我们创建一个插件的资源加载器,然后在应用启动的时候替换掉,在Application中:

    class DynamicApp : Application() {
        // 插件的资源
        private var mResources: Resources? = null
    
    
        override fun onCreate() {
            super.onCreate()
            // 获取插件的资源
            mResources = LoadUtils.loadAsset(this)
    
        }
    
        // 在这里重写getResources方法进行替换
        override fun getResources(): Resources {
    
            if (mResources == null) {
                return super.getResources()
            }
            return mResources!!
        }
    }
    

    在宿主里面替换完成,插件里面要使用宿主的Resources怎么办?很简单,直接使用application的Resources不就可以了吗。因为插件中的类是会合并到宿主的,所以他们的Application是相同的。

    所以可以在插件里面创建一个BaseActivity, 重写getResources方法,让它使用application中的:

    abstract class BaseActivity : Activity() {
    
        override fun getResources(): Resources {
            if (application != null && application.resources != null)
                return application.resources
            return super.getResources()
        }
    }
    

    然后插件的MainActivity就可以直接使用了

    class MainActivity : BaseActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            Toast.makeText(this, "插件的MainActivity", Toast.LENGTH_SHORT).show()
        }
    }
    

    插件布局里面是一个TextView, 显示 "Hello World"。 下面来看看运行效果吧~

    <img src="https://img-blog.csdnimg.cn/20200121160113331.gif" alt="在这里插入图片描述" style="zoom: 33%;" />

    不同版本API的适配

    到这里一个插件化应用就完成了,下面我们来看一下不同版本API的适配,其实github中的demo是做了的。很简单,我们来看一下每个版本都有哪些不同呢?

    一、 ActivityManager

    在这里插入图片描述

    可以看懂, API26以前,用的是ActivityManagerNative, API26以后用的是ActivityManager, 而且两个静态常量名也变了,所以我们Hook的时候可以做一下判断:

    public static void hookAms() {
    
    
            try {
                Object singleTon = null;
                /*
                 * android 26或以上版本的API是一样的
                 */
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    
                    Class<?> activityManagerClass = Class.forName("android.app.ActivityManager");
                    Field iActivityManagerSingletonField = activityManagerClass.getDeclaredField("IActivityManagerSingleton");
                    iActivityManagerSingletonField.setAccessible(true);
                    singleTon = iActivityManagerSingletonField.get(null);
                } else {
                    /*
                     *  android 26或以下版本的API是一个系列
                     */
                    Class<?> activityManagerClass = Class.forName("android.app.ActivityManagerNative");
                    Field iActivityManagerSingletonField = activityManagerClass.getDeclaredField("gDefault");
                    iActivityManagerSingletonField.setAccessible(true);
                    singleTon = iActivityManagerSingletonField.get(null);
                }
    
    
                Class<?> singleTonClass = Class.forName("android.util.Singleton");
                Field mInstanceField = singleTonClass.getDeclaredField("mInstance");
                mInstanceField.setAccessible(true);
    
                // 获取到IActivityManagerSingleton的对象
                final Object mInstance = mInstanceField.get(singleTon);
    
    
                Class<?> iActivityManagerClass = Class.forName("android.app.IActivityManager");
    
                Object newInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                        new Class[]{iActivityManagerClass},
                        new InvocationHandler() {
    
                            @Override
                            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                                if (method.getName().equals("startActivity")) {
    
                                    int index = 0;
    
                                    for (int i = 0; i < args.length; i++) {
                                        if (args[i] instanceof Intent) {
                                            index = i;
                                            break;
                                        }
                                    }
    
                                    Intent proxyIntent = new Intent();
                                    proxyIntent.setClassName("com.kangf.dynamic",
                                            "com.kangf.dynamic.ProxyActivity");
                                    proxyIntent.putExtra("oldIntent", (Intent) args[index]);
                                    args[index] = proxyIntent;
                                }
                                return method.invoke(mInstance, args);
                            }
                        });
    
                mInstanceField.set(singleTon, newInstance);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    二、ActivityThread

    在API28之后,加入了Lifecycle,所以接收消息的时候加入了很多东西,我们上一篇文章已经讲到了它的流程,而在API28以前,是很简单的,直接通过一个LAUNCH_ACTIVITY(value = 100)常量接收,msg.obj就是一个ActivityClientRecord对象,然后调用了handleLaunchActivity()。所以我们在hook的时候可以加个判断,处理也非常简单:

    public static void hookHandler() {
    
            try {
                // 获取ActivityThread实例
                final Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
                Field activityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
                activityThreadField.setAccessible(true);
                final Object activityThread = activityThreadField.get(null);
    
                // 获取Handler实例
                Field mHField = activityThreadClass.getDeclaredField("mH");
                mHField.setAccessible(true);
                Object mH = mHField.get(activityThread);
    
    
                Class<?> handlerClass = Class.forName("android.os.Handler");
                Field mCallbackField = handlerClass.getDeclaredField("mCallback");
                mCallbackField.setAccessible(true);
                mCallbackField.set(mH, new Handler.Callback() {
    
                    @Override
                    public boolean handleMessage(Message msg) {
    
                        Log.e("kangf", "handling code = " + msg.what);
    
                        switch (msg.what) {
                            case 100: // API 28 以前直接接收
                                try {
                                    // 获取ActivityClientRecord中的intent对象
                                    Field intentField = msg.obj.getClass().getDeclaredField("intent");
                                    intentField.setAccessible(true);
                                    Intent proxyIntent = (Intent) intentField.get(msg.obj);
    
                                    // 拿到插件的Intent
                                    Intent intent = proxyIntent.getParcelableExtra("oldIntent");
    
                                    // 替换回来
                                    proxyIntent.setComponent(intent.getComponent());
                                } catch (Exception e) {
                                    e.printStackTrace();
                                }
                                break;
    
                            case 159: // API 28 以后加入了 lifecycle, 这里msg发生了变化
                                try {
                                    Field mActivityCallbacksField = msg.obj.getClass().getDeclaredField("mActivityCallbacks");
                                    mActivityCallbacksField.setAccessible(true);
                                    List<Object> mActivityCallbacks = (List<Object>) mActivityCallbacksField.get(msg.obj);
                                    for (int i = 0; i < mActivityCallbacks.size(); i++) {
                                        Class<?> itemClass = mActivityCallbacks.get(i).getClass();
                                        if (itemClass.getName().equals("android.app.servertransaction.LaunchActivityItem")) {
                                            Field intentField = itemClass.getDeclaredField("mIntent");
                                            intentField.setAccessible(true);
                                            Intent proxyIntent = (Intent) intentField.get(mActivityCallbacks.get(i));
                                            Intent intent = proxyIntent.getParcelableExtra("oldIntent");
                                            proxyIntent.setComponent(intent.getComponent());
                                            break;
                                        }
                                    }
                                } catch (Exception e) {
                                    e.printStackTrace();
                                }
                                break;
                        }
    
                        // 这里必须返回false
                        return false;
    
                    }
                });
    
    
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    

    这样就完成了不同版本API的适配

    三 、AppCompatActivity

    细心的同学发现,插件的Activity是继承自了Activity,如果想用AppCompatActivity的话是会报错的:

    在这里插入图片描述

    可以看到,错误定位到了AppCompatDelegateImpl.java的第753行,我们点进去看一下:

    在这里插入图片描述

    这里就不点进去看了,在这可以看到加载了一个资源文件,我们知道,资源文件在编译器会生成一个静态常量,在宿主中这个文件生成了一个静态常量,在插件中这个布局文件的常量发生了变化,这时候还是使用宿主中的常量,就会产生冲突,这个问题怎么解决呢?

    我们只需要在插件中,去加载自己的资源就可以了。下面我们来修改一下之前的资源加载的代码。

    1. 在插件中创建资源加载器

    object LoadUtils {
    
        private var mResources: Resources? = null
    
    
        /*
            只有mResources为空时才创建
         */
        fun getResources(context: Context): Resources {
    
            if (mResources == null) {
                mResources = loadAsset(context)
            }
            return mResources!!
        }
    
        private fun loadAsset(context: Context): Resources? {
            try {
                // 创建AssetManager对象
                val assetManager = AssetManager::class.java.newInstance()
                // 执行addAssetPath方法,添加资源加载路径
                val addAssetPathMethod =
                    assetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
                addAssetPathMethod.isAccessible = true
                addAssetPathMethod.invoke(assetManager, "sdcard/plugin-debug.apk")
                // 创建Resources
                return Resources(
                    assetManager,
                    context.resources.displayMetrics,
                    context.resources.configuration
                )
            } catch (e: Exception) {
                e.printStackTrace()
            }
            return null
        }
    }
    

    2. 修改BaseActivity

    我们可以直接创建一个Context,替换掉里面的Resources,Activity中就使用我们自己的Context

    abstract class BaseActivity : AppCompatActivity() {
    
        protected lateinit var mContext: Context
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            // 获取自己创建的resources
            val resources = LoadUtils.getResources(application)
            // 创建自己的Context
            mContext = ContextThemeWrapper(baseContext, 0)
            // 把自己的Context中的resources替换为我们自己的
            val clazz = mContext::class.java
            val mResourcesField = clazz.getDeclaredField("mResources")
            mResourcesField.isAccessible = true
            mResourcesField.set(mContext, resources)
    
    
        }
    
    //    override fun getResources(): Resources {
    //        if (application != null && application.resources != null)
    //            return application.resources
    //        return super.getResources()
    //    }
    }
    

    3. 修改MainActivity

    MainActivity 中加载资源就需要使用我们自己创建的Context了

    class MainActivity : BaseActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
    
            super.onCreate(savedInstanceState)
    
            val view = LayoutInflater.from(mContext).inflate(R.layout.activity_main, null, false)
            setContentView(view)
    
            Toast.makeText(this, "插件的MainActivity", Toast.LENGTH_SHORT).show()
        }
    }
    

    4. 去掉宿主中的resouces

    class DynamicApp : Application() {
    
    
        // private var mResources: Resources? = null
    
    
    //    override fun onCreate() {
    //        super.onCreate()
    //
    //         mResources = LoadUtils.loadAsset(this)
    //
    //    }
    
    
    //    override fun getResources(): Resources {
    //
    //        if (mResources == null) {
    //            return super.getResources()
    //        }
    //        return mResources!!
    //    }
    }
    

    这里就不多说了,来看一下效果

    <img src="https://img-blog.csdnimg.cn/20200121165516174.gif" alt="在这里插入图片描述" style="zoom:33%;" />

    插件里面多了个toolbar,是不是好看了很多勒?

    总结

    好啦,插件化的内容到这里就完结了。demo已上传至github,有兴趣的可以看看,其实demo中集成了一个DroidPlugin,我们这个demo的思路就是按照这个框架来的,读完了这几篇文章,再看DroidPlugin的源码就轻松多了。

    github传送门

    相关文章

      网友评论

        本文标题:从零开始实现一个插件化框架(三)

        本文链接:https://www.haomeiwen.com/subject/cneywktx.html