一、说明
Android插件化相关文章是根据包建强大佬的《Android插件开发指南》书籍学习而来,只是后续的代码部分均是根据Android P源码所实现。
二、什么是插件化
在真正去学习Android插件化编程之前我们需要知道一下什么是插件化,所谓插件化简单来说就是Android应用程序在运行时能够动态的去加载那些不属于当前应用程序的外部dex文件,而这些dex文件大多数都是存在于从服务器下载下来的lib、zip以及apk等文件中。
三、插件化的好处
既然通过插件的方式我们能够加载到那些不属于当前应用程序的java代码,因此它能够带来的好处也就不言而喻了。比如
1、我们的业务需求能够不跟随版本的发布而发布,增加了业务需求迭代的频率;
2、部分业务需求通过插件的方式下发能够有效的减少包体积的大小;
3、能够在不发版的情况下快速修复线上Bug;
等等。
四、插件化的实现方式
在Android中通过从服务器下载的lib、zip以及apk等包涵dex的文件生成对应ClassLoader,然后通过该ClassLoader加载插件中的class类。
1、ClassLoader选择
在Android中分别提供了PathClassLoader和DexClassLoader两种类加载器,从源码的解释以及各个博客来看,PathClassLoader只用于加载data/app目录下的apk,它是Android默认的类加载器;而DexClassLoader则可用于加载外部存储目录中的lib、apk、zip等包含dex文件的文件;
但是从实际的源码角度以及实验结果来看,PathClassLoader和DexClassLoader均可加载外部存储目录下的lib、apk、zip等文件。具体可参考这篇博客
这里我们根据官方的推荐使用类加载器DexClassLoader来实现最终的插件化。
五、加载插件中的Activity源码实现
在正式撸代码之前我们需要知道的是,在插件里面不论是我们经常使用到的Activity、broadcast、Service、ContentProvider还是那些普通的Java类,对于宿主而言它们都是普通的java类,并没有任何的特殊性可言;因此如果想要插件中的四大组件拥有和正常Android应用四大组件同等的生命周期,这里我们就需要采用一种欺骗AMS方式来达到目的。既然需要去欺骗AMS,那么对于Activity的启动流程我们就必须有所了解才行。有需要的直戳Android9.0 ActivityManagerService源码之启动Activity开篇和Android中Activity启动流程后篇
1、动态代理
由于动态代理在后续的出现频率较高,所以我们简单的去了解一下动态代理的实现方式
1.1 什么是动态代理
简单来说就是在代码运行时,为某一个接口动态的生成实现类,即代理对象。
1.2 插件化中运用场景
我们可以通过动态代理动态的为framework层的某个系统接口生成代理对象,然后再通过反射将framework层生成的系统接口的对象替换成动态生成的代理对象,这样的话我们就能够对framework层的方法实现进行入侵了。当然上述所有的操作都只针对当前应用进程。
比如我们通过动态代理的方式为IActivityManager接口生成代理对象a,然后将系统中的ActivityManager.getService()方法中需要获取的对象替换成对象a,这样我们就能够知道应用在什么时候调用了比如startActivity等方法。具体实现后续会有对应的源码。
1.3 动态代理实现方式
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{interface.class}, new xxInvocationHandler());
public class xxInvocationHandler implements InvocationHandler {
//被代理的原始对象
private Object mBase;
public xxInvocationHandler(Object base) {
mBase = base;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
//对想要入侵的方式实现入侵
//最后再通过反射的方式调用到被代理的对象实现
return method.invoke(mBase, objects);
}
}
2、加载插件中的Activity
在有了Activity启动流程、动态代理等知识之后,Activity的插件化具体实现我们就可以正式的开始了。当然这个实现过程我们肯定会遇到各种各样的问题点;不要怂,我们对着framework层源码一个一个的去解决。
2.1 插件apk预备
为了实现的方便,我们自己将对应的插件apk放置在当前的assets目录下,然后在代码的运行过程将apk复制到data/user/0/应用包名/xx目录下。基本代码如下:
/**
*
* @param context
* @param pluginName 插件名
*/
public static void copyApk(Context context, String pluginName) {
DePluginSP sp = DePluginSP.getInstance(context);
//获取插件apk保存路径
String filePath = sp.getString(Constants.COPY_FILE_PATH, "");
if (TextUtils.isEmpty(filePath)) {
//如果插件apk保存路径为空,说明没有copy插件apk到对应目录成功
File saveApkFile = context.getFileStreamPath(pluginName);
if (null == saveApkFile) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
saveApkFile = context.getDataDir();
filePath = saveApkFile.getAbsolutePath() + pluginName;
} else {
filePath = "data/user/0/" + context.getPackageName() + "/" + pluginName;
}
} else {
filePath = saveApkFile.getAbsolutePath();
}
boolean result = extractAssets(context, filePath, pluginName);
if (result) {
sp.setString(Constants.COPY_FILE_PATH, filePath);
}
Log.i(TAG, "copy " + result);
} else {
//如果插件apk保存路径不为空,并且本地存在了apk则不在进行二次copy,否则可能已经被删除则重新复制一份到对应到目录下
//当然在实际到开发中这里到情况会复杂的多,比如与服务器插件版本进行对比判断是否需要重新下载等
File file = new File(filePath);
if (file.exists()) {
Log.i(TAG, "had copy apk before,so no need copy again");
} else {
Log.i(TAG, "althogh save apk file path success,but file not exists");
extractAssets(context, filePath, pluginName);
}
}
}
上述方法就是判断是否需要复制插件apk到对应的目录下,接下来就是copy部分了。因为代码量不到逻辑也很简单,就直接看代码了。
public static boolean extractAssets(Context context, String filePath, String pluginName) {
AssetManager assetManager = context.getAssets();
FileOutputStream fileOutputStream = null;
InputStream inputStream = null;
try {
Log.i(TAG, "save apk file path is " + filePath);
fileOutputStream = new FileOutputStream(filePath);
//获取assets目录下的插件apk输入流
inputStream = assetManager.open(pluginName);
byte[] bytes = new byte[1024];
int length = -1;
//将apk文件复制到对应到文件目录下
while ((length = inputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, length);
}
fileOutputStream.flush();
return true;
} catch (Exception e) {
Log.e(TAG, "copy file failed " + e.getMessage());
} finally {
try {
if (null != inputStream) {
inputStream.close();
}
if (null != fileOutputStream) {
fileOutputStream.close();
}
} catch (Exception e) {
Log.i(TAG, "extractAssets: " + e.getMessage());
}
}
return false;
}
2.2 ActivityManager的Hook
这一块儿的hook,简单来说就是通过动态代理的方式将ActivityManager.getService所获取到的Binder代理对象进行替换,那么我们就能够对诸如start Activity等方法进行入侵了。相关代码实现如下。RefInvoke.java类可到github上查看。
public static void hookAMN() {
try {
//通过反射获取到ActivityManager的class对象
Class<?> mActivityManagerCls = RefInvoke.getClass("android.app.ActivityManager");
//首先通过反射获取到ActivityManager类中的单例对象IActivityManagerSingleton
//然后通过反射获取到对象对象IActivityManagerSingleton的值
Object mIActivityManagerSingletonObj = RefInvoke.getStaticFieldValue(RefInvoke.getField(mActivityManagerCls, "IActivityManagerSingleton"), mActivityManagerCls);
//获取到ActivityManager与AMS的Binder通信接口IActivityManager的class对象,用于后续生成对应的代理对象
Class<?> mIActivityManagerCls = RefInvoke.getClass("android.app.IActivityManager");
if (null != mIActivityManagerSingletonObj) {
//因为上述的单例对象是Singleton实现类,所以通过反射首先获取到该类中的mInstance属性
Field mInstanceField = RefInvoke.getField("android.util.Singleton", "mInstance");
//然后通过反射获取到上述单例对象中的mInstance属性对应的值
Object mInstance = RefInvoke.getFieldValue(mInstanceField, mIActivityManagerSingletonObj);
//根据上述提供的接口以及当前的ClassLoader生成代理对象
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{mIActivityManagerCls}, new AMSHookHelperInvocationHandler(mInstance));
//将ActivityManager.getService获取到的单例对象替换成代理对象
RefInvoke.setFieldValue(mInstanceField, mIActivityManagerSingletonObj, proxy);
} else {
Log.i(TAG, "IActivityManagerSingleton not exists");
}
} catch (Exception e) {
Log.i(TAG, "hook ATM failed " + e);
}
}
接下来就是实现InvocationHandler接口对startActivity方法进行入侵了,相关代码如下。
public class AMSHookHelperInvocationHandler implements InvocationHandler {
private static final String TAG = Constants.TAG + "AMSHookHandler";
//被代理对象
private Object mBase;
public AMSHookHelperInvocationHandler(Object base) {
mBase = base;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
//劫持startActivity方法对上层应用真正要启动的Activity进行替换
if (TextUtils.equals(method.getName(), "startActivity")) {
Log.i(TAG, "replace start up activity");
int index = -1;
//获取上层应用传递过来的Intent对象
for (int i = 0; i < objects.length; i++) {
if (objects[i] instanceof Intent) {
index = i;
break;
}
}
if (-1 == index) {
Log.i(TAG, "not found intent in params");
return method.invoke(mBase, objects);
}
//这就是上层应用所需要启动的插件Activity对应的Intent了
Intent realIntent = (Intent) objects[index];
//根据宿主中预先声明的Activity生成对应的Intent对象用于替换上层应用传递过来的插件Activity相关的Intent对象,以达到欺骗AMS的目的
Intent replacedStartUpIntent = realIntent.getParcelableExtra(Constants.REPLACED_START_UP_INTENT);
if (null != replacedStartUpIntent) {
Log.i(TAG, "origin intent is " + realIntent);
realIntent.putExtra(Constants.REPLACED_START_UP_INTENT,"");
replacedStartUpIntent.putExtra(Constants.START_UP_INTENT, realIntent);
objects[index] = replacedStartUpIntent;
Log.i(TAG, "replaced start up intent is " + replacedStartUpIntent);
} else {
Log.i(TAG, "replaced intent activity is null");
}
}
//继续通过Binder的方式调用到AMS
return method.invoke(mBase, objects);
}
}
有了上述对ActivityManager的hook过程,接下来我们就可以直接在应用中启动插件Activity了,使用方式如下。
public void click(View view) {
int id = view.getId();
switch (id) {
case R.id.plugin:
Intent intent = new Intent();
ComponentName componentName = new ComponentName(getPackageName(), "com.xx.xx.pluginActivity");
intent.setComponent(componentName);
intent.putExtra(Constants.REPLACED_START_UP_INTENT, createStartUpIntent());
startActivity(intent);
break;
}
}
//使用宿主中预先声明好的Activity构造Intent对象用于欺骗AMS,后续统称为中转页面
private Intent createStartUpIntent() {
Intent startUpIntent = new Intent();
ComponentName componentName = new ComponentName(DePluginApplication.getContext(), StandardStubActivity.class.getName());
startUpIntent.setComponent(componentName);
startUpIntent.putExtra(Constants.DEX_PATH, DePluginSP.getInstance(DePluginApplication.getContext()).getString(Constants.COPY_FILE_PATH, ""));
return startUpIntent;
}
虽然我们能够在宿主中直接去启动插件中的Activity并且不会报出ActivityNotFound异常了,但是最后会发现启动的Activity并不是插件中的Activity,而是我们的中转页面StandardStubActivity。因此为了能够实现最终启动Activity是插件中的Activity,我们还需要对ActivityThread中中的各个对象进行hook。
2.3 ActivityThread中的hook
这一块儿所涉及的流程就稍微复杂一点了,因此代码量也稍微多一点。所以,建议大家有时间多可以去瞅瞅Activity启动流程源码分析相关的文章。
2.3.1 mH的hook
对于ActivityThread中mH属性,如果采用生成Handler对象直接通过反射的方式去替换,最终系统会无情的给你抛出一个hook H failed java.lang.IllegalArgumentException;这是因为虽然mH属性对应的类继承了Handler对象,但是它的实际引用类型却是H。所以此路肯定是行不通的。这个时候我们不妨去看看Handler.java类中最终msg分发的dispatchMessage函数源码实现,如下:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
//重点我们看这里,如果当前Handler中的CallBack实例对象为空才会走到handleMessage方法
//因此我们可以为mH这个继承了Handler的实例对象构造一个实现了CallBack的接口实例对象,那说干就干
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
所以我们首先构造一个实现了Handler中CallBack接口的实例对象,并通过反射的方式将这个实例对象赋值给mH对象。代码如下:
public static void hookH() {
try {
//通过反射获取到ActivityThread实例对象
Object sCurrentActivityThread = RefInvoke.getStaticFieldValue(RefInvoke.getField("android.app.ActivityThread", "sCurrentActivityThread"), RefInvoke.getClass("android.app.ActivityThread"));
//获取到ActivityThread中的mH实例对象
Field mHField = RefInvoke.getField(sCurrentActivityThread.getClass(), "mH");
Handler mH = (Handler) RefInvoke.getFieldValue(mHField, sCurrentActivityThread);
//首先通过反射获取到Handler中的mCallBack属性
//通过反射的方式将mH实例对象中的mCallBack属性赋值为ActivityThreadHandler的实例对象
RefInvoke.setFieldValue(RefInvoke.getField(Handler.class, "mCallback"), mH, new ActivityThreadHandler(mH));
Log.i(TAG, "hook H complete");
} catch (Exception e) {
Log.i(TAG, "hook H failed " + e);
}
}
接着就是在CallBack中的handleMessage方法中对Activity的启动进行拦截了,然后将需要加载的Activity替换成插件中的Activity,并将加载Activity的ClassLoader对象替换成以插件apk生成的ClassLoader对象,最后在ActivityThread中实际所加载的Activity就是插件中的Activity了。
public boolean handleMessage(@NonNull Message message) {
int what = message.what;
switch (what) {
//这里为什么是159可以到ActivityThread中找到答案
case 159:
//首先获取从AMS中传递过来的ClientTransaction对象
Object object = message.obj;
try {
//这里的CallBack对象就是实现Activity生命周期的各个对象了
List<Object> mActivityCallbacks = RefInvoke.on(object, "getCallbacks").invoke();
//获取开始执行Activity onCreate方法的实例对象,并将其中的Intent对象中的ComponentName对象替换成插件Activity对应的ComponentName对象
Class<?> mLaunchActivityItemCls = RefInvoke.getClass("android.app.servertransaction.LaunchActivityItem");
for (Object obj : mActivityCallbacks) {
if (mLaunchActivityItemCls.isInstance(obj)) {
Intent intent = getIntent(mLaunchActivityItemCls, obj);
if (null == intent) {
break;
}
//只对需要实现插件化的Activity进行拦截,防止出现误拦截的情况
String path = intent.getStringExtra(Constants.DEX_PATH);
if (TextUtils.isEmpty(path)) {
Log.i(TAG, "dex path is empty,so do need replace class loader");
break;
}
//替换成加载插件类的ClassLoader
replaceClassloader(mLaunchActivityItemCls, obj, path);
//将实际需要加载的Activity替换成插件中的Activity
replace(intent);
break;
}
}
} catch (Exception e) {
Log.e(TAG, "getActivityToken failed " + e.getMessage());
}
break;
default:
}
mBase.handleMessage(message);
return true;
}
2.3.2 ClassLoader的hook
对于加载Activity的ClassLoader替换则稍显复杂了,因此在代码实现之前我们还是简单去看一下源码,了解一下如何将加载Activity的ClassLoader替换成加载插件中Activity的ClassLoader。
源码解析
ActivityThread中对Activity初始化是在performLaunchActivity中完成,部分源码如下:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
//生成LoadedApk对象
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
ComponentName component = r.intent.getComponent();
if (component == null) {
component = r.intent.resolveActivity(
mInitialApplication.getPackageManager());
r.intent.setComponent(component);
}
if (r.activityInfo.targetActivity != null) {
component = new ComponentName(r.activityInfo.packageName,
r.activityInfo.targetActivity);
}
//为当前启动的Activity生成对应的Context对象
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
//获取ClassLoader对象以加载需要启动的Activity类
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
.........
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
............
return activity;
}
在该方法中首先会根据要启动Activity中所携带的ApplicationInfo等对象生成LoadedApk对象,然后通过LoadedApk中携带的ClassLoader属性为当前需要启动的Activity生成对对应的Context对象,并通过该ClassLoader加载需要启动的Activity类。其中Context对象创建是在ContextImpl的createActivityContext方法中完成,部分源码如下:
static ContextImpl createActivityContext(ActivityThread mainThread,
LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
Configuration overrideConfiguration) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
String[] splitDirs = packageInfo.getSplitResDirs();
//获取LoadedApk中的ClassLoader对象,并根据该ClassLoader创建对应的Context对象
ClassLoader classLoader = packageInfo.getClassLoader();
........
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
activityToken, null, 0, classLoader);
.......
final ResourcesManager resourcesManager = ResourcesManager.getInstance(); 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;
}
既然最终加载Activity类的ClassLoader是从LoadedApk对象来的,所以我们只需要将上述getPackageInfo方法所得来的LoadedApk对象中的ClassLoader对象替换成通过插件apk生成的插件就行了。但是有个问题就是这里的getPackageInfo方法我们并不能hook住,因此并不能把握住该方法的调用时机,所以通过getPackageInfo方法生成的LoadedApk我们并不能动态的去替换掉;
因此这里我们所采用的是直接通过getPackageInfo方法创建一个属于我们自己的LoadedApk对象,至于我们为什么可以这样做,还是需要去看一下源码才知道。performLaunchActivity中调用的getPackageInfo方法最终会调用到该方法的重载方法中,实现如下:
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
ref = null;
//最终会进入到这个if中
//可以看到的是首先会去mPackages这个ArrayMap中查找该Activity所属的包名是否存在LoadedApk的缓存,如果存在缓存则直接使用
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
//如果不存在缓存则重新生成LoadedApk对象,并添加到mPackages
LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
: "Loading resource-only package ") + aInfo.packageName
+ " (in " + (mBoundApplication != null
? mBoundApplication.processName : null)
+ ")");
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
if (mSystemThread && "android".equals(aInfo.packageName)) {
packageInfo.installSystemApplicationInfo(aInfo,
getSystemContext().mPackageInfo.getClassLoader());
}
if (differentUser) {
// Caching not supported across users
} else if (includeCode) {
mPackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
}
}
return packageInfo;
}
}
根据上述的源码解析,如何生成对应LoadedApk对象以及ClassLoader对象我们的思路就很明确了。
(1)首先我们通过反射的方式以插件apk生成对应的ApplicationInfo对象以及CompatibilityInfo;
(2)接着调用如下方法生成对应的LoadedApk对象并将ActivityInfo所对应的ApplicationInfo中的PackageName设置为插件的ApplicatioInfo,这样Activity初始化的时候就能够直接获取缓存中的我们所生成的LoadedApk对象了;
(3)最后再将以插件Apk生成的ClassLoader再以反射的方式赋值给LoadedApk对象中的成员变量,到这里整个Activity的插件化也就完成了一大半了。
public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo,
int flags) {
.....
}
至于如何创建插件apk对应的ApplicationInfo对象,这里我们提供了两种方式,第一种是通过反射调用到类PackageParser中的generateApplicationInfo方法;第二种则稍显简单,直接获取到宿主应用到PackageManager对象,然后调用到PackageManager中的getPackageArchiveInfo方法。篇幅有限,这里介绍第二种,有需要的可到github上了解一下第一种方法。
ApplicationInfo对象创建
private static ApplicationInfo getApplicationInfoByPackageArchiveInfo(String pluginPath) {
//根据插件apk路径首先生成对应的PackageInfo对象
PackageManager packageManager = DePluginApplication.getContext().getPackageManager();
if (null == packageManager) {
Log.i(TAG, "get PackageManager failed");
return null;
}
PackageInfo packageInfo = packageManager.getPackageArchiveInfo(pluginPath, 0);
if (null == packageInfo) {
Log.i(TAG, "get packageInfo failed");
return null;
}
//返回PackageInfo中的ApplicationInfo对象
return packageInfo.applicationInfo;
}
public static ApplicationInfo generateApplicationInfo(String pluginPath) {
try {
ApplicationInfo applicationInfo = getApplicationInfoByPackageArchiveInfo(pluginPath);
if (null == applicationInfo) {
Log.i(TAG, "get applicationInfo failed");
return null;
}
//设置资源加载路径(后续会有对应的文章)
applicationInfo.sourceDir = pluginPath;
applicationInfo.publicSourceDir = pluginPath;
//设置隶属于哪一个uid
applicationInfo.uid = Process.myUid();
return applicationInfo;
} catch (Exception e) {
Log.i(TAG, "generateApplicationzInfo failed " + e.getMessage());
}
return null;
}
通过上述的getPackageInfo方法生成对应的LoadedApk对象在有了ApplicationInfo对象之后,那么就差一个CompatibilityInfo对象,对于该对象我们可以直接在CompatibilityInfo.java源码中找到一个默认的实例对象,即DEFAULT_COMPATIBILITY_INFO。因此我们可以直接通过反射的方式拿到该对象。
生成LoadedApk对象并添加到缓存
有了上面的准备,那么我们就只欠东风了。getPackageInfo方法源码实现如下:
private void replaceClassloader(Class<?> mLaunchActivityItemCls, Object obj, String path) throws Exception {
//获取到ActivityThread对象
Object sCurrentActivityThread = RefInvoke.getStaticFieldValue(RefInvoke.getField("android.app.ActivityThread", "sCurrentActivityThread"), RefInvoke.getClass("android.app.ActivityThread"));
//获取缓存LoadedApk对象的map集合;
//当然这里也可以省略,因为在调用getPackageInfo方法的结尾已经自动给我们做了缓存
Field mPackagesField = RefInvoke.getField(sCurrentActivityThread.getClass(), "mPackages");
if (null == mPackagesField) {
Log.i(TAG, "get mPackages field failed");
return;
}
ArrayMap mPackages = (ArrayMap) mPackagesField.get(sCurrentActivityThread);
if (null == mPackages) {
Log.i(TAG, "can not get mPackages");
return;
}
//获取插件apk对应的ApplicationInfo对象
ApplicationInfo applicationInfo = Utils.generateApplicationInfo(DePluginSP.getInstance(DePluginApplication.getContext()).getString(Constants.COPY_FILE_PATH, ""));
if (null != applicationInfo) {
//获取CompatibilityInfo类中的默认对象
Object defaultCompatibilityInfo = RefInvoke.getStaticFieldValue(RefInvoke.getField("android.content.res.CompatibilityInfo", "DEFAULT_COMPATIBILITY_INFO"), RefInvoke.getClass("android.content.res.CompatibilityInfo"));
//调用ActivityThread中的getPackageInfo方法生成对应的LoadedApk对象
Object loadedApk = RefInvoke.on(sCurrentActivityThread, "getPackageInfo", ApplicationInfo.class, RefInvoke.getClass("android.content.res.CompatibilityInfo"), int.class).invoke(applicationInfo, defaultCompatibilityInfo, Context.CONTEXT_INCLUDE_CODE);
String pluginPkgName = applicationInfo.packageName;
if (!TextUtils.isEmpty(pluginPkgName)) {
Log.i(TAG, "plugin pkg name is " + pluginPkgName);
//替换ActivityInfo中的包名为插件的包名,用于在启动过程中能够直接获取到我们创建的LoadedApk对象
replacePkgName(mLaunchActivityItemCls, obj, pluginPkgName);
//为LoadedApk对象生成插件ClassLoader对象用于加载我们需要启动的Activity类
setClassloader(loadedApk, path);
//因为我们在调用getPackageInfo方法的时候有自动为我们缓存LoadedApk对象,因此该步骤可省略
//mPackages.put(pluginPkgName, new WeakReference<>(loadedApk));
} else {
Log.i(TAG, "get plugin pkg name failed");
}
} else {
Log.i(TAG, "can not get application info");
}
}
//替换ActivityInfo中的包名为插件apk包名
private void replacePkgName(Class<?> mLaunchActivityItemCls, Object obj, String pkgName) throws Exception {
ActivityInfo activityInfo = (ActivityInfo) RefInvoke.getFieldValue(RefInvoke.getField(mLaunchActivityItemCls, "mInfo"), obj);
activityInfo.applicationInfo.packageName = pkgName;
}
//为LoadedApk生成对应的ClassLoader对象用于加载我们需要启动的Activity类
private void setClassloader(Object loadedApk, String path) throws Exception {
Log.i(TAG, "dex path is " + path);
DexClassLoader dexClassLoader = DeHostDexClassloader.getInstance().getDexClassLoader(DePluginApplication.getContext(), path);
RefInvoke.setFieldValue(RefInvoke.getField(loadedApk.getClass(), "mClassLoader"), loadedApk, dexClassLoader);
}
最后再将AMS传递过来的Intent中的ComponentName对象替换为我们需要启动Activity对应Intent的Component对象,代码如下:
private boolean replace(Intent intent) throws Exception {
//获取我们在ActivityManger中的缓存在Intent中的Intent对象
Intent realIntent = intent.getParcelableExtra(Constants.START_UP_INTENT);
if (null == realIntent) {
return false;
}
Log.i(TAG, "origin start up Intent is " + realIntent);
//将需要启动Intent中的Component对象替换成插件Activity所生成的Component对象
ComponentName componentName = realIntent.getComponent();
if (null != componentName) {
intent.setComponent(componentName);
return true;
} else {
Log.i(TAG, "real start up intent has not component,please set");
}
return false;
}
代码写到这里,Activity的整个插件化流程基本上也算是完成了,但是当你点击hook+启动插件Activity按钮之后,系统就会给你无情的抛出一个 Unable to instantiate application android.app.Application: java.lang.IllegalStateException: Unable to get package info异常。到代码中搜索一下,该异常出现在类LoadedApk.java的initializeJavaContextClassLoader函数中,源码如下:
private void initializeJavaContextClassLoader() {
IPackageManager pm = ActivityThread.getPackageManager();
android.content.pm.PackageInfo pi;
try {
pi = pm.getPackageInfo(mPackageName, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
UserHandle.myUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
if (pi == null) {
throw new IllegalStateException("Unable to get package info for "
+ mPackageName + "; is package not installed?");
}
......
}
有了报错原单支撑,最后的处理方式也就很简单了,就是通过动态代理的方式生成IPackageManager的代理对象,并拦截方法getPackageInfo的调用流程,最后直接返回一个创建好的PackageInfo就OK了。具体可参考github,现在就可以非常愉快的启动插件中的Activity了。
3、最后
以上就是在Android 9上实现Activity插件化编程的基本步骤以及部分代码了,有需要完整 demo可到github上下载。
当然除了上诉通过插件apk生成对应ClassLoader的方式去加载插件中的Activity之外,我们还可以将插件apk通过反射的方式merge到类DexPathList.java的dexElements数组中,并将插件apk的资源添加到当前应用到AssetsManager中。因为这一块儿还存在资源ID冲突的问题,所有后续有文章专门进行介绍。
除了Activity的插件化之后,后续还会有Service插件化、BroadcastReceiver插件化以及ContentProvider插件化的相关文章以及demo产出。