Android插件化-Activity篇

} 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 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®;
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 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(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference(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 {

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
道该从何学起的朋友,同时减轻大家的负担。**

[外链图片转存中…(img-jiSCtYDB-1715424077984)]

[外链图片转存中…(img-cGpwLhA7-1715424077987)]

[外链图片转存中…(img-wA4J6XwV-1715424077988)]

[外链图片转存中…(img-4RVK7eCu-1715424077989)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 13
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值