} 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 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(),
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
最后
由于题目很多整理答案的工作量太大,所以仅限于提供知识点,详细的很多问题和参考答案我都整理成了 PDF文件,需要的小伙伴可以私信我【面试】免费领取或者点击GitHub免费获取!
截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频**
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-I7XFhpRD-1710568771728)]
最后
由于题目很多整理答案的工作量太大,所以仅限于提供知识点,详细的很多问题和参考答案我都整理成了 PDF文件,需要的小伙伴可以私信我【面试】免费领取或者点击GitHub免费获取!
[外链图片转存中…(img-brFgtqtP-1710568771728)]
[外链图片转存中…(img-CRmdkutd-1710568771729)]