05 项目架构-插件化-资源加载
背景
前面两篇文章我们实现了普通类的插件化和Activity的插件化,
如果你对插件化没有了解过,不妨看一下前面的文章,今天我们来讲解一下资源文件的插件化,这也是插件化中非常重要的一个内容,我们常用插件化去实现换肤功能,主题替换等等。
常用的访问资源的方式
通常我们是通过Resources去访问res中的资源,使用AssManager访问asset里面的资源,来看一下常用的代码调用
String appName = getResources().getString(R.string.app_name);
try {
InputStream is = getAssets().open("ic_launcher.png");
} catch (IOException e) {
e.printStackTrace();
}
复制代码
实际上Resources也是通过AssManager来访问那些被编译过的应用程序资源文件的,不过在访问之前会先根据资源ID查找得到对应的资源文件名。而AssetManager对象既可以通过文件名访问那些被编译过的,也可以访问那些没被编译过的。
来看一下Resources的getString方法的代码实现过程
@NonNull
public String getString(@StringRes int id) throws NotFoundException {
return getText(id).toString();
}
@NonNull public CharSequence getText(@StringRes int id) throws NotFoundException {
CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
if (res != null) {
return res;
}
throw new NotFoundException("String resource ID #0x"
+ Integer.toHexString(id));
}
复制代码
可以看到Resources的实现类是ResourcesImpl,getAssets方法返回的是AssManager,也就是说资源的加载实际上是通过AssManager来加载的。
下面我们来看看AssManager是如何初始化的,又是如何加载Apk的资源,只有掌握了原理,我们才能懂得如何去加载插件Apk中的资源。我们需要首先看一下APP的启动流程
App启动流程中创建AssetManager
先来看一下App的大致启动流程
-
首先是点击App图标,此时是运行在Launcher进程,通过ActivityManagerService Binder IPC的形式向system_server进程发起startActivity的请求
-
system_server进程接收到请求后,通过
Process.start
方法向zygote进程发送创建进程的请求 -
zygote进程fork出新的子进程,即App进程
-
然后进入
ActivityThread.main
方法中,这时运行在App进程中,通过ActivityManagerServiceBinder IPC的形式向system_server进程发起attachApplication请求 -
system_server接收到请求后,进行一系列准备工作后,再通过Binder IPC向App进程发送scheduleLaunchActivity请求
-
App进程binder线程(ApplicationThread)收到请求后,通过Handler向主线程发送LAUNCH_ACTIVITY消息
-
主线程收到Message后,通过反射机制创建目标Activity,并回调Activity的onCreate
我们来看看第四步,attachApplication方法,最终会调用thread#bindApplication
然后调用ActivityThread#handleBindApplication
方法,我们从这个方法开始看
ActivityThread#handleBindApplication
private void handleBindApplication(AppBindData data) {
...
final InstrumentationInfo ii;
...
// 创建 mInstrumentation 实例
if (ii != null) {
final ApplicationInfo instrApp = new ApplicationInfo();
ii.copyTo(instrApp);
instrApp.initForUser(UserHandle.myUserId());
final LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
appContext.getClassLoader(), false, true, false);
final ContextImpl instrContext = ContextImpl.createAppContext(this, pi);
try {
final ClassLoader cl = instrContext.getClassLoader();
mInstrumentation = (Instrumentation)
cl.loadClass(data.instrumentationName.getClassName()).newInstance();
} catch (Exception e) {
...
}
...
} else {
mInstrumentation = new Instrumentation();
}
...
Application app;
...
// 创建 Application 实例
try {
...
app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
...
try {
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
...
}
} finally {
...
}
...
}
// http://androidxref.com/8.1.0_r33/xref/frameworks/base/core/java/android/app/LoadedApk.java#959
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
...
try {
...
//注释1
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
...
}
...
return app;
}
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
return new ContextImpl(null, mainThread,
packageInfo, null, null, 0, null, null, Display.INVALID_DISPLAY);
}
//这个方法我们只留下了最核心的内容,我们看下注释1, ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);这个方法会直接new一个新的ContextImpl
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
....
//LoadApk赋值
mPackageInfo = packageInfo;
mResourcesManager = ResourcesManager.getInstance();
...
//通过LoadApk.getResources获取Resources对象
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
if (container != null) {
// This is a nested Context, so it can't be a base Activity context.
// Just create a regular Resources object associated with the Activity.
resources = mResourcesManager.getResources(
activityToken,
packageInfo.getResDir(),
packageInfo.getSplitResDirs(),
packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
packageInfo.getClassLoader());
} else {
// This is not a nested Context, so it must be the root Activity context.
// All other nested Contexts will inherit the configuration set here.
resources = mResourcesManager.createBaseActivityResources(
activityToken,
packageInfo.getResDir(),
packageInfo.getSplitResDirs(),
packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
packageInfo.getClassLoader());
}
}
}
//为mResources变量赋值
mResources = resources;
...
}
复制代码
packageInfo.getResources
,packageInfo是LoadApk类型的,我们看下这个方法
LoadApk#getResources
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
}
return mResources;
}
复制代码
其中调用了ActivityThread的getTopLevelResources方法,我们继续看一下
ActivityThread#getTopLevelResources
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
String[] libDirs, int displayId, LoadedApk pkgInfo) {
// 获取 ResourcesManager 对象的单例,然后调用 getResources 方法去获取 Resources 对象
return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs,
displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader());
}
复制代码
ResourcesManager#getResources
public @NonNull Resources getResources(@Nullable IBinder activityToken,
@Nullable String resDir,
@Nullable String[] splitResDirs,
@Nullable String[] overlayDirs,
@Nullable String[] libDirs,
int displayId,
@Nullable Configuration overrideConfig,
@NonNull CompatibilityInfo compatInfo,
@Nullable ClassLoader classLoader) {
try {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
final ResourcesKey key = new ResourcesKey(
resDir,
splitResDirs,
overlayDirs,
libDirs,
displayId,
overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
compatInfo);
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
return getOrCreateResources(activityToken, key, classLoader);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
private @NonNull Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
...
// 创建ResourcesImpl
ResourcesImpl resourcesImpl = createResourcesImpl(key);
....
final Resources resources;
if (activityToken != null) {
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl);
} else {
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
}
return resources;
}
}
private @NonNull ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
// 创建 AssetManager 对象
final AssetManager assets = createAssetManager(key);
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
final Configuration config = generateConfig(key, dm);
// 将 assets 对象传入到 ResourcesImpl 类中
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
if (DEBUG) {
Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
}
return impl;
}
protected @NonNull AssetManager createAssetManager(@NonNull final ResourcesKey key) {
AssetManager assets = new AssetManager();
// resDir can be null if the 'android' package is creating a new Resources object.
// This is fine, since each AssetManager automatically loads the 'android' package
// already.
if (key.mResDir != null) {
// 通过 addAssetPath 方法添加 apk 文件的路径
if (assets.addAssetPath(key.mResDir) == 0) {
throw new Resources.NotFoundException("failed to add asset path " + key.mResDir);
}
}
···
return assets;
}
private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
@NonNull ResourcesImpl impl) {
// Find an existing Resources that has this ResourcesImpl set.
final int refCount = mResourceReferences.size();
for (int i = 0; i < refCount; i++) {
WeakReference<Resources> weakResourceRef = mResourceReferences.get(i);
Resources resources = weakResourceRef.get();
if (resources != null &&
Objects.equals(resources.getClassLoader(), classLoader) &&
resources.getImpl() == impl) {
if (DEBUG) {
Slog.d(TAG, "- using existing ref=" + resources);
}
return resources;
}
}
// Create a new Resources reference and use the existing ResourcesImpl object.
Resources resources = new Resources(classLoader);
resources.setImpl(impl);
mResourceReferences.add(new WeakReference<>(resources));
if (DEBUG) {
Slog.d(TAG, "- creating new ref=" + resources);
Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
}
return resources;
}
复制代码
回顾上面的流程,我们首先调用createResourcesImpl,创建ResourcesImpl,我们看下这个方法内部创建了AssetManager assets = new AssetManager();,然后调用assets.addAssetPath添加资源地址,最后返回final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);,最后查看是否有缓存,如果有则返回缓存的resources,如果没有就重新构建Resources,然后返回。
Activity启动流程中创建AssetManager
同理我们如果看Activity的启动流程,也可以看到类似上面的一个过程,只要是Application或者四大组件的启动流程,都有Context的创建过程,然后我们都能看到AssetManager的创建过程,Activity的启动流程中创建AssetManager,最终调用assets.addAssetPath
添加资源地址的流程图如下:
结合前面的常用方法分析,我们发现Apk的资源时通过AssetManager.addAssetPath
方法来完成加载,那么我们就可通过反射构建自己的AssManager对象,然后调用addAssetPath加载自己的资源,然后把自己构建的AssetManager通过反射设置给mAssets变量,这样下次加载资源就是用我们的AssetManager。
资源加载的两种实现方案
- 插件的资源和宿主的资源直接合并,直接用宿主的
(Resources)AssetManager
进行加载资源,可能会有冲突的问题,需要用aapt修改一下资源ID。 - 专门创建一个
(Resources)AssetManager
加载插件的资源
方案一涉及到使用aapt修改插件中资源的ID,较为麻烦,本文使用第二种方案
实现步骤:
- 创建一个AssetManager对象,并调用addAssetPath方法,将插件Apk的路径作为参数传入。
- 将第一步创建的AssetManager对象作为参数,创建一个新的Resources对象,并返回给插件使用。
在第二个方案的实现过程可能产生一个异常,也就是空指针异常,在AppCompatDelegateImpl中产生,因为插件和宿主都是继承自AppCompatActivity,所以两者都会执行到AppCompatDelegateImpl中的如下代码
// 这块代码执行的是宿主的
// mDecorContentParent == null 可能发生该空指针异常
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(R.id.decor_content_parent);
mDecorContentParent.setWindowCallback(getWindowCallback());
// 宿主
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(0x7f07004e);
// 宿主的
0x7f07004e decor_content_parent false
// 插件的
0x7f07004d decor_content_parent false
复制代码
因为宿主的dex包是放在插件dex前面的,为什么要放在前面呢,因为这里是插件化而不是热修复。同时由于双亲委派机制的原因,插件Apk中的AppCompatActivity,AppCompatDelegateImpl等类都是由宿主中的类加载器去加载的。所以当插件中执行该代码的时候,就会执行mDecorContentParent = (DecorContentParent) subDecor.findViewById(0x7f07004e)
,也就是说会执行0x7f07004e这个id,但是插件中的0x7f07004e并不是对应decor_content_parent
这个内容,而是对应其他的id,因为插件中0x7f07004d才是decor_content_parent
,所以就抛出空指针异常了。
上面的两个方案都可能会产生这个问题,为什么是可能呢,如果两个id刚好对应上了,就不会产生该异常了。那么如何解决该异常呢?
这是因为AppCompat在使用Context的时候,其实使用的是宿主的Context,我们的插件需要自己创建一个Context,然后绑定启动插件资源的Resources,就可以解决该问题了。
public class BaseActivity extends AppCompatActivity {
protected Context mContext;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Resources resources = LoadResourcesUtils.getResources(getApplication());
mContext = new ContextThemeWrapper(getBaseContext(),0);
Class<? extends Context> clazz = mContext.getClass();
try{
Field mResourcesField = clazz.getDeclaredField("mResources");
mResourcesField.setAccessible(true);
mResourcesField.set(mContext,resources);
}catch (Exception e){
e.printStackTrace();
}
}
复制代码
LoadResourcesUtils类的实现如下:
public class LoadResourcesUtils {
private final static String apkPath = "/sdcard/plugin-debug.apk";
private static Resources mResources;
public static Resources getResources(Context context){
if (mResources == null){
mResources = loadResource(context);
}
return mResources;
}
public static Resources loadResource(Context context) {
try {
Class<?> assetManagerClass = AssetManager.class;
AssetManager assetManager = (AssetManager) assetManagerClass.newInstance();
Method addAssetPathMethod = assetManagerClass.getMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, apkPath);
//如果传入的是Activity的context死循环,导致崩溃
Resources resources = context.getResources();
//用来加载插件包中的资源
return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
复制代码
在PluginActivity中实现如下,注意这里的写法,因为我们要用插件自己的Context,所以需要使用该方式来加载View。
public class PluginActivity extends BaseActivity {
private static final String TAG = "PluginActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, "onCreate: ======启动插件的Activity");
View view = LayoutInflater.from(mContext).inflate(R.layout.activity_main,null);
setContentView(view);
}
}
复制代码
raw文件夹和assets文件夹有什么区别
raw:Android会自动的为这目录中所有资源文件生成一个ID,意味着很容易就可以访问到这个资源,甚至在xml中都是可以访问的,使用ID访问的速度是最快的。
assets:不会生成ID,只能通过AssetManager访问,xml中不能访问,访问速度会慢些,不过操作更加方便。
总结
到这里,我们已经讲解完资源文件的插件化,前面两篇文章还讲到了普通类的插件化和Activity的插件化,当然插件化的内容远远不是这么简单的,比如我们要是在Activity配置了一些属性,比如启动模式等,也需要我们做对应的处理,当然还有其他三大组件的插件化,还有很多的场景需要我们去处理。
那有人就问了,为什么不研究的仔细一些呢,继续研究呢?原因很简单,专业的事情交给专业的人去做,插件化往往在大公司由专门的团队负责,个人的力量毕竟有限。同时,我所在的项目也没有用到过插件化,如果用到的话,我们也可以基于目前的研究(虽然只是一些皮毛,但是可以很快的进入并深入了解)。
在公司做开发,最重要的是把当前的项目做好,做好自己的产品,同时自己有意识的去研究一些专业的内容,像插件化,热修复,ASM等一些非常复杂的内容,我们当然需要研究他们,但是如果你目前的项目也没有用到他们,那也不必气馁,我们自己也可以研究到一定的深度,当你的项目接触到他们后,你就可以的进行更深入的研究。也不必担心到时候时间不够,公司肯定会给你留一些预研的时间,毕竟像上面说的那些内容都需要花费很长的时间才能充分了解的。
最后,学习技术,不仅仅要刻苦钻研,更要懂得权衡利弊。