A, description,

Android plug-in related articles are based on Bao Jianqiang’s “Android plug-in development Guide” books to learn, but the subsequent code part is based on Android P source code.

What is plug-in

Before we really go to learn Android plug-in programming we need to know what is plug-in, the so-called plug-in simply means that the Android application can dynamically load those external dex files that do not belong to the current application. Most of these dex files are stored in lib, ZIP, and APK files downloaded from the server.

Three, the benefits of plug-in

Since the plug-in allows you to load Java code that is not part of your current application, the benefits are obvious. Such as

1. Our business requirements can be released without following the release of the version, increasing the frequency of business requirements iteration;

2. Delivering some business requirements through plug-ins can effectively reduce the package size;

3. Able to quickly fix online bugs without issuing a version;

And so on.

Fourth, the implementation of plug-in

In Android, the corresponding ClassLoader is generated by lib, ZIP, apk and other files containing dex downloaded from the server, and then the class class in the plug-in is loaded by the ClassLoader.

1. Select ClassLoader

Android provides two kinds of loaders, PathClassLoader and DexClassLoader respectively. From the interpretation of the source code and various blogs, PathClassLoader is only used to load APK in the data/app directory. It is the default Class loader for Android; DexClassLoader can be used to load lib, APK, zip and other files containing dex in external storage directories.

However, according to the actual source code and experimental results, both PathClassLoader and DexClassLoader can load lib, APK, zip and other files in the external storage directory. See this blog for details

Here we use the class loader DexClassLoader according to the official recommendation to achieve the final plug-in.

Fifth, load the source code implementation of the Activity in the plug-in

Before we start coding, we need to know that the activities, broadcast, Service, ContentProvider, and ordinary Java classes that we use in plugins are all ordinary Java classes to the host. There’s nothing special about it; So if you want the four components of the plug-in to have the same life cycle as the four components of a normal Android app, you need to trick AMS to do it. Since we need to fool AMS, we need to know something about the Activity startup process. Android9.0activitymanagerservice (ActivityManagerService, Android9.0ActivityManagerService

1. Dynamic proxy

Because dynamic proxy in the follow-up frequency is high, so we simply to understand the implementation of dynamic proxy

1.1 What is dynamic Proxy

In simple terms, an implementation class, a proxy object, is dynamically generated for an interface while the code is running.

1.2 Application scenarios in plug-in

We can use dynamic proxies to dynamically generate proxy objects for a system interface of the framework layer, and then use reflection to replace the object of the system interface of the framework layer with the dynamically generated proxy object, so that we can hack the method implementation of the framework layer. Of course, all of the above actions only apply to the current application process.

For example, we generated proxy for IActivityManager interface by means of dynamic proxy objects a, then the system ActivityManager. GetService () method needs access to replace object a, This allows us to know when the application calls methods such as startActivity. Specific implementation will follow the corresponding source code.

1.3 Dynamic proxy implementation

Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{interface.class}, new xxInvocationHandler()); Public class xxInvocationHandler implements InvocationHandler {// Private Object mBase; public xxInvocationHandler(Object base) { mBase = base; } @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable {return method.invoke(mBase, objects); }}Copy the code

2. Load the Activity in the plug-in

After having the knowledge of Activity startup process, dynamic proxy and so on, we can formally start the implementation of the Activity plug-in. Of course there are all sorts of problems with this implementation; Don’t be afraid, we will solve the framework layer source code one by one.

2.1 Plug-in APK preparation

To facilitate the implementation, we put the corresponding plug-in APK in the current assets directory, and then copy the APK to the data/user/0/ application package name /xx directory during the operation of the code. The basic code is as follows:

/** ** @param context * @param pluginName */ public static void copyApk(context, String pluginName) { DePluginSP sp = DePluginSP.getInstance(context); String filePath = sp.getString(Constants.COPY_FILE_PATH, ""); If (textutils.isempty (filePath)) {// if the plugin apk save path isEmpty, Show that no copy plugin apk File directory to the corresponding success saveApkFile = context. GetFileStreamPath (pluginName); if (null == saveApkFile) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { saveApkFile = context.getDataDir(); filePath = saveApkFile.getAbsolutePath() + pluginName; } else { filePath = "data/user/0/" + context.getPackageName() + "/" + pluginName; } } else { filePath = saveApkFile.getAbsolutePath(); } boolean result = extractAssets(context, filePath, pluginName); if (result) { sp.setString(Constants.COPY_FILE_PATH, filePath); } Log.i(TAG, "copy " + result); } else {// If the plug-in apk save path is not empty, and the local apK exists, do not copy again, otherwise it may have been deleted, then copy again to the corresponding directory // Of course, in the actual development of the situation will be much more complex. File File = new File(filePath); if (file.exists()) { Log.i(TAG, "had copy apk before,so no need copy again"); } else { Log.i(TAG, "althogh save apk file path success,but file not exists"); extractAssets(context, filePath, pluginName); }}}Copy the code

The above method is to determine whether to copy the plug-in APK to the corresponding directory, next is the copy part. Because the amount of code is not enough and the logic is very simple, I’ll just look at the code.

public static boolean extractAssets(Context context, String filePath, String pluginName) { AssetManager assetManager = context.getAssets(); FileOutputStream fileOutputStream = null; InputStream inputStream = null; try { Log.i(TAG, "save apk file path is " + filePath); fileOutputStream = new FileOutputStream(filePath); InputStream = assetManager.open(pluginName); byte[] bytes = new byte[1024]; int length = -1; While ((length = inputStream.read(bytes))! = -1) { fileOutputStream.write(bytes, 0, length); } fileOutputStream.flush(); return true; } catch (Exception e) { Log.e(TAG, "copy file failed " + e.getMessage()); } finally { try { if (null ! = inputStream) { inputStream.close(); } if (null ! = fileOutputStream) { fileOutputStream.close(); } } catch (Exception e) { Log.i(TAG, "extractAssets: " + e.getMessage()); } } return false; }Copy the code

2.2 ActivityManager的Hook

This hook together, simply by means of dynamic proxy will ActivityManager. GetService have access to the object of Binder agent to replace, then we can to invade methods such as start an Activity. The relevant code is as follows. The Refinvoke. Java class is available on Github.

Public static void hookAMN() {try {// Get ActivityManager class <? > mActivityManagerCls = RefInvoke.getClass("android.app.ActivityManager"); / / first in the class by reflecting access to ActivityManager singleton IActivityManagerSingleton / / then the Object to the Object is obtained through reflection IActivityManagerSingleton value Object mIActivityManagerSingletonObj = RefInvoke.getStaticFieldValue(RefInvoke.getField(mActivityManagerCls, "IActivityManagerSingleton"), mActivityManagerCls); // Obtain ActivityManager class object with Binder interface between ActivityManager and AMS. > mIActivityManagerCls = RefInvoke.getClass("android.app.IActivityManager"); if (null ! = mIActivityManagerSingletonObj) {/ / because the Singleton is the Singleton implementation class, MInstanceField = Refinvoke.getField (" Android.util.singleton ", "mInstance"); Object mInstance = refinvoke.getFieldValue (mInstanceField, mInstanceField, mInstance = refinvoke.getFieldValue) mIActivityManagerSingletonObj); Object proxy = proxy.newProxyInstance (thread.currentThread ().getContextClassLoader()), new Class[]{mIActivityManagerCls}, new AMSHookHelperInvocationHandler(mInstance)); . / / the ActivityManager getService get replaced singleton proxy objects RefInvoke. SetFieldValue (mInstanceField mIActivityManagerSingletonObj, proxy); } else { Log.i(TAG, "IActivityManagerSingleton not exists"); } } catch (Exception e) { Log.i(TAG, "hook ATM failed " + e); }}Copy the code

The next step is to implement the InvocationHandler interface to invade the startActivity method.

public class AMSHookHelperInvocationHandler implements InvocationHandler { private static final String TAG = Constants.TAG + "AMSHookHandler"; // Private Object mBase; public AMSHookHelperInvocationHandler(Object base) { mBase = base; } @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable {// Hijack the startActivity method to replace the Activity that the upper application really wants to start. If (textutils.equals (method.getName(), "startActivity")) { Log.i(TAG, "replace start up activity"); int index = -1; For (int I = 0; i < objects.length; i++) { if (objects[i] instanceof Intent) { index = i; break; } } if (-1 == index) { Log.i(TAG, "not found intent in params"); return method.invoke(mBase, objects); Intents = (Intent) objects[index]; // Generate a corresponding Intent object based on the predeclared Activity in the host to replace the Activity related Intent object passed by the upper-layer application. In order to reach the purpose of deceiving AMS Intent replacedStartUpIntent = realIntent. GetParcelableExtra (the REPLACED_START_UP_INTENT); if (null ! = replacedStartUpIntent) { Log.i(TAG, "origin intent is " + realIntent); realIntent.putExtra(Constants.REPLACED_START_UP_INTENT,""); replacedStartUpIntent.putExtra(Constants.START_UP_INTENT, realIntent); objects[index] = replacedStartUpIntent; Log.i(TAG, "replaced start up intent is " + replacedStartUpIntent); } else { Log.i(TAG, "replaced intent activity is null"); // call AMS return method.invoke(mBase, objects) with Binder; }}Copy the code

With the above hook to ActivityManager, we can now start the plug-in Activity directly in the application, using the following method.

public void click(View view) { int id = view.getId(); switch (id) { case R.id.plugin: Intent intent = new Intent(); ComponentName componentName = new ComponentName(getPackageName(), "com.xx.xx.pluginActivity"); intent.setComponent(componentName); intent.putExtra(Constants.REPLACED_START_UP_INTENT, createStartUpIntent()); startActivity(intent); break; }} // Create an Intent object with a predeclared Activity in the host to trick AMS, Private Intent createStartUpIntent() {Intent startUpIntent = new Intent(); ComponentName componentName = new ComponentName(DePluginApplication.getContext(), StandardStubActivity.class.getName()); startUpIntent.setComponent(componentName); startUpIntent.putExtra(Constants.DEX_PATH, DePluginSP.getInstance(DePluginApplication.getContext()).getString(Constants.COPY_FILE_PATH, "")); return startUpIntent; }Copy the code

Although we were able to launch the Activity in the plugin directly from the host without raising an ActivityNotFound exception, we ended up seeing that the Activity that was launched was not the Activity in the plugin, but our transit page StandardStubActivity. So in order to finally start the Activity as an Activity in the plug-in, we also need to hook each object in the ActivityThread.

2.3 Hooks in ActivityThread

This part of the process is a little more complicated, and therefore a little more code. Therefore, it is recommended that you have more time to look at the Activity start process source code analysis related articles.

2.3.1 mH的hook

MH in ActivityThread attribute, if the Handler is generated object directly through the reflection of the way to replace, final system will give you throw a ruthless hook H failed Java. Lang. IllegalArgumentException; This is because although the class corresponding to the mH attribute inherits the Handler object, its actual reference type is H. So it’s definitely not going to work. Java class dispatchMessage (MSG);

public void dispatchMessage(Message msg) { if (msg.callback ! = null) { handleCallback(msg); } else {// If the current Handler instance is empty, it will go to the handleMessage method. // So we can construct an interface instance for the mH instance that inherits the Handler. If (mCallback! = null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); }}Copy the code

So we first construct an instance object that implements the CallBack interface in Handler, and then assign this instance object to the mH object by reflection. The code is as follows:

Public static void hookH() {try {// Obtain ActivityThread instance Object ScurrenVityThread = by reflection RefInvoke.getStaticFieldValue(RefInvoke.getField("android.app.ActivityThread", "sCurrentActivityThread"), RefInvoke.getClass("android.app.ActivityThread")); / / get the mH in ActivityThread instance object Field mHField = RefInvoke. GetField (sCurrentActivityThread. GetClass (), "mH"); Handler mH = (Handler) RefInvoke.getFieldValue(mHField, sCurrentActivityThread); // The mCallBack attribute in the mH instance object is reflected to the instance object of ActivityThreadHandler RefInvoke.setFieldValue(RefInvoke.getField(Handler.class, "mCallback"), mH, new ActivityThreadHandler(mH)); Log.i(TAG, "hook H complete"); } catch (Exception e) { Log.i(TAG, "hook H failed " + e); }}Copy the code

The handleMessage method in the CallBack intercepts the start of the Activity, and replaces the Activity to be loaded with the Activity in the plug-in. Replace the ClassLoader object that loads the Activity with the ClassLoader object generated by the plugin APK. Finally, the Activity actually loaded in the ActivityThread is the Activity in the plugin.

public boolean handleMessage(@NonNull Message message) { int what = message.what; Switch (what) {// If ActivityThread < ActivityThread > Object = message.obj; List<Object> mActivityCallbacks = refinvoke. on(Object, "getCallbacks").invoke(); Class<? ComponentName = ComponentName = ComponentName = ComponentName = Class<? > mLaunchActivityItemCls = RefInvoke.getClass("android.app.servertransaction.LaunchActivityItem"); for (Object obj : mActivityCallbacks) { if (mLaunchActivityItemCls.isInstance(obj)) { Intent intent = getIntent(mLaunchActivityItemCls, obj); if (null == intent) { break; String path = intent.getStringExtra(Constants.DEX_PATH); if (TextUtils.isEmpty(path)) { Log.i(TAG, "dex path is empty,so do need replace class loader"); break; } // Replace it with ClassLoader replaceClassloader(mLaunchActivityItemCls, obj, path); // Replace the Activity in the plugin with the Activity replace(intent). break; } } } catch (Exception e) { Log.e(TAG, "getActivityToken failed " + e.getMessage()); } break; default: } mBase.handleMessage(message); return true; }Copy the code

2.3.2 this hook

The ClassLoader replacement for loading an Activity is a bit more complicated, so before implementing the code let’s take a quick look at the source code to see how to replace the ClassLoader that loads the Activity with the ClassLoader that loads the Activity in the plug-in.

The source code parsing

Initializing the Activity in ActivityThread is done in performLaunchActivity.

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ActivityInfo aInfo = r.activityInfo; If (r.packageInfo == null) {r.packageInfo = getPackageInfo(aInfo. ApplicationInfo, r.patinfo, Context.CONTEXT_INCLUDE_CODE); } ComponentName component = r.intent.getComponent(); if (component == null) { component = r.intent.resolveActivity( mInitialApplication.getPackageManager()); r.intent.setComponent(component); } if (r.activityInfo.targetActivity ! = null) { component = new ComponentName(r.activityInfo.packageName, r.activityInfo.targetActivity); } / / for the current start Activity generates the corresponding Context object ContextImpl appContext = createBaseContextForActivity (r); Activity activity = null; Try {/ / for this object to load need to start the Activity class. Java lang. This cl = appContext. GetClassLoader (); activity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); . } catch (Exception e) { if (! mInstrumentation.onException(activity, e)) { throw new RuntimeException( "Unable to instantiate activity " + component + ": " + e.toString(), e); }}... return activity; }Copy the code

In this method, the LoadedApk object is first generated based on objects such as ApplicationInfo carried in the Activity to be started. Then LoadedApk uses the ClassLoader attribute to generate a Context object for the Activity that needs to be started, and loads the Activity class that needs to be started through the ClassLoader. CreateActivityContext; ContextImpl createActivityContext;

static ContextImpl createActivityContext(ActivityThread mainThread, LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId, Configuration overrideConfiguration) { if (packageInfo == null) throw new IllegalArgumentException("packageInfo"); String[] splitDirs = packageInfo.getSplitResDirs(); / / get LoadedApk in this object, and based on the this create corresponding Context object this this = packageInfo. GetClassLoader (); . ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName, activityToken, null, 0, classLoader); . final ResourcesManager resourcesManager = ResourcesManager.getInstance(); context.setResources(resourcesManager.createBaseActivityResources(activityToken, packageInfo.getResDir(), splitDirs, packageInfo.getOverlayDirs(), packageInfo.getApplicationInfo().sharedLibraryFiles, displayId, overrideConfiguration, compatInfo, classLoader)); context.mDisplay = resourcesManager.getAdjustedDisplay(displayId, context.getResources()); return context; }Copy the code

Since the ClassLoader that ultimately loads the Activity class comes from the LoadedApk object, all we need to do is replace the ClassLoader object in the LoadedApk object from the getPackageInfo method above with the plug-in generated by the apK plug-in. But the problem is that we can’t hook the getPackageInfo method, so we can’t catch the call timing of the method, so we can’t dynamically replace the LoadedApk generated by getPackageInfo.

So what we’re going to do here is create our own LoadedApk object directly through getPackageInfo. Why we can do this, we still need to look at the source code to know. The getPackageInfo method called in performLaunchActivity will eventually be called in the overloaded method of that method as follows:

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) { final boolean differentUser = (UserHandle.myUserId() ! = UserHandle.getUserId(aInfo.uid)); synchronized (mResourcesManager) { WeakReference<LoadedApk> ref; if (differentUser) { ref = null; LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk } else if (includeCode) {ref = mpackages.get (ainfo.packagename); } else { ref = mResourcePackages.get(aInfo.packageName); } // If no cache exists, regenerate LoadedApk object and add it to mPackages LoadedApk packageInfo = ref! = null ? ref.get() : null; if (packageInfo == null || (packageInfo.mResources ! = null && ! packageInfo.mResources.getAssets().isUpToDate())) { if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package " : "Loading resource-only package ") + aInfo.packageName + " (in " + (mBoundApplication ! = null ? mBoundApplication.processName : null) + ")"); packageInfo = new LoadedApk(this, aInfo, compatInfo, baseLoader, securityViolation, includeCode && (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) ! = 0, registerPackage); if (mSystemThread && "android".equals(aInfo.packageName)) { packageInfo.installSystemApplicationInfo(aInfo, getSystemContext().mPackageInfo.getClassLoader()); } if (differentUser) { // Caching not supported across users } else if (includeCode) { mPackages.put(aInfo.packageName, new WeakReference<LoadedApk>(packageInfo)); } else { mResourcePackages.put(aInfo.packageName, new WeakReference<LoadedApk>(packageInfo)); } } return packageInfo; }}Copy the code

According to the above source code analysis, how to generate the corresponding LoadedApk object and ClassLoader object our idea is very clear.

(1) First, we generate the corresponding ApplicationInfo object and CompatibilityInfo in apK by reflection;

(2) Then call LoadedApk and set the PackageName in ActivityInfo ApplicationInfo to ApplicatioInfo. So when the Activity initializes, we can directly fetch the LoadedApk object that we created in the cache.

(3) Finally, the ClassLoader generated by the plug-in Apk is assigned to the member variables in the LoadedApk object in a reflective way, and here the plug-in of the whole Activity is completed.

public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo, int flags) { ..... }Copy the code

The generateApplicationInfo method of the PackageParser class is called by reflection. The generateApplicationInfo method of the PackageParser class is called by reflection. The second is slightly simpler, directly get the host application to the PackageManager object, and then call the getPackageArchiveInfo method in the PackageManager. Space is limited, here is the second method, if you need to go to Github to learn about the first method.

ApplicationInfo object is created

private static ApplicationInfo getApplicationInfoByPackageArchiveInfo(String pluginPath) { / / according to the path of the plugin apk first generate the corresponding PackageInfo object PackageManager PackageManager. = DePluginApplication getContext () getPackageManager (); if (null == packageManager) { Log.i(TAG, "get PackageManager failed"); return null; } PackageInfo packageInfo = packageManager.getPackageArchiveInfo(pluginPath, 0); if (null == packageInfo) { Log.i(TAG, "get packageInfo failed"); return null; } / / return the PackageInfo ApplicationInfo object return PackageInfo. ApplicationInfo; } public static ApplicationInfo generateApplicationInfo(String pluginPath) { try { ApplicationInfo applicationInfo = getApplicationInfoByPackageArchiveInfo(pluginPath); if (null == applicationInfo) { Log.i(TAG, "get applicationInfo failed"); return null; } / / set resource loading paths have corresponding articles (subsequent) applicationInfo. SourceDir = pluginPath; applicationInfo.publicSourceDir = pluginPath; // Set which uid applicationInfo.uid = process.myuid (); return applicationInfo; } catch (Exception e) { Log.i(TAG, "generateApplicationzInfo failed " + e.getMessage()); } return null; }Copy the code

Generate the LoadedApk object using the getPackageInfo method above. After you have the ApplicationInfo object, all you need is a CompatibilityInfo object, For this object, we can directly find a default instance object in the compatibilityInfo.java source code, that is, DEFAULT_COMPATIBILITY_INFO. So we can get the object directly by reflection.

Generate the LoadedApk object and add it to the cache

With the above preparations, all we need is the east wind. GetPackageInfo method source code implementation is as follows:

private void replaceClassloader(Class<? > mLaunchActivityItemCls, Object obj, String PATH) throws Exception {// Obtain ActivityThread Object ScurrenVityThread = RefInvoke.getStaticFieldValue(RefInvoke.getField("android.app.ActivityThread", "sCurrentActivityThread"), RefInvoke.getClass("android.app.ActivityThread")); // Get the map set of cached LoadedApk objects; // Because at the end of the call getPackageInfo method has gave us the cache automatically Field mPackagesField = RefInvoke. GetField (sCurrentActivityThread. GetClass (), "mPackages"); if (null == mPackagesField) { Log.i(TAG, "get mPackages field failed"); return; } ArrayMap mPackages = (ArrayMap) mPackagesField.get(sCurrentActivityThread); if (null == mPackages) { Log.i(TAG, "can not get mPackages"); return; } // Get ApplicationInfo object ApplicationInfo ApplicationInfo = Utils.generateApplicationInfo(DePluginSP.getInstance(DePluginApplication.getContext()).getString(Constants.COPY_FILE_PAT H, "")); if (null ! = applicationInfo) {// Get the default Object defaultCompatibilityInfo = RefInvoke.getStaticFieldValue(RefInvoke.getField("android.content.res.CompatibilityInfo", "DEFAULT_COMPATIBILITY_INFO"), RefInvoke.getClass("android.content.res.CompatibilityInfo")); LoadedApk = Refinvoke. on(sCurrentActivityThread, "getPackageInfo", ApplicationInfo.class, RefInvoke.getClass("android.content.res.CompatibilityInfo"), int.class).invoke(applicationInfo, defaultCompatibilityInfo, Context.CONTEXT_INCLUDE_CODE); String pluginPkgName = applicationInfo.packageName; if (! TextUtils.isEmpty(pluginPkgName)) { Log.i(TAG, "plugin pkg name is " + pluginPkgName); ReplacePkgName (mLaunchActivityItemCls, obj, pluginPkgName); replacePkgName(mLaunchActivityItemCls); SetClassloader (LoadedApk, path); LoadedApk (LoadedApk, path) // Since the LoadedApk object is automatically cached for us when we call getPackageInfo, // mpackages. put(pluginPkgName, new WeakReference<>(loadedApk)); } else { Log.i(TAG, "get plugin pkg name failed"); } } else { Log.i(TAG, "can not get application info"); Private void replacePkgName(Class<? > mLaunchActivityItemCls, Object obj, String pkgName) throws Exception { ActivityInfo activityInfo = (ActivityInfo) RefInvoke.getFieldValue(RefInvoke.getField(mLaunchActivityItemCls, "mInfo"), obj); activityInfo.applicationInfo.packageName = pkgName; Private void setClassloader(LoadedApk, LoadedApk, LoadedApk, LoadedApk) String path) throws Exception { Log.i(TAG, "dex path is " + path); DexClassLoader dexClassLoader = DeHostDexClassloader.getInstance().getDexClassLoader(DePluginApplication.getContext(), path); RefInvoke.setFieldValue(RefInvoke.getField(loadedApk.getClass(), "mClassLoader"), loadedApk, dexClassLoader); }Copy the code

Finally, replace ComponentName in the Intent passed by AMS with Component corresponding to the Intent for which we need to start the Activity. The code is as follows:

Private Boolean replace(Intent Intent) throws Exception {// Get the Intent object that we cache in ActivityManger  intent.getParcelableExtra(Constants.START_UP_INTENT); if (null == realIntent) { return false; } Log.i(TAG, "origin start up Intent is " + realIntent); ComponentName = ComponentName (); ComponentName = Component.getComponent (); if (null ! = componentName) { intent.setComponent(componentName); return true; } else { Log.i(TAG, "real start up intent has not component,please set"); } return false; }Copy the code

When you click hook+ to start the Activity plug-in, The system will give you relentlessly throwing a Unable to instantiate the application android. The app. Application: Java. Lang. An IllegalStateException: Unable to get package info Exception. To search in the code, the exception in class LoadedApk. Java initializeJavaContextClassLoader function, source code is as follows:

private void initializeJavaContextClassLoader() { IPackageManager pm = ActivityThread.getPackageManager(); android.content.pm.PackageInfo pi; try { pi = pm.getPackageInfo(mPackageName, PackageManager.MATCH_DEBUG_TRIAGED_MISSING, UserHandle.myUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } if (pi == null) { throw new IllegalStateException("Unable to get package info for " + mPackageName + "; is package not installed?" ); }... }Copy the code

With the error of the original single support, the final processing is very simple, is through the dynamic proxy generated IPackageManager proxy object, and intercept method getPackageInfo call process, and finally directly return a created good PackageInfo OK. See Github for details, and you can now happily start your Activity in the plugin.

3, the last

The above is the basic steps to implement the Activity plug-in programming on Android 9 and part of the code, there is a need for a complete demo can be downloaded on Github.

Of course, in addition to loading the Activity in the plugin by using the corresponding ClassLoader generated by the plugin APK, we can also merge the plugin APK into the dexElements array of class DexPathList. Java by reflection. And add the resources of the plug-in APK to the AssetsManager currently applied. Because this part also has the resource ID conflict problem, all subsequent articles will be specially introduced.

In addition to the Activity plug-in, there will be Service plug-in, BroadcastReceiver plug-in and ContentProvider plug-in related articles and demo output.