The author

Hello everyone, my name is Xiao Xin, you can also call me crayon xiao Xin 😊;

I graduated from Sun Yat-sen University in 2017 and joined the 37 mobile Game Android team in July 2018. I once worked in Jubang Digital as an Android development engineer.

Currently, I am the overseas head of android team of 37 mobile games, responsible for relevant business development; Also take care of some infrastructure related work

directory

  • Hot Repair Thoughts (This chapter explains)

  • Hot thought Demo article

  • Thermal repair actual combat

Introduction There are two ways to implement plug-in in the market: hook way and pile way. In hook mode, because hook system API is required, continuous adaptation is required with the change of system API. Therefore, the future trend of piling scheme, I am more optimistic about the implementation of agent scheme

About steps

  • Design criteria
  • Follow this standard when developing plug-ins
  • The host uses a custom ClassLoader, Resources, to prepare the environment for loading the plug-in
  • Load the plug-in Activity with an empty Activity peg in the host manifest file

Implement case

Design standards (can be a separate module because the host and plug-in need the same set of standards)

public interface IActivityInterface {
    public void setAppContext(Activity activity);

    public void onCreate(Bundle bundle);

    public void setContentView(int layoutId);
}
Copy the code

Develop plug-ins to follow this standard (note that only code snippets are captured below)

public class BaseActivity implements IActivityInterface {

    private Activity mActivity;

    @Override
    public void setAppContext(Activity activity) {
        Log.i("I'm a plug-in."."setAppContext");
        mActivity = activity;
    }

    @Override
    public void onCreate(Bundle bundle) {
        Log.i("I'm a plug-in."."onCreate");
    }

    @Override
    public void setContentView(int layoutId) {
        Log.i("I'm a plug-in."."setContentView"); mActivity.setContentView(layoutId); }}Copy the code
public class PluMainActivity extends BaseActivity {

    @Override
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle); setContentView(R.layout.activity_plu); }}Copy the code

The host uses a custom ClassLoader, Resources, to prepare the environment for loading the plug-in

  • 1) Processing of ClassLoader

The Android ClassLoader is derived from DexClassLoader and PathClassLoader. The difference between the two

DexClassLoader: loads uninstalled JAR, APK, and dex

PathClassLoader: Only apK installed in the system can be loaded

During the vm installation, the CLASS_ISPREVERIFIED flag is marked for the classes. If the following conditions are met:

During class loading, due to the parent delegation mechanism of the ClassLoader, if the class in the plug-in is loaded, the host class will not be loaded and the plug-in will be used, and vice versa. This is easy to trigger verify the above said problems, so as to quote us exception “Java. Lang. IllegalAccessError: Class ref in the pre – verified Class…”

How to avoid it?

You can customize the ClassLoader to modify the classloading logic so that the plug-in and the classes in the host are loaded separately.

Benefits of separate loading: Plug-in and host-dependent common modules require no special handling.

package com.sq.a37syplu10.plugin.loader;

import android.os.Build;

import dalvik.system.DexClassLoader;

public class ApkClassLoader extends DexClassLoader {

    private ClassLoader mGrandParent;
    private final String[] mInterfacePackageNames;

    public ApkClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent, String[] interfacePackageNames) {

        super(dexPath, optimizedDirectory, librarySearchPath, parent);

        ClassLoader grand = parent;
        mGrandParent = grand.getParent();
        this.mInterfacePackageNames = interfacePackageNames;
    }

    @Override
    protectedClass<? > loadClass(String className,boolean resolve) throws ClassNotFoundException {
        String packageName;
        int dot = className.lastIndexOf('. ');
        if(dot ! = -1) {
            packageName = className.substring(0, dot);
        } else {
            packageName = "";
        }

        boolean isInterface = false;
        for (String interfacePackageName : mInterfacePackageNames) {
            if (packageName.equals(interfacePackageName)) {
                isInterface = true;
                break; }}if (isInterface) {
            return super.loadClass(className, resolve);
        } else{ Class<? > clazz = findLoadedClass(className);if (clazz == null) {
                ClassNotFoundException suppressed = null;
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    suppressed = e;
                }

                if (clazz == null) {
                    try {
                        clazz = mGrandParent.loadClass(className);
                    } catch (ClassNotFoundException e) {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                            e.addSuppressed(suppressed);
                        }
                        throwe; }}}returnclazz; }}/** * Read the implementation of the interface from apK **@paramClazz interface class *@paramClassName specifies the className of the implementation class *@param<T> Interface type *@returnInterface required *@throws Exception
     */
    public <T> T getInterface(Class<T> clazz, String className) throws Exception {
        try{ Class<? > interfaceImplementClass = loadClass(className); Object interfaceImplement = interfaceImplementClass.newInstance();return clazz.cast(interfaceImplement);
        } catch (ClassNotFoundException | InstantiationException
                | ClassCastException | IllegalAccessException e) {
            throw newException(e); }}}Copy the code

In addition to separating the host from the plug-in’s classloading, the above code also reserves a whitelist. Because when the host and the plug-in follow the same set of standards, you need to convert the classes loaded in the plug-in to the type of the host standard. A class that is loaded according to the same class loader and has the same name is considered the same class. The interface loaded by the parent loader is required to perform type conversion. Therefore, IActivityInterface needs to be whitelisted.

BaseActivity refers to the IActivityInterface, and the classes referenced by BaseActivity belong to the same dex, so BaseActivity is identified. An error is reported when using the host’s IActivityInterface.

So, what’s the solution?

Handle the plug-in standards into JARS, compileOnly dependencies, not into the plug-in APK. This way the BaseActivity is not marked, and the problem is solved. That is, if interface type conversion is required between the host and the plug-in, the interface is removed from the plug-in.

  • 2) Deal with Resources

Routine scheme:

AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, mPluginPath);
Resources resources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
Copy the code

Disadvantage 1: Reflection is used, and the addAssetPath method is deprecated, even in older versions

Disadvantage 2: If only the plugin Resouces is used, other resources in front of the host setContentView method cannot be loaded, and an exception is reported in the log indicating that resources related to the support package cannot be found.

Adopt the scheme in Tencent Shadow:

The first step is to load resources from the plug-in without reflection:

 private Resources buildPluginResources(a) {
        try {
            PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(
            mContext.getPackageName(),
                    PackageManager.GET_ACTIVITIES
                            | PackageManager.GET_META_DATA
                            | PackageManager.GET_SERVICES
                            | PackageManager.GET_PROVIDERS
                            | PackageManager.GET_SIGNATURES);
            packageInfo.applicationInfo.publicSourceDir = mPluginPath;
            packageInfo.applicationInfo.sourceDir = mPluginPath;
            return mContext.getPackageManager().getResourcesForApplication(packageInfo.applicationInfo);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
Copy the code

The second step is to mix up a new resource using the Resouces of the host package and the Resouces of the plug-in package. To obtain resources, first search the plugin’s Resouces, if not found, from the host Resouces, code as follows:

package com.sq.a37syplu10.plugin.resources;

import android.annotation.TargetApi;
import android.content.res.AssetFileDescriptor;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Movie;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.TypedValue;
import java.io.InputStream;

/** * Resources get the Resources from the plug-in first, if not from the host */
public class MixResources extends ResourcesWrapper {

    private Resources mHostResources;

    public MixResources(Resources hostResources, Resources pluginResources) {
        super(pluginResources);
        mHostResources = hostResources;
    }

    @Override
    public CharSequence getText(int id) throws NotFoundException {
        try {
            return super.getText(id);
        } catch (NotFoundException e) {
            returnmHostResources.getText(id); }}@Override
    public String getString(int id) throws NotFoundException {
        try {
            return super.getString(id);
        } catch (NotFoundException e) {
            returnmHostResources.getString(id); }}@Override
    public String getString(int id, Object... formatArgs) throws NotFoundException {
        try {
            return super.getString(id,formatArgs);
        } catch (NotFoundException e) {
            returnmHostResources.getString(id,formatArgs); }}@Override
    public float getDimension(int id) throws NotFoundException {
        try {
            return super.getDimension(id);
        } catch (NotFoundException e) {
            returnmHostResources.getDimension(id); }}@Override
    public int getDimensionPixelOffset(int id) throws NotFoundException {
        try {
            return super.getDimensionPixelOffset(id);
        } catch (NotFoundException e) {
            returnmHostResources.getDimensionPixelOffset(id); }}@Override
    public int getDimensionPixelSize(int id) throws NotFoundException {
        try {
            return super.getDimensionPixelSize(id);
        } catch (NotFoundException e) {
            returnmHostResources.getDimensionPixelSize(id); }}@Override
    public Drawable getDrawable(int id) throws NotFoundException {
        try {
            return super.getDrawable(id);
        } catch (NotFoundException e) {
            returnmHostResources.getDrawable(id); }}@TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public Drawable getDrawable(int id, Theme theme) throws NotFoundException {
        try {
            return super.getDrawable(id, theme);
        } catch (NotFoundException e) {
            returnmHostResources.getDrawable(id,theme); }}@Override
    public Drawable getDrawableForDensity(int id, int density) throws NotFoundException {
        try {
            return super.getDrawableForDensity(id, density);
        } catch (NotFoundException e) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
                return mHostResources.getDrawableForDensity(id, density);
            } else {
                return null; }}}@TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public Drawable getDrawableForDensity(int id, int density, Theme theme) {
        try {
            return super.getDrawableForDensity(id, density, theme);
        } catch (Exception e) {
            returnmHostResources.getDrawableForDensity(id,density,theme); }}@Override
    public int getColor(int id) throws NotFoundException {
        try {
            return super.getColor(id);
        } catch (NotFoundException e) {
            returnmHostResources.getColor(id); }}@TargetApi(Build.VERSION_CODES.M)
    @Override
    public int getColor(int id, Theme theme) throws NotFoundException {
        try {
            return super.getColor(id,theme);
        } catch (NotFoundException e) {
            returnmHostResources.getColor(id,theme); }}@Override
    public ColorStateList getColorStateList(int id) throws NotFoundException {
        try {
            return super.getColorStateList(id);
        } catch (NotFoundException e) {
            returnmHostResources.getColorStateList(id); }}@TargetApi(Build.VERSION_CODES.M)
    @Override
    public ColorStateList getColorStateList(int id, Theme theme) throws NotFoundException {
        try {
            return super.getColorStateList(id,theme);
        } catch (NotFoundException e) {
            returnmHostResources.getColorStateList(id,theme); }}@Override
    public boolean getBoolean(int id) throws NotFoundException {
        try {
            return super.getBoolean(id);
        } catch (NotFoundException e) {
            returnmHostResources.getBoolean(id); }}@Override
    public XmlResourceParser getLayout(int id) throws NotFoundException {
        try {
            return super.getLayout(id);
        } catch (NotFoundException e) {
           returnmHostResources.getLayout(id); }}@Override
    public String getResourceName(int resid) throws NotFoundException {
        try {
            return super.getResourceName(resid);
        } catch (NotFoundException e) {
            returnmHostResources.getResourceName(resid); }}@Override
    public int getInteger(int id) throws NotFoundException {
        try {
            return super.getInteger(id);
        } catch (NotFoundException e) {
            returnmHostResources.getInteger(id); }}@Override
    public CharSequence getText(int id, CharSequence def) {
        try {
            return super.getText(id,def);
        } catch (NotFoundException e) {
            returnmHostResources.getText(id,def); }}@Override
    public InputStream openRawResource(int id) throws NotFoundException {
        try {
            return super.openRawResource(id);
        } catch (NotFoundException e) {
            returnmHostResources.openRawResource(id); }}@Override
    public XmlResourceParser getXml(int id) throws NotFoundException {
        try {
            return super.getXml(id);
        } catch (NotFoundException e) {
            returnmHostResources.getXml(id); }}@TargetApi(Build.VERSION_CODES.O)
    @Override
    public Typeface getFont(int id) throws NotFoundException {
        try {
            return super.getFont(id);
        } catch (NotFoundException e) {
            returnmHostResources.getFont(id); }}@Override
    public Movie getMovie(int id) throws NotFoundException {
        try {
            return super.getMovie(id);
        } catch (NotFoundException e) {
            returnmHostResources.getMovie(id); }}@Override
    public XmlResourceParser getAnimation(int id) throws NotFoundException {
        try {
            return super.getAnimation(id);
        } catch (NotFoundException e) {
            returnmHostResources.getAnimation(id); }}@Override
    public InputStream openRawResource(int id, TypedValue value) throws NotFoundException {
        try {
            return super.openRawResource(id,value);
        } catch (NotFoundException e) {
            returnmHostResources.openRawResource(id,value); }}@Override
    public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException {
        try {
            return super.openRawResourceFd(id);
        } catch (NotFoundException e) {
            returnmHostResources.openRawResourceFd(id); }}}Copy the code

Register a proxy Activity in the host as a container to load the plug-in Activity

package com.sq.a37syplu10.plugin;

import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;

import com.sq.a37syplu10.MainActivity;
import com.sq.a37syplu10.plugin.loader.ApkClassLoader;
import com.sq.aninterface.IActivityInterface;

public class ProxyPluginActivity extends Activity {

    @Override
    public ApkClassLoader getClassLoader(a) {
        return MainActivity.mPlugin.mClassLoader;
    }

    @Override
    public Resources getResources(a) {
        return MainActivity.mPlugin.mResource;
    }

    private IActivityInterface pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent = getIntent();

        if(intent ! =null && !TextUtils.isEmpty(intent.getStringExtra("activity"))) {
            try {
                pluginActivity = getClassLoader().getInterface(IActivityInterface.class, intent.getStringExtra("activity"));
                pluginActivity.setAppContext(this);
                pluginActivity.onCreate(new Bundle());
            } catch(Exception e) { e.printStackTrace(); }}else {
            Log.e("I am the host."."Intent does not contain plug-in activity information"); }}@Override
    public void startActivity(Intent intent) {
        if(! TextUtils.isEmpty(intent.getStringExtra("activity"))) {
            intent.setClass(this, ProxyPluginActivity.class);
        }
        super.startActivity(intent); }}Copy the code

The test results

After testing, the simulator, the real machine from Android 4-10 are normal. No compatibility problems have been encountered

The Demo source code

Juejin. Cn/post / 687032…

conclusion

Students who have problems or need to communicate in the process can scan the TWO-DIMENSIONAL code to add friends, and then enter the group for problems and technical exchanges, etc.