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的大致启动流程

  1. 首先是点击App图标,此时是运行在Launcher进程,通过ActivityManagerService Binder IPC的形式向system_server进程发起startActivity的请求

  2. system_server进程接收到请求后,通过Process.start方法向zygote进程发送创建进程的请求

  3. zygote进程fork出新的子进程,即App进程

  4. 然后进入ActivityThread.main方法中,这时运行在App进程中,通过ActivityManagerServiceBinder IPC的形式向system_server进程发起attachApplication请求

  5. system_server接收到请求后,进行一系列准备工作后,再通过Binder IPC向App进程发送scheduleLaunchActivity请求

  6. App进程binder线程(ApplicationThread)收到请求后,通过Handler向主线程发送LAUNCH_ACTIVITY消息

  7. 主线程收到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添加资源地址的流程图如下:

image

结合前面的常用方法分析,我们发现Apk的资源时通过AssetManager.addAssetPath方法来完成加载,那么我们就可通过反射构建自己的AssManager对象,然后调用addAssetPath加载自己的资源,然后把自己构建的AssetManager通过反射设置给mAssets变量,这样下次加载资源就是用我们的AssetManager。

资源加载的两种实现方案

  1. 插件的资源和宿主的资源直接合并,直接用宿主的(Resources)AssetManager进行加载资源,可能会有冲突的问题,需要用aapt修改一下资源ID。
  2. 专门创建一个(Resources)AssetManager加载插件的资源

方案一涉及到使用aapt修改插件中资源的ID,较为麻烦,本文使用第二种方案

实现步骤:

  1. 创建一个AssetManager对象,并调用addAssetPath方法,将插件Apk的路径作为参数传入。
  2. 将第一步创建的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等一些非常复杂的内容,我们当然需要研究他们,但是如果你目前的项目也没有用到他们,那也不必气馁,我们自己也可以研究到一定的深度,当你的项目接触到他们后,你就可以的进行更深入的研究。也不必担心到时候时间不够,公司肯定会给你留一些预研的时间,毕竟像上面说的那些内容都需要花费很长的时间才能充分了解的。

最后,学习技术,不仅仅要刻苦钻研,更要懂得权衡利弊。

推荐阅读更多精彩内容