android 传参启动apk_Android插件化初体验
今日科技快讯1月3日,近日华为关于华为官方Twitter账号出现事故的问责决定文件在网上流出,该事件对华为手机的品牌带来了不良影响。其中,对直接责任人予以通报批评,个人职级降1级,月薪下调5000元。对数字营销团队主管予以通报批评,个人职级降级1级,月薪降5000元,年度考核不高于B。冻结个人职级晋升、涨薪,冻结期12个月,自2019年1月2日起计算。作者简介大家好周五好,有没有感觉这一...
今日科技快讯
1月3日,近日华为关于华为官方Twitter账号出现事故的问责决定文件在网上流出,该事件对华为手机的品牌带来了不良影响。其中,对直接责任人予以通报批评,个人职级降1级,月薪下调5000元。对数字营销团队主管予以通报批评,个人职级降级1级,月薪降5000元,年度考核不高于B。冻结个人职级晋升、涨薪,冻结期12个月,自2019年1月2日起计算。
作者简介
大家好周五好,有没有感觉这一周过的很快呢。祝大家周末愉快!
本篇来自 zhuliyuan丶的投稿,和大家分享了他对 android 插件化的一些心得,希望对大家有所帮助!
zhuliyuan丶的博客地址:
https://blog.csdn.net/zly921112
概述
插件化顾名思义,就是将一个 APK 拆成多个,当需要的时候下载对应插件 APK 加载的技术。本文 demo 中除了下载是通过 adb 命令,其他都是模拟真实环境的,这里先理下流程。
将插件工程打包为 APK,然后通过 adb push 命令发送到宿主 APK 目录(模拟下载流程)。
利用 ClassLoader 加载插件 APK 中的类文件。
hook Activity 启动流程中部分类,利用占坑 Activity 帮助 PluginActivity 绕过 AMS验证,在真正启动的时候又替换回 PluginActivity。
创建插件 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 的验证,既然这样我们换一个思路。
在宿主项目中提前弄一个 SubActivity 占坑,在启动 PluginActivity 的时候替换为启动这个 SubActivity 绕过验证。
在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 gDefault = new Singleton() {protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");//获取amsif (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 IActivityManagerSingleton =new Singleton() {@Overrideprotected IActivityManager create() {final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);//拿到amsfinal 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 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 pendingResults, List 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的生命周期
其实不用做任何处理就已经有生命周期了,那么我们看看是为何。
先回顾下启动的流程
AMS 通知 ApplicationThread 启动 Activity
ApplicationThread 发送事件到 Handler
Handler 调用 handleLaunchActivity 去执行启动逻辑
然后在 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 pendingResults, List 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 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 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
}@Overridepublic 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
}
}
总结
测试流程这里说明下
将插件项目打包成 APK
然后通过 adb 命令adb push 将 APK 推到内存卡中
宿主应用加载插件 APK,能显示插件 Activity 布局即为成功
更多推荐
所有评论(0)