插桩式的插件化实现,每次都必须使用宿主的Context ,去加载插件的layout.xml,activity,service,对于静态广播的注册使用到反射,需要对不同版本的sdk的进行兼容。
Hook式插件化的实现,就是通过Hook的方式,用动态代理拦截系统的源代码实现,加上自己的处理逻辑。以一个简单的小例子,来说明动态代理的原理实现 以及 逻辑。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn = findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.e("TAG","内容为:"+((Button)view).getText());
// Toast.makeText(MainActivity.this,((Button)view).getText(),Toast.LENGTH_LONG).show();
}
});
try {
hookClick(btn);
} catch (Exception e) {
e.printStackTrace();
}
}
//InvocationHandler Object proxy 的解析
// https://blog.youkuaiyun.com/yaomingyang/article/details/81040390
private void hookClick(Button button) throws Exception {
// 得到 View 的 ListenerInfo 对象
Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
//修改getListenerInfo为可访问(View中的getListenerInfo不是public)
getListenerInfo.setAccessible(true);
Object listenerInfo = getListenerInfo.invoke(button);
// 得到 原始的 OnClickListener 对象
Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
Field mOnClickListenerFiled = listenerInfoClz.getDeclaredField("mOnClickListener");
mOnClickListenerFiled.setAccessible(true);
final Object originOnClickListener = mOnClickListenerFiled.get(listenerInfo);
Object OnClickListenerProxy = Proxy.newProxyInstance(getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] objects) throws Throwable {
Button button1 = new Button(MainActivity.this);
button1.setText("我爱你中国");
Log.e("TAG",proxy.toString());
return method.invoke(originOnClickListener,button1);
}
});
mOnClickListenerFiled.set(listenerInfo,OnClickListenerProxy);
}
}
用动态代理Proxy.newProxyInstance来拦截onClick事件:
// Proxy.newProxyInstance 返回所要监听类 View.OnClickListener.class 的接口类的一个实例
// 本例中就是View.OnClickListener 本身 大部分时候 可以使用 XXXX.class.getInterfaces() 作为第二个参数
Object OnClickListenerProxy = Proxy.newProxyInstance(getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() {
/**
*
* @param proxy 真实对象的一个代理对象 $Proxy3 也就是button 的 OnClickListener代理对象
* @param method 真实代理对象(OnClickListener) 的方法
* @param objects 方法所需要的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] objects) throws Throwable {
Button button1 = new Button(MainActivity.this);
button1.setText("我爱你中国");
Log.e("TAG",proxy.getClass().getSimpleName());
return method.invoke(originOnClickListener,button1);
}
});
将Button设置的onClickListener 设置为代理对象OnClickListenerProxy ,真实对象onClickListener对象执行某个方法的时候,需要回调
InvocationHandler的的invoke方法,意思就是 我们所代理的真实对象,执行对象中的所有方法,都会回调InvocationHandler的invoke方法,因此我们可以在真实对象的某些方法调用之前 或者 之后,甚至直接替换 某个方法,不让真实对象的方法不执行
上面中hookClick前面代码的意思 就是利用反射 替换所传入的Button的 onClickListener对象。
代理对象中的InvocationHandler的回调方法invoke的第一个参数Object proxy的详细解释可以看这篇博客
Hook实现插件化思路
我们知道我们使用插件化的最终目的肯定是使用插件当中类似Activity,Service的组件....或者资源文件,那么要使用插件当中的Activity就绕不开ActiviytyManagerService的检查,当我们直接使用宿主APP,去加载插件当中的Activity的时候,需要解决2个难题:
1.我们知道在App中要想启动一个activity,那么这个activity必须在AndroidManifest.xml中注册,否则是通过不了AMS的检查的,系统会抛出异常(类似下面的异常提示),那么怎么绕过系统检查,不让宿主APP加载插件的时候抛出异常,这就是需要解决的一道难题。
Caused by: android.content.ActivityNotFoundException: Unable to find explicit activity class {com.android.hook.hook.plugin/com.android.hook.hook.plugin.TestActivity};
have you declared this activity in your AndroidManifest.xml?
2。让系统不抛出异常之后,我们需要用宿主App的ClassLoader(PathClassLoader)去加载插件当中的activity类,会抛出找不到系统该类的错误.
让未注册的Activity绕过AMS检查
基本流程如下:
代码实现如下(我的SDK版本是28,不同版本有差异,后面会做Hook适配):
跟踪startActivity(Intent)这个源代码发现最终 启动Activity的是调用Instrumentation的 execStartActivity 。最终调用
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
int result = ActivityManager.getService()
.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);
}
ActivityManager.getService().startActivity ...... (这里就是不同版本的可能不同的hook点) 需要去拦截IActivityManager的startActivity方法
/**
* @hide
*/
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;
}
};
通过上面分析 我们需要有几个Hook点,要想达到最终Hook IActivityManager的startService方法,我们就需要Hook它的实现类,而实现类是由上面的Singleton来实现的。
/** ======== 此代码在 Android 28 的sdk的情况下可用=======
* 要在执行 AMS之前,替换可用的 Activity,替换在AndroidManifest里面配置的Activity
*/
private void hookAmsAction() throws Exception {
// 动态代理
Class mActivityManagerClass = Class.forName("android.app.ActivityManager");
//通过ActivityManager 的getService 静态方法 得到IActivityManager
Object mIActivityManager = mActivityManagerClass.getMethod("getService").invoke(null);
// @2 的获取 动态代理
Class mIActivityManagerClass = Class.forName("android.app.IActivityManager");
//得到 ActivityManager 中的 IActivityManagerSingleton
Field mIActivityManagerSingletonField = mActivityManagerClass.getDeclaredField("IActivityManagerSingleton");
mIActivityManagerSingletonField.setAccessible(true);
Object mIActivityManagerSingletonObj = mIActivityManagerSingletonField.get(null);
final Object finalMIActivityManager = mIActivityManager;
// 本质是IActivityManager
Object mIActivityManagerProxy = Proxy.newProxyInstance(
getClassLoader(),
new Class[]{mIActivityManagerClass}, // @2 要监听的接口
new InvocationHandler() { // IActivityManager 接口的回调方法
/**
* @param proxy
* @param method IActivityManager里面的方法
* @param args IActivityManager里面的参数
* @return
* @throws
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {
// 做自己的业务逻辑
// 换成 可以 通过 AMS检查的 ProxyActivity
// 用ProxyActivity 绕过了 AMS检查
Intent intent = new Intent(HookApplication.this, ProxyActivity.class);
intent.putExtra("actionIntent", ((Intent) args[2])); // 把之前TestActivity保存 携带过去
args[2] = intent;
}
Log.e("hook", "拦截到了IActivityManager里面的方法" + method.getName());
// 让系统继续正常往下执行
return method.invoke(finalMIActivityManager, args);
}
});
// 替换点
Class mSingletonClass = Class.forName("android.util.Singleton");
// 获取此字段 mInstance
Field mInstanceField = mSingletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true); // 让虚拟机不要检测 权限修饰符
// 替换
mInstanceField.set(mIActivityManagerSingletonObj, mIActivityManagerProxy);
}
上面的代码放在Application的onCreate方法中执行,就能Hook住AMS的检查,让未注册的TestActivity绕过AMS的检查。
加载未注册的Activity
上面已经绕过AMS的检查,将我们要启动的TestActivity换成了我们在AndroidManifest.xml文件中注册的ProxyActivity。绕过AMS的检查,当通过AMS的检查之后,我们就需要将ProxyActivity替换成我们需要启动的TestActivity。这就需要查看Activity的启动流程,在SDK中的Activity的启动流程如下,所经过的主要类和方法为:
Android SDK 28的源代码 Activity的启动流程 以及Hook点分析
startActivity-->最终走到AMS startActivity-->mActivityStartController.obtainStarter().execute()
-->ActivityStarter的execute方法 -->ActivityStarter的startActivityUnchecked
-->ActivityStackSupervisor.resumeFocusedStackTopActivityLocked-->ActivityStack的resumeTopActivityUncheckedLocked
-->ActivityStackSupervisor的startSpecificActivityLocked-->ActivityStackSupervisor 的realStartActivityLocked
-->ActivityStackSupervisor 的 1523-1533行代码封装一个ClientTransaction 将要运行的Intent封装成LaunchActivityItem放在里面
-->mService.getLifecycleManager().scheduleTransaction(clientTransaction);
ClientLifecycleManager 的 scheduleTransaction方法
-->ClientTransaction的schedule() -->IApplicationThread的scheduleTransaction方法
-->ActivityThread的内部类 ApplicationThread实现了IApplicationThread
-->ClientTransactionHandler 的 scheduleTransaction -->ActivityThread.H.EXECUTE_TRANSACTION 的Message
-->ActivityThread H(就是一个Handler)的handlerMessage case EXECUTE_TRANSACTION
-->msg.obj == ClientTransaction 就是上面封装了LaunchActivityItem 的 ClientTransaction
Hook点 :
Hook Hander mH 的handlerMessage 的case EXECUTE_TRANSACTION 分支
替换ClientTransaction 中的 存放的LaunchActivityItem
下面的代码就是Hook ActivityThread的相关代码,将ProxyActivity替换成要跳转的未注册的Activity
private void hookLuanchActivity() throws Exception {
Field mCallbackFiled = Handler.class.getDeclaredField("mCallback");
mCallbackFiled.setAccessible(true); // 授权
/**
* handler对象怎么来
* 1.寻找H,先寻找ActivityThread
*
* 执行此方法 public static ActivityThread currentActivityThread()
*
* 通过ActivityThread 找到 H
*
*/
Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
// 获得ActivityThrea对象
Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);
Field mHField = mActivityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
// 获取真正对象
Handler mH = (Handler) mHField.get(mActivityThread);
mCallbackFiled.set(mH, new MyCallback()); // 替换 增加我们自己的实现代码
}
public static final int EXECUTE_TRANSACTION = 159;
class MyCallback implements Handler.Callback {
@Override
public boolean handleMessage(Message msg) {
// 做我们在自己的业务逻辑(把ProxyActivity 换成 TestActivity)
//实际上是一个 ClientTransaction
Object mClientTransaction = msg.obj;
try {
Class mLaunchActivityItemClass = Class.forName("android.app.servertransaction.LaunchActivityItem");
//调用ClientTransaction 的 getCallbacks方法 得到集合mActivityCallbacks
Method getCallbacksMethod = mClientTransaction.getClass().getDeclaredMethod("getCallbacks");
getCallbacksMethod.setAccessible(true);
// private List<ClientTransactionItem> mActivityCallbacks;
List mActivityCallbacks = (List) getCallbacksMethod.invoke(mClientTransaction);
if (mActivityCallbacks == null || mActivityCallbacks.isEmpty()) {
return false;
}
//从mActivityCallbacks 中取出 最后一个 也就是我们刚刚要启动的activity
Object mLaunchAcitiveItemObj = mActivityCallbacks.get(0);
//得到 LaunchActivityItem 中的 private Intent mIntent 对象 就是存放ProxyActivity的intent
if (!mLaunchActivityItemClass.isInstance(mLaunchAcitiveItemObj)) {
return false;
}
Field mIntentFiled = mLaunchActivityItemClass.getDeclaredField("mIntent");
mIntentFiled.setAccessible(true);
Intent mProxyIntent = (Intent) mIntentFiled.get(mLaunchAcitiveItemObj);
// 得到我们需要启动的TestActivivty的intent
Intent actionIntent = mProxyIntent.getParcelableExtra("actionIntent");
if (actionIntent != null) {
//替换intent
mIntentFiled.set(mLaunchAcitiveItemObj, actionIntent);
}
} catch (Exception e) {
e.printStackTrace();
}
return false; // 系统不会往下执行
}
}
下面一段代码 ,运行插件中的PluginActivity
public void jumpActivity(View view) {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.android.pluginpackager", "com.android.pluginpackager.PluginActivity"));
startActivity(intent);
}
会报下面的错误
Caused by: java.lang.ClassNotFoundException: Didn't find class "com.android.pluginpackager.PluginActivity"
源码分析:最终这个异常由 BaseDexClassLoader 的findClass方法抛出,下面为源码分析:
第二个Hook点:
关于Android中的ClassLoader的源代码地址可以在线查看,现在查看地址为:https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/dalvik/src/main/java/dalvik/system
PathClassLoader.loadClass ---》 BaseDexClassLoader --》ClassLoader.loadClass--findClass(空方法) 让覆盖的子类方法去完成 --》
BaseDexClassLoader.findClass() ---》pathList.findClass
BaseDexClassLoader.findClass() -- c 为什么为null,--》 DexPathList.findClass(className) ---》DexFile.loadClassBinaryName(系列步骤后 NDK)
for遍历 dexElements == Element[] ,分析 Element 是什么 ,为什么Element.dexFile==null?
Android虚拟机 dex文件的 dex == 对Dex表现形式的描述 Element --- dexFile拥有可执行
为什么 Element ==null?
答:就是因为类加载机制加载的是 ---》 宿主的 classes.dex--Elements, 【没有插件的Element】
解决方案:把插件的dexElements 和 宿主中的 dexElements 融为一体 PathClassLoader 就能加载到 插件/宿主 都可以加载到了
Hook的实现
/**
* 把插件的dexElements 和 宿主中的 dexElements 融为一体
*/
private void pluginToAppAction() throws Exception {
// 第一步:找到宿主 dexElements 得到此对象 PathClassLoader代表是宿主
PathClassLoader pathClassLoader = (PathClassLoader) this.getClassLoader(); // 本质就是PathClassLoader
Class mBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
// private final DexPathList pathList;
Field pathListField = mBaseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object mDexPathList = pathListField.get(pathClassLoader);
Field dexElementsField = mDexPathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
// 本质就是 Element[] dexElements
Object dexElements = dexElementsField.get(mDexPathList);
/*** ---------------------- ***/
// 第二步:找到插件 dexElements 得到此对象,代表插件 DexClassLoader--代表插件
File file = new File(Environment.getExternalStorageDirectory() + File.separator + "pluginpackage-debug.apk");
Log.e("hook","file 的真实路径:"+file.getAbsolutePath());
if (!file.exists()) {
throw new FileNotFoundException("没有找到插件包!!");
}
String pluginPath = file.getAbsolutePath();
File fileDir = this.getDir("pluginDir", Context.MODE_PRIVATE); // data/data/包名/pluginDir/
DexClassLoader dexClassLoader = new
DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, getClassLoader());
Class mBaseDexClassLoaderClassPlugin = Class.forName("dalvik.system.BaseDexClassLoader");
// private final DexPathList pathList;
Field pathListFieldPlugin = mBaseDexClassLoaderClassPlugin.getDeclaredField("pathList");
pathListFieldPlugin.setAccessible(true);
Object mDexPathListPlugin = pathListFieldPlugin.get(dexClassLoader);
Field dexElementsFieldPlugin = mDexPathListPlugin.getClass().getDeclaredField("dexElements");
dexElementsFieldPlugin.setAccessible(true);
// 本质就是 Element[] dexElements
Object dexElementsPlugin = dexElementsFieldPlugin.get(mDexPathListPlugin);
// 第三步:创建出 新的 dexElements []
int mainDexLeng = Array.getLength(dexElements);
int pluginDexLeng = Array.getLength(dexElementsPlugin);
int sumDexLeng = mainDexLeng + pluginDexLeng;
Log.e("hook","dex 的list的size="+sumDexLeng);
// 参数一:int[] String[] ... 我们需要Element[]
// 参数二:数组对象的长度
// 本质就是 Element[] newDexElements
Object newDexElements = Array.newInstance(dexElements.getClass().getComponentType(),sumDexLeng); // 创建数组对象
// 第四步:宿主dexElements + 插件dexElements =----> 融合 新的 newDexElements
for (int i = 0; i < sumDexLeng; i++) {
// 先融合宿主
if (i < mainDexLeng) {
// 参数一:新要融合的容器 -- newDexElements
Array.set(newDexElements, i, Array.get(dexElements, i));
} else { // 再融合插件的
Array.set(newDexElements, i, Array.get(dexElementsPlugin, i - mainDexLeng));
}
}
// 第五步:把新的 newDexElements,设置到宿主中去
// 宿主
dexElementsField.set(mDexPathList, newDexElements);
// 处理加载插件中的布局
doPluginLayoutLoad();
}
private Resources resources;
private AssetManager assetManager;
/**
* 处理加载插件中的布局
* Resources
*/
private void doPluginLayoutLoad() throws Exception {
assetManager = AssetManager.class.newInstance();
// 把插件的路径 给 AssetManager
File file = new File(Environment.getExternalStorageDirectory() + File.separator + "pluginpackage-debug.apk");
if (!file.exists()) {
throw new FileNotFoundException("没有找到插件包!!");
}
// 执行此 public final int addAssetPath(String path) 方法,才能把插件的路径添加进去
Method method = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class); // 类类型
method.setAccessible(true);
method.invoke(assetManager, file.getAbsolutePath());
Resources r = getResources(); // 拿到的是宿主的 配置信息
// 特殊:专门加载插件资源
resources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());
}
Demo中,插件中的我们自己写的ProxyActivity 必须 继承BaseActivity,在BaseActivity必须重写 getResources 和 getAssets 方法,用自己在宿主中所创建的Resources和AssetsManager来加载资源文件。
Hook式的插件化框架,在github上有360的开源项目,github地址为https://github.com/Qihoo360/DroidPlugin
注意:Hook式插件化框架,存在缺点,就是各个不同版本的SDK,需要去适配,并且我们加载Dex的时候,将插件的class加载到主App会增加内存,比如我们上面得都是以android28的源代码为例的,对于低版本的SDK,可能Hook的方法有些小小的改变。在这里有一个原则,也是Google开发工程师在sdk版本的迭代的时候遵循的一个原则,就是Hook,利用反射的时候,尽量去Hook,反射系统public修饰的方法,或者属性,因为就算Android SDK版本迭代的时候,google工程师不会随便修改public修饰的方法,或者属性,就算是修改也会考虑到适配的问题,但是对于私有属性或者方法,google工程师可以随便修改
PS:当我们的插件与主包的包名相同并且某个Class与主包中的class名称相同,我们融合 dexElements 的时候,将插件放在整个dexElements的前面的时候,系统找到插件中的Class的时候,就不会再在数组中去查找了。这样就可以达到替换主包中某个或者某些的Class的目的,达到替换主包中有bug的class或者方法的目的