What is plug-in

  • Plug-in technology originally came from the idea of running apK without installation, which can be understood as a plug-in
  • Some function modules that are not commonly used in the APP are made into plug-ins. On the one hand, the size of the installation package is reduced, and on the other hand, the function of the APP can be dynamically extended.
  • Plug-in frameworks serve two purposes: self-decoupling and installation-free
    • Self-decoupling refers to an application that was originally compiled from a single piece of code, but instead wants to compile some of its functions separately and dynamically plug into the main application like a plug-in. This makes the main application smaller and easier to download and install. Second, it can be more independent functions can be developed and debugged separately, or even updated separately.
    • Install-free refers to an application that normally requires an installation process to start and run, but would like to start from an already installed and running App without installation. The main purpose of this requirement is to improve traffic reuse capability.

History of plugins

  1. AndroidDynamicLoader: give Fragment to achieve plug-in framework, can dynamically load the Fragment in the plug-in to achieve page switch;
  2. Dynamic-load-apk (Ren Yugang) : The first use of ProxyActivity static proxy technology, ProxyActivity to control the life cycle of PluginActivity (disadvantages: Activities in plugins must inherit from pluginActivities, so be careful with context development.
  3. DroidPlugin: Start the Activity in the plug-in by Hook system service, so that the process of plug-in development is no different from that of ordinary APP development (disadvantages: complicated and unstable due to too many Hook system services)
  • Later, each framework tends to select as few hooks as possible in the realization principle, and realizes the plug-in of the four major components by embedding some components in the manifest, and makes different degrees of extension
  1. Ctrip DynamicApk
  2. VirtualApp: Can simulate the running environment of the APP completely, can realize the operation of the APP without installation and dual-open technology
  3. Small: a cross-platform plug-in framework
  4. 360 RePlugin
  5. Drops VirtualApk
  6. Aliatlas: an app infrastructure that combines componentization and hotfix technologies, billed as a containerized framework
  7. Tencent Shadow: a completely Hack free, even zero reflection implementation of Android plug-in framework, plug-in code is completely a normal installable App code, no need to reference any Shadow library

Principle of plug-in

Class loading this

This in Java:

  1. BootstrapClassLoader: Is responsible for loading core classes of JVM runtime, such as JAVA_HOME/lib/rt.jar, etc
  2. ExtensionClassLoader: Responsible for loading JVM extension classes, such as jar packages under JAVA_HOME/lib/ext
  3. AppClassLoader: Loads jar packages and directories in the classpath

Android ClassLoader:

  • Both PathClassLoader and DexClassLoader can load external dex/APK, but the difference is that DexClassLoader can specify optimizedDirectory, Odex is the product of Dex2OAT, and PathClassLoader can only use the system default location.
  • However, the optimizedDirectory has been removed since Android 8.0, and only the default directory is used. That is, there is no difference between PathClassLoader and DexClassLoader on Android 8.0.

Parent delegate mechanism:

  • Each ClassLoader has a parent object, which represents the parent ClassLoader. When loading a class, the parent ClassLoader is used first. If the parent object is not found in the parent ClassLoader, the system ClassLoader is used to load the class. This mechanism ensures that all system classes are loaded by the system class loader.

How do I load classes in a plug-in

  • The classes can be accessed by generating corresponding DexClassLoader for the plug-in APK. Here there are two implementation schemes: single DexClassLoader and multiple DexClassLoader
Single DexClassLoader
  • The pathList in the DexClassLoader of the plug-in is merged into the DexClassLoader of the main project
  • The advantage is that different plug-ins and main projects can directly call each other’s classes and methods, and the common modules of different plug-ins can be extracted and put into a common plug-in for direct use by other plug-ins (Small adopts this scheme).
  • Disadvantages: If two different plug-in projects reference different versions of a library, the program can go wrong, so there are specifications to avoid this.
  • The implementation code is as follows
binding.btnSingleDexClassLoader.setOnClickListener { loadDex(this, listOf(plugin001Path,plugin002Path)) val clazzApp = Class.forName("com.jinyang.plugindemo.TestApp") val methodApp = clazzApp.getMethod("test") methodApp.invoke(clazzApp.newInstance()) val clazzPlugin001 = Class.forName("com.jinyang.plugin001.TestPlugin001") val methodPlugin001 = clazzPlugin001.getMethod("test") methodPlugin001.invoke(clazzPlugin001.newInstance()) val clazzPlugin002 = Class.forName("com.jinyang.plugin002.TestPlugin002") val methodPlugin002 = clazzPlugin002.getMethod("test") methodPlugin002.invoke(clazzPlugin002.newInstance()) } fun loadDex(context: Context, pluginPaths: A List < String >) {try {/ / get pathList val systemClassLoader = Class. Class.forname (" dalvik. System. BaseDexClassLoader ") val PathListField = systemClassLoader. GetDeclaredField (" pathList) pathListField. IsAccessible = true / / get dexElements val dexPathListClass = Class.forName("dalvik.system.DexPathList") val dexElementsField = DexPathListClass. GetDeclaredField (" dexElements ") dexElementsField. IsAccessible = true / / access to a host of Elements val hostClassLoader = context.classLoader val hostPathList = pathListField.get(hostClassLoader) val hostElements = dexElementsField.get(hostPathList) as kotlin.Array<*> var newElements: Kotlin. Array<*> = hostElements // Navigate to Elements for (path in pluginPaths) {val pluginClassLoader = PathClassLoader(path, context.classLoader) val pluginPathList = pathListField.get(pluginClassLoader) val pluginElements = Get (pluginPathList) as kotlin.Array<*> val temp = array.newinstance ( pluginElements.javaClass.componentType!! Size + pluginElements. Size) as kotlin.Array<*> Arraycopy (newElements, 0, temp, 0, newElements. Size) system. arrayCopy (pluginElements, 0, temp, pluginElements) Set (hostPathList, temp) newElements = temp}} catch (e: Exception) { e.printStackTrace() } }Copy the code
More DexClassLoader
  • A DexClassLoader is generated for each plug-in. Classes in the plug-in need to be loaded using the corresponding DexClassLoader
  • The advantage of this scheme is that the classes of different plug-ins are isolated, so there are no problems when different plug-ins reference different versions of the same class library.
  • The following code
val nativeLibDir = File(filesDir, "pluginlib").absolutePath
val dexOutPath = File(filesDir, "dexout").absolutePath

val plugin001Path = File(filesDir.absolutePath, "plugin001.apk").absolutePath
val pluginClassLoader = DexClassLoader(plugin001Path, dexOutPath, nativeLibDir, this::class.java.classLoader)

val plugin002Path: String = File(filesDir.absolutePath, "plugin002.apk").absolutePath
val pluginClassLoader2 = DexClassLoader(plugin002Path, dexOutPath, nativeLibDir, this::class.java.classLoader)
Copy the code

Resource to load

  • The Android system loads resources through the Resource object, so you can access the resources of the plug-in by adding the path of the plug-in APK to the AssetManager
Processing of resource paths
1. Combined form:
  • AddAssetPath specifies the path to add all plug-ins and the main project.
  • Advantages: Plug-ins and hosts have direct access to each other’s resources
  • Disadvantages: Introduces resource conflicts (since the main project and each plug-in are compiled independently, the generated resource ids will be the same)
  • The implementation code is as follows, where I set the package name of the plug-in to be the same as the host, for specific reasons, see the following description in shadow:
binding.btnPrintResources.setOnClickListener { val plugin001Path = File(filesDir.absolutePath, "plugin001.apk").absolutePath val plugin002Path: String = File(filesDir.absolutePath, "plugin002.apk").absolutePath val mResources = loadResources(this,resources.assets, listOf(pluginPath, pluginPath2)) val strAppId = mResources? .getIdentifier("str_app", "string", "com.jinyang.plugindemo") log("str_app:"+ strAppId? .let { it1 -> mResources.getString(it1) }) val strPlugin001Id = mResources? .getIdentifier("str_plugin001", "string", "com.jinyang.plugindemo") log("str_plugin001:"+ strPlugin001Id? .let { it1 -> mResources.getString(it1) }) val strPlugin002Id = mResources? .getIdentifier("str_plugin002", "string", "com.jinyang.plugindemo") log("str_plugin002:"+ strPlugin002Id? .let { it1 -> mResources.getString(it1) }) } fun loadResources(context: Context,assetManager:AssetManager, pluginPaths: List<String>): Resources? { try { val addAssetPathMethod = assetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java) addAssetPathMethod.isAccessible = true for (path in pluginPaths) { addAssetPathMethod.invoke(assetManager, path) } return Resources( assetManager, context.resources.displayMetrics, context.resources.configuration ) } catch (e: Exception) { e.printStackTrace() } return null }Copy the code
2. Independent:
  • Each plug-in only adds its own APK path
  • Advantages: Resource isolation and no resource conflict exists
  • Disadvantages: Troublesome Resource sharing (if you want to achieve the sharing of resources, you must get the corresponding Resource object)
  • This is done by overriding the getResources, getAssets methods in the baseActivity of each plug-in
open class PluginBaseActivity : Activity() { private var pluginClassLoader: ClassLoader? = null private var pluginPath: String? =null private var pluginAssetManager: AssetManager? = null private var pluginResources: Resources? = null private var pluginTheme: Resources.Theme? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val nativeLibDir = File(filesDir, "pluginlib").absolutePath val dexOutPath = File(filesDir, "dexout").absolutePath pluginPath = File(filesDir.absolutePath, "plugin002.apk").absolutePath pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader) handleResources() } override fun getResources(): Resources? { return pluginResources ? : super.getResources() } override fun getAssets(): AssetManager { return pluginAssetManager ? : super.getAssets() } override fun getClassLoader(): ClassLoader { return pluginClassLoader ? : super.getClassLoader() } private fun handleResources() { try { pluginAssetManager = AssetManager::class.java.newInstance() val addAssetPathMethod = pluginAssetManager? .javaClass? .getMethod("addAssetPath", String::class.java) addAssetPathMethod? .invoke(pluginAssetManager, pluginPath) } catch (e: Exception) { } pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration) pluginTheme = pluginResources? .newTheme() pluginTheme? .setTo(super.getTheme()) } }Copy the code
Resolve the resource ID conflictMethod:
  1. Modify AAPT source code, customize AAPT tools, modify PP section during compilation; DynamicAPK uses this solution to change the ID of a compiled resource in Android
  2. Rearrange the resources of the plugin Apk and arrange the ID. (VirtualApk uses this solution), principle reference: Plug-in – Resolve the problem that the plug-in resource ID conflicts with the host resource ID
  3. Add the following code to aaptOptions build.gradle android nodes by configuring aaptOptions, but this only works if compileSdkVersion is 28 or higher
android {
    aaptOptions {
        additionalParameters  "--package-id", "0x66","--allow-reserved-package-id"
    }
    ...
}
Copy the code
The Context of the processing
  • Usually we access resources through the Context object, and creating the Resource object is not enough, so there is some extra work to be done
Val Resources = loadutils.getResources (application) // Create your own Context ContextThemeWrapper(baseContext, 0) // Replace resources in our Context with val clazz = mContext::class.java val mResourcesField = clazz.getDeclaredField("mResources") mResourcesField.isAccessible = true mResourcesField.set(mContext, resources)Copy the code
  • Can also be reference implementation in VirtualAPK: com. Didi. VirtualAPK. Internal. ResourcesManager
public static void hookResources(Context base, Resources resources) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return; } try {// Replace the mResource object of LoadedApk in the main project context Reflector = Reflector. With (base); reflector.field("mResources").set(resources); Object loadedApk = reflector.field("mPackageInfo").get(); Reflector.with(loadedApk).field("mResources").set(resources); // Add the new Resource to the mResourceManager of the main project ActivityThread. And according to the Android version did different processing Object activityThread = activityThread. CurrentActivityThread (); Object resManager; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { resManager = android.app.ResourcesManager.getInstance(); } else { resManager = Reflector.with(activityThread).field("mResourcesManager").get(); } Map<Object, WeakReference<Resources>> map = Reflector.with(resManager).field("mActiveResources").get(); Object key = map.keySet().iterator().next(); map.put(key, new WeakReference<>(resources)); } catch (Exception e) { Log.w(TAG, e); }}Copy the code

Loading four components

  • The plug-in of the four components is the core of the plug-in technology

Loading a plug-in Activity

Two problems with plug-in activities
  • Manifest not registered on the host: plugins are dynamically loaded, so your plug-in’s four major components can’t be registered to host the Manifest file, start a not registered in the Manifest Activity complains ActivityNotFoundException
  • Lifecycle cannot be invoked: An Activity’s main work is invoked in its lifecycle methods
The solution
  1. Manually invoke the plug-in Activity lifecycle;
  2. Tricking the system into thinking the Activity is registered in the Manifest
  • There are three main ways to implement the life cycle of calling plug-in Activity, reflection, interface and Hook implementation, among which reflection and interface implementation is relatively simple, do not need to do too much understanding of the internal system implementation; And more stable, do not need to adapt to a variety of manufacturers ROM and different Android versions of THE API, but through reflection efficiency is too low, through the interface need to achieve a lot of methods;
1. Reflection implements the life cycle of calling the plug-in Activity:
  1. Create a reflection life cycle tools ReflectActivityLifeCircle, including by class. GetMethod to reflect each statement cycle method, call the Activity code is as follows:
class ReflectActivityLifeCircle(activity: String? , activityClassLoader: ClassLoader?) { private var clazz: Class<Activity>? = activityClassLoader? .loadClass(activity) as Class<Activity>? private var activity: Activity? = clazz? .newInstance() private fun getMethod(methodName: String, vararg params: Class<*>): Method? { return clazz? .getMethod(methodName, *params) } fun attach(proxyActivity: Activity?) { getMethod("attach", Activity::class.java)? .invoke(activity, proxyActivity) } fun onCreate(savedInstanceState: Bundle?) { getMethod("onCreate", Bundle::class.java)? .invoke(activity, savedInstanceState) } fun onStart() { getMethod("onStart")? .invoke(activity) } fun onResume() { getMethod("onResume")? .invoke(activity) } fun onPause() { getMethod("onPause")? .invoke(activity) } fun onStop() { getMethod("onStop")? .invoke(activity) } fun onDestroy() { getMethod("onDestroy")? .invoke(activity) } }Copy the code
  1. Create an agent in the host Activity, its life cycle called directly ReflectActivityLifeCircle method, through reflection to invoke the plugin Activity lifecycle, the code is as follows
class ProxyReflectActivity : ProxyBaseActivity() { private var reflectActivityLifeCircle: ReflectActivityLifeCircle? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val nativeLibDir = File(filesDir, "pluginlib").absolutePath val dexOutPath = File(filesDir, "dexout").absolutePath val pluginPath = intent.getStringExtra("pluginPath") val pluginActivityName = intent.getStringExtra("activityName") val pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader) reflectActivityLifeCircle= ReflectActivityLifeCircle(pluginActivityName,pluginClassLoader)  reflectActivityLifeCircle? .attach(this) reflectActivityLifeCircle? .onCreate(savedInstanceState) } override fun onStart() { super.onStart() reflectActivityLifeCircle? .onStart() } override fun onResume() { super.onResume() reflectActivityLifeCircle? .onResume() } override fun onPause() { super.onPause() reflectActivityLifeCircle? .onPause() } override fun onStop() { super.onStop() reflectActivityLifeCircle? .onStop() } override fun onDestroy() { super.onDestroy() reflectActivityLifeCircle? .onDestroy() } companion object{ fun startPluginActivity(context: Context, pluginPath: String, activityName: String) { val intent = Intent(context, ProxyReflectActivity::class.java) intent.putExtra("pluginPath", pluginPath) intent.putExtra("activityName", activityName) context.startActivity(intent) } } }Copy the code
  • Reflection implementations have a performance impact and are not adopted by mainstream plug-in frameworks
2. Implement the life cycle of invoking the plug-in Activity through the interface
  1. Define an interface, noting that the full path of the interface used by the host and plug-in should be the same
interface IPluginActivity {
    fun attach(proxyActivity: Activity)
    fun onCreate(savedInstanceState: Bundle?)
    fun onStart()
    fun onResume()
    fun onPause()
    fun onStop()
    fun onDestroy()
}
Copy the code
  1. Implement this interface in the baseActivity of the plug-in
open class BasePluginActivity : Activity(), IPluginActivity { var proxyActivity: Activity? = null override fun attach(proxyActivity: Activity) { this.proxyActivity = proxyActivity } override fun onCreate(savedInstanceState: Bundle?) { if (proxyActivity == null) { super.onCreate(savedInstanceState) } } override fun setContentView(layoutResID: Int) { log("proxyActivity=$proxyActivity,layoutResID=$layoutResID") proxyActivity? .let { it.setContentView(layoutResID) } ? : run { super.setContentView(layoutResID) } } override fun setContentView(view: View?) { proxyActivity? .let { it.setContentView(view) } ? : run { super.setContentView(view) } } override fun onStart() { if (proxyActivity == null) { super.onStart() } } override fun onResume() { if (proxyActivity == null) { super.onResume() } } override fun onPause() { if (proxyActivity == null) {  super.onPause() } } override fun onStop() { if (proxyActivity == null) { super.onStop() } } override fun onDestroy() { if (proxyActivity == null) { super.onDestroy() } } override fun getResources(): Resources? { if (proxyActivity == null) { return super.getResources() } return proxyActivity? .resources } override fun getTheme(): Resources.Theme? { if (proxyActivity == null) { return super.getTheme() } return proxyActivity? .theme } override fun getLayoutInflater(): LayoutInflater { if (proxyActivity == null) { return super.getLayoutInflater() } return proxyActivity? .layoutInflater!! }}Copy the code
  1. Create a proxy Activity in the host, use the plug-in’s classLoader and the plug-in ActivityName to get the plug-in Activity instance, and force it into an IPluginActivity type. The IPluginActivity corresponding method is called during the life cycle of the host Activity
class ProxyInterfaceActivity : Activity() { private var activity: IPluginActivity? =null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val nativeLibDir = File(filesDir, "pluginlib").absolutePath val dexOutPath = File(filesDir, "dexout").absolutePath val pluginPath = intent.getStringExtra("pluginPath") val pluginActivityName = intent.getStringExtra("activityName") val pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, This: : class. Java. This) / / by this plug-in and ActivityName for plugin Activity instance, Activity =pluginClassLoader? .loadClass(pluginActivityName)? .newinstance () as IPluginActivity // Call IPluginActivity in the lifecycle of the host Activity? .attach(this) activity? .onCreate(savedInstanceState) } override fun onStart() { super.onStart() activity? .onStart() } override fun onResume() { super.onResume() activity? .onResume() } override fun onPause() { super.onPause() activity? .onPause() } override fun onStop() { super.onStop() activity? .onStop() } override fun onDestroy() { super.onDestroy() activity? .onDestroy() } companion object { fun startPluginActivity(context: Context, pluginPath: String, activityName: String) { val intent = Intent(context, ProxyInterfaceActivity::class.java) intent.putExtra("pluginPath", pluginPath) intent.putExtra("activityName", activityName) context.startActivity(intent) } } }Copy the code
3. The Hook
  • There are two main solutions, one is through Hook IActivityManager to achieve, one is Hook Instrumentation
The starting process of an Activity
  • It is divided into two kinds, one is the start process of the root Activity, one is the start process of the ordinary Activity
  • Start the root Activity: The process Launcher asks AMS to create the root Activity. AMS determines whether the application process required by the root Activity exists and starts it. If it does not, it asks Zygote to create the application process. After the application process starts, AMS asks the application process to create and start the root Activity.
  • Normal Activity startup process: An Activity in an application process requests AMS to create a normal Activity. AMS manages the Activty lifecycle tube and stack, validates the Activity, and so on. If the Activity meets AMS’s verification, AMS asks the ActivityThread in the application process to create and start a normal Activity.
Hook IActivityManager scheme implementation
  • AMS is in the SystemServer process, cannot be directly modified, can only be used in the application process. Androidmanifest.xml can be pre-marred by using an Activity registered in androidmanifest.xml to pass the AMS check. Then replace the spotty Activity with the plug-in Activity
  1. Create a pit occupying Activity in the host
class StubHookActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
}

<activity
    android:name=".hook.StubHookActivity"
    android:exported="false" />
Copy the code
  1. Pass THE AMS validation using trap Activity
// There are some differences between the AMS family in Android 8.0 and 7.0. The main difference is that Android 8.0 removes ActivityManagerProxy for AMS and replaces it with IActivityManager, which directly uses AIDL for inter-process communication. // the Activity starts on Android7.0 calls ActivityManagerNative's getDefault method, The Activity on Android8.0 starts by calling ActivityManager's getService method, and both return objects of type IActivityManager. public static void hookAMS() { try { Object singleTon = null; If (build.version.sdk_int >= build.version_codes.q) {// API for Android 29 or later @suppressLint ("PrivateApi") Class<? > activityManagerClass = Class.forName("android.app.ActivityTaskManager"); Field iActivityManagerSingletonField = activityManagerClass.getDeclaredField("IActivityTaskManagerSingleton"); iActivityManagerSingletonField.setAccessible(true); singleTon = iActivityManagerSingletonField.get(null); } else if (build.version.sdk_int >= build.version_codes.o) {// Android 26 or later API is the same Class<? > activityManagerClass = Class.forName("android.app.ActivityManager"); Field iActivityManagerSingletonField = activityManagerClass.getDeclaredField("IActivityManagerSingleton"); iActivityManagerSingletonField.setAccessible(true); singleTon = iActivityManagerSingletonField.get(null); } else {// Android 26 or below API is a series of Class<? > activityManagerClass = Class.forName("android.app.ActivityManagerNative"); Field iActivityManagerSingletonField = activityManagerClass.getDeclaredField("gDefault"); iActivityManagerSingletonField.setAccessible(true); singleTon = iActivityManagerSingletonField.get(null); } Class<? > singleTonClass = Class.forName("android.util.Singleton"); Field mInstanceField = singleTonClass.getDeclaredField("mInstance"); mInstanceField.setAccessible(true); / / get IActivityManagerSingleton Object final Object iActivityManager = mInstanceField. Get (singleTon); Class<? > iActivityManagerClass; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { iActivityManagerClass = Class.forName("android.app.IActivityTaskManager"); } else { iActivityManagerClass = Class.forName("android.app.IActivityManager"); } Object newInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{iActivityManagerClass}, (o, method, Args) -> {if ("startActivity".equals(method.getName())) { Intent Intent = null; 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]; StubActivity Intent subIntent = new Intent(); String packageName = "com.jinyang.plugindemo"; subIntent.setClassName(packageName, packageName + ".hook.StubHookActivity"); PutExtra (hookhelper.target_intent, Intent);  // Assign a subIntent value to the args parameter, so that the target of the launch becomes StubActivity, which is used to pass the AMS verification. args[index] = subIntent; } return method.invoke(iActivityManager, args); }); mInstanceField.set(singleTon, newInstance); } catch (Exception e) { e.printStackTrace(); }}Copy the code
  • Try jumping to Plugin002Activity and you will find that instead of jumping to Plugin002Activity, you will jump to StubHookActivity because it is blocked by our hookAMS
Binding. BtnHookIActivityManager. SetOnClickListener {hookAMS () / / can be put in Application for effective global val intent = intent (val) packageName = "com.jinyang.plugindemo" val activityName = "com.jinyang.plugin002.Plugin002Activity" intent.component = ComponentName(packageName, activityName) startActivity(intent) }Copy the code
  1. Restore plug-in Activity: The sCurrentActivityThread class has a static variable that represents the current ActivityThread object. Replace it with mH:Handler, override handleMessage, and block the corresponding MSG. Replace the Intent that started StubHookActivity with the Intent that started Plugin002Activity
Public static void hookHandler() {try {// Get ActivityThread final Class<? > activityThreadClass = Class.forName("android.app.ActivityThread"); Field activityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); activityThreadField.setAccessible(true); final Object activityThread = activityThreadField.get(null); / / get Handler instance Field mHField = activityThreadClass. GetDeclaredField (" mH "); mHField.setAccessible(true); Object mH = mHField.get(activityThread); Class<? > handlerClass = Class.forName("android.os.Handler"); Field mCallbackField = handlerClass.getDeclaredField("mCallback"); mCallbackField.setAccessible(true); mCallbackField.set(mH, (Handler.Callback) msg -> { switch (msg.what) { case 100: // Get the intent object Field intentField = in ActivityClientRecord msg.obj.getClass().getDeclaredField("intent"); intentField.setAccessible(true); Intent proxyIntent = (Intent) intentField.get(msg.obj); / / get a plug-in Intent Intent Intent. = proxyIntent getParcelableExtra (TARGET_INTENT); // replace proxyintent.setComponent (intent.getComponent()); } catch (Exception e) { e.printStackTrace(); } break; case 159: Lifecycle is added after API 28. Try {Field mActivityCallbacksField = msg.obj.getClass().getDeclaredField("mActivityCallbacks"); mActivityCallbacksField.setAccessible(true); List<Object> mActivityCallbacks = (List<Object>) mActivityCallbacksField.get(msg.obj); for (int i = 0; i < mActivityCallbacks.size(); i++) { Class<? > itemClass = mActivityCallbacks.get(i).getClass(); Log.d("LJY_LOG","itemClass:"+itemClass); if (itemClass.getName().equals("android.app.servertransaction.LaunchActivityItem")) { Field intentField = itemClass.getDeclaredField("mIntent"); intentField.setAccessible(true); Intent proxyIntent = (Intent) intentField.get(mActivityCallbacks.get(i)); Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT); proxyIntent.setComponent(intent.getComponent()); break; } } } catch (Exception e) { Log.d("LJY_LOG", "e = " + e.getMessage()); } break; default: break; } return false; // we must return false}); } catch (Exception e) { e.printStackTrace(); }}Copy the code
  • Try calling the jump again
binding.btnHookIActivityManager.setOnClickListener {
    hookAMS()
    hookHandler()
    val intent = Intent()
    val packageName = "com.jinyang.plugindemo"
    val activityName = "com.jinyang.plugin002.Plugin002Activity"
    intent.component = ComponentName(packageName, activityName)
    startActivity(intent)
}
Copy the code
  • We need to load the plugin class into the classLoader. Remember our loadDex method from the single DexClassLoader in class loading
binding.btnHookIActivityManager.setOnClickListener {
    loadDex(this, listOf(plugin001Path,plugin002Path))
    hookAMS()
    hookHandler()
    val intent = Intent()
    val packageName = "com.jinyang.plugindemo"
    val activityName = "com.jinyang.plugin002.Plugin002Activity"
    intent.component = ComponentName(packageName, activityName)
    startActivity(intent)
}
Copy the code
  • Will find error Resources $NotFoundException, here also need to replace the Resources in the plugin baseActivity, can refer to the above we will resource replacement loadResources method
class Plugin002Activity : PluginBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_plugin002) } } open class PluginBaseActivity2 : Activity() { private var mResources: Resources? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val plugin002Path: String = File(filesDir.absolutePath, "plugin002.apk").absolutePath mResources = loadResources(this, AssetManager::class.java.newInstance(), listOf( plugin002Path)) } override fun getResources(): Resources? { return mResources ? : super.getResources() } override fun getAssets(): AssetManager { return mResources? .assets ? : super.getAssets() } }Copy the code
  • Try the jump again and it will work
Hook Instrumentation program implementation
  • The startActivityForResult method calls the execStartActivity method of the Instrumentation to activate the Activity lifecycle;
  • Launching an Activity by an ActivityThread calls the performLaunchActivity method of the ActivityThread, which calls the newActivity method of the mInstrumentation, Internally, class loaders are used to create instances of the Activity.
  • So we can replace the mInstrumentation with a custom Instrumentation, and pass the AMS verification with the host’s pit Activity in the Instrumentation execStartActivity method, Revert to plug-in Activity in the newActivity method of Instrumentation;
  • Hook Instrumentation implementation is simpler than Hook IActivityManager implementation, the implementation steps are as follows
  1. Custom Instrumentation
class InstrumentationProxy( var realContext: Context, var base: Instrumentation, var context: ContextWrapper ) : Instrumentation() {private val KEY_COMPONENT = "commontec_component" companion object {/** * hook Replace Instrumentation with our own InstrumentationProxy */ Fun inject(activity: activity, context: ContextWrapper) {// Reflect is a reflection class copied from VirtualApp, Val reflect = reflect. on(activity) val activityThread = reflect.get<Any>("mMainThread") val base = Reflect.on(activityThread).get<Instrumentation>("mInstrumentation") val mInstrumentation = InstrumentationProxy(activity, base, context) Reflect.on(activityThread).set("mInstrumentation", MInstrumentation) reflect.on (activity). Set ("mInstrumentation", mInstrumentation)}} /** * newActivity creates an activity instance, Return the actual plug-in Activity that needs to be run, * so that the system will later call the corresponding lifecycle based on the Activity instance. */ override fun newActivity(cl: ClassLoader, className: String, intent: Intent): Activity? { val componentName = intent.getParcelableExtra<ComponentName>(KEY_COMPONENT) var clazz = context.classLoader.loadClass(componentName? .className) intent.component = componentName return clazz.newinstance () as Activity?} */ Private fun injectActivity(Activity: Activity?) {val intent = Activity?. Intent val base = Activity?. BaseContext try Reflect.on(base).set("mResources", context.resources) Reflect.on(activity).set("mResources", context.resources) Reflect.on(activity).set("mBase", context) Reflect.on(activity).set("mApplication", context.applicationContext) // for native activity val componentName: ComponentName? = intent!! .getParcelableExtra<ComponentName>(KEY_COMPONENT) val wrapperIntent = Intent(intent) wrapperIntent.setClassName(componentName? .packageName!! , componentName.className) activity.intent = wrapperIntent } catch (e: Exception) { } } override fun callActivityOnCreate(activity: Activity?, icicle: Bundle?) { injectActivity(activity) super.callActivityOnCreate(activity, icicle) } override fun callActivityOnCreate( activity: Activity?, icicle: Bundle?, persistentState: PersistableBundle? ) { injectActivity(activity) super.callActivityOnCreate(activity, icicle, PersistentState)} /** * Activity */ private fun injectIntent(intent: intent?) {var component: ComponentName? = null var oldComponent = intent?.component if (component == null || component.packageName == realContext.packageName) { component = ComponentName( "com.jinyang.plugindemo", "com.jinyang.plugindemo.hook.StubHookActivity" ) intent?.component = component intent?.putExtra(KEY_COMPONENT, OldComponent)}} /** * execStartActivity is a process that must be performed to start the Activity before AMS is reached. Replace the Activity with the StubActivity registered in the host * so that AMS will consider the Activity registered when detecting it */ fun execStartActivity(who: Context, contextThread: IBinder, token: IBinder, target: Activity, intent: Intent, requestCode: Int ): Instrumentation.ActivityResult? { log("exec..." ) injectIntent(intent) return Reflect.on(base) .call("execStartActivity", who, contextThread, token, target, intent, requestCode).get() } fun execStartActivity( who: Context? , contextThread: IBinder? , token: IBinder? , target: Activity? , intent: Intent, requestCode: Int, options: Bundle? ) : Instrumentation.ActivityResult? { log("exec..." ) injectIntent(intent) return Reflect.on(base) .call( "execStartActivity", who, contextThread, token, target, intent, requestCode, options ? : Bundle() ) .get() } fun execStartActivity( who: Context, contextThread: IBinder, token: IBinder, target: Fragment, intent: Intent, requestCode: Int, options: Bundle? ) : Instrumentation.ActivityResult? { log("exec..." ) injectIntent(intent) return Reflect.on(base) .call( "execStartActivity", who, contextThread, token, target, intent, requestCode, options ? : Bundle() ) .get() } fun execStartActivity( who: Context, contextThread: IBinder, token: IBinder, target: String, intent: Intent, requestCode: Int, options: Bundle? ) : Instrumentation.ActivityResult? { log("exec..." ) injectIntent(intent) return Reflect.on(base) .call( "execStartActivity", who, contextThread, token, target, intent, requestCode, options ? : Bundle() ) .get() } }Copy the code
  1. Call InstrumentationProxy. Inject, hook system, replacing Instrumentation for our own AppInstrumentation, and then to jump
binding.btnHookInstrumentation.setOnClickListener { InstrumentationProxy.inject( this, loadContext(this, resources.assets, listOf(plugin001Path, plugin002Path)) ) val intent = Intent() intent.setClass(this, pluginClassLoader.loadClass(activityName)) startActivity(intent) } fun loadContext(baseContext: Context, assetManager: AssetManager, pluginPaths: List<String>): ContextWrapper { loadDex(baseContext, pluginPaths) val resources = loadResources(baseContext,assetManager, Val mContext = ContextThemeWrapper(baseContext, 0) // Replace resources in our Context with val clazz = mContext::class.java val mResourcesField = clazz.getDeclaredField("mResources") mResourcesField.isAccessible = true mResourcesField.set(mContext, resources) return mContext }Copy the code

Loading plug-in Service

  1. Create a service in the plug-in
class PluginService : Service() { override fun onCreate() { log("plugin onCreate") super.onCreate() } override fun onStartCommand(intent: Intent? , flags: Int, startId: Int): Int { log("plugin onStartCommand") return super.onStartCommand(intent, flags, startId) } override fun onDestroy() { log("plugin onDestroy") super.onDestroy() } override fun onBind(intent: Intent?) : IBinder? { log("plugin onBind") return null } override fun onUnbind(intent: Intent?) : Boolean { log("plugin onUnbind") return super.onUnbind(intent) } }Copy the code
  1. Add a placeholder Service to the host app and call the plug-in Service’s lifecycle method in the corresponding lifecycle
<service android:name=".service.StubService" /> class StubService : Service() { var serviceName: String? = null var pluginService: Service? = null companion object { var pluginClassLoader: ClassLoader? = null fun startService(context: Context, classLoader: ClassLoader, serviceName: String) { log("StubService.startService") pluginClassLoader = classLoader val intent = Intent(context, StubService::class.java) intent.putExtra("serviceName", serviceName) context.startService(intent) } fun stopService(context: Context, classLoader: ClassLoader, serviceName: String) { log("StubService.stopService") pluginClassLoader = classLoader val intent = Intent(context, StubService::class.java) intent.putExtra("serviceName", serviceName) context.stopService(intent) } } override fun onCreate() { super.onCreate() log("StubService.onCreate") } override fun onStartCommand(intent: Intent? , flags: Int, startId: Int): Int { log("StubService.onStartCommand") val res = super.onStartCommand(intent, flags, startId) serviceName = intent? .getStringExtra("serviceName") if (pluginService == null) { pluginService = pluginClassLoader? .loadClass(serviceName)? .newInstance() as Service pluginService? .onCreate() } return pluginService? .onStartCommand(intent, flags, startId) ? : res } override fun onDestroy() { super.onDestroy() log("StubService.onDestroy") if (pluginService! =null) { pluginService? .onDestroy() pluginService = null } } override fun onBind(intent: Intent?) : IBinder? { log("StubService.onBind") return pluginService? .onBind(intent) } override fun onUnbind(intent: Intent?) : Boolean { log("StubService.onUnbind") return pluginService? .onUnbind(intent) ? : super.onUnbind(intent) } }Copy the code
  1. Start and end the plug-in Service with the trap Service
val plugin001Path = File(filesDir.absolutePath, "plugin001.apk").absolutePath
val nativeLibDir = File(filesDir, "pluginlib").absolutePath
val dexOutPath = File(filesDir, "dexout").absolutePath
val pluginClassLoader =
    DexClassLoader(plugin001Path, dexOutPath, nativeLibDir, this::class.java.classLoader)
val serviceName = "com.jinyang.plugin001.PluginService"

binding.btnStartService.setOnClickListener {
    StubService.startService(this, pluginClassLoader, serviceName)
}

binding.btnStopService.setOnClickListener {
    StubService.stopService(this, pluginClassLoader, serviceName)
}
Copy the code

Load the BroadcastReceiver plug-in

  • Dynamic broadcast: Load the broadcast classes in apK by ClassLoader and register them directly
  1. Create a utility class that registers the plug-in broadcast
class BroadcastUtils { companion object { private val broadcastMap = HashMap<String, BroadcastReceiver>() fun registerBroadcastReceiver(context: Context, classLoader: ClassLoader, action: String, broadcastName: String) { log("BroadcastUtils.registerBroadcastReceiver") val receiver = classLoader.loadClass(broadcastName).newInstance() as BroadcastReceiver val intentFilter = IntentFilter(action) context.registerReceiver(receiver, intentFilter) broadcastMap[action] = receiver } fun unregisterBroadcastReceiver(context: Context, action: String) { log("BroadcastUtils.unregisterBroadcastReceiver") val receiver = broadcastMap.remove(action) if (receiver! =null) { context.unregisterReceiver(receiver) } } } }Copy the code
  1. Register and use broadcasts in the plug-in
val testAction = "com.ljy.action.testBroadcastReceiver"
val broadcastName = "com.jinyang.plugin001.PluginBroadcastReceiver"
binding.btnRegisterBroadcastReceiver.setOnClickListener {
    BroadcastUtils.registerBroadcastReceiver(
        this,
        pluginClassLoader,
        testAction,
        broadcastName
    )
}
binding.btnSendBroadcast.setOnClickListener {
    sendBroadcast(Intent(testAction))
}
binding.btnUnregisterBroadcastReceiver.setOnClickListener {
    BroadcastUtils.unregisterBroadcastReceiver(this, testAction)
}
Copy the code
  • Static broadcast: The VirtualApk process is to register the static registered Receiver in the androidmanifest.xml plug-in apK to the host Context through the dynamic registerReceiver. The code is as follows
/** * Register the statically registered Receiver in the androidmanifest.xml plugin apk into the host Context via the dynamic registerReceiver */ fun parserPluginStaticBroadcast(context: Context, pluginPath: String?) {the try {/ / instantiate PackageParser object val mPackageParserClass = Class. Class.forname (" android. Content. PM. PackageParser ") val mPackageParser = mPackageParserClass.newInstance() // 1. Public Package parsePackage(File packageFile, int flags) To get the Package val mPackageParserMethod = mPackageParserClass. GetMethod (" parsePackage, "File: : class. Java, Int::class.javaPrimitiveType ) val mPackage = mPackageParserMethod.invoke( mPackageParser, File(pluginPath), Packagemanager.get_activities) // Get the ArrayList<Activity> property val receiversField = in mPackage mPackage.javaClass.getDeclaredField("receivers") val receivers = receiversField[mPackage] val arrayList = receivers as ArrayList<*> // This Activity is not a component Activity, it is an internal class in PackageParser for (mActivity in ArrayList) {// mActivity --> <receiver Android :name=".StaticReceiver"> // Get intents ArrayList<II> intents; A < receiver > tag can be corresponding to multiple Intent - Filter val mComponentClass = Class. Class.forname (" android. Content. PM. PackageParser \ $Component ") val  intentsField = mComponentClass.getDeclaredField("intents") val intents: ArrayList<IntentFilter> = intentsField[mActivity] as ArrayList<*> Get the component name activityInfo.name /** * ActivityInfo * public static final ActivityInfo generateActivityInfo(Activity A, int flags, * PackageUserState state, int userId) */ val mPackageUserState = Class.forName("android.content.pm.PackageUserState") val mUserHandle = Class.forName("android.os.UserHandle") val userId = mUserHandle.getMethod("getCallingUserId").invoke(null) as Int val generateActivityInfoMethod = mPackageParserClass.getDeclaredMethod( "generateActivityInfo", mActivity.javaClass, Int::class.javaPrimitiveType, mPackageUserState, Int::class.javaPrimitiveType ) generateActivityInfoMethod.isAccessible = true val mActivityInfo = generateActivityInfoMethod.invoke( null, mActivity, 0, mPackageUserState.newInstance(), userId ) as ActivityInfo val receiverClassName = mActivityInfo.name Log.e("LJY_LOG", "receiverClassName : $receiverClassName") val mStaticReceiverClass = context.classLoader.loadClass(receiverClassName) val broadcastReceiver =  mStaticReceiverClass.newInstance() as BroadcastReceiver for (intentFilter in intents) { Log.e("LJY_LOG", "intentFilter mActions size " + intentFilter.countActions()) context.registerReceiver(broadcastReceiver, intentFilter) } } } catch (e: Exception) { e.printStackTrace() } }Copy the code

Load the ContentProvider plug-in

  • How to forward the Uri to the correct plug-in Provider is the solution to define different plug-in paths in the Uri. Such as plugin1 corresponding is the Uri of the content: / / com. Zy. Stubprovider/plugin1, plugin2 corresponding Uri is the content: / / com. Zy. Stubprovider/plugin2, Then distribute different plug-in providers according to the corresponding plugin in StubContentProvider.
  1. Create a trap ContentProvider in the host and load the plugin ContentProvider through the plugin classLoader
<provider android:name=".contentprovider.StubContentProvider" android:authorities="com.ljy.StubContentProvider" /> class  StubContentProvider : ContentProvider() { private var pluginProvider: ContentProvider? = null private var uriMatcher: UriMatcher? = UriMatcher(UriMatcher.NO_MATCH) override fun insert(uri: Uri, values: ContentValues?) : Uri? { log("StubContentProvider.insert") return loadPluginProvider()? .insert(uri, values) } override fun query(uri: Uri, projection: Array<out String>? , selection: String? , selectionArgs: Array<out String>? , sortOrder: String?) : Cursor? { log("StubContentProvider.query: uri=$uri") if (isPlugin1(uri)) { return loadPluginProvider()? .query(uri, projection, selection, selectionArgs, sortOrder) } return null } override fun onCreate(): Boolean { log("StubContentProvider.onCreate") uriMatcher? .addURI("com.ljy.StubContentProvider", "plugin001", 1) uriMatcher? .addURI("com.ljy.StubContentProvider", "plugin002", 2) return true } override fun update(uri: Uri, values: ContentValues? , selection: String? , selectionArgs: Array<out String>?) : Int { log("StubContentProvider.update") return loadPluginProvider()? .update(uri, values, selection, selectionArgs) ? : 0 } override fun delete(uri: Uri, selection: String? , selectionArgs: Array<out String>?) : Int { log("StubContentProvider.delete") return loadPluginProvider()?.delete(uri, selection, selectionArgs) ?: 0 } override fun getType(uri: Uri): String { log("StubContentProvider.getType") return loadPluginProvider()?.getType(uri) ?: "" } private fun loadPluginProvider(): ContentProvider? { if (pluginProvider == null) { pluginProvider = classLoader?.loadClass("com.jinyang.plugin001.PluginContentProvider")?.newInstance() as ContentProvider? } return pluginProvider } private fun isPlugin1(uri: Uri?): Boolean { log("StubContentProvider.isPlugin1:${uriMatcher?.match(uri)}") if (uriMatcher?.match(uri) == 1) { return true } return false } }Copy the code
  1. use
binding.btnQueryContentProvider1.setOnClickListener { val uri = Uri.parse("content://com.ljy.StubContentProvider/plugin001") val cursor = contentResolver.query(uri, null, null, null, null) cursor? .moveToFirst() val res = cursor? .getString(0) log("provider query res: $res") cursor? .close() } binding.btnQueryContentProvider2.setOnClickListener { val uri = Uri.parse("content://com.ljy.StubContentProvider/plugin002") val cursor = contentResolver.query(uri, null, null, null, null) cursor? .moveToFirst() val res = cursor? .getString(0) log("provider query res: $res") cursor? .close() }Copy the code
  • Now that we’re done with the principles of plug-in, let’s take a look at Shadow. After all, this series of articles is exploring the Android open source framework

Shadow

Shadow principle:

  • Through the use of AOP ideas, the use of bytecode editing tools, in the compile time to change all activities in the plug-in’s parent class into a common class, and then let the shell hold the common type of the parent class to transfer it without Hack any system implementation. (Any software engineering problem can be solved by adding an intermediate layer.)

Why does Shadow require the plugin and host package names to be the same

  • The ApplicationId is typically set in build.gradle, and the string is recorded in two locations at compile time. The first file is logged in the application’s Androidmanifest.xml and the second file is logged in the application’s resources.arsc file.
  • The package name recorded in androidmanifest.xml is used to construct the Context object of the application. The system also obtains the package name from the Context to identify the installed application
  • Shadow is designed to avoid using proprietary apis and use a layer of middleware to make the plug-in code part of the host code. Why would it be any different if it were part of the ApplicationId? So by requiring the plug-in to be consistent with the host’s ApplicationId, the system will never be exposed that the plug-in code is not installed;
  • The resources object has some apis that receive package names as parameters. If the package name is dynamically obtained in a standalone installation or plug-in environment, these apis may fail and the desired resources may not be found.

Shadow using

  • The path is shadow-master \projects\sample\maven. There are three projects, host-project is the host project, manager-project is the plug-in management project. Plugin-project is a plug-in project;

reference

  • Deep understanding of Android plug-in technology
  • 【Android training manual 】 common technology – Android plug-in analysis
  • Implement a plug-in framework from scratch
  • Tencent Shadow — Zero reflection full dynamic Android plug-in framework officially open source
  • Full dynamic Design principle analysis of Shadow (this is one of a series of articles about Shadow by Shadow author)
  • Android Tencent Shadow plug-in Access guide
  • Shadow plug-in framework analysis