【插件&热修系列】Shadow源码解析之sample-manager(一)

·  阅读 1134
【插件&热修系列】Shadow源码解析之sample-manager(一)

引言

上阶段我们学习了插件编程和动态设计思想,了解了插件开发模式相关脚本和shadow的动态设计的思想;

接下来,我们将基于上一篇的思想篇来解析下sample-manager模块的源码

概要

123.png

上图是shadow的相关插件的物料,启动的时候宿主会去下载这些物料,进行插件的更新等

1)sample-manager-release.apk

这个是插件apk,主要用来对具体业务的插件包(如:plugin-release.zip里面的插件)

下载/解压等工作,这个也是本文的源码解析重点

2)plugin-release.zip

插件和相关配置压缩包,里面包含:

2.1)json文件

{
      "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"
 }
复制代码

主要是插件相关信息,如:版本号/白名单等

2.2)sample-loader-release.apk

负责加载插件

2.3)sample-runtime-release.apk

插件运行时需要,包括占位 Activity,占位 Provider 等等

2.4)sample-plugin-app-release.apk

业务插件1

2.5)sample-plugin-app-release2.apk

业务插件2

代码分析

上一篇我们了解了工程架构情况,接下来我们将通过代码一步步解析

PS:基于官方工程的裁剪代码

1.插件的准备

代码位置

11.png

这里把生成的插件apk(具体怎么生成的见上一篇文章中的插件相关脚本)拷贝到本地,具体实现如下:

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() {
                preparePlugin();
            }
        });
    }
 
复制代码

private void preparePlugin() {
        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("从assets中复制apk出错", e);
        }
 }
复制代码

2.PluginManager 实例化

public interface PluginManager {
    /**
     * @param context  context
     * @param formId   标识本次请求的来源位置,用于区分入口
     * @param bundle   参数列表
     * @param callback 用于从PluginManager实现中返回View
     */
    void enter(Context context, long formId, Bundle bundle, EnterCallback callback);
}
复制代码

PluginManager 是一个接口,这个接口是乔接插件(sample-manager-release.apk)和宿主用的,其中宿主主要是接口的调用,然后插件是具体的实现,下面我们看下怎么一步步构建的:

2.1)输入和输出

private void loadPluginManager(File apk) {
     if (mPluginManager == null) {
         mPluginManager = Shadow.getPluginManager(apk);
     }
}
复制代码

输入是apk文件,输出是PluginManager接口

2.2)getPluginManager

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

        //它只提供需要升级时的功能,如下载和向远端查询文件是否还可用。
        final FixedPathPmUpdater fixedPathPmUpdater = new FixedPathPmUpdater(apk);
        File tempPm = fixedPathPmUpdater.getLatest();

        if (tempPm != null) {
            return new DynamicPluginManager(fixedPathPmUpdater);
        }
        return null;
}
复制代码

这里啥都没做,只是确保传进来的apk是最新的后,作为输入传进DynamicPluginManager

2.3)DynamicPluginManager

public DynamicPluginManager(PluginManagerUpdater updater) {
        if (updater.getLatest() == null) {
            throw new IllegalArgumentException("构造DynamicPluginManager时传入的PluginManagerUpdater" +
                    "必须已经已有本地文件,即getLatest()!=null");
        }
        mUpdater = updater;
}
复制代码

这里也没啥特别,只是做了简单的属性赋值

3.PluginManager的调用

mPluginManager.enter(this, FROM_ID_START_ACTIVITY, bundle, null);
复制代码

mPluginManager 为上面实力化的对象(DynamicPluginManager)

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

        //1)根据mUpdater,确认文件是否更新,进一步确认 mManagerImpl 是否重新构建
        //2)load plumanager apk
        updateManagerImpl(context);

        //入口进入
        mManagerImpl.enter(context, fromId, bundle, callback);

        mUpdater.update();
}

复制代码

这里做了3件事:

a)updateManagerImpl,根据apk文件对插件进行加载

b)mManagerImpl.enter,调用插件的具体的实现

c)mUpdater.update(),更新插件

下面我们对这3件事进行近一步的解析

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)) {
            //文件更新了
            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;
        }
}
复制代码

上面代码主要做了:

a)根据md5确认插件文件是否有变化

b)如果有变化,则进行插件加载

ManagerImplLoader implLoader = new ManagerImplLoader(context, latestManagerImplApk);
PluginManagerImpl newImpl = implLoader.load();
复制代码

怎么加载的?我们来看看

首先是 ManagerImplLoader 对象

ManagerImplLoader(Context context, File apk) {
        //odexDir 创建
        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);
 }
复制代码

这里构建了InstalledApk对象,即是插件的内存抽象

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() {
        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 new InstalledApk[size];
        }
    };
}

复制代码

然后是 implLoader.load() 方法的调用

PluginManagerImpl load() {
        String[] strArr = {"张三", "李四", "王二麻"};
        //Apk插件加载专用ClassLoader,将宿主apk和插件apk隔离。
        ApkClassLoader apkClassLoader = new ApkClassLoader(
                installedApk,
                getClass().getClassLoader(),// 宿主ClassLoader
                loadWhiteList(installedApk),
                1
        );

        //将原Context的《Resource》和《ClassLoader》重新修改为新的Apk。
        Context pluginManagerContext = new ChangeApkContextWrapper(
                applicationContext,
                installedApk.apkFilePath,
                apkClassLoader
        );

        try {
            //从apk中读取接口的实现
            ManagerFactory managerFactory = apkClassLoader.getInterface(
                    ManagerFactory.class,
                    MANAGER_FACTORY_CLASS_NAME
            );
            return managerFactory.buildManager(pluginManagerContext);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
 }
复制代码

这里是加载的核心实现,这边主要做了3件事:

a)构建加载器(ApkClassLoader),具体构建原理这里不展开,可以看前面的博文方案

b)构建上下文(ChangeApkContextWrapper),让可以使用插件的资源等

c)最后是读取插件的实现类,然后实现《接口的插件实现》传递,返回给宿主调用

下面我们主要解析后面的两点

首先是构建上下文(ChangeApkContextWrapper)

ChangeApkContextWrapper(Context base, String apkPath, ClassLoader mClassloader) {
        super(base);
        this.mClassloader = mClassloader;
        mResources = createResources(apkPath, base);
 }
复制代码
 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 new RuntimeException(e);
        }
}
复制代码

根据代码看出,主要是插件信息构建出Resources对象;

到这里明白了shadow采用的是创建一个新的Resources(核心接口:getResourcesForApplication),实现和宿主隔离,这样的优点是宿主和插件的资源不存在冲突,不需要特殊处理;

另外一种通过合并资源方式,即AssetManager 的 addAssetPath 方法,这种拓展资源的方式存在宿主和插件的资源冲突问题(比如:我们知道资源的ID前2位以7f开头,如果插件apk编译,字段id段也是以7f开头,那么就会和宿主的资源id段冲突);虽然可以通过修改aapt,自定义插件的字段id分段,如:

11.png

但是如果插件的数量比较多,那么会出现资源ID分区不够的问题

好了,构建上下文已经讲完,下面看读取插件的实现类部分

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

先试通过 apkClassLoader 来加载插件的 ManagerFactory 接口的实现类com.example.sample_manager.ManagerFactoryImpl

然后通过调用 ManagerFactory 的 buildManager方法构建出宿主调用插件的PluginManagerImpl实现类

mManagerImpl.enter

上面宿主得到插件的PluginManagerImpl实现类后,直接调用enter方法,这样代码就从宿主到了插件里面去了,然后里面就是具体的一些插件(sample-manager.apk 插件)业务,如:加载其他插件/更新插件逻辑等

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("不认识的fromId==" + fromId);
        }
}
复制代码
private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
        //1)加载插件
        executorService.execute(() -> {

        });
        //2)插件启动 todo 这个环节先不展开,下个阶段展开
        Log.e(TAG, "SamplePluginManager, 插件启动,这个环节先不展开,下个阶段展开");
}
复制代码

到这里我们了解了宿主到sample-manager插件的链路打通,具体入口(enter)之后是什么?我们下一篇再展开

结尾

哈哈,该篇就写到这里(一起体系化学习,一起成长)

Tips

更多精彩内容,请关注 ”Android热修技术“ 微信公众号

分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改