动态加载技术
在应用程序运行时,动态的去加载一些程序中原本不存的可执行文件并运行这些文件里的代码逻辑。
可执行文件总的来说分为两种:
一种是动态链接库so
一种是dex相关文件(dex以及包含dex的jar/apk文件 )
随着应用开发技术和业务的逐步发展,动态加载技术派生出两个技术:热修复技术以及插件化技术。
热修复技术主要用来修复Bug ,插件化技术主要来解决应用越来越庞大以及功能模块的解耦 。
插件化 插件化产生 在开发初期时,业务需求以及应用开发的复杂度都不是很高,在后续的开发过程中,容易出现以下情况:
业务复杂,模块耦合
随着开发过程,应用的体积以及复杂度都会越来越大,模块的耦合也会越来越严重。
应用间的接入
一个应用不再是单独的应用,他可能还需要接入其他的应用来完善功能。
65535限制
代码量的增大,方法数也会增加,就很容易超出限制。
插件化定义 让我们不用像原来一样把所有的内容都放在一个apk中 ,把一些功能和逻辑单独的放到插件Apk中,由宿主Apk按需调用。方便减少Apk的体积,也可以简单实现热插拔,更加动态化。
插件化的客户端由宿主和插件两个部分组成,宿主多指安装好的Apk,插件就为经过处理的Apk、so的dex等文件。插件可以被宿主加载也可以单独运行。
插件化基本原理 类加载 Android中常用的有两种类加载器,DexClassLoader
和PathClassLoader
,它们都继承于BaseDexClassLoader
。这两个加载器的区别是DexClassLoader
多了一个optimizedDirectory
参数,这个是用来缓存系统创建的Dex文件。在PathClassLoader
中这个参数为null,所以只能去加载内部存储(/data/data/XX )中的Dex文件。
通过双亲委托机制 可以保证类不会重复加载,通过先查看该类是否已被加载,未加载时首先让父加载器先去尝试加载,无法加载再交由自身处理。
单DexClassLoader与多DexClassLoader 通过给插件apk生成相应的DexClassLoader
便可以去访问其中的类。这边又分成两种形式:
宿主和插件相互调用时需要注意以下几点:
资源加载 Android系统通过Resource
加载资源,Resource
又要依赖AssetManager
去加载资源。
因此,只要将插件Apk的路径加入到AssetManager
中,便能够实现对插件资源的访问。
资源的插件化方式主要有两种:
方式
优点
缺点
合并资源方案
插件和主工程可以直接相互访问资源
导致资源冲突
独立构建资源方案
资源隔离,不会造成冲突
资源共享比较麻烦
插件化实现实例 Activity插件化 Activity插件化主要有3种实现方式,分别是反射实现、接口实现以及Hook技术实现
。
反射实现
会对应用的性能造成影响。
接口实现
可以阅读dynamic-load-apk
源码,框架提供基础四大组件基类,由需要插件化的组件进行继承。
Hook技术实现
主流插件化的实现方案。
我们从Activity启动过程 了解到了Activity的启动过程。如果我们需要对Activity进行插件化,需要对这段过程有很好的了解。
通过Hook方式
去实现Activity插件化,主要需要解决两个问题:
插件中的Activity并没有在AndroidManifest.xml
进行注册,如何绕过AMS
校验
如何去构造Activity的实例,并同步生命周期
Hook IActivityManager 1.注册占坑Activity
采用预先占坑 的方式,即在AndroidManifest.xml
中先注册一个占坑Activity来代表即将加入进来的插件Activity。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.wxy.ipc"> <application android:allowBackup="true" android:label="@string/app_name" android:name="com.example.wxy.ipc.App" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> <activity android:name=".LoadActivity" android:launchMode="singleTop"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <!--设置占位Activity--> <activity android:name=".StubActivity"/> </application> </manifest>
2.使用占坑Activity绕过AMS验证 分析Activity启动流程时,Instrumentation.execStartActivity()
去启动Activity,内部实质是依靠远程调用AMS.startActivity()
去执行启动流程。
在Android 8.0之前,依靠的是ActivityManagerNative.getDefault()
执行远程调用
./android/app/Instrumentation.java before Android8.0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public ActivityResult execStartActivity ( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { ... int result = ActivityManagerNative.getDefault() .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null , requestCode, 0 , null , options); }static public IActivityManager getDefault () { return gDefault.get(); }private static final Sigleton<IActivityManager> gDefault = new Singleton<IActivityManager>(){ protected IActivityManager create () { IBinder b = ServiceManager.getService("activity" ); IActivityManager am = asInterface(b); return am; } }
第一次调用到getDefault()
时,就会调用到IActivityManagerSingleton.get()
,由源码可知,该类是一个单例类。
在Android8.0时,依靠的是ActivityManager.getService()
执行远程调用
./android/app/Instrumentation.java in Android8.0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public ActivityResult execStartActivity ( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { ... int result = ActivityManager.getService() .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null , requestCode, 0 , null , options); } 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; } };
在其中先去获取名为activity
的一个代理对象(IBinder
),后续实现利用了AIDL
,根据asInterface()
可以获得IActivityManager
对象,他是AMS
在本地的代理对象。然后就可以直接调用到AMS
的startActivity()
。
根据上述两段源码分析,最终都需要通过IActivityManager
去远程调用到AMS
,可以将其作为Hook点
,由于又是接口类型,应该使用动态代理方式
去生成代理对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class IActivityManagerProxy implements InvocationHandler { private Object mActivityManager; private static final String TAG = "IActivityManagerProxy" ; public IActivityManagerProxy (Object _object) { mActivityManager = _object; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { if ("startActivity" .equals(method.getName())) { Intent intent = null ; int index = 0 ; for (int i = 0 ; i < args.length; i++) { if (args[i] instanceof Intent) { index = i; break ; } } intent = (Intent) args[index]; Intent subIntent = new Intent(); String packageName = "com.example.wxy.ipc" ; subIntent.setClassName(packageName, packageName + ".hook.StubActivity" ); subIntent.putExtra("target_intent" , intent); Log.d(TAG,"hook 成功" ); args[index] = subIntent; } return method.invoke(mActivityManager, args); } }
通过定义上述的代理对象后,跳转到其他Activity时都会被定位到StubActivity
上,无论是否在AndroidManifest.xml
进行过注册 。
接下来要把设置好的代理对象Hook到原有的结构上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class HookHelper { public static void hookAMS () throws Exception { Object defaultSingleton = null ; if (Build.VERSION.SDK_INT >= 26 ) { Class<?> activityManagerClazz = Class.forName("android.app.ActivityManager" ); defaultSingleton = FieldUtil.getField(activityManagerClazz, null , "IActivityManagerSingleton" ); } else { @SuppressLint ("PrivateApi" ) Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative" ); defaultSingleton = FieldUtil.getField(activityManagerNativeClazz, null , "gDefault" ); } Class<?> singletonClazz = Class.forName("android.util.Singleton" ); Field mInstanceField = FieldUtil.getField(singletonClazz, "mInstance" ); Object iActivityManager = mInstanceField.get(defaultSingleton); Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager" ); Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{iActivityManagerClazz}, new IActivityManagerProxy((iActivityManager))); mInstanceField.set(defaultSingleton, proxy); } }
在Application
引用HookHelper
类即可完成绕过验证操作
1 2 3 4 5 6 7 8 9 10 11 public class MyApplication extends Application { @Override public void attachBaseContext (Context base) { super .attachBaseContext(base); try { HookHelper.hookAMS(); }catch (Exception e){ e.printStackTrace(); } } }
在执行startActivity()
跳转时,都会跳转到StubActivity
界面。至此完成了通过AMS验证步骤 。
3.还原插件Activity
使用占坑Activity通过AMS
校验后,因为当前的情况就是把跳转的都指向到了StubActivity
中,需要做的是还原原本要跳转的Activity,使用原本Activity对StubActivity
进行替换。
要实现替换功能,关键点在于找到真正开始绘制Activity的地方,然后实际绘制需要跳转的Activity。
在Activity启动过程 这节中,了解到绘制Activity的流程是从ActivityThread.handleLaunchActivity()
开始执行,并调用到onCreate()
。那就可以在执行这个方法之前,替换掉即将启动的Activity,在上一节中启动的就是StubActivity
,需要把这个再替换成原本的目标Activity。
控制Activity的一套流程都是通过H
这个Handler类去执行的,在其中定义了很多code,来分发不同的流程。可以通过Hook这套流程拦截原本的启动Activity流程,替换成自定义的启动流程。
使用Handler时如果想拦截原有的handleMessage()
,就需要为Handler设置一个Callback
,这样在分发消息(dispatchMessage()
)的时候,就会去执行到Callback.handlerMessage(msg)
而不执行原有处理。在此基础上,可以对ActivityThread.H
设置一个Callback
拦截启动Activity的事件。
在此先自定义一个Callback
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class HCallback implements Handler .Callback { Handler mHandler; public HCallback (Handler _handler) { mHandler = _handler; } @Override public boolean handleMessage (Message msg) { Object r = msg.obj; switch (msg.what) { case 100 : try { Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent" ); Intent target = intent.getParcelableExtra("target_intent" ); intent.setComponent(target.getComponent()); } catch (Exception e) { e.printStackTrace(); } break ; case 159 : try { List<Object> mCallbacks = (List<Object>) FieldUtil.getField(r.getClass(), r, "mActivityCallbacks" ); if (!mCallbacks.isEmpty()) { String className = "android.app.servertransaction.LaunchActivityItem" ; if (mCallbacks.get(0 ).getClass().getCanonicalName().equals(className)) { Object object = mCallbacks.get(0 ); Intent intent = (Intent) FieldUtil.getField(object.getClass(), object, "mIntent" ); Intent target = intent.getParcelableExtra("target_intent" ); intent.setComponent(target.getComponent()); } } } catch (Exception e) { e.printStackTrace(); } break ; } mHandler.handleMessage(msg); return true ; } }
实现了自定义Callback对象HCallback
后,就需要把它设置到ActivityThread.H
中使其拦截后续启动动作。
1 2 3 4 5 6 7 8 9 10 11 12 public class HookHelper { public static void hookHandler() throws Exception { Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); //当前对应的ActivityThread对象 Object currentActivityThread = FieldUtil.getField(activityThreadClass, null, "sCurrentActivityThread"); //对应Handler对象 Field mHField = FieldUtil.getField(activityThreadClass, "mH"); Handler mH = (Handler) mHField.get(currentActivityThread); //替换掉mh中的mCallback对象 FieldUtil.setField(Handler.class, mH, "mCallback", new HCallback(mH)); } }
上述执行完毕后,启动的就会是目标Activity。
4.插件Activity的生命周期 上述三步执行完毕后,就可以打开插件Activity,但是这种操作下会不会影响到原有的生命周期,实际上还是依赖了StubActivity
。
Activity生命周期的回调代码都是交由Instrumentation.callActivityOnXX(ActivityClientRecord.activity)
执行对应的回调代码。其中ActivityClientRecoed
用于描述应用进程中的Activity。我们只要分析ActivityClientRecord.activity
对应的是否为目标Activity,是的话那么生命周期就没有问题。
./android/app/ActivityThread.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 private Activity performLaunchActivity (ActivityClientRecord r, Intent customIntent) { ... Activity activity = null ; try { java.lang.ClassLoader cl = appContext.getClassLoader(); activity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); ... } catch (Exception e) { ... } ... try { Application app = r.packageInfo.makeApplication(false , mInstrumentation); if (activity != null ) { ... activity.attach(appContext, this , getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor, window, r.configCallback); ... if (r.isPersistable()) { mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState); } else { mInstrumentation.callActivityOnCreate(activity, r.state); } r.activity = activity; ... mActivities.put(r.token,r); } }catch (Exception e){ ... } }
从以上源码分析可知,performLaunchActivity()
时会设置当前Activity为目标Activity,生命周期也会跟着当前Activity去执行,即生命周期是同步的。
Hook Instrumentation
该实现相对上面会简单很多,主要就是去操作Instrumentation
,Hook掉其中的两个方法:
newActivity()
:新建Activity 用目标Activity替换掉StubActivity
execStartActivity()
:启动Activity 拦截跳转到StubActivity上
1.注册占坑Activity 方法同上
2.设置Instrumentation代理对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class InstrumentationProxy extends Instrumentation { private static final String TAG = "InstrumentationProxy"; private Instrumentation mInstrumentation; private PackageManager mPackageManager; public InstrumentationProxy(Instrumentation _instrumentation, PackageManager _packageManager) { mInstrumentation = _instrumentation; mPackageManager = _packageManager; } public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { String intentName = intent.getStringExtra("target_intent"); if (!TextUtils.isEmpty(intentName)) { return super.newActivity(cl, intentName, intent); } return super.newActivity(cl, className, intent); } public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL); //判断需要启动的Activity是否已被注册 if (infos.isEmpty()) { intent.putExtra("target_intent", intent.getComponent().getClassName()); //未注册则指向StubActivity intent.setClassName(who, "com.example.wxy.ipc.hook.StubActivity"); } try { //反射调用 execStartActivity @SuppressLint("PrivateApi") Method execStartActivity = Instrumentation.class.getDeclaredMethod( "execStartActivity", Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class); execStartActivity.setAccessible(true); return (ActivityResult) execStartActivity.invoke(mInstrumentation, who, contextThread, token, target, intent, requestCode, options); } catch (Exception e) { e.printStackTrace(); } return null; } }
设置好代理对象后,需要把代理对象Hook到ActivityThread
上,方便后续调用
1 2 3 4 5 6 7 8 9 10 11 12 13 public class HookHelper { public static void hookInstrumentation (Context context) throws Exception { Class<?> activityThreadClass = Class.forName("android.app.ActivityThread" ); Object activityThread = FieldUtil.getField(activityThreadClass,null ,"sCurrentActivityThread" ); Field mInsrumentationField = FieldUtil.getField(activityThreadClass, "mInstrumentation" ); Object mInstrumentation = mInsrumentationField.get(activityThread); FieldUtil.setField(activityThreadClass, activityThread, "mInstrumentation" , new InstrumentationProxy((Instrumentation) mInstrumentation, context.getPackageManager())); } }
在Application
中的attachBaseContext()
调用HookHelper.hookInstrumentation()
即可完成插件Activity的加载。