众所周知,在Android中,四大组件是需要在AndroidManifest.xml文件中注册之后才能调用的,但是插件APP可能并没有安装,只是放在终端的某个存储路径上,故系统会找不到插件里面用到的四大组件;那么如果宿主APP需要调用插件APP的四大组件呢,比如,宿主APP要启动插件APP中的某个Activity,需要怎么操作呢?
本篇文章只讲述两个问题:
- 宿主如何启动插件中的Activity;
- 插件APP不安装的情况下,插件APP中的资源并没有解压到系统data目录,需要怎么去调用插件中的资源(String,drawable等);
用到的技术:
- Hook Activity;
- Java动态代理;
- Java 反射;
- 熟悉AMS启动Activity相关流程,可参考;
- 熟悉AssetManager加载app资源相关流程;
不熟悉相关技术的,可以先行做个了解,明白基础的用法即可。
宿主APP中启动插件APP中的Activity
要从宿主中启动插件的Activity,但是插件并没有安装,如果直接启动,Android系统会抛出没有在AndroidManifest.xml中注册该Activity的错误,要解决这个问题,这里就需要用到Hook技术,绕过AMS对Activity的检查。
HOOK简介
在Java中,正常情况下,对象A调用对象B,对象B处理完数据之后,会将结果返回给A,如下图:
加入HOOK技术后,就会变成如下形式:
HOOK可以是方法,也可以是对象,它像一个钩子一样挂在对象B上,当A调用B的时候,会先经过HOOK,那么HOOK就可以做一些小动作,起到“欺上瞒下”的作用。对象B就是我们常说的HOOK点,为了保证HOOK的稳定性,HOOK点一般选择容易找到,并且不易变化的对象,如静态变量和单例。
那么按照HOOK的思路,首先我们在宿主APP中创建一个代理ProxyActivity继承自Activity,并且在宿主的清单文件(AndroidManifest.xml)中注册,当启动插件Activity的时候,在AMS检测之前,先找到一个HOOK点,将目标Activity替换为ProxyActivity(也就是假装启动ProxyActivity),等检测完之后,再找一个HOOK点,将目标Activity替换回来,这样就成功绕过了系统的检测。
要寻找到合适的HOOK点,需要熟悉AMS启动Activity的流程:
通过上图,我们可以确定HOOK的大概位置:
- 在进入AMS之前,找HOOK点,将目标Activity(插件中的Activity)替换为ProxyActivity;
- 在从AMS出来之后,找HOOK点,将ProxyActivity替换为目标Activity;
启动Activity的过程,在这里不做详述,这里只说明具体的HOOK点(注意此HOOK点不是唯一的,可以根据自己对Android源码的理解,自己选择认为合适的HOOK点),这里也可以参照滴滴的VirtualAPK中的PluginManager中的操作,选择HOOK点:
//android/app/Instrumentation.java
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, String target,
Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
if (mActivityMonitors != null) {
synchronized (mSync) {
final int N = mActivityMonitors.size();
for (int i=0; i<N; i++) {
final ActivityMonitor am = mActivityMonitors.get(i);
ActivityResult result = null;
if (am.ignoreMatchingSpecificIntents()) {
result = am.onStartActivity(intent);
}
if (result != null) {
am.mHits++;
return result;
} else if (am.match(who, null, intent)) {
am.mHits++;
if (am.isBlocking()) {
return requestCode >= 0 ? am.getResult() : null;
}
break;
}
}
}
}
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
// 这儿就是我们的 Hook 点,替换传入 startActivity 方法中的 intent 参数
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target, requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
找到了HOOK点,具体怎么操作呢?
这儿就需要用到前面提到的动态代理,需要替换的就是ActivityManager.getService()这个对象;简单的说,就是在此处代码执行之前,将ActivityManager.getService()这个对象替换我们自己代码中提供的代理对象,这样在执行startActivity方法的时候,其实执行的就是在我们自己在代码中做过一些小动作的startActivity方法,把startActivity方法中的第三个参数intent,原本用于启动目标Activity的Intent替换为启动ProxyActivity的Intent。
这儿这么做的目的就是因为ProxyActivity是在宿主APP中的,并且是在AndroidManifest.xml中声明的,进入AMS之后,就不会抛出Activity没有声明的错误。
接着上面的思路,要替换ActivityManager.getService()这个对象,先看下它返回的是啥?
//android/app/ActivityManager.java
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);
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};
ActivityManager.getService()返回的是一个IActivityManager对象,并且可以看出这儿的IActivityManager是从Singleton的get()方法中获取的,看一下Singleton的get方法:
//android/util/Singleton.java
package android.util;
/**
* Singleton helper class for lazily initialization.
*
* Modeled after frameworks/base/include/utils/Singleton.h
*
* @hide
*/
public abstract class Singleton<T> {
private T mInstance;
protected abstract T create();
//可以看到get方法其实返回的就是mInstance
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
屡清楚上面这个流程之后,需要做以下两点去执行HOOK的动作:
- 通过动态代理替换掉startActivity方法中的intent参数;
- 通过反射替换到调用startActivity方法的IActivityManager对象;
Talk is easy,show me the code:
public static final String TARGET_INTENT = "target_intent";
public static void hookAMS(){
try {
Class<?> iActivityManagerClass = Class.forName("android.app.IActivityManager");
Class<?> activityManagerCls = Class.forName("android.app.ActivityManager");
Field singletonField = activityManagerCls.getDeclaredField("IActivityManagerSingleton");
if(!singletonField.isAccessible()){
singletonField.setAccessible(true);
}
//IActivityManagerSingleton在ActivityManager中是静态的,可通过反射直接获取
Object singleObj = singletonField.get(null);
//Singletone中的get()方法返回值就是IActivityManager类型的对象
Class<?> singleClz = Class.forName("android.util.Singleton");
Field instanceField = singleClz.getDeclaredField("mInstance");
if(!instanceField.isAccessible())
instanceField.setAccessible(true);
final Object mInstance = instanceField.get(singleObj);
//也可通过Singleton类的get()获取,get方法返回的是mInstance,mInstance就是IActivityManager对象
//Method getMethod = singleClz.getDeclaredMethod("get");
//final Object mInstance = getMethod.invoke(singleObj);
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<?>[]{iActivityManagerClass}, new InvocationHandler() {
Intent targetIntent = null;
@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].getClass().getName().contains("intent")){
if(args[i] instanceof Intent){
index = i;
break;
}
}
//目标intent,也就是启动插件Activity的Intent
targetIntent = (Intent) args[index];
Intent proxyIntent = new Intent();
//保存目标intent,绕过AMS之后,再次hook的时候需要用到
proxyIntent.putExtra(TARGET_INTENT,targetIntent);
Log.d("Rayman", "invoke: extra intent = "+targetIntent);
//此处Intnet应该是用于启动代理Activity
proxyIntent.setClassName("com.enjoy.myplugin",
"com.enjoy.myplugin.ProxyActivity");
ComponentName cpn = targetIntent.getComponent();
if(cpn != null){
String clsName = cpn.getClassName();
//此处做判断,因为可能会启动很多个不同的Activity,需要对不同的Activity做区分,有可能是启动宿主本身的Activity,就不需要执行替换的动作
if(!TextUtils.isEmpty(clsName) && clsName.equals("com.enjoy.plugin.MainActivity")){
//替换目标intent
args[index] = proxyIntent;
}
}
//到此处,启动插件Activity的intent就被我们替换了
}
//此处要反射调用IActivityManager的startActivity方法,需要一个IActivityManager对象
return method.invoke(mInstance,args);
}
});
//利用反射,替换调用startActivity的IActivityManager对象
instanceField.set(singleObj,proxy);
} catch (Exception e) {
e.printStackTrace();
}
}
那么到这儿,进入AMS之前的HOOK就结束了,接下来要做的就是从AMS出来之后,再次HOOK,把启动目标Activity的intent再替换回来,用以启动目标Activity。
根据Activity启动流程,参考前面的流程图可知,在从AMS出来之后,进入ActivityThread,会调用Hander的handleMessage,我们看相关源码:(这儿查找源码目的就是再找出一个启动Activity之前的Intent,将之前已经被替换的Intent再替换回来)
//android/app/ActivityThread.java
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
...
case EXECUTE_TRANSACTION:
final ClientTransaction transaction = (ClientTransaction) msg.obj;
mTransactionExecutor.execute(transaction);
if (isSystem()) {
// Client transactions inside system process are recycled on the client side
// instead of ClientLifecycleManager to avoid being cleared before this // message is handled.
transaction.recycle();
}
// TODO(lifecycler): Recycle locally scheduled transactions.
break;
...
}
}
EXECUTE_TRANSACTION的值是159。这里的ClientTransaction,也就是msg.obj,来看一下TransactionExecutor的execute方法。
//android/app/servertransaction/TransactionExecutor.java
public void execute(ClientTransaction transaction) {
......
executeCallbacks(transaction);
......
}
/** Cycle through all states requested by callbacks and execute them at proper times. */
@VisibleForTesting
public void executeCallbacks(ClientTransaction transaction) {
//获取transaction的Callbacks
final List<ClientTransactionItem> callbacks = transaction.getCallbacks();
......
final int size = callbacks.size();
for (int i = 0; i < size; ++i) {
final ClientTransactionItem item = callbacks.get(i);
if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item);
final int postExecutionState = item.getPostExecutionState();
final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
item.getPostExecutionState());
if (closestPreExecutionState != UNDEFINED) {
cycleToPath(r, closestPreExecutionState, transaction);
}
//此处会调用ClientTransactionItem的execute
item.execute(mTransactionHandler, token, mPendingActions);
item.postExecute(mTransactionHandler, token, mPendingActions);
......
}
}
上面ClientTransaction的getCallbacks()方法如下:
//android/app/servertransaction/ClientTransaction.java
/** A list of individual callbacks to a client. */
@UnsupportedAppUsage
private List<ClientTransactionItem> mActivityCallbacks;
/**
* Add a message to the end of the sequence of callbacks.
* @param activityCallback A single message that can contain a lifecycle request/callback.
*/
public void addCallback(ClientTransactionItem activityCallback) {
if (mActivityCallbacks == null) {
mActivityCallbacks = new ArrayList<>();
}
mActivityCallbacks.add(activityCallback);
}
/** Get the list of callbacks. */
@Nullable
@UnsupportedAppUsage
List<ClientTransactionItem> getCallbacks() {
return mActivityCallbacks;
}
getCallbacks()方法返回的是mActivityCallbacks,而mActivityCallbacks是通过addCallback方法赋值的,addCallback方法的掉用是在下面这ActivityStackSupervisor.java中的realStartActivityLocked方法中的:
//android/server/ActivityStackSupervisor.java
boolean realStartActivityLocked(ActivityRecord r, WindowProcessController proc,
boolean andResume, boolean checkConfig) throws RemoteException {
......
// Create activity launch transaction.
final ClientTransaction clientTransaction = ClientTransaction.obtain(
proc.getThread(), r.appToken);
final DisplayContent dc = r.getDisplay().mDisplayContent;
//此处调用addCallbacks,LauncherActivityItem是ClientTransactionItem的子类
clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
System.identityHashCode(r), r.info,
// TODO: Have this take the merged configuration instead of separate global
// and override configs.
mergedConfiguration.getGlobalConfiguration(),
mergedConfiguration.getOverrideConfiguration(), r.compat,
r.launchedFromPackage, task.voiceInteractor, proc.getReportedProcState(),
r.icicle, r.persistentState, results, newIntents,
dc.isNextTransitionForward(), proc.createProfilerInfoIfNeeded(),
r.assistToken));
// Set desired final state.
final ActivityLifecycleItem lifecycleItem;
if (andResume) {
lifecycleItem = ResumeActivityItem.obtain(dc.isNextTransitionForward());
} else {
lifecycleItem = PauseActivityItem.obtain();
}
clientTransaction.setLifecycleStateRequest(lifecycleItem);
// Schedule transaction.
mService.getLifecycleManager().scheduleTransaction(clientTransaction);
......
}
看下LaunchActivityItem源码:
//android/app/servertransaction/LaunchActivityItem.java
public class LaunchActivityItem extends ClientTransactionItem {
//这里我们发现LaunchActivityItem中正好有个Intent
@UnsupportedAppUsage
private Intent mIntent;
private int mIdent;
@UnsupportedAppUsage
private ActivityInfo mInfo;
private Configuration mCurConfig;
private Configuration mOverrideConfig;
......
}
这里LaunchActivityItem,中就有一个Intent可用于HOOK,这个流程scheduleTransaction方法最终会调用到上面提到的ActivityThread中handleMessage方法中,msg.obj对象就是ClientTransaction,ClientTransaction中的mActivityCallbacks中可获取LaunchActivityItem,再从LaunchActivityItem中获取需要的Intent。
找到Intent之后,接下来就是通过反射将此处的Intent替换掉,替换Intent,需要操作一下前面提到的ActivityThread中的Handler,也就是ActivityThread类的成员mH,那么看下Handler的源码:
//android/os/Handler.java
/**
* Handle system messages here.
*/
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
当 mCallback != null 时,首先会执行 mCallback.handleMessage(msg),再执行 handleMessage(msg),所以我
们可以将 mCallback 作为 Hook 点,创建它。代码如下:
public static void hookHandler(){
try {
final Class<?> activtyThreadCls = Class.forName("android.app.ActivityThread");
Field activitThreadField = activtyThreadCls.getDeclaredField("sCurrentActivityThread");
activitThreadField.setAccessible(true);
final Object activityTheadObj = activitThreadField.get(null);
Field mHField = activtyThreadCls.getDeclaredField("mH");
if(!mHField.isAccessible()){
mHField.setAccessible(true);
}
Object mHObj = mHField.get(activityTheadObj);
Class<?> handlerCls = Class.forName("android.os.Handler");
Field mCallbackField = handlerCls.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mHObj,new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
switch(msg.what){
case 159:
try {
//Class<?> transExecutorCls = Class.forName("android.app.servertransaction.TransactionExecutor");
Object clientTransObj = msg.obj;
/*Method getActivityTokenMethod = clientTransObj.getClass().getDeclaredMethod("getActivityToken");
getActivityTokenMethod.setAccessible(true);
Object tokenObj = getActivityTokenMethod.invoke(clientTransObj);
此方法会报错,(greylist-max-o, reflection, denied),在Android Q上已经禁止反射调用了
Method getActivityClientMethod = activtyThreadCls.getDeclaredMethod("getActivityClient", IBinder.class);
getActivityClientMethod.setAccessible(true);
Object activityClientRecordObj = getActivityClientMethod.invoke(activityTheadObj,tokenObj);
Field intentField = activityClientRecordObj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
Intent proxyIntent = (Intent)intentField.get(activityClientRecordObj);*/
Field mActivityCallbacksField = clientTransObj.getClass().getDeclaredField("mActivityCallbacks");
mActivityCallbacksField.setAccessible(true);
List<Object> mActivityCallbacksObj = (List<Object>) mActivityCallbacksField.get(clientTransObj);
Object launchActivityItem = null;
for(Object obj:mActivityCallbacksObj){
Log.d("Rayman", "name: "+obj.getClass().getName());
if(obj.getClass().getName().contains("android.app.servertransaction.LaunchActivityItem")){
launchActivityItem = obj;
break;
}
}
if(launchActivityItem == null) return false;
Log.d("Rayman", "launchActivityItem = "+launchActivityItem+",clsname = "+launchActivityItem.getClass().getName());
Field mIntentField = launchActivityItem.getClass().getDeclaredField("mIntent");
mIntentField.setAccessible(true);
Intent proxyIntent = (Intent)mIntentField.get(launchActivityItem);
//设置目标Intent
Intent targetIntent = proxyIntent.getParcelableExtra(TARGET_INTENT);
Log.d("Rayman","targetIntent = "+targetIntent+","+proxyIntent.getComponent().getClassName());
//判断是否是之前我们hook时候,启动的代理Activity
if("com.enjoy.myplugin.ProxyActivity".equals(proxyIntent.getComponent().getClassName())){
/*if(targetIntent == null){
targetIntent = new Intent();
targetIntent.setClassName("com.enjoy.plugin","com.enjoy.plugin.MainActivity");
}*/
if(targetIntent != null)
mIntentField.set(launchActivityItem,targetIntent);
}
} catch (Exception e) {
e.printStackTrace();
}
break;
}
return false;
}
});
/*Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<?>[]{mHObj.getClass()}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});*/
} catch (Exception e) {
e.printStackTrace();
}
}
到这里,HOOK操作就完成了,只需要再宿主APP的Application的onCreate方法中,分别调用hookAMS和hookHandler方法就可以了。
接下来看下,怎么操作资源的加载,由于插件app没有安装,即便是在插件app中,也是没法调用本身的资源的,运行时会报找不到资源的错误。
一般在Android中通过,getResources获取Resources,然后通过Resources调用app相应资源。需要通过反射替换到原本系统中的Resources,或者说通过反射,自己写代码加载插件APP中的资源,这里当然需要跟踪Android系统是怎样加载APP资源的。
比如通常用如下方式获取调用app资源:
String appName = getResources().getString(R.string.app_name);
InputStream is = getAssets().open("icon_1.png");
getResources()方法的实现是在ContextImpl.java中,看ContextImpl源码:
//android/app/ContextImpl.java
@Override
public Resources getResources() {
return mResources;
}
void setResources(Resources r) {
if (r instanceof CompatResources) {
((CompatResources) r).setContext(this);
}
mResources = r;
}
@Override
public Context createApplicationContext(ApplicationInfo application, int flags)
throws NameNotFoundException {
LoadedApk pi = mMainThread.getPackageInfo(application, mResources.getCompatibilityInfo(),
flags | CONTEXT_REGISTER_PACKAGE);
if (pi != null) {
ContextImpl c = new ContextImpl(this, mMainThread, pi, null, mActivityToken,
new UserHandle(UserHandle.getUserId(application.uid)), flags, null, null);
final int displayId = getDisplayId();
//此处会调用createResources方法创建新的Resources
c.setResources(createResources(mActivityToken, pi, null, displayId, null,
getDisplayAdjustments(displayId).getCompatibilityInfo()));
if (c.mResources != null) {
return c;
}
}
throw new PackageManager.NameNotFoundException(
"Application package " + application.packageName + " not found");
}
private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
final String[] splitResDirs;
final ClassLoader classLoader;
try {
splitResDirs = pi.getSplitPaths(splitName);
classLoader = pi.getSplitClassLoader(splitName);
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
return ResourcesManager.getInstance().getResources(activityToken,
pi.getResDir(),
splitResDirs,
pi.getOverlayDirs(),
pi.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfig,
compatInfo,
classLoader);
}
看ResourcesManager的,getResources方法:
//android/app/ResourcesManager.java
public @Nullable Resources getResources(@Nullable 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#getResources");
final ResourcesKey key = new ResourcesKey(
resDir,//这个resDir其实就是需要加载资源的app路径
splitResDirs,
overlayDirs,
libDirs,
displayId,
overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
compatInfo);
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
return getOrCreateResources(activityToken, key, classLoader);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
......
// If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
ResourcesImpl resourcesImpl = createResourcesImpl(key);
if (resourcesImpl == null) {
return null;
}
......
}
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;
}
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 {
//我们需要跟踪AssetManager是怎么加载app资源的,也就是addApkAssets方法
builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,
false /*overlay*/));
} catch (IOException e) {
Log.e(TAG, "failed to add asset path " + key.mResDir);
return null;
}
}
......
}
AssetManager
//android/content/res/AssetManager.java
public static class Builder {
private ArrayList<ApkAssets> mUserApkAssets = new ArrayList<>();
//从这里继续跟踪,需要看mUserApkAssets在哪儿调用
public Builder addApkAssets(ApkAssets apkAssets) {
mUserApkAssets.add(apkAssets);
return this;
}
public AssetManager build() {
// Retrieving the system ApkAssets forces their creation as well.
final ApkAssets[] systemApkAssets = getSystem().getApkAssets();
final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size();
final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount];
System.arraycopy(systemApkAssets, 0, apkAssets, 0, systemApkAssets.length);
final int userApkAssetCount = mUserApkAssets.size();
for (int i = 0; i < userApkAssetCount; i++) {
//mUserApkAssets列表最中会加入apkAssets变量中
apkAssets[i + systemApkAssets.length] = mUserApkAssets.get(i);
}
// Calling this constructor prevents creation of system ApkAssets, which we took care
// of in this Builder.
final AssetManager assetManager = new AssetManager(false /*sentinel*/);
//apkAssets变量又被赋值给assetManager.mApkAssets
assetManager.mApkAssets = apkAssets;
//调用native方法添加app资源文件
AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
false /*invalidateCaches*/);
return assetManager;
}
}
//在此利用反射调用Builder的addApkAssets方法可以添加插件app的资源,又或者在9.0之前,我们可以看另一个方法如下:
/**
* @deprecated Use {@link #setApkAssets(ApkAssets[], boolean)}
* @hide
*/
@Deprecated
@UnsupportedAppUsage
public int addAssetPath(String path) {
return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
}
private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {
......
//此处有没有很熟悉,跟前面Builder中最终调用的native方法是同一个,而且参数都是mApkAssets,在builder中有assetManager.mApkAssets = apkAssets;赋值语句,殊途同归。。。
nativeSetApkAssets(mObject, mApkAssets, true);
......
}
用反射调用addAssetPath方法比较简单,因为需要的参数较少,这儿就用addAssetpath方法做反射处理,代码如下:
public static Resources loadResources(Context context){
Class<?> assetManagerCls = AssetManager.class;
try {
Object assetManagerObj = assetManagerCls.newInstance();
Method addAssetPathMethod = assetManagerCls.getDeclaredMethod("addAssetPath",String.class);
addAssetPathMethod.setAccessible(true);
//此处第一个参数请根据自己运行环境(手机或者虚拟机),申请权限,并且能够访问的存储路径
addAssetPathMethod.invoke(assetManagerObj,"/storage/emulated/0/plugin-debug.apk");
//创建需要替换掉的Resource
Resources resources = context.getResources();
return new Resources((AssetManager) assetManagerObj,resources.getDisplayMetrics(),resources.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
紧接着在插件APP中创建BaseActivity如下:
public class BaseActivity extends AppCompatActivity {
protected Context mContext;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Resources resources = LoadResourcesUtils.loadResources(getApplicationContext());
// 替换这个 context 的resource 为我们自己通过反射写入的 resource
mContext = new ContextThemeWrapper(getBaseContext(),0);
Class<?> contextCls = mContext.getClass();
try {
/*
//ContextThemeWrapper.java
@Override
public Resources getResources() {
return getResourcesInternal();
}
private Resources getResourcesInternal() {
if (mResources == null) {
if (mOverrideConfiguration == null) {
mResources = super.getResources();
} else {
final Context resContext = createConfigurationContext(mOverrideConfiguration);
mResources = resContext.getResources();
}
}
return mResources;
}*/
Field mResourcesField = contextCls.getDeclaredField("mResources");
mResourcesField.setAccessible(true);
mResourcesField.set(mContext,resources);
} catch (Exception e) {
e.printStackTrace();
}
}
/*@Override
public Resources getResources() {
Resources resources = LoadUtils.loadResources(getApplicationContext());
return resources != null ? resources:super.getResources();
}*/
}
插件中的所有Activity都要继承自BaseActivity:
package com.enjoy.plugin;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("Rayman", "onCreate: this is plugin Activity...");
String res = mContext.getResources().getString(R.string.test_plugin_res);
Log.d("Rayman","onCreate plugin Activity res = "+res);
//下面这种写法是不可用的,因为插件APP没有安装,在调用getResource的时候AssetManager无法获取资源路径,会报找不到资源的错误
//Log.d("Rayman", "onCreate: xxx = "+getResources().getString(R.string.test_plugin_xxx));
}
}
按照MainActivity中的方法调用资源就可以了,插件中其他的Activity调用资源也是相同的,到这儿通过反射,HOOK等技术调用插件App的Activity和资源就结束了。
总结一下,主要的操作步骤:
- 利用ClassLoader加载没有安装的插件APP,这个是前提条件;
- 利用HOOK、动态代理、反射等技术绕过AMS的检测,实现启动未注册的Activity;
- 利用反射技术,通过AssetManager加载插件APP的资源,实现对安装的APP资源的调用;
以上就是动态加载的基本内容了,实际操作可以参考github上滴滴的VirtualAPK的源码,参考实现,其实也没有必要重复造轮子,只是通过撸源码了解其中原理,做到知其然,知其所以然。