Plug-in Development-Dynamic Loading Technology Loading Installed and Uninstalled APKs

Links to the original text: https://blog.csdn.net/u010687392/article/details/47121729

First, a concept is introduced. What is dynamic loading technology? Why introduce dynamic loading? What good is it? First of all, we need to understand these questions.

Starting with the application, we all know that in Android App, the maximum number of methods for an application dex file can not exceed 65536, otherwise, your app

It will be abnormal, so if the bigger project is sure to exceed, such as the US group, Alipay and so on are using dynamic loading technology, Alipay in last year's technology.

Tech-sharing conferences have promoted plug-in applications, and American Corps has also announced their solutions: Dex automatic unpacking and dynamic loading technology. So make

Dynamic loading technology is used to solve this kind of problem. And its advantage can let the application implement plug-in and plug-in structure, which need not be said about the later maintenance function.

1. What is dynamic loading technology?
Dynamic loading technology is to use class loader to load the corresponding apk, dex, jar (must contain DEX files), and then get the internal resources (class, image, color, etc.) of the apk, dex, jar through reflection for the use of the host app.

2. Class Loaders for Dynamic Loading
These two classes of loaders are commonly used when using dynamic loading technology:
PathClassLoader - Only the installed apk, that is, the APK in the / data/app directory, can be loaded.
DexClassLoader - can load apk, jar, dex that are not installed in the mobile phone, as long as the corresponding path can be found.
These two loaders correspond to different scenarios, so next, we will explain the use of the same plug-in apk loaded by each loader.
3. Load the installed apk plug-in using PathClassLoader to obtain the corresponding resources for the host app to use.
Here's a demo to illustrate the use of PathClassLoader:
1. First, we need to know the attribute in a manifest: Shared UserId.

What is this attribute used for? Simply put, when an application is installed on an Android system from the beginning, the system assigns it a linux user id.
If the sharedUserId of the two applications is the same, then they will run together in the same linux process, so they can share data and access resources. So we define the same sharedUserId on both the manifest of the host app and the plug-in app.

2. How does the host app know if the plug-in is a plug-in of our application when we install the plug-in apk on the mobile phone?
Have we defined that the plug-in apk uses the same sharedUserId before? Then, I can think about whether we can get all the sharedUserId installed in the mobile phone. Then we can judge whether the sharedUserId is the same as the host app. If so, the app is our plug-in app. This is the way of thinking, then the biggest problem with thinking is how to get sharedUserId in an application. We can get it through PackageInfo.sharedUserId. See the code:

/**
     * Find all the plug-ins in your phone
     * @return Return a plug-in List
     */
    private List<PluginBean> findAllPlugin() {
        List<PluginBean> plugins = new ArrayList<>();
        PackageManager pm = getPackageManager();
        //Find all installed apk files through the package manager
        List<PackageInfo> packageInfos = pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);
        for (PackageInfo info : packageInfos) {
            //Get the package name of the current apk
            String pkgName = info.packageName;
            //sharedUserId to get the current apk
            String shareUesrId = info.sharedUserId;
            //Determine whether this apk is a plug-in for our application
            if (shareUesrId != null && shareUesrId.equals("com.sunzxyong.myapp") && !pkgName.equals(this.getPackageName())) {
                String label = pm.getApplicationLabel(info.applicationInfo).toString();//Get the name of the plug-in apk
                PluginBean bean = new PluginBean(label,pkgName);
                plugins.add(bean);
            }
        }
        return plugins;
    }

With this code, we can easily get all the plug-ins that exist in the mobile phone. PluginBean is just an entity class defined, so we don't paste its code.

3. If the plug-in is found, the available plug-in is displayed. If not, the user can be prompted to download the plug-in first.

   List<HashMap<String, String>> datas = new ArrayList<>();
            List<PluginBean> plugins = findAllPlugin();
            if (plugins != null && !plugins.isEmpty()) {
                for (PluginBean bean : plugins) {
                    HashMap<String, String> map = new HashMap<>();
                    map.put("label", bean.getLabel());
                    datas.add(map);
                }
            } else {
                Toast.makeText(this, "No plug-in was found, please download it first!", Toast.LENGTH_SHORT).show();
            }
            showEnableAllPluginPopup(datas);

4. If found, then when we select the corresponding plug-in, we load the corresponding resources in the plug-in in the host app, which is the focus of PathClassLoader. Let's first see how it can be achieved.

/**
     * Load the installed apk
     * @param packageName Application package name
     * @param pluginContext Context of plug-in app
     * @return id of corresponding resource
     */
    private int dynamicLoadApk(String packageName, Context pluginContext) throws Exception {
        //The first parameter is the path of the apk or jar containing dex, and the second parameter is the parent loader.
        PathClassLoader pathClassLoader = new PathClassLoader(pluginContext.getPackageResourcePath(),ClassLoader.getSystemClassLoader());
//        Class <?> clazz = pathClassLoader. loadClass (packageName + ".R $mipmap"); // Reflect the mipmap class by using its own loader and then use its functions
        //Parameters: 1. Full name of class, 2. Initialization of class, 3. Class loader used in loading
        Class<?> clazz = Class.forName(packageName + ".R$mipmap", true, pathClassLoader);
        //Both of the above methods can be used. Here we get the internal class mipmap in R class, through which we can get the corresponding picture id, and then use it for us.
        Field field = clazz.getDeclaredField("one");
        int resourceId = field.getInt(R.mipmap.class);
        return resourceId;
    }

This method is to load the plug-in package named packageName, and then get the resource id of the image named one.png in the plug-in for the host app to use the image. Now let's explain step by step:
First, a PathClassLoader object is generated by new, which is constructed by:

public PathClassLoader(String dexPath, ClassLoader parent)

The first parameter is to get the path of the plug-in APK through the context of the plug-in, which is / data/app/apkthemeplugin.apk. So how to get the context of the plug-in? In the host app, we only have the context of the app. The answer is to create a context for the plug-in app:

 //Get the context in the corresponding plug-in, through which you can get the Resouurce of the plug-in
            Context plugnContext = this.createPackageContext(packageName, CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_CODE);

The context is created by the package name of the plug-in, but this method is only suitable for getting the installed app context. Or it doesn't need to be reflected directly through the plug-in context getResource (). getxx (R.); it's OK, and here's the reflection method.
The second parameter is the parent loader, both ClassLoader. getSystem ClassLoader ().

Okay, we created the class loader of the plug-in app, and then we got the resources of the corresponding class by reflection. Here I got the internal class mipmap class in R class, and then we got the value of the field named one in the mipmap class by reflection.

plugnContext.getResources().getDrawable(resouceId)

You can get the corresponding id of Drawable to get the image resources and then host app can use it to set the background and so on.
Of course, you can also get other resources or Acitivity classes, etc. Here's just an example.
Prepare: For class R, the directory in AS is: / build / generated / source / R / debug /< - packageName ->. Its internal classes are:

At this time, some people will think that this plug-in needs to be downloaded and installed into the mobile phone. This is not just an apk installed, but not shown. This is not a friendly way. So, can you download it only, without installing it, but also for the use of the host app? It's certainly possible to run airplane battles without installations on Wechat. This requires another loader, DexClassLoader.

4. DexClassLoader loads the uninstalled apk to provide resources for the host app to use
For dynamically loading an uninstalled apk, I first describe the idea: First, we get the directory where our plug-in APK is stored in advance, then get the information of the plug-in apk (name, package name, etc.), then display the available plug-ins, and finally dynamically load the APK to obtain resources.
According to the above idea, we need to solve several problems:
1. How to get the information of the unassembled apk
2. How to get the context or resource of the plug-in, because it is not installed and can not pass through the createPackageContext(... Method to construct a context, so at this point only work on Resource.
Now let's answer these questions one by one.
1. Uninstalled apk information can be obtained through the mPackageManager.getPackageArchiveInfo() method.

public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags)

It just passes in a FilePath and returns the PackageInfo information of the apk file:

/**
     * Get information about the unassembled apk
     * @param context
     * @param archiveFilePath apk The path of the file
     * @return
     */
    private String[] getUninstallApkInfo(Context context, String archiveFilePath) {
        String[] info = new String[2];
        PackageManager pm = context.getPackageManager();
        PackageInfo pkgInfo = pm.getPackageArchiveInfo(archiveFilePath, PackageManager.GET_ACTIVITIES);
        if (pkgInfo != null) {
            ApplicationInfo appInfo = pkgInfo.applicationInfo;
            String versionName = pkgInfo.versionName;//version number
            Drawable icon = pm.getApplicationIcon(appInfo);//Icon
            String appName = pm.getApplicationLabel(appInfo).toString();//app name
            String pkgName = appInfo.packageName;//Package name
            info[0] = appName;
            info[1] = pkgName;
        }
        return info;
    }

2. To get esource objects that correspond to unstalled apk s, we need reflection to obtain:

/**
     * @param apkName 
     * @return Get the Resource object of the corresponding plug-in
     */
    private Resources getPluginResources(String apkName) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);//Reflective call method addAssetPath(String path)
            //The second parameter is the path of apk: Environment. getExternal Storage Directory (). getPath ()+File. separator+ "plugin"+File. separator+ "apkplugin. apk"
            addAssetPath.invoke(assetManager, apkDir+File.separator+apkName);//Add the uninstalled Apk file to Asset Manager, the second parameter is the path of the APK file with the APK name
            Resources superRes = this.getResources();
            Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),
                    superRes.getConfiguration());
            return mResources;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

By getting the internal method addAssetPath in AssetManager, the unassembled apk path is added to the AssetManager, and then the AssetManager is passed into the construction method through the new Resource, and the corresponding Resource object of the unassembled apk is obtained.

Okay! The above two problems are solved, so the next step is to load the uninstalled apk to get its internal resources.

/**
     * Loading apk to get internal resources
     * @param apkDir apk Catalog
     * @param apkName apk Name, with. apk
     * @throws Exception
     */
    private void dynamicLoadApk(String apkDir, String apkName, String apkPackageName) throws Exception {
        File optimizedDirectoryFile = getDir("dex", Context.MODE_PRIVATE);//Create a folder directory called app_dex under the application installation directory, but not if it already exists
        Log.v("zxy", optimizedDirectoryFile.getPath().toString());// /data/data/com.example.dynamicloadapk/app_dex
        //Parameters: 1. Path of apk file or jar file containing dex, 2. Decompression of apk and jar to generate directory stored in dex, 3. Local library directory, generally null, 4. Parent ClassLoader
        DexClassLoader dexClassLoader = new DexClassLoader(apkDir+File.separator+apkName, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
        Class<?> clazz = dexClassLoader.loadClass(apkPackageName + ".R$mipmap");//By using apk's own class loader, the corresponding internal classes in R class are reflected to obtain the resource id we need.
        Field field = clazz.getDeclaredField("one");//Get the picture field named one
        int resId = field.getInt(R.id.class);//Get the picture id
        Resources mResources = getPluginResources(apkName);//Get Resource s in the Plug-in apk
        if (mResources != null) {
            //Get resId resources through Resource in plug-in apk
            findViewById(R.id.background).setBackgroundDrawable(mResources.getDrawable(resId));
        }
    }

Here we create a class loader with no apk installed through new DexClassLoader(). Let's take a look at its parameters:

public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

dexPath - The path to the apk file
OptidDirectory - APK decompressed directory to store dex, it is noteworthy that after 4.1, the directory is not allowed on the sd card to see official documents:

A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.
 
This class loader requires an application-private, writable directory to cache optimized classes. Use Context.getDir(String, int) to create such a directory:
 
   File dexOutputDir = context.getDir("dex", 0);
 
Do not cache optimized classes on external storage. External storage does not provide access controls necessary to protect your application from code injection atta

So we use the getDir() method to create a dexOutputDir inside the application.
libraryPath - Local library, generally null
Parent-parent loader
Next, it is through reflection method to obtain the required resources.

Let's take a look at the demo demonstration. I put three apk plug-ins in the assets directory first, then copy them to sd to imitate the download process, and then load the resources of the corresponding plug-ins:
First copy only one plug-in:

copyApkFile("apkthemeplugin-1.apk");

You can see that the resources without the apk are normally captured.
Look at three copies of plug-ins:

copyApkFile("apkthemeplugin-1.apk");
copyApkFile("apkthemeplugin-2.apk");
copyApkFile("apkthemeplugin-3.apk");

You can see that once a plug-in is downloaded, it can be displayed and used.

Of course, plug-in development is not just as simple as this skin change. It's just a demo. Learn the plug-in development idea. From this we can imagine whether this plug-in development is like the expression packages and background skins in QQ. By downloading online and maintaining offline, the corresponding skins can be downloaded online and deleted when not in use. Therefore, plug-in development is the decoupling of plug-ins and host apps, even without plug-ins. In this case, it will not have any impact on the host app, and in some cases it will be used selectively by the user.

Keywords: Mobile Android Attribute Linux

Added by zachatk1 on Wed, 31 Jul 2019 18:32:41 +0300