Android插件化开发指南——Hook技术(一)【长文】
文章目录
- 1. 前言
- 2. 将外部dex加载到宿主app的dexElements中
- 3. 插件中四大组件的调用思路
- 4. Hook
- 2.1 对startActivity进行Hook
- 2.1.1 AMS
- 2.1.2 源码分析
- 2.1.2.1 得到AMS实例对象
- 2.1.3 对startActivity进行Hook
- 2.1.3.1 创建AMS的代理对象
- 5. References
- 6. 完整代码
1. 前言
在Android插件化开发指南——插件化技术简介一文中曾提到插件化技术的实现需要使用 Android
系统底层的各种 Hook
。在这篇博客中将来简单的介绍下什么是Hook
,以及在Android
中的一些实践。
Hook
中文意思为钩子,在编程中意为钩子函数。Hook
原理为在某段SDK
源码执行的过程中,通过把原始对象替换为我们的代理对象,我们就可以为所欲为,也就是Hook
。
其实在上篇Android插件化开发指南——类加载器最后给了一个案例,在案例中我们可以通过DexClassLoader
来动态加载外部apk
、dex
等中的字节码问题。这种感觉和插件化很类似,但是确有一个致命的问题没有解决,不妨再次观察下这个加载代码:
public class OtherActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_other);File file = new File(Environment.getExternalStorageDirectory(), "plugin-debug.apk");DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath(),getDir("cache_plugin", MODE_PRIVATE).getAbsolutePath(),null,getClassLoader());try {Class<?> aClass = dexClassLoader.loadClass("com.weizu.plugin.ToastUtils");Method showInfo = aClass.getMethod("showInfo", Context.class);showInfo.setAccessible(true);showInfo.invoke(aClass.newInstance(), OtherActivity.this);} catch (Exception e) {e.printStackTrace();}}
}
但是需要注意的是,上面这种方式虽然可以加载外部插件,但是需要调用者每次都得到DexClassLoader
对象。虽然我们可以写为单例模式,但是从代码的书写角度来讲,每次都需要来得到外部dex
的DexClassLoader
比较麻烦。而且如果在一个应用中有很多个外部dex
或者apk
插件的时候,难道需要让程序员记住每个dex
中有哪些类?这显然不现实。所以需要通过另外一种方式来实现。
2. 将外部dex加载到宿主app的dexElements中
为了知道宿主App
中在哪里加载类的,所以需要从类加载器开始看起。这里从OtherActivity
中的getClassLoader()
方法开始追踪。如下图所示:
这里的Context为一个抽象类,且getClassLoader
方法为一个抽象方法,所以我们需要找到其实现类ContextImpl
。其源码可以查看链接:ContextImpl.java。
// ContextImpl
final LoadedApk mPackageInfo;
@Override
public ClassLoader getClassLoader() {return mPackageInfo != null ?mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
}
所以需要先了解下mPackageInfo
这个变量在哪里赋值:
// ContextImpl
static ContextImpl createSystemContext(ActivityThread mainThread) {LoadedApk packageInfo = new LoadedApk(mainThread);ContextImpl context = new ContextImpl(null, mainThread,packageInfo, null, null, 0, null, null, Display.INVALID_DISPLAY);context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),context.mResourcesManager.getDisplayMetrics());return context;
}
也就是说这个LoadedApk
是和Main
线程挂钩的。这里继续查看getClassLoader
这个方法:
// LoadedApk.java
public ClassLoader getClassLoader(){synchronized (this) {if (mClassLoader == null) {createOrUpdateClassLoaderLocked(null);}return mClassLoader;}
}
至于这个createOrUpdateClassLoaderLocked
方法中,其实也是使用ClassLoader.getSystemClassLoader()
来获取一个类加载器。所以这里就关注于这个方法:
ClassLoader.getSystemClassLoader()
这个方法最终会调用ClassLoader.createSystemLoader()
方法,该方法如下:
private static ClassLoader createSystemClassLoader() {String classPath = System.getProperty("java.class.path", ".");String librarySearchPath = System.getProperty("java.library.path", "");return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}
也就是说其实返回的是一个PathClassLoader
类加载器。也就是说在ContextImpl
文件中的getClassLoader()
方法调用之后,返回得到的是一个PathClassLoader
类加载器。找到PathClassLoader.java
的源文件:PathClassLoader.java。一目了然,这个类的功能基本来自其父类BaseDexClassLoader
,因为其只有两个构造方法。所以这里可以查看BaseDexClassLoader.java的源文件。
这个文件的代码也比较简单,如下:
public class BaseDexClassLoader extends ClassLoader {private final DexPathList pathList;public BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, ClassLoader parent) {super(parent);this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);}protected Class<?> findClass(String name) throws ClassNotFoundException {List<Throwable> suppressedExceptions = new ArrayList<Throwable>();Class c = pathList.findClass(name, suppressedExceptions);if (c == null) {ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);for (Throwable t : suppressedExceptions) {cnfe.addSuppressed(t);}throw cnfe;}return c;}...
}
从上面的代码中可以看出其实查找是从其属性字段DexPathList
中查找。也就是这里其实还需要进一步查找这个类的源码,因为DexPathList.java这个类的代码较多,这里就只查看pathList.findClass
方法。
// DexPathList
public Class findClass(String name, List<Throwable> suppressed) {for (Element element : dexElements) {DexFile dex = element.dexFile;if (dex != null) {Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);if (clazz != null) {return clazz;}}}if (dexElementsSuppressedExceptions != null) {suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));}return null;
}
观察上面的代码可以知道其实在执行findClass
的时候,其实是在dexElements
中进行查找。而这个dexElements
直接定义为一个数组:
private Element[] dexElements;
故而如果我们能够将外部插件的dex
或者apk
文件中的dexElements
加入到宿主app
的dexElements
中就可以完成预期。
不妨将上面的逻辑用时序图来进行表示:
那么对应的可以写一个工具类,用于完成上面的步骤。代码如下:
public class LoadUtils {private static String pluginPath = "/sdcard/plugin-debug.apk";public static void init(Context context) {if(context == null) return;try {// 获取应用程序App的dexElementsPathClassLoader classLoader = (PathClassLoader) context.getClassLoader();Class<?> baseDexClassLoaderClazz = Class.forName("dalvik.system.BaseDexClassLoader");Field dexPathListField = baseDexClassLoaderClazz.getDeclaredField("pathList");dexPathListField.setAccessible(true);Object dexPathListValue = dexPathListField.get(classLoader);Field dexElementsField = dexPathListValue.getClass().getDeclaredField("dexElements");dexElementsField.setAccessible(true);Object dexElementsValue = dexElementsField.get(dexPathListValue);// 获取外部插件的dexElementsDexClassLoader dexClassLoader = new DexClassLoader(pluginPath,context.getDir("plugin", Context.MODE_PRIVATE).getAbsolutePath(),null, context.getClassLoader());Object pluginDexPathListValue = dexPathListField.get(dexClassLoader);Object pluginDexElementsValue = dexElementsField.get(pluginDexPathListValue);// 合并两个dexElementsint appDexElementsLength = Array.getLength(dexElementsValue);int pluginDexElementsLength = Array.getLength(pluginDexElementsValue);int newLength = appDexElementsLength + pluginDexElementsLength;Class<?> componentType = dexElementsValue.getClass().getComponentType();Object newArray = Array.newInstance(componentType, newLength);System.arraycopy(dexElementsValue, 0, newArray, 0, appDexElementsLength);System.arraycopy(pluginDexElementsValue, 0, newArray, appDexElementsLength, pluginDexElementsLength);// 设置新的内容到app的PathList中的Elements[]dexElementsField.set(dexPathListValue, newArray);} catch (Exception e){e.printStackTrace();}}
}
那么对应的加载方法为:
public class OtherActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_other);LoadUtils.init(this);try {Class<?> aClass = getClassLoader().loadClass("com.weizu.plugin.ToastUtils");Method showInfo = aClass.getMethod("showInfo", Context.class);showInfo.setAccessible(true);showInfo.invoke(aClass.newInstance(), OtherActivity.this);} catch (Exception e) {e.printStackTrace();}}
}
就可以直接使用context
中得到的PathClassLoader
来进行反射。结果为:
至于其余的细节部分可以参考:Android插件化开发指南——类加载器。
3. 插件中四大组件的调用思路
在上面的加载外部apk
中的类的时候,我们加载的只是一个普通的类。而在Android
中四大组件具有一定的特殊性,因为都需要在清单文件中注册。对于外部插件中的Activity
或者Service
等我们却又不可能在宿主App
中进行注册,所以实际上更加复杂。因为我们至少需要一种方式可以绕过系统加载的时候对于清单文件中配置信息的检查。
我们知道在实际场景中我们的插件中都会涉及到一些界面的更新,而这个更新过程也就是会涉及到Activity
的增加或者修改。所以实际中上面的简单加载外部普通类确实不适用。不妨来个简单的案例,因为在之前导出的plugin_debug.apk
中其实包含了一个Activity
:
所以这里可以在app
模块中进行测试:
public class OtherActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_other);File file = new File(Environment.getExternalStorageDirectory(), "plugin-debug.apk");DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath(),getDir("cache_plugin", MODE_PRIVATE).getAbsolutePath(),null,getClassLoader());try {Class<?> aClass = dexClassLoader.loadClass("com.weizu.plugin.MainActivity");Intent intent = new Intent(OtherActivity.this, aClass);startActivity(intent);} catch (Exception e) {e.printStackTrace();}}
}
运行结果:
这里其实也再次证明了四大组件的特殊性。所以我们需要更加合理的方式来绕开检查。
4. Hook
2.1 对startActivity进行Hook
2.1.1 AMS
要对startActivity
进行Hook
,那么就首选需要搞清楚在startActivity(intent)
之后发生了什么事情。那么首先需要了解的就是AMS
(ActivityManagerService
)虽然这个字面意思是Activity
的管理服务,但是其实四大组件都归它管。
AMS
主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作,其职责与操作系统中的进程管理和调度模块相类似。当发起进程启动或者组件启动时,都会通过Binder
通信机制将请求传递给AMS
,AMS
再做统一处理。
既然AMS
管理着四大组件,那么为什么不直接在AMS
层对想要的功能进行Hook
呢?因为如果可以在AMS
层进行Hook
,很明显就是病毒程序了,所以在Android
中也不允许这么做。所以我们的Hook
点只能是在四大组件,因为至少需要保证受影响的只是当前程序,而不能影响别的程序。
这里以ActivityA
启动ActivityB
为例:
ActivityA
向AMS
发送需要启动ActivityB
的消息;AMS
保存ActivityB
的信息,同时AMS
要检查ActivityB
是否在清单文件中注册,如果注册了才继续下面的步骤;ActivityA
休眠,AMS
通知ActivityThread
去启动ActivityB
;
2.1.2 源码分析
从自定义的OtherActivity.java
文件中的startActivity(intent);
出发,可以看见下面的调用流程:
也就是说通过startActivity(intent);
最终会请求Activity
类的startActivityForResult
方法,在方法中可以看见两个成员变量:
mInstrumentation // Instrumentation
mMainThread // ActivityThread// 对应逻辑摘要
Instrumentation.ActivityResult ar =mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this,intent, requestCode, options);if (ar != null) {
mMainThread.sendActivityResult(mToken, mEmbeddedID, requestCode, ar.getResultCode(),ar.getResultData());
}
那么首先看下Instrumentation
这个类中的execStartActivity
方法,地址为:Instrumentation.java。
// Instrumentation
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options) {...try {intent.migrateExtraStreamToClipData();intent.prepareToLeaveProcess(who);int result = ActivityManagerNative.getDefault().startActivity(whoThread, who.getBasePackageName(), intent,intent.resolveTypeIfNeeded(who.getContentResolver()),token, target != null ? target.mEmbeddedID : null,requestCode, 0, null, options);checkStartActivityResult(result, intent);} catch (RemoteException e) {throw new RuntimeException("Failure from system", e);}return null;
}
在execStartActivity
这个方法中,最终会通过ActivityManagerNative.getDefault().startActivity()
方法来启动目标的Activity
。而ActivityManagerNative.getDefault()
最后返回的其实也就是一个IActivityManager
对象,也就是常说的AMS
对象。不妨继续看看这个AMS
是如何得到的,我们继续追踪:
// ActivityManagerNative
static public IActivityManager getDefault() {return gDefault.get();
}private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {protected IActivityManager create() {IBinder b = ServiceManager.getService("activity");if (false) {Log.v("ActivityManager", "default service binder = " + b);}IActivityManager am = asInterface(b);if (false) {Log.v("ActivityManager", "default service = " + am);}return am;}
};static public IActivityManager asInterface(IBinder obj) {if (obj == null) {return null;}IActivityManager in =(IActivityManager)obj.queryLocalInterface(descriptor);if (in != null) {return in;}return new ActivityManagerProxy(obj);
}
而对于这里的单例泛型类Singleton
为:
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
定义为单例对象,这个单例使用上面的Singleton<T>
来进行修饰,真正的创建方法由new
的时候来指定。而在gDefault
的创建过程中,使用了IBinder
和ActivityManagerProxy
进行转换。
2.1.2.1 得到AMS实例对象
那么如果我们需要通过反射来得到应用程序中的AMS
,那么我们就可以在通过反射来得到这个单例对象gDefault
。然后再得到其中定义为泛型的mInstance
,也就是AMS
对象。然后就可以通过动态代理的方式来拦截AMS
调用的startActivity
方法。那么这里可以简单通过反射来得到AMS
对象,由于AMS
是在ActivityManagerNative.java
文件中,通过getDefault()
得到的,所以这里为:
public class HookAMSUtils {public static void getActivityManagerService() {try {Class<?> aClass = Class.forName("android.app.ActivityManagerNative");Field getDefault = aClass.getDeclaredField("gDefault");getDefault.setAccessible(true);// 获取静态的gDefault对象Object getDefaultObj = getDefault.get(null);// 而实际上AMS在单例Singleton中Class<?> singletonClazz = Class.forName("android.util.Singleton");Field mInstance = singletonClazz.getDeclaredField("mInstance");mInstance.setAccessible(true);Object amsObj = mInstance.get(getDefaultObj); // AMSLog.e("TAG", "getActivityManagerService: " + amsObj.toString());} catch (Exception e) {e.printStackTrace();}}
}
但是很遗憾:
因为我看的源代码为:Nougat - 7.1.2_r36。可以查看一下Android版本和代号等对应的API级别。可以查看:代号、标记和细分版本号,这里摘要部分:
代号 | 版本 | API 级别 |
---|---|---|
Oreo | 8.1.0 | API 级别 27 |
Oreo | 8.0.0 | API 级别 26 |
Nougat | 7.1 | API 级别 25 |
Nougat | 7.0 | API 级别 24 |
Marshmallow | 6.0 | API 级别 23 |
Lollipop | 5.1 | API 级别 22 |
Lollipop | 5.0 | API 级别 21 |
而我的项目中设置的buildToolsVersion
为30
。所以这里还是换为API
级别 25
,下载对应的SDK
:
创建一个对应版本的虚拟设备:
然后修改gradle
文件:
android {compileSdkVersion 28buildToolsVersion "25.0.3"defaultConfig {applicationId "com.weizu.myapplication"minSdkVersion 25targetSdkVersion 28versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"}...
然后继续运行:
也就是说这里可以Hook容纳AMS
的单例得到得到AMS
对象。
上面分析的这个过程的时序图可以表示为:
2.1.3 对startActivity进行Hook
经过上面的逻辑分析,我们知道当一个Activity
去启动另一个Activity
后,最终根据一系列的调用会到AMS
的startActivity
方法,这里再次粘贴一下相关代码:
// Instrumentation
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options) {...try {intent.migrateExtraStreamToClipData();intent.prepareToLeaveProcess(who);int result = ActivityManagerNative.getDefault().startActivity(whoThread, who.getBasePackageName(), intent,intent.resolveTypeIfNeeded(who.getContentResolver()),token, target != null ? target.mEmbeddedID : null,requestCode, 0, null, options);checkStartActivityResult(result, intent);} catch (RemoteException e) {throw new RuntimeException("Failure from system", e);}return null;
}
在前面得到的AMS
对象,根据ActivityManagerNative
中getDefault()
的返回值类型,很容易我们知道其为IActivityManager
接口对象。
而如果我们需要在AMS
中做欺骗,即绕过清单文件中对四大组件的注册检查。这里需要使用动态代理模式,然后拦截AMS
的startActivity
方法。
2.1.3.1 创建AMS的代理对象
关于动态代理可以查看:Android常见设计模式——代理模式(Proxy Pattern)。
因为这里是代理接口IActivityManager.java
,源码地址为:IActivityManager.java。所以使用动态代理在进行invoke
的时候会得到很多的方法,而这里我们只需要startActivity
,这个方法定义为:
// IActivityManager.java
public int startActivity(IApplicationThread caller, String callingPackage, Intent intent,String resolvedType, IBinder resultTo, String resultWho, int requestCode, int flags,ProfilerInfo profilerInfo, Bundle options) throws RemoteException;
那么我们在动态代理方法中,就可以拦截到这个startActivity
方法。为了能够做到绕过清单文件检查的目的,我们可以事先在清单文件中注册一个代理的Activity
,然后在拦截到的startActivity
方法中进行Activity
对象的替换即可。比如下面的代码:
public class HookAMSUtils {public static final String ORIGIN_INTENT = "ORIGIN_INTENT";public static void getActivityManagerService(Context context, Class<? extends Activity> proxyActivityClazz) {try {Class<?> aClass = Class.forName("android.app.ActivityManagerNative");Field getDefault = aClass.getDeclaredField("gDefault");getDefault.setAccessible(true);// 获取静态的gDefault对象Object getDefaultObj = getDefault.get(null);// 而实际上AMS在单例Singleton中Class<?> singletonClazz = Class.forName("android.util.Singleton");Field mInstance = singletonClazz.getDeclaredField("mInstance");mInstance.setAccessible(true);Object amsObj = mInstance.get(getDefaultObj); // AMSLog.e("TAG", "getActivityManagerService: " + amsObj.toString());// 创建AMS的代理对象Class<?> aClass1 = Class.forName("android.app.IActivityManager");// 得到AMS的代理对象Object amsProxy = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{aClass1}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 代理方法处理Log.e("TAG", "invoke: startActivity");if (method.getName().equals("startActivity")) {// 查找参数,找到Intent对象int index = 0;for (int i = 0; i < args.length; i++) {if (args[i] instanceof Intent) {index = i;break;}}// 拿到意图Intent oldIntent = (Intent) args[index];String name = oldIntent.getStringExtra("NAME");Log.e("TAG", "invoke: " + name);// 创建一个新的意图,将这个旧的意图添加到新的意图中Intent newIntent = new Intent(context, proxyActivityClazz);// 将旧的意图放入到新的意图中newIntent.putExtra(ORIGIN_INTENT, oldIntent);// 设置startActivity的意图对象为新的意图args[index] = newIntent;}return method.invoke(amsObj, args);}});// 将AMS代理对象设置为原本的AMS对象,// 也就是设置ActivityManagerNative.java中属性字段gDefault的值为代理对象mInstance.set(getDefaultObj, amsProxy);} catch (Exception e) {e.printStackTrace();}}
}
当然需要创建一个在清单文件中注册的ProxyActivity
类。然后在MainActivity
中测试:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);LoadUtils.init(this);HookAMSUtils.getActivityManagerService(this, ProxyActivity.class);try {Class<?> aClass = getClassLoader().loadClass("com.weizu.plugin.MainActivity");Log.e("TAG", "onCreate: " + aClass.getName());Intent intent = new Intent(MainActivity.this, aClass);intent.putExtra("NAME", "123");startActivity(intent);} catch (Exception e) {e.printStackTrace();}}
}
结果:
也就是说这里的跳转替换为了代理的Activity
对象。所以我们还需要在某个地方将原本的目标com.weizu.plugin.MainActivity
替换回来。当然,具体将这里的代理Activity
替换为原本的MainActivity
这里需要在ActivityThread
中完成。这个过程的时序图可以表示为:
从上图中可以知道在ActivityThread
中使用了Hanlder
来发送消息。所以我们可以处理Handler
的回调接口来进行Activity
的替换。故而首先第一步为得到ActivityThread
的实例对象,然后再将处理消息的方法设置为我们自己的方法。而ActivityThread
中定义了自己的一个静态引用,故而可以比较容易的得到该对象。对应的代码为:
public static void hookActivityThreadToLaunchActivity(){try {// 得到ActivityThread的对象Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");Field sCurrentActivityThreadField = activityThreadClazz.getDeclaredField("sCurrentActivityThread");sCurrentActivityThreadField.setAccessible(true);Object activityThreadValue = sCurrentActivityThreadField.get(null);// 找到Handler,即mHField mHField = activityThreadClazz.getDeclaredField("mH");mHField.setAccessible(true);Object mHValue = mHField.get(activityThreadValue);// 重新赋值Class<?> handlerClazz = Class.forName("android.os.Handler");Field mCallBackField = handlerClazz.getDeclaredField("mCallback");mCallBackField.setAccessible(true);mCallBackField.set(mHValue, new HandlerCallBack());} catch (Exception e) {e.printStackTrace();}
}// 因为在ActivityThread中通过Handler来接受消息,
// 所以这里为了替换,就实现其回调接口private static class HandlerCallBack implements Handler.Callback{@Overridepublic boolean handleMessage(Message message) {// 处理消息if(message.what == 100) { // H.LAUNCH_ACTIVITYhandleLaunchActivity(message);}return false;}private void handleLaunchActivity(Message message) {try {// 得到ActivityClientRecord r对象Object r = message.obj;// 而在得到ActivityClientRecord中就存储着传进来的Intent意图对象// 所以可以先获取到意图,然后修改意图对象Field intentField = r.getClass().getDeclaredField("intent");// 取出intent的值intentField.setAccessible(true);Intent newIntent = (Intent) intentField.get(r);// 从这个newIntent得到真正的意图Intent oldIntent = newIntent.getParcelableExtra(ORIGIN_INTENT);Log.e("TAG", "handleLaunchActivity: " + oldIntent.toString());if(oldIntent != null){// 设置r中的intent为当前的这个oldIntentintentField.set(r, oldIntent);}} catch (Exception e) {e.printStackTrace();}}
}
那么在调用的时候使用:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);LoadUtils.init(this);HookAMSUtils.getActivityManagerService(this, ProxyActivity.class);HookAMSUtils.hookActivityThreadToLaunchActivity();}// onClickpublic void jump(View view){try {Class<?> aClass = getClassLoader().loadClass("com.weizu.plugin.MainActivity");Log.e("TAG", "onCreate: " + aClass.getName());Intent intent = new Intent(MainActivity.this, aClass);startActivity(intent);} catch (Exception e) {e.printStackTrace();}}
}
即可实现点击文本框然后进行跳转。需要注意的是在跳转到插件的MainActivity
后,其实使用的布局的文件还是当前应用的布局文件。其实如果需要完整加载Activity
组件,其实还需要解决加载布局文件(资源文件)的问题。这部分内容将放置在下篇进行介绍。
5. References
References
- 29讲玩转插件化:深入底层分析Android插件化原理,从0到1手写实现360插件化项目架构【1-20】
- 《Android插件化开发指南》
- Android插件化开发指南——插件化技术简介
- Android插件化开发指南——类加载器
- PathClassLoader.java
- 代号、标记和细分版本号
- Android常见设计模式——代理模式(Proxy Pattern)
6. 完整代码
涉及到的两个类的完整代码:
public class LoadUtils {private static String pluginPath = "/sdcard/plugin-debug.apk";public static void init(Context context) {if(context == null) return;try {// 获取应用程序App的dexElementsPathClassLoader classLoader = (PathClassLoader) context.getClassLoader();Class<?> baseDexClassLoaderClazz = Class.forName("dalvik.system.BaseDexClassLoader");Field dexPathListField = baseDexClassLoaderClazz.getDeclaredField("pathList");dexPathListField.setAccessible(true);Object dexPathListValue = dexPathListField.get(classLoader);Field dexElementsField = dexPathListValue.getClass().getDeclaredField("dexElements");dexElementsField.setAccessible(true);Object dexElementsValue = dexElementsField.get(dexPathListValue);// 获取外部插件的dexElementsDexClassLoader dexClassLoader = new DexClassLoader(pluginPath,context.getDir("plugin", Context.MODE_PRIVATE).getAbsolutePath(),null, context.getClassLoader());Object pluginDexPathListValue = dexPathListField.get(dexClassLoader);Object pluginDexElementsValue = dexElementsField.get(pluginDexPathListValue);// 合并两个dexElementsint appDexElementsLength = Array.getLength(dexElementsValue);int pluginDexElementsLength = Array.getLength(pluginDexElementsValue);int newLength = appDexElementsLength + pluginDexElementsLength;Class<?> componentType = dexElementsValue.getClass().getComponentType();Object newArray = Array.newInstance(componentType, newLength);System.arraycopy(dexElementsValue, 0, newArray, 0, appDexElementsLength);System.arraycopy(pluginDexElementsValue, 0, newArray, appDexElementsLength, pluginDexElementsLength);// 设置新的内容到app的PathList中的Elements[]dexElementsField.set(dexPathListValue, newArray);} catch (Exception e){e.printStackTrace();}}}
public class HookAMSUtils {public static final String ORIGIN_INTENT = "ORIGIN_INTENT";public static void getActivityManagerService(Context context, Class<? extends Activity> proxyActivityClazz) {try {Class<?> aClass = Class.forName("android.app.ActivityManagerNative");Field getDefault = aClass.getDeclaredField("gDefault");getDefault.setAccessible(true);// 获取静态的gDefault对象Object getDefaultObj = getDefault.get(null);// 而实际上AMS在单例Singleton中Class<?> singletonClazz = Class.forName("android.util.Singleton");Field mInstance = singletonClazz.getDeclaredField("mInstance");mInstance.setAccessible(true);Object amsObj = mInstance.get(getDefaultObj); // AMSLog.e("TAG", "getActivityManagerService: " + amsObj.toString());// 创建AMS的代理对象Class<?> aClass1 = Class.forName("android.app.IActivityManager");// 得到AMS的代理对象Object amsProxy = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{aClass1}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 代理方法处理if (method.getName().equals("startActivity")) {// 查找参数,找到Intent对象int index = 0;for (int i = 0; i < args.length; i++) {if (args[i] instanceof Intent) {index = i;break;}}// 拿到意图Intent oldIntent = (Intent) args[index];// 创建一个新的意图,将这个旧的意图添加到新的意图中Intent newIntent = new Intent(context, proxyActivityClazz);// 将旧的意图放入到新的意图中newIntent.putExtra(ORIGIN_INTENT, oldIntent);// 设置startActivity的意图对象为新的意图args[index] = newIntent;}return method.invoke(amsObj, args);}});// 将AMS代理对象设置为原本的AMS对象,// 也就是设置ActivityManagerNative.java中属性字段gDefault的值为代理对象mInstance.set(getDefaultObj, amsProxy);} catch (Exception e) {e.printStackTrace();}}public static void hookActivityThreadToLaunchActivity(){try {// 得到ActivityThread的对象Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");Field sCurrentActivityThreadField = activityThreadClazz.getDeclaredField("sCurrentActivityThread");sCurrentActivityThreadField.setAccessible(true);Object activityThreadValue = sCurrentActivityThreadField.get(null);// 找到Handler,即mHField mHField = activityThreadClazz.getDeclaredField("mH");mHField.setAccessible(true);Object mHValue = mHField.get(activityThreadValue);// 重新赋值Class<?> handlerClazz = Class.forName("android.os.Handler");Field mCallBackField = handlerClazz.getDeclaredField("mCallback");mCallBackField.setAccessible(true);mCallBackField.set(mHValue, new HandlerCallBack());} catch (Exception e) {e.printStackTrace();}}// 因为在ActivityThread中通过Handler来接受消息,// 所以这里为了替换,就实现其回调接口private static class HandlerCallBack implements Handler.Callback{@Overridepublic boolean handleMessage(Message message) {// 处理消息if(message.what == 100) { // H.LAUNCH_ACTIVITYhandleLaunchActivity(message);}return false;}private void handleLaunchActivity(Message message) {try {// 得到ActivityClientRecord r对象Object r = message.obj;// 而在得到ActivityClientRecord中就存储着传进来的Intent意图对象// 所以可以先获取到意图,然后修改意图对象Field intentField = r.getClass().getDeclaredField("intent");// 取出intent的值intentField.setAccessible(true);Intent newIntent = (Intent) intentField.get(r);// 从这个newIntent得到真正的意图Intent oldIntent = newIntent.getParcelableExtra(ORIGIN_INTENT);Log.e("TAG", "handleLaunchActivity: " + oldIntent.toString());if(oldIntent != null){// 设置r中的intent为当前的这个oldIntentintentField.set(r, oldIntent);}} catch (Exception e) {e.printStackTrace();}}}
}
以及调用:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);LoadUtils.init(this);HookAMSUtils.getActivityManagerService(this, ProxyActivity.class);HookAMSUtils.hookActivityThreadToLaunchActivity();}// onClickpublic void jump(View view){try {Class<?> aClass = getClassLoader().loadClass("com.weizu.plugin.MainActivity");Log.e("TAG", "onCreate: " + aClass.getName());Intent intent = new Intent(MainActivity.this, aClass);startActivity(intent);} catch (Exception e) {e.printStackTrace();}}
}
清单文件中,注册权限和代理Activity
:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/><activity android:name=".ProxyActivity"></activity>
Android插件化开发指南——Hook技术(一)【长文】相关推荐
- Android插件化开发指南——Hook技术(二)
文章目录 1. 前言 2. 分析 3. 加载外部资源文件代码 4. References 1. 前言 在上篇Android插件化开发指南--Hook技术(一)[长文]中提到最终的效果其实在插件中的Ma ...
- Android插件化开发指南——插件化技术简介
文章目录 1. 为什么需要插件化技术 2. 插件化技术的历史 3. 插件化实现思路 3.1 InfoQ:您在 GMTC 中的议题叫做<Android 插件化:从入门到放弃>,请问这个标题代 ...
- Android插件化开发指南——实践之仿酷狗音乐首页
文章目录 1. 前言 2. 布局分析 3. 底部导航栏的实现 4. 顶部导航栏和ViewPager+Fragment的关联 1. 前言 在Android插件化开发指南--2.15 实现一个音乐播放器A ...
- Android插件化开发指南——实践之Activity转场效果(仿酷狗音乐启动页)
文章目录 1. 前言 2. Activity退出动画 2.1 简单使用 2.2 overridePendingTransition 3. 后记 1. 前言 在Android插件化开发指南--2.15 ...
- Android插件化开发指南——实践之仿酷狗音乐首页(自定义ImageView控件)
文章目录 1. 前言 2. 基础环境--实现RecyclerView的网格布局 3. 自定义ImageView 3. 后记 1. 前言 拟定实现效果部分为下图的歌单列表部分,也就是图中红线框出来的部分 ...
- android 禁止插件化,Android 插件化实现方式(Hook)
一.首先我们要找到Hook的点 1. 分析 我们先大概看下activity的启动流程(图片来自Android 插件化开发指南) image 当我们调用startActivity的时候,AMS对我们要启 ...
- Android 插件化原理解析——Hook机制之AMSPMS
在前面的文章中我们介绍了DroidPlugin的Hook机制,也就是代理方式和Binder Hook:插件框架通过AOP实现了插件使用和开发的透明性.在讲述DroidPlugin如何实现四大组件的插件 ...
- Android 插件化原理学习 —— Hook 机制之动态代理
前言 为了实现 App 的快速迭代更新,基于 H5 Hybrid 的解决方案有很多,由于 webview 本身的性能问题,也随之出现了很多基于 JS 引擎实现的原生渲染的方案,例如 React Nat ...
- Android插件化开发之解决OpenAtlas组件在宿主的注冊问题
Android插件化开发之解决OpenAtlas组件在宿主的注冊问题 OpenAtlas有一个问题,就是四大组件必须在Manifest文件里进行注冊,那么就必定带来一个问题,插件中的组件都要反复在宿主 ...
最新文章
- 阿里大数据技术如何进化?资深技术专家带你回顾
- max导出fbx设置_真3D虚拟偶像制作教程——虚拟偶像人物模型导出前的处理
- .NET Core 中使用 Humanizer 显示友好时间格式
- 广东2022年上半年系统集成项目管理工程师上午真题及答案解析
- EverMonkey-VSCode上最好用的印象笔记插件
- cαr怎么发音_英语c的发音怎么读
- Lambda表达式----“进化论”
- 【论文解读 KDD 2018 | PME】PME: Projected Metric Embedding on Heterogeneous Networks for Link Prediction
- ECharts 打造在线个人简历
- 如何启动android模拟器,如何从命令行启动Android模拟器?
- 《14天从0到1学Java》第一天之04第一行Java代码
- 金秋杭州游 只为桂花香
- 微信浏览器、手机版浏览器、pc版浏览器网站的缓存怎么清理? 比较好的三点解决方案
- Python4班平均成绩统计_2021小升初必看!成都各个初中成绩如何?师资咋样?不同成绩的孩子该怎么准备?...
- 剑指Offer——毕业生求职网站汇总(干货)
- python暴力破解wifi密码原理
- c语言count函数的作用,count通达信什么意思,count是啥意思
- angular5监听浏览器的前进和后退按钮(PlatformLocation)
- 解压版MySQL的安装
- 尚医通 (一)项目介绍