30

Android APP Bundle动态交付技术资源加载原理

 3 years ago
source link: https://zhuanlan.zhihu.com/p/137551996
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Android APP Bundle动态交付技术资源加载原理

58同城 Android工程师

之前为大家介绍了Android App Bundle解析 支持apk按维度拆分和动态交付技术减少包大小。其中动态交付技术 其实和我们业内的插件化技术类似。将插件APK下载后无需安装,宿主apk对插件APK进行类加载和资源加载完成后,就可以使用插件APK中的四大组件与资源等。本篇文章主要结束动态交付技术资源加载原理后续为大家介绍类加载原理

官方动态交付资源加载使用

动态交付使用官方文档

You can also enable SplitCompat in specific activities or services at runtime. Enabling SplitCompat this way is required to launch activities included in feature modules immediately after module installation. To do this, override attachBaseContext as seen below.

文档中说明当我们安装完成SpliteAPK后,需要在SpliteAPK中定义的每个activity service重写attachBaseContext方法,添加SplitCompat.install(this)。添加该方法后SpltiteAPK就可访问baseAPK与已安装SplitAPK模块的资源了。

abstract class BaseSplitActivity : AppCompatActivity() {

    override fun attachBaseContext(newBase: Context) {
        val ctx = LanguageHelper.getLanguageConfigurationContext(newBase)
        super.attachBaseContext(ctx)
        SplitCompat.install(this)
    }
}

SplitCompat.install(this)做了啥?

对插件化技术有一定了解的同学,应该会猜出来。获取Context对象的AssetManager实例然后通过反射调用addAssetPath方法,将已下载的SpliteAPK路径追加进去,这样SpliteAPK的Activity就可以访问自身的资源了,因为AssetManger包含了资源路径。

AssetManager var24 = var1.getAssets();//获取Context对象的AssetManager实例
Iterator var25 = var22.iterator();
while(var25.hasNext()) {
    File var12 = (File)var25.next();
    int var27 = (Integer)bb.a(var24, "addAssetPath", Integer.class, String.class, var12.getPath());
    Log.d("SplitCompat", (new StringBuilder(39)).append("addAssetPath completed with ").append(var27).toString());
}

由于代码被混淆了,但我们然可以看出其中的大致逻辑和我们保持一致。是不是很简单!
有的同学应该就会好奇了,可不可以在插件APK下载完成的时候同时进行dex注入到classloader和资源注入呢?原理上是可以的。classloader是单实例的,所以我们可以很方便的获取classloader直接通过任意一定对象getClassLoader就可以方便得到,然后进行dex注入。但是AssetManager就不一样了,他是多实例的。并且还有缓存策略。业内的一些插件化就是拦截了整个AssetManager实例的生成策略。但是该方案需要不断的兼容不同的Android版本。但是官方使用的方案就不会有上述的问题,而是对已有的AssetManager对象进行重新注入资源路径。但是官方使用的方法也有一个缺点。

If you want to access assets or resources that exist in the newly installed module from a different installed module of your app, you must do so using the application context. The context of the component that's trying to access the resources will not yet be updated. Alternatively, you can recreate that component or install SplitCompat upon it after the dynamic feature module install.

缺点如上所说:如果我们从一个已安装的modle中去访问一个新安装的资源需要我们使用application的context或者重新创建Activity组件。这是为啥那?因为我们是在Activity创建的时候进行AssetManager对象资源路径注入的。当安装完成在回到当前Activity时,当前Activity并没有触发新的插件资源路径加载。所以需要我们重新创建组件。使用Application的Context可以是因为在安装完成后,playcoresdk会对acpplication对象的AssetManager实例进行资源路径更新。对于这个缺点我们有没有好的处理方式呢。不用重新创建组件。有的。在国内的插件化技术中就有了答案。还有爱奇艺推出的Qigsaw(由于AAB在国内无法使用,爱奇艺实现了一套AAB方案,使用API与AAB一直可以做到无缝转换)。这样我们每次在使用资源是都会检测一下插件APK资源是否安装。

@Override
public Resources getResources() {
    SplitCompat.install(this);
    return super.getResources();
}

@Override
public AssetManager getAssets() {
    SplitCompat.install(this);
    return super.getAssets();
}

理论说的那么多,我们在代码角度看一下介绍AssetManger的作用

AssetManger内置资源加载

无论我们在使用xml布局指定布局资源src还是代码本质上都是走的如下代码

Drawable drawable = getResources().getDrawable(resId);

最终都会通过AssetManger去查找与提取APK中的资源

void getValueForDensity(@AnyRes int id, int density, TypedValue outValue,
        boolean resolveRefs) throws NotFoundException {
    boolean found = mAssets.getResourceValue(id, density, outValue, resolveRefs);
    if (found) {
        return;
    }
    throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}

其中的mAssets就是我们的AssetManger对象实例。

AssetManger创建流程

resoureces对象的获取在Activity的基类对象ContextWrapper的mBase获取的,所以改对象应该是在Activity对象创建的时候填充的。

public class ContextWrapper extends Context {
Context mBase;
@Override
public Resources getResources() {
    return mBase.getResources();
}

下面我们一步一步分析Activity的创建过程。
ActivityThread类实例化Activity关键代码,如下

/**  Core implementation of activity launch. */
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    //创建Context实例
    ContextImpl appContext = createBaseContextForActivity(r);
    Activity activity = null;
try {
    java.lang.ClassLoader cl = appContext.getClassLoader();
    //创建Activity实例
    activity = mInstrumentation.newActivity(
            cl, component.getClassName(), r.intent);
    ....
    // Context与Acitiviy进行关联
    activity.attach(appContext, this, getInstrumentation(), r.token,
        r.ident, app, r.intent, r.activityInfo, title, r.parent,
        r.embeddedID, r.lastNonConfigurationInstances, config,
        r.referrer, r.voiceInteractor, window, r.configCallback,
        r.assistToken);

其中在创建Conetxt对象的内部,进行Resources对象填充。

 ContextImpl appContext = createBaseContextForActivity(r);

ResourcesManager创建Resources关键代码

private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
        @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        //通过key在缓存中查找
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
    if (DEBUG) {
        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
    }
    return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
}
//缓存中没有进行创建ResourcesImpl实例对象与创建AssetManager实例
ResourcesImpl resourcesImpl = createResourcesImpl(key);

通过上述流程我们了解了AssetManager的创建流程,有同学会好奇资源为什么会有key缓存呢?key我们我可以看出包含如下字段。只要下面的字段值发生变化,缓存中没有我们就会新创建一个,如apk的资源路径变了就需要重新创建AssetManager

final ResourcesKey key = new ResourcesKey(
        resDir,
        splitResDirs,
        overlayDirs,
        libDirs,
        displayId,
        overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
        compatInfo);

ResourcesKey重写了equals方法用于在ArrayMap中查找。

@Override
public boolean equals(Object obj) {
    if (!(obj instanceof ResourcesKey)) {
        return false;
    }
    ResourcesKey peer = (ResourcesKey) obj;
    if (mHash != peer.mHash) {
        // If the hashes don't match, the objects can't match.
        return false;
    }

为了验证ResourcesImpl的缓存策略我们可以进行如下操作,我们在不同的Activity实例中调用如下方法。你会发现输出的对象地址是一样的偶。

//getImpl不是开发API 他返回的是ResourcesImpl实例,这里只是个例子
Log.d(TEG,this.getResources().getImpl().toString());
//使用这个方法
Log.d(TEG,this.getAssets().toString());

欢迎大家评论,点赞共同学习,共同进步。

https://blog.csdn.net/sggdjfkf147896325/article/details/52231933
https://juejin.im/post/5d83409f6fb9a06ae0723b03
https://developer.android.com/guide/playcore/dynamic-delivery


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK