Implement a plug-in framework from scratch (3)-the end

Past review

Activity startup process The
last two articles talked about the class loading and activity jump in the plug-in apk, so does the activity startup process that are often asked in interviews have the answer?

Activity start process, we can start from the startActivity of Context, its implementation is the startActivity of ContextImpl, and then internally will try to start the Activity through Instrumentation, it will call the startActivity method of ams, this is a cross-process process, when ams check After the legitimacy of the activity, it will call back to our process through ApplicationThread, which is also a cross-process process, and applicationThread is a binder, the callback logic is completed in the binder thread pool, so it needs to be switched to ui through Handler H The thread receives the jump message in handleMessage, and finally calls handleLaunchActivity. In this method, the creation and startup of the Activity are completed.

There are two final problems to be solved here:

Load the resources in the plugin
Version adaptation of each API

Plugin resource loading

We know that resource loading in android is loaded through Resources, but Resources can only load host resources, and plug-in resources cannot be loaded directly.

In fact, the Resources class also uses the AssetManager class to access those compiled application resource files, but before accessing, it will first find the corresponding resource file name based on the resource ID. The AssetManager object can access both compiled and uncompiled application resource files by file name.

Resource loading process in Android

Still starting from the start process of the Activity, let's first look at the handleLaunchActivity in ActvityThread:

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

Found that it called 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”。 下面来看看运行效果吧~


不同版本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()
    }
}
  1. 去掉宿主中的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!!
//    }
}

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


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

总结

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

原文作者:Pan Geng
原文链接:https://blog.csdn.net/qq_22090073/java/article/details/104063781

Guess you like

Origin blog.csdn.net/AndroidAlvin/article/details/107532728