引言
上阶段我们学习了插件编程和动态设计思想,了解了插件开发模式相关脚本和shadow的动态设计的思想;
接下来,我们将基于上一篇的思想篇来解析下sample-manager模块的源码
概要
上图是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.插件的准备
代码位置
这里把生成的插件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分段,如:
但是如果插件的数量比较多,那么会出现资源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热修技术“ 微信公众号