Android插件化初体验

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

最近把Activity启动流程整体看了一遍,估摸着弄个啥来巩固下,发现插件化正好是这块技术的实践,而说道插件化其实有好几种实现方式,这里我用的是hook的方式实现,主要目的呢是为了对activity启动流程有个整体的认识,当然了本篇的插件化也只是一个demo版本并没有任何兼容适配,重在流程和原理的理解。

概述

插件化顾名思义,就是将一个APK拆成多个,当需要的时候下载对应插件APK加载的技术。本文demo中除了下载是通过adb命令,其他都是模拟真实环境的,这里先理下流程。

  1. 将插件工程打包为APK,然后通过adb push命令发送到宿主APK目录(模拟下载流程)。
  2. 利用ClassLoader加载插件APK中的类文件。
  3. hook Activity启动流程中部分类,利用占坑Activity帮助PluginActivity绕过AMS验证,在真正启动的时候又替换回PluginActivity。
  4. 创建插件Apk的Resources对象,完成插件资源的加载。

对整体流程有个大概认识后,下面将结合源码和Demo来详细讲解,本文贴出的源码基于API27。

初始化插件APK类文件

既然插件APK是通过网络下载下来的,那么APK中的类文件就需要我们自己加载了,这里我们要用到DexClassLoader去加载插件APK中的类文件,然后将DexClassLoader中的Element数组和宿主应用的PathClassLoader的Element数组合并再设置回PathClassLoader,完成插件APK中类的加载。对ClassLoader不太熟悉的可以看下我另篇Android ClassLoader浅析

public class InjectUtil {

    private static final String TAG = "InjectUtil";
    private static final String CLASS_BASE_DEX_CLASSLOADER = "dalvik.system.BaseDexClassLoader";
    private static final String CLASS_DEX_PATH_LIST = "dalvik.system.DexPathList";
    private static final String FIELD_PATH_LIST = "pathList";
    private static final String FIELD_DEX_ELEMENTS = "dexElements";


    public static void inject(Context context, ClassLoader origin) throws Exception {
        File pluginFile = context.getExternalFilesDir("plugin");// /storage/emulated/0/Android/data/$packageName/files/plugin
        if (pluginFile == null || !pluginFile.exists() || pluginFile.listFiles().length == 0) {
            Log.i(TAG, "插件文件不存在");
            return;
        }
        pluginFile = pluginFile.listFiles()[0];//获取插件apk文件
        File optimizeFile = context.getFileStreamPath("plugin");// /data/data/$packageName/files/plugin
        if (!optimizeFile.exists()) {
            optimizeFile.mkdirs();
        }
        DexClassLoader pluginClassLoader = new DexClassLoader(pluginFile.getAbsolutePath(), optimizeFile.getAbsolutePath(), null, origin);
        Object pluginDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), pluginClassLoader, FIELD_PATH_LIST);
        Object pluginElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), pluginDexPathList, FIELD_DEX_ELEMENTS);//拿到插件Elements

        Object originDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), origin, FIELD_PATH_LIST);
        Object originElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS);//拿到Path的Elements

        Object array = combineArray(originElements, pluginElements);//合并数组
        FieldUtil.setField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS, array);//设置回PathClassLoader
        Log.i(TAG, "插件文件加载成功");
    }

    private static Object combineArray(Object pathElements, Object dexElements) {//合并数组
        Class<?> componentType = pathElements.getClass().getComponentType();
        int i = Array.getLength(pathElements);
        int j = Array.getLength(dexElements);
        int k = i + j;
        Object result = Array.newInstance(componentType, k);
        System.arraycopy(dexElements, 0, result, 0, j);
        System.arraycopy(pathElements, 0, result, j, i);
        return result;
    }

}

这里我们约定将插件APK放在/storage/emulated/0/Android/data/$packageName/files/plugin目录,然后为了尽早加载所以在Application中执行加载逻辑。

public class MyApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        try {
            InjectUtil.inject(this, getClassLoader());//加载插件Apk的类文件
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Hook启动流程

在说之前我们得先了解下Activity的启动流程。

上图抽象的给出了Acticity的启动过程。在应用程序进程中的Activity向AMS请求创建Activity(步骤1),AMS会对这个Activty的生命周期栈进行管理,校验Activity等等。如果Activity满足AMS的校验,AMS就会请求应用程序进程中的ActivityThread去创建并启动Activity。

那么在上一步我们已经将插件Apk的类文件加载进来了,但是我们并不能通过startActivity的方式去启动PluginActivity,因为PluginActivity并没有在AndroidManifest中注册过不了AMS的验证,既然这样我们换一个思路。

  1. 在宿主项目中提前弄一个SubActivity占坑,在启动PluginActivity的时候替换为启动这个SubActivity绕过验证。
  2. 在AMS处理完相应验证通知我们ActivityThread创建Activty的时候在替换为PluginActivity。

占坑SubActivity非常简单

public class SubActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

然后在AndroidManifest注册好即可

<activity android:name=".SubActivity"/>

对于startActivity()最终都会调到ActivityManagerService的startActivity()方法。

ActivityManager.getService()//获取AMS
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);

那么我们可以通过动态代理hook ActivityManagerService,然后在startActivity()的时候将PluginActivity替换为SubActivity,不过对于ActivityManagerService的获取不同版本方式有所不同。

在Android7.0以下会调用ActivityManagerNative的getDefault方法获取,如下所示。

    static public IActivityManager getDefault() {
        return gDefault.get();
    }

    private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
        protected IActivityManager create() {
            IBinder b = ServiceManager.getService("activity");//获取ams
            if (false) {
                Log.v("ActivityManager", "default service binder = " + b);
            }
            IActivityManager am = asInterface(b);//拿到ams代理对象
            if (false) {
                Log.v("ActivityManager", "default service = " + am);
            }
            return am;
        }
    };

getDefault()返回的是IActivityManager,而gDefault是一个单例对象Singleton并且是静态的是非常容易用反射获取。

Android8.0会调用ActivityManager的getService方法获取,如下所示。

    public static IActivityManager getService() {
        return IActivityManagerSingleton.get();
    }

    private static final Singleton<IActivityManager> IActivityManagerSingleton =
            new Singleton<IActivityManager>() {
                @Override
                protected IActivityManager create() {
                    final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);//拿到ams
                    final IActivityManager am = IActivityManager.Stub.asInterface(b);//拿到ams代理对象
                    return am;
                }
            };

返回一个IActivityManager,而IActivityManagerSingleton是一个单例对象Singleton并且是静态非常容易获取。

在看下上面提到的Singleton等会hook会用到

public abstract class Singleton<T> {
    private T mInstance;
    protected abstract T create();
    public final T get() {
        synchronized (this) {
            if (mInstance == null) {
                mInstance = create();
            }
            return mInstance;
        }
    }
}

到这里会发现其实返回的都是AMS的接口IActivityManager,那么我们只要能通过反射拿到,然后通过动态代理去Hook这个接口在启动的时候把PluginActivity替换为SubActivity即可绕过AMS的验证。

public class IActivityManagerProxy implements InvocationHandler {//动态代理

    private final Object am;

    public IActivityManagerProxy(Object am) {//传入代理的AMS对象
        this.am = am;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("startActivity".equals(method.getName())) {//startActivity方法
            Intent oldIntent = null;
            int i = 0;
            for (; i < args.length - 1; i++) {//获取startActivity Intent参数
                if (args[i] instanceof Intent) {
                    oldIntent = (Intent) args[i];
                    break;
                }
            }
            Intent newIntent = new Intent();//创建新的Intent
            newIntent.setClassName("rocketly.demo", "rocketly.demo.SubActivity");//启动目标SubActivity
            newIntent.putExtra(HookHelper.TRANSFER_INTENT, oldIntent);//保留原始intent
            args[i] = newIntent;//把插件Intent替换为占坑Intent
        }
        return method.invoke(am, args);
    }
}

动态代理写好后,我们还需要通过反射去hook住原始AMS。因为会用到反射弄了一个简单的工具类

public class FieldUtil {
    public static Object getField(Class clazz, Object target, String name) throws Exception {
        Field field = clazz.getDeclaredField(name);
        field.setAccessible(true);
        return field.get(target);
    }

    public static Field getField(Class clazz, String name) throws Exception {
        Field field = clazz.getDeclaredField(name);
        field.setAccessible(true);
        return field;
    }

    public static void setField(Class clazz, Object target, String name, Object value) throws Exception {
        Field field = clazz.getDeclaredField(name);
        field.setAccessible(true);
        field.set(target, value);
    }
}

接下来是hook代码

public class HookHelper {
    public static final String TRANSFER_INTENT = "transfer_intent";

    public static void hookAMS() throws Exception {
        Object singleton = null;
        if (Build.VERSION.SDK_INT >= 26) {//大于等于8.0
            Class<?> clazz = Class.forName("android.app.ActivityManager");
            singleton = FieldUtil.getField(clazz, null, "IActivityManagerSingleton");//拿到静态字段
        } else {//8.0以下
            Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
            singleton = FieldUtil.getField(activityManagerNativeClazz, null, "gDefault");//拿到静态字段
        }
        Class<?> singleClazz = Class.forName("android.util.Singleton");
        Method getMethod = singleClazz.getMethod("get");
        Object iActivityManager = getMethod.invoke(singleton);//拿到AMS
        Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
        Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{iActivityManagerClazz}, new IActivityManagerProxy(iActivityManager));//生成动态代理
        FieldUtil.setField(singleClazz, singleton, "mInstance", proxy);//将代理后的对象设置回去
    }
}

接下来我们需要在Application去执行hook

public class MyApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        try {
            InjectUtil.inject(this, getClassLoader());//加载插件Apk的类文件
            HookHelper.hookAMS();//hookAMS
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

那么这里我们已经实现了第一步

在宿主项目中提前弄一个SubActivity占坑,在启动PluginActivity的时候替换为启动这个SubActivity绕过验证。

接下来我们在看如何在收到AMS创建Activity的通知时替换回PluginActivity。

AMS创建Activity的通知会先发送到ApplicationThread,然后ApplicationThread会通过Handler去执行对应逻辑。

private class ApplicationThread extends IApplicationThread.Stub {
            @Override
        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) {//收到AMS启动Activity事件
            ActivityClientRecord r = new ActivityClientRecord();
            r.intent = intent;//给r赋上要启动的intent
           	...//省略很多r属性初始化
            sendMessage(H.LAUNCH_ACTIVITY, r);//发送r到Handler
        }
    
        private void sendMessage(int what, Object obj) {
        	sendMessage(what, obj, 0, 0, false);
    	}
    
        private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
        Message msg = Message.obtain();
        msg.what = what;
        msg.obj = obj;
        msg.arg1 = arg1;
        msg.arg2 = arg2;
        if (async) {
            msg.setAsynchronous(true);
        }
        mH.sendMessage(msg);//发送到mH
    }
}

private class H extends Handler {
    public static final int LAUNCH_ACTIVITY         = 100;
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case LAUNCH_ACTIVITY: {
                final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
                r.packageInfo = getPackageInfoNoCheck(
                    r.activityInfo.applicationInfo, r.compatInfo);
                handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");//执行启动activity
            } break;
        }
    }
}

既然是通过sendMessage()方式通知Handler去执行对应的方法,那么在调用handleMessage()之前会通过dispatchMessage()分发事件。

public class Handler {
    final Callback mCallback;
    public void dispatchMessage(Message msg) {
            if (msg.callback != null) {
                handleCallback(msg);
            } else {
                if (mCallback != null) {
                    if (mCallback.handleMessage(msg)) {
                        return;
                    }
                }
                handleMessage(msg);
            }
	}
    
    public interface Callback {
        public boolean handleMessage(Message msg);
    }
}

可以发现一个很好的hook点就是mCallback这个接口,可以让我们在handleMessage方法之前将ActivityClientRecord中的SubActivity Intent替换回PluginActivity Intent。

public class HCallback implements Handler.Callback {//实现Callback接口

    public static final int LAUNCH_ACTIVITY = 100;

    @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case LAUNCH_ACTIVITY://启动事件
                Object obj = msg.obj;
                try {
                    Intent intent = (Intent) FieldUtil.getField(obj.getClass(), obj, "intent");//拿到ActivityClientRecord的intent字段
                    Intent targetIntent = intent.getParcelableExtra(HookHelper.TRANSFER_INTENT);//拿到我们要启动PluginActivity的Intent
                    intent.setComponent(targetIntent.getComponent());//替换为启动PluginActivity
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;

        }
        return false;
    }
}

接下来就是我们需要将这个Callback设置给Handler,而刚刚说的Handler是ActivityThread的成员变量mH,ActivityThread实例则可以通过他的静态字段sCurrentActivityThread获取。

public final class ActivityThread {   
	private static volatile ActivityThread sCurrentActivityThread;
    final H mH = new H();
}

然后我们通过反射给mH设置Callback

public class HookHelper {
    ...//省略前面的hookAMS()方法
    
    public static void hookH() throws Exception {
            Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
            Object activityThread = FieldUtil.getField(activityThreadClazz, null, "sCurrentActivityThread");//拿到activityThread
            Object mH = FieldUtil.getField(activityThreadClazz, activityThread, "mH");//拿到mH
            FieldUtil.setField(Handler.class, mH, "mCallback", new HCallback());//给mH设置callback
        }
}

依旧是在Application初始化这段hook逻辑

public class MyApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        try {
            InjectUtil.inject(this, getClassLoader());//加载插件Apk的类文件
            HookHelper.hookAMS();
            HookHelper.hookH();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

到这里完成了我们上面说的第二步,并且成功启动了PluginActivity

在AMS处理完相应验证通知我们ActivityThread创建Activty的时候在替换为PluginActivity。

不过这里肯定会有人问启动是启动了但是没有生命周期,对于AMS那边他只知道我们启动的是SubActivity,那么接下来我们解释生命周期如何处理。

插件Activity生命周期

其实不用做任何处理就已经有生命周期了,那么我们看看是为何。

先回顾下启动的流程

  1. AMS通知ApplicationThread启动Activity
  2. ApplicationThread发送事件到Handler
  3. Handler调用handleLaunchActivity去执行启动逻辑
  4. 然后在handleLaunchActivity方法中创建对应的Activity

对你会发现Activity是在应用进程创建的,AMS是没有该Activity的引用的,那么AMS必须得有一个唯一标识来标识该Activity,然后应用进程存储这个标识和Activity的对应关系,这样当AMS通知应用进程生命周期事件的时候只需要告诉应用进程需要执行该事件的Activity标识就可以了,然后应用进程通过标识找到Activity具体执行即可。

那我们先看下创建Activity的时候是如何存储这个关系的。

private class ApplicationThread extends IApplicationThread.Stub {
            @Override
        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) {//AMS通知启动Activity
            ActivityClientRecord r = new ActivityClientRecord();
            r.token = token;//这个token正是Activity的唯一标示
          	...//省略很多r属性初始化
            sendMessage(H.LAUNCH_ACTIVITY, r);//发送到Handler
        }
}

private class H extends Handler {
    public void handleMessage(Message msg) {
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {//启动Activity事件
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");//执行启动的方法
                } break;
            }
    }
}

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
    Activity a = performLaunchActivity(r, customIntent);//真正执行启动的方法
}

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ComponentName component = r.intent.getComponent();//拿到intent中的组件
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = appContext.getClassLoader();
        activity = mInstrumentation.newActivity(
            cl, component.getClassName(), r.intent);//通过反射创建Activity
    } catch (Exception e) {
        if (!mInstrumentation.onException(activity, e)) {
            throw new RuntimeException(
                "Unable to instantiate activity " + component
                + ": " + e.toString(), e);
        }
    }
    r.activity = activity;//将创建的好的Activity存储在ActivityClientRecord对象中
    mActivities.put(r.token, r);//然后用一个Map存储token和ActivityClientRecord的对应关系
    return activity;
}

final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();//存储对应关系的Map

从代码中可以看出,在创建Activity之后将Activity存储到了ActivityClientRecord对象中,然后用AMS传来的token作为键ActivityClientRecord作为值存储到Map中。

而在ActivityThread中执行生命周期的方法一般命名为perform$事件名Activity(),那么直接看该方法

    public final ActivityClientRecord performResumeActivity(IBinder token,
            boolean clearHide, String reason) {
        ActivityClientRecord r = mActivities.get(token);//通过AMS token拿到ActivityClientRecord
        r.activity.performResume();//执行Resume事件
    }

    private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
                                                        int configChanges, boolean getNonConfigInstance) {
        ActivityClientRecord r = mActivities.get(token);//通过AMS token拿到ActivityClientRecord
        mInstrumentation.callActivityOnDestroy(r.activity);//执行Destroy事件
    }

随便找了两个执行生命周期事件的方法,都是通过AMS的token找到ActivityClientRecord然后拿到里面的Activity执行生命周期方法。

那么在分析下为啥,创建的PluginActivity会有生命呢,因为我们是在Handler将StubActivity替换为PluginActivity,然后在performLaunchActivity方法中,会将PluginActivity创建并且添加到ActivityClientRecord然后用AMS传来的token作为键ActivityClientRecord作为值存储到Map中,那么在接下来的生命周期方法AMS是通过token来通知应用进程执行生命周期方法,而这个token所对应的Activity就是PluginActivity,所以PluginActivity就有了生命。

初始化插件资源

前面我们已经完成了PluginActivity的启动和生命周期事件,但是PluginActivity没法setContentView()这种方式通过id去操作布局,因为凡是通过id去获取资源的方式都是通过Resource去获取的,但是宿主APK并不知道插件APK的存在,所以宿主Resource也没法加载插件APK的资源。

那么这里我们可以给插件APK创建一个Resources,然后插件APK中都通过这个Resource去获取资源。这里看下Resources构造方法

public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
    this(null);
    mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}

有三个参数

  • AssetManager真正加载资源的(根据插件APK路径创建AssetManager加载资源)
  • DisplayMetrics显示配置(直接用宿主的Resources的配置即可)
  • Configuration配置(直接用宿主的Resources的配置即可)

接下来看AssetManager如何创建

public final class AssetManager implements AutoCloseable {
        public AssetManager() {
        synchronized (this) {
            if (DEBUG_REFS) {
                mNumRefs = 0;
                incRefsLocked(this.hashCode());
            }
            init(false);
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }
    
    /**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public final int addAssetPath(String path) {//传入需要加载资源的路径
        return  addAssetPathInternal(path, false);
    }
}

直接通过空参构造方法创建,然后调用addAssetPath()去加载对应路径的资源。

接下来我们在Application中创建插件的Resources,之所以在这里创建也是有原因的,方便插件APK中获取到这个Resources。

public class MyApplication extends Application {

    private Resources pluginResource;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        try {
            InjectUtil.inject(this, getClassLoader());//加载插件Apk的类文件
            HookHelper.hookAMS();
            HookHelper.hookH();
            initPluginResource();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void initPluginResource() throws Exception {
        Class<AssetManager> clazz = AssetManager.class;
        AssetManager assetManager = clazz.newInstance();//创建AssetManager
        Method method = clazz.getMethod("addAssetPath", String.class);//拿到addAssetPath方法
        method.invoke(assetManager, getExternalFilesDir("plugin").listFiles()[0].getAbsolutePath());//调用addAssetPath传入插件APk路径
        pluginResource = new Resources(assetManager, getResources().getDisplayMetrics(), getResources().getConfiguration());//生成插件Resource
    }

    @Override
    public Resources getResources() {
        return pluginResource == null ? super.getResources() : pluginResource;
    }
}

这里我们解释下为啥插件Resources在Application初始化插件APK方便获取,因为插件APK中的四大组件实际都是在宿主APK创建的,那么他们拿到的Application实际上都是宿主的,所以他们只需要通过getApplication().getResources()就可以非常方便的拿到插件Resource。

插件工程

插件工程比较简单,就是一个Activity,不过有点需要注意的是重写了getResources()方法,因为我们需要通过插件Resources才能用id去操作资源文件。

public class PluginActivity extends Activity {//这里需要注意继承的是Activity不是AppCompatActivity,因为AppCompatActivity做了很多检查用它的话还需要多hook几个类,而我们主要是流程和原理的掌握就没有进行适配了。

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_plugin);
    }

    @Override
    public Resources getResources() {//重写getResources()是因为对于activity中通过id获取资源的Resources都是通过该方法获取
        return getApplication() != null && getApplication().getResources() != null ? getApplication().getResources() : super.getResources();//拿到插件Resources
    }
}

测试流程

测试流程这里说明下

  1. 将插件项目打包成APK
  2. 然后通过adb命令adb push <local> <remote>将APK推到内存卡中
  3. 宿主应用加载插件APK,能显示插件Activity布局即为成功
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页