The introduction

In the last stage, we learned the idea of plug-in programming and dynamic design, and understood the idea of dynamic design of plug-in development mode related script and shadow.

Next, we will parse the source code of the sample-Manager module based on the ideas in the previous article

The profile

Above is the plugin for Shadow

1) the sample – the manager – the apk

This is the plugin APK, which is used for business specific plug-in packages (e.g. Plugin-release.zip).

Download/decompress and other work, this is the focus of the source code analysis of this article

2) the plugin – release. Zip

Plug-in and related configuration zip package, which contains:

2.1) JSON file

{
      "compact_version": [1.2.3]."pluginLoader": {"apkName":"sample-loader-release.apk"."hash":"11654AE11DF3C43642A10CCF21461468"
      },
      "plugins":[
          {
              "partKey":"sample-plugin-app"."apkName":"sample-plugin-app-release.apk"."businessName":"sample-plugin-app"."hostWhiteList": ["com.tencent.shadow.sample.host.lib"]."hash":"13FC58F2176FCF9BF3CCF92E14F0FDD3"
          },
          {
              "partKey":"sample-plugin-app2"."apkName":"sample-plugin-app-release2.apk"."businessName":"sample-plugin-app2"."hostWhiteList": ["com.tencent.shadow.sample.host.lib"]."hash":"13FC58F2176FCF9BF3CCF92E14F0FDD3"}]."runtime": {"apkName":"sample-runtime-release.apk"."hash":"FEC73F1212FD22D7261E9064D9DFAF3B"
      },
      "UUID":"A0AE9AF8-330A-4D80-9D29-F7B903AEE90B"."version":4."UUID_NickName":"1.1.5."
 }
Copy the code

Information about plug-ins, such as version number/whitelist, is displayed

2.2) the sample – loader – the apk

Responsible for loading plug-ins

2.3) the sample – the runtime – the apk

Plug-ins are required at runtime, including placeholder activities, placeholder providers, and so on

2.4) the sample – the plugin – app – the apk

Business Plug-in 1

2.5) the sample – the plugin – app – release2. Apk

Business Plug-in 2

The code analysis

In the last article, we looked at the engineering architecture, and we’ll go through the code step by step

PS: Tailored code based on official projects

1. Plug-in preparation

Code position

Here is a copy of the generated plug-in APK (see the plugin script in the previous article) to the local, the specific implementation is as follows:

public void init(Context context) {
        pluginManagerFile = new File(context.getFilesDir(), sPluginManagerName);
        //pluginZipFile = new File(context.getFilesDir(), sPluginZip);
        mContext = context.getApplicationContext();
        Log.i(TAG, "PluginHelper, pluginManagerFile = " + pluginManagerFile.getAbsolutePath());
        //Log.i(TAG, "PluginHelper, pluginZipFile = " + pluginZipFile.getAbsolutePath());

        singlePool.execute(new Runnable() {
            @Override
            public void run(a) { preparePlugin(); }}); }Copy the code

private void preparePlugin(a) {
        try {
            //pluginmanager.apk
            InputStream is = mContext.getAssets().open(sPluginManagerName);
            FileUtils.copyInputStreamToFile(is, pluginManagerFile);
            if (pluginManagerFile.exists()) {
                Log.i(TAG, "PluginHelper, copy ok ... ");
            }
            //zip
            //InputStream zip = mContext.getAssets().open(sPluginZip);
            //FileUtils.copyInputStreamToFile(zip, pluginZipFile);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("Error copying APK from assets", e); }}Copy the code

2. The PluginManager instantiation

public interface PluginManager {
    / * * *@param context  context
     * @paramFormId identifies the source of the request and is used to distinguish entry *@paramBundle argument list *@paramCallback is used to return View */ from the PluginManager implementation
    void enter(Context context, long formId, Bundle bundle, EnterCallback callback);
}
Copy the code

PluginManager is an interface that is used by the PluginManager (sample-manager-release.apk) and the host. The host is the call to the interface, and the plugin is the concrete implementation.

2.1) Input and output

private void loadPluginManager(File apk) {
     if (mPluginManager == null) { mPluginManager = Shadow.getPluginManager(apk); }}Copy the code

The input is an APK file and the output is the PluginManager interface

2.2) getPluginManager

public static PluginManager getPluginManager(File apk) {
        Log.i(TAG, "Shadow, getPluginManager, apk = " + apk.getAbsolutePath());

        // It only provides functions that need to be upgraded, such as downloading and querying remote files to see if they are still available.
        final FixedPathPmUpdater fixedPathPmUpdater = new FixedPathPmUpdater(apk);
        File tempPm = fixedPathPmUpdater.getLatest();

        if(tempPm ! =null) {
            return new DynamicPluginManager(fixedPathPmUpdater);
        }
        return null;
}
Copy the code

Nothing is done here, just make sure the apk passed in is up to date and then pass it as input to the DynamicPluginManager

2.3) DynamicPluginManager

public DynamicPluginManager(PluginManagerUpdater updater) {
        if (updater.getLatest() == null) {
            throw new IllegalArgumentException("The plugin erupdater passed in when constructing DynamicPluginManager" +
                    "You must already have a local file, getLatest()! =null");
        }
        mUpdater = updater;
}
Copy the code

There’s nothing special here, just a simple property assignment

3. The PluginManager calls

mPluginManager.enter(this, FROM_ID_START_ACTIVITY, bundle, null);
Copy the code

MPluginManager is the DynamicPluginManager.

public void enter(Context context, long fromId, Bundle bundle, EnterCallback callback) {
        Log.i(TAG, "enter fromId:" + fromId + " callback:" + callback);

        //1) Check whether the file is updated according to mUpdater, and further check whether mManagerImpl is rebuilt
        //2)load plumanager apk
        updateManagerImpl(context);

        // Enter the entry
        mManagerImpl.enter(context, fromId, bundle, callback);

        mUpdater.update();
}

Copy the code

Three things are done here:

A) updateManagerImpl: load the plug-in according to the APK file

B) mManagerImp.enter, which calls the specific implementation of the plug-in

C) updater.update (), update plug-in

Let’s take a closer look at these three things

updateManagerImpl

private void updateManagerImpl(Context context) {
        File latestManagerImplApk = mUpdater.getLatest();
        String md5 = md5File(latestManagerImplApk);

        Log.i(TAG, "DynamicPluginManager, updateManagerImpl," +
                "TextUtils.equals(mCurrentImplMd5, md5) : " + (TextUtils.equals(mCurrentImplMd5, md5)));

        if(! TextUtils.equals(mCurrentImplMd5, md5)) {// The file is updated
            ManagerImplLoader implLoader = new ManagerImplLoader(context, latestManagerImplApk);
            PluginManagerImpl newImpl = implLoader.load();
            Bundle state;
            if(mManagerImpl ! =null) {
                state = new Bundle();
                mManagerImpl.onSaveInstanceState(state);
                mManagerImpl.onDestroy();
            } else {
                state = null; } newImpl.onCreate(state); mManagerImpl = newImpl; mCurrentImplMd5 = md5; }}Copy the code

The above code mainly does:

A) Check whether the plug-in file has changed according to MD5

B) If there are changes, the plug-in will be loaded

ManagerImplLoader implLoader = new ManagerImplLoader(context, latestManagerImplApk);
PluginManagerImpl newImpl = implLoader.load();
Copy the code

How does it load? Let’s see

The first is the ManagerImplLoader object

ManagerImplLoader(Context context, File apk) {
        / / odexDir created
        applicationContext = context.getApplicationContext();
        File root = new File(applicationContext.getFilesDir(), "ManagerImplLoader");
        File odexDir = new File(root, Long.toString(apk.lastModified(), Character.MAX_RADIX));
        odexDir.mkdirs();
        Log.i(TAG, "ManagerImplLoader, start, odexDir =" + odexDir.getAbsolutePath());

        installedApk = new InstalledApk(apk.getAbsolutePath(), odexDir.getAbsolutePath(), null);
 }
Copy the code

Here we build the InstalledApk object, which is a memory abstraction for the plug-in

public class InstalledApk implements Parcelable {

    public final String apkFilePath;

    public final String oDexPath;

    public final String libraryPath;

    public final byte[] parcelExtras;

    public InstalledApk(String apkFilePath, String oDexPath, String libraryPath) {
        this(apkFilePath, oDexPath, libraryPath, null);
    }

    public InstalledApk(String apkFilePath, String oDexPath, String libraryPath, byte[] parcelExtras) {
        this.apkFilePath = apkFilePath;
        this.oDexPath = oDexPath;
        this.libraryPath = libraryPath;
        this.parcelExtras = parcelExtras;
    }

    protected InstalledApk(Parcel in) {
        apkFilePath = in.readString();
        oDexPath = in.readString();
        libraryPath = in.readString();
        int parcelExtrasLength = in.readInt();
        if (parcelExtrasLength > 0) {
            parcelExtras = new byte[parcelExtrasLength];
        } else {
            parcelExtras = null;
        }
        if(parcelExtras ! =null) { in.readByteArray(parcelExtras); }}@Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(apkFilePath);
        dest.writeString(oDexPath);
        dest.writeString(libraryPath);
        dest.writeInt(parcelExtras == null ? 0 : parcelExtras.length);
        if(parcelExtras ! =null) { dest.writeByteArray(parcelExtras); }}@Override
    public int describeContents(a) {
        return 0;
    }

    public static final Creator<InstalledApk> CREATOR = new Creator<InstalledApk>() {
        @Override
        public InstalledApk createFromParcel(Parcel in) {
            return new InstalledApk(in);
        }

        @Override
        public InstalledApk[] newArray(int size) {
            return newInstalledApk[size]; }}; }Copy the code

The implLoader.load() method is then called

PluginManagerImpl load(a) {
        String[] strArr = {"Zhang"."Bill"."Wang Erma"};
        // The Apk plug-in loads the dedicated ClassLoader, separating the host Apk from the plug-in Apk.
        ApkClassLoader apkClassLoader = new ApkClassLoader(
                installedApk,
                getClass().getClassLoader(),/ / this host
                loadWhiteList(installedApk),
                1
        );

        // Modify the Resource and ClassLoader of the original Context to the new Apk.
        Context pluginManagerContext = new ChangeApkContextWrapper(
                applicationContext,
                installedApk.apkFilePath,
                apkClassLoader
        );

        try {
            // Read the implementation of the interface from apK
            ManagerFactory managerFactory = apkClassLoader.getInterface(
                    ManagerFactory.class,
                    MANAGER_FACTORY_CLASS_NAME
            );
            return managerFactory.buildManager(pluginManagerContext);
        } catch (Exception e) {
            throw newRuntimeException(e); }}Copy the code

Here is the core implementation of loading, which does three main things:

A) Build loader (ApkClassLoader), the specific construction principle is not expanded here, you can see the previous blog scheme

B) Build the context (ChangeApkContextWrapper) to make the resources of the plug-in available, etc

C) Finally, the implementation class of the plug-in is read, and then the “plug-in implementation of the interface” is passed back to the host call

Next we mainly analyze the latter two points

The first is the build context (ChangeApkContextWrapper)

ChangeApkContextWrapper(Context base, String apkPath, ClassLoader mClassloader) {
        super(base);
        this.mClassloader = mClassloader;
        mResources = createResources(apkPath, base);
 }
Copy the code
 private Resources createResources(String apkPath, Context base) {
        PackageManager packageManager = base.getPackageManager();
        PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(apkPath, GET_META_DATA);
        packageArchiveInfo.applicationInfo.publicSourceDir = apkPath;
        packageArchiveInfo.applicationInfo.sourceDir = apkPath;
        Log.i(TAG, "ChangeApkContextWrapper, createResources, applicationInfo.publicSourceDir = " + apkPath);
        Log.i(TAG, "ChangeApkContextWrapper, createResources, applicationInfo.sourceDir = " + apkPath);
        try {
            return packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
        } catch (PackageManager.NameNotFoundException e) {
            throw newRuntimeException(e); }}Copy the code

According to the code, it is mainly the plug-in information that builds the Resources object;

To see shadow used here is to create a new Resources (core interface: getResourcesForApplication), implementation and host isolation, so has the advantage of the host and the Resources of the plug-in does not exist conflict, need not special processing;

Another way to expand resources by merging resources, namely the addAssetPath method of AssetManager, has the problem of resource conflict between the host and the plug-in (for example: We know that the first two bits of the resource ID start with 7f. If the plugin apK is compiled and the field ID segment starts with 7F, it will conflict with the host resource ID segment. Although it is possible to customize the field ID segment of the plugin by modifying AAPT, for example:

However, if the number of plug-ins is large, there will be insufficient resource ID partitions

Now that the build context is over, let’s look at the implementation classes for reading the plug-in

ManagerFactory managerFactory = apkClassLoader.getInterface(
                    ManagerFactory.class,
                    MANAGER_FACTORY_CLASS_NAME
            );
return managerFactory.buildManager(pluginManagerContext);
Copy the code
 private static final String MANAGER_FACTORY_CLASS_NAME = "com.example.sample_manager.ManagerFactoryImpl";
Copy the code
public interface ManagerFactory {
    PluginManagerImpl buildManager(Context context);
}
Copy the code

Try to load the plug-in by apkClassLoader ManagerFactory interface implementation class com. Example. Sample_manager. ManagerFactoryImpl

The PluginManagerImpl implementation class for the host calling the plug-in is then built by calling the ManagerFactory buildManager method

mManagerImpl.enter

After receiving the PluginManagerImpl implementation class, the host directly calls The Enter method, which takes the code from the host to the plug-in. Then, there are specific plug-in (samplemanager. apk) services, such as loading other plug-ins/updating plugin logic, etc

public void enter(final Context context, long fromId, Bundle bundle, final EnterCallback callback) {
        if (fromId == Constant.FROM_ID_NOOP) {
            //do nothing.
        } else if (fromId == Constant.FROM_ID_START_ACTIVITY) {
            Log.i(TAG, "SamplePluginManager, enter : onStartActivity");
            onStartActivity(context, bundle, callback);
        } else {
            throw new IllegalArgumentException("Unknown fromId=="+ fromId); }}Copy the code
private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
        //1) Load the plug-in
        executorService.execute(() -> {

        });
        //2) Todo will not be expanded at the next stage
        Log.e(TAG, "SamplePluginManager, the plug-in is started, so I'm not going to expand it, I'm going to expand it in the next stage.");
}
Copy the code

The link between the host and the sample-Manager plug-in is open. What is the specific entry (Enter)? We’ll talk about that in the next article

At the end

Haha, that’s all for this article (systematic learning and growing together)

Tips

For more exciting content, please follow “DaviAndroid” wechat public account