插件化换肤原理—— 布局加载过程、View创建流程、Resources 浅析

news2024/11/29 6:27:02

作者:孙先森Blog
本文主要分析了 Android 布局加载流程

分析

一般的换肤功能大概是这样的:在 App 的皮肤商城内下载“皮肤包”,下载完成后点击更换界面上的 View 相关资源(颜色、样式、图片、背景等)发生改变;

大多数 App 的换肤功能是不需要重启页面的,那么就有两个问题需要搞明白:

  1. 如何通知 View 进行换肤操作?
  2. 换肤是如何加载皮肤包中的资源的?

想彻底搞清楚这两个问题首先要熟悉 View 是怎么从 XML 文件中出现到屏幕上的,熟悉布局以及 View 的创建流程后上面的问题解决思路就出来了。

布局创建流程

以 Activity 中加载 XML 布局的过程作为切入点来了解这一流程,这部分其实和换肤功能关联性不大,但是作为基础知识还是过一遍流程吧。

我们一般都通过 setContentView 方法给 Activity 设置布局,如下所示:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main) // 核心代码
    }
}

源码流程

跟踪 setContentView 源码到 Activity 类中:

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

最终调用了 mWindow 的 setContentView 方法,那么就从这里开始,先来分析下 mWindow 是怎么来的。此处调用的生命周期在 onCreate,那么说明 mWindow 的初始化在 onCreate 之前,直接跳到 ActivityThread 类中的 performLaunchActivity 方法查看(关于 Activity 的启动流程请自行查阅资料):

ActivityThread.java

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // ...
    Activity activity = null;
    try {
        // 通过反射创建 Activity
        java.lang.ClassLoader cl = appContext.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
    }
    // ...
    // 注意这个 attach 方法在触发 onCreate 之前调用
    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, r.shareableActivityToken);
    // ...
    // 这里是触发 Activity onCreate 生命周期
    mInstrumentation.callActivityOnCreate(activity, r.state);
    // ...
}

看一下 Activity attach 方法中 mWindow 初始化部分:

Activity.java

final void attach(Context context, ActivityThread aThread, Instrumentation instr, 参数省略...) {
    // ...
    // mWindow 初始化
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    // ...
    // 给 mWindow 中的 mWindowManager 赋值
    mWindow.setWindowManager(
        (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
        mToken, mComponent.flattenToString(),
        (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    // 将上面 mWindow 中的 mWindowManager 又赋值给 Activity 的 mWindowManager
    mWindowManager = mWindow.getWindowManager();
    // ...
}

从上面源码中可以知道 mWindow 是 PhoneWindow,在 Activity 中调用 setContentView 就直接调用到了 PhoneWindow 中,看一下 PhoneWindow 中的源码都做了什么:

PhoneWindow.java

public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor(); // 核心 1
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        // ...
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent); // 核心 2
    }
    // ...
}

上述代码主要有两处核心代码,逐个来进行分析,先记住一点,此时的 Activity 还是一片空白。

installDecor()

此时的 mContentParent 还是 null,进入 installDecor 方法看源码:

PhoneWindow.java

private void installDecor() {
    // ...
    if (mDecor == null) {
        // 初始化 mDecor
        mDecor = generateDecor(-1);
        // ...
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        // 初始化 mContentParent
        mContentParent = generateLayout(mDecor);
        // ...
    }
}

mDecor 初始化代码:

PhoneWindow.java

protected DecorView generateDecor(int featureId) {
    // ...
    // 直接 new DecorView 进行初始化
    // 注意第三个参数 this,将当前 PhoneWindow 对象赋值给 DecorView 中的 mWindow
    return new DecorView(context, featureId, this, getAttributes());
}

注意这个 DecorView 是继承自 FrameLayout 就不贴源码了,重点来看一下 mContentParent 的初始化代码:

PhoneWindow.java

protected ViewGroup generateLayout(DecorView decor) {
    // ...
    // 先是一系列的属性设置贴了一些平时常用的
    // 取消标题栏
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
        requestFeature(FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
        // Don't allow an action bar if there is no title.
        requestFeature(FEATURE_ACTION_BAR);
    }
    // ...
    // 设置全屏
    if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
        setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
    }
    // ...
    WindowManager.LayoutParams params = getAttributes();
    // 一系列的窗口属性设置
    // 如:SDK 31 新增的高斯模糊
    if (a.getBoolean(R.styleable.Window_windowBlurBehindEnabled, false)) {
        if ((getForcedWindowFlags() & WindowManager.LayoutParams.FLAG_BLUR_BEHIND) == 0) {
            params.flags |= WindowManager.LayoutParams.FLAG_BLUR_BEHIND;
        }

        params.setBlurBehindRadius(a.getDimensionPixelSize(
                android.R.styleable.Window_windowBlurBehindRadius, 0));
    }
    // ...
    // 整体布局文件
    int layoutResource;
    // 根据一系列判断选择 SDK 中的布局一般默认是 R.layout.screen_simple
    if (...){
    }else if(...){
    }else{
        layoutResource = R.layout.screen_simple;
    }

    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    // 拿到 screen_simple.xml 布局的内容部分 (R.id.content)
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    // ...
    return contentParent; // 返回
}

最终,是调用了 DecorView 的 onResourcesLoaded 方法,并且将 R.layout.screen_simple 布局传递过去,先来看一下布局文件源码:

R.layout.screen_simple.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

可以看出整个 Activity 默认是一个 LinearLayout 布局中包含着标题栏和内容部分,标题栏部分采用 ViewStub 形式加载,内容部分是一个空白的 FrameLayout。

接着来看 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource) 源码:

DecorView.java

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    // ...
    // 通过 LayoutInflater 将 screen_simple.xml 解析成 View
    final View root = inflater.inflate(layoutResource, null);
    if (mDecorCaptionView != null) {
        // ...
    } else {
        // 调用 addView 将 root 添加到 DecorView 上
        addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    mContentRoot = (ViewGroup) root;
    // ...
}

onResourcesLoaded 主要将默认布局文件解析成 View 并且添加到 DecorView 上,解析的过程这里先不分析,放到后面的小节中,addView 则调用其父类 ViewGroup 的 addView。

此时 DecorView 应该是这样的:

内容部分目前还是空白,还没有解析添加我们在 Activity setContentView 传入的布局。

onResourcesLoaded 执行完成后,通过 findViewById 方法获取内容部分的 FrameLayout,并且将其返回。

installDecor 方法到此就分析完了,总结下:

  1. mDecor 也就是 DecorView 进行初始化
  2. 解析出默认布局添加到 DecorView 中,并且将内容部分 View 赋值给 mContentParent

mLayoutInflater.inflate(layoutResID, mContentParent)

分析完 installDecor 方法得知 mContentParent 就是承载 Activity 布局的 View,这里继续跟踪源码来看一下 inflate 做了哪些工作:

LayoutInflate.java

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    // 注意这里第三个参数 也就是 attachToRoot 为 ture
    // root 也就是 mContentParent,installDecor 中已经初始化
    return inflate(resource, root, root != null);
}

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    // ...
    // 获取布局文件的解析器
    XmlResourceParser parser = res.getLayout(resource);
    // ...
    return inflate(parser, root, attachToRoot);
}

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    // ...
    // root 赋值给 result
    View result = root;

    // 将解析器推进到第一个START_TAG 也就是根View
    advanceToRootNode(parser); 
    // 拿到根 View 名字
    final String name = parser.getName();
    if (TAG_MERGE.equals(name)) { // merge 布局进 if
        // ...
    } else { // 普通布局进 else
        // 创建出根 View ## 后面小节着重分析这里是如何创建 View 的
        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
        ViewGroup.LayoutParams params = null; // 布局参数
        if (root != null) { // root 不为 null
            params = root.generateLayoutParams(attrs); // 初始化布局参数
            if (!attachToRoot) { // atachToRoot 为 true 不进入
                temp.setLayoutParams(params);
            }
        }
        // ...

        // 解析布局中的其他 View 并且添加到 temp 根 View 中
        rInflateChildren(parser, temp, attrs, true);
        // ...

        // 将创建的根 View 添加到 root 也就是 mContentParent 中
        if (root != null && attachToRoot) {
            root.addView(temp, params);
        }
        // ...
    }
    // ...
    return result;
}

可以看出这里的调用将我们传入的 Activity 布局的根 View 解析出来并且添加到了 mContentParent 中,此时的 DecorView 是这样的:

到这里为止我们的布局就全部创建完成了,但是要注意,onCreate 中调用 setContentView 后流程就走到了这里,此时布局文件虽然创建完成了,但是还并没有绘制到 Activity 中,View 的三大流程 measure、draw、layout 都还没有进行,本篇博客主要分析换肤功能,View 的绘制流程将在后续其他博客中会另行分析。

View 的创建

在上述布局文件创建流程,在最后的 LayoutInflate 文件中的 inflate 方法里创建出了布局文件的所有 View。根布局的 View 直接通过 createViewFromTag 方法创建,直接点进去源码查看:

LayoutInflate.java

private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
    return createViewFromTag(parent, name, context, attrs, false);
}

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
    // ...

    // 先通过 tryCreateView 尝试创建 View
    View view = tryCreateView(parent, name, context, attrs);

    if (view == null) { // 如果创建失败进入 if
        // ...
        try {
            if (-1 == name.indexOf('.')) { // 表示 sdk 中的 View (Text、Button...)
                view = onCreateView(context, parent, name, attrs);
            } else { // 表示自定义 View 或者 support 包中的 View (androidx.appcompat.widget.AppCompatButton...)
                view = createView(context, name, null, attrs);
            }
        }
        // ...
    }

    return view;
    // ...
}

上述代码的逻辑并不复杂,共有三个创建 View 的方法,逐个对其进行分析

tryCreateView(parent, name, context, attrs)

LayoutInflate.java

public final View tryCreateView(@Nullable View parent, @NonNull String name,
    // ...
    View view;
    if (mFactory2 != null) { // 优先通过 mFactory2 创建 View
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {  // 第二选择 mFactory 创建 View
        view = mFactory.onCreateView(name, context, attrs);
    } else { // 都创建失败返回 null
        view = null;
    }
    // ...
    return view;
}

逻辑很简单,那么就有分别看看 mFactory2 和 mFactory 是什么:

LayoutInflate.java

public interface Factory {
    @Nullable
    View onCreateView(@NonNull String name, @NonNull Context context,
            @NonNull AttributeSet attrs);
}

public interface Factory2 extends Factory {
    @Nullable
    View onCreateView(@Nullable View parent, @NonNull String name,
            @NonNull Context context, @NonNull AttributeSet attrs);
}

两个均是 LayoutInflate 的内部接口,本博客以 Activity 布局创建流程作为切入点分析,且 Activity 也实现了 Factory2 接口,

onCreateView(context, parent, name, attrs)

LayoutInflate.java

public View onCreateView(@NonNull Context viewContext, @Nullable View parent,
        @NonNull String name, @Nullable AttributeSet attrs)
        throws ClassNotFoundException {
    return onCreateView(parent, name, attrs);
}

protected View onCreateView(View parent, String name, AttributeSet attrs)
        throws ClassNotFoundException {
    return onCreateView(name, attrs);
}

protected View onCreateView(String name, AttributeSet attrs)
        throws ClassNotFoundException {
    // 因为是sdk原生View 增加前缀 android.view. 用于反射创建 View
    return createView(name, "android.view.", attrs);
}

public final View createView(String name, String prefix, AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    Context context = (Context) mConstructorArgs[0];
    if (context == null) {
        context = mContext;
    }
    return createView(context, name, prefix, attrs);
}

可以看出 onCreateView 方法最后还是调用到了 createView 方法,仅仅是增加了 “android.view.” 前缀传递过去。

createView(context, name, null, attrs)

public final View createView(@NonNull Context viewContext, @NonNull String name,
        @Nullable String prefix, @Nullable AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    // ...
    // 优先从 sConstructorMap 中获取 View 的构造方法
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    // ...
    Class<? extends View> clazz = null;
    // ...

    // sConstructorMap 中获取不到 则通过反射获取 View 的构造方法并且保存到 sConstructorMap 里
    if (constructor == null) {
        clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                mContext.getClassLoader()).asSubclass(View.class);
        // ...
        constructor = clazz.getConstructor(mConstructorSignature);
        constructor.setAccessible(true);
        sConstructorMap.put(name, constructor);
    } else {
        // ...
    }
    // ...

    // 通过反射获取的构造方法创建 View
    final View view = constructor.newInstance(args);

    // ...
    return view;
}

createView 方法主要是是通过反射的方式创建出 View,并且对 View 的构造方法做了缓存。

Factory 和 Factory2

熟悉了 View 创建过程,总结下来就是优先使用 Factory2 去创建,创建失败则尝试使用 Factory,再失败直接反射创建,那么我们来找一下 Factory2 是什么时候设置的。

通常我们继承的 Activity 是 AppCompatActivity,在其 onCreate 方法中有这样一行代码:

AppCompatActivity.java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    final AppCompatDelegate delegate = getDelegate();
    delegate.installViewFactory(); // 注意这行代码
    delegate.onCreate(savedInstanceState);
    super.onCreate(savedInstanceState);
}

先看一下这个 delegate 是什么:

AppCompatActivity.java

public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}

AppCompatDelegate.java

public static AppCompatDelegate create(@NonNull Activity activity,
        @Nullable AppCompatCallback callback) {
    return new AppCompatDelegateImpl(activity, callback);
}

原来是 AppCompatDelegateImpl 的实例,直接查看其 installViewFactory 方法:

AppCompatDelegate.java

public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        // 看到这里的 this 就说明 AppCompatDelegate 实现了 Factory2 接口
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else {
        if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}

在 installViewFactory 方法中手动给 LayoutInflater 设置了 mFactory2,既然 AppCompatDelegate 实现了 Factory2,直接查看其 onCreateView 方法实现:

AppCompatDelegate.java

@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    return createView(parent, name, context, attrs);
}

public View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs) {
    if (mAppCompatViewInflater == null) {
        // ...
        mAppCompatViewInflater = new AppCompatViewInflater();
        // ...
    }
    // ...
    return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
}

最终又调用了 AppCompatViewInflater 的 createView 方法,继续跟踪源码:

AppCompatViewInflater.java

final View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs, boolean inheritContext,
        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    // ...
    View view = null;
    switch (name) {
        case "TextView": // 根据名字 创建出 View
            view = createTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageView":
            view = createImageView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "Button":
            // 还有很多不贴了
            // ...
        default:
            // 没有列举出来的 走 createView 方法 返回的 null
            view = createView(context, name, attrs);
    }

    return view;
}

// 直接创建出了 appcompat 包下的 View 并没有使用反射
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
    return new AppCompatTextView(context, attrs);
}

// 直接创建出了 appcompat 包下的 View 并没有使用反射
protected AppCompatImageView createImageView(Context context, AttributeSet attrs) {
    return new AppCompatImageView(context, attrs);
}

// createView 返回的 null 也就意味着 mFactory2 创建 View 失败了
protected View createView(Context context, String name, AttributeSet attrs) {
    return null;
}

从上述代码流程中可以看出,AppCompatActivity 默认设置了 Factory2,并且其实现创建 View 是直接通过 new 的方式,并没有使用反射,性能也比较好。

Resources 浅析

熟悉了 View 创建流程,接着熟悉下资源文件的获取过程。

所谓插件化换肤,就是将皮肤资源单独打包下发给 App,App 从打包文件中获取皮肤资源进行设置。实现这个功能需要对 Android 的资源文件获取有一定的了解。

当我们的 App 打包后,资源文件会在打包进 resources.arsc 文件中,随便找一个apk,可以在 AS 中打开进行分析,如下图所示:

可以看出颜色、drawable、mipmap等等资源都类似于数据库形式存在,当我们调用 getDrawable(id) 时,会找出对应的资源返回。

接着就来分析下 Android Resource 文件是如何获取的。通常我们在 Activity 中获取一个 Drawable 资源代码是这样的:

getResources().getDrawable(R.drawable.xxx)

源码分析

getResources()

先来分析 getResources 方法,跟踪源码会跳到 Context 中的 :

Context.java

public abstract Resources getResources();

返回的是一个 Resources 对象。Context 的实现类是 ContextImpl,同样是在 ActivityThread 的 performLaunchActivity 方法中创建,通过 Activity 的 attach 传递给 Activity:

ActivityThread.java

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // ...
    ContextImpl appContext = createBaseContextForActivity(r);
    // ...
    activity.attach(appContext, this, getInstrumentation(), 参数省略...);
}

private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
    // ...
    ContextImpl appContext = ContextImpl.createActivityContext(
            this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
    // ...
    return appContext;
}

可以看出是通过 ContextImpl 的 createActivityContext 方法创建,查看其源码:

ContextImpl.java

static ContextImpl createActivityContext(ActivityThread mainThread,
        LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
        Configuration overrideConfiguration) {
    // ...
    // 创建出 context
    ContextImpl context = new ContextImpl(null, mainThread, packageInfo, ContextParams.EMPTY,
            attributionTag, null, activityInfo.splitName, activityToken, null, 0, classLoader,
            null);
    // ...
    // ResourcesManager 单例
    final ResourcesManager resourcesManager = ResourcesManager.getInstance();
    // 在这里调用了 setResources 给 mResources 赋值
    context.setResources(resourcesManager.createBaseTokenResources(activityToken,
            packageInfo.getResDir(),
            splitDirs,
            packageInfo.getOverlayDirs(),
            packageInfo.getOverlayPaths(),
            packageInfo.getApplicationInfo().sharedLibraryFiles,
            displayId,
            overrideConfiguration,
            compatInfo,
            classLoader,
            packageInfo.getApplication() == null ? null
                    : packageInfo.getApplication().getResources().getLoaders()));
    // ...
    return context;
}

接着查看 ResourcesManager 的 createBaseTokenResources 方法是如何创建 Resources 的:

ResourcesManager.java

public @Nullable Resources createBaseTokenResources(@NonNull IBinder token, 参数省略...) {
    try {
        // ...
        // 又调用到了 createResourcesForActivity 方法
        return createResourcesForActivity(token, key,
                /* initialOverrideConfig */ Configuration.EMPTY, /* overrideDisplayId */ null,
                classLoader, /* apkSupplier */ null);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
    }
}

private Resources createResourcesForActivity(@NonNull IBinder activityToken, 参数省略...) {
    synchronized (mLock) {
        // ...
        // 创建出了 ResourcesImpl 这里注意下一会会分析这个方法
        ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key, apkSupplier);
        // ...
        // 又调用到了 createResourcesForActivityLocked 方法
        return createResourcesForActivityLocked(activityToken, initialOverrideConfig,
                overrideDisplayId, classLoader, resourcesImpl, key.mCompatInfo);
    }
}

private Resources createResourcesForActivityLocked(@NonNull IBinder activityToken,
        @NonNull Configuration initialOverrideConfig, @Nullable Integer overrideDisplayId,
        @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl,
        @NonNull CompatibilityInfo compatInfo) {
    // ...
    // 创建出 Resources 对象
    Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
            : new Resources(classLoader);
    // 将上面创建的 ResourcesImpl 赋值
    resources.setImpl(impl);
    // ...
    return resources;
}

Resources 的创建过程中值得注意的是 ResourcesImpl 的创建,回到 findOrCreateResourcesImplForKeyLocked 方法中查看其创建过程:

ResourcesManager.java

private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(
        @NonNull ResourcesKey key, @Nullable ApkAssetsSupplier apkSupplier) {
    // 缓存获取
    ResourcesImpl impl = findResourcesImplForKeyLocked(key); 
    if (impl == null) {
        // 缓存获取不到 直接创建
        impl = createResourcesImpl(key, apkSupplier);
        if (impl != null) { // 放入缓存
            mResourceImpls.put(key, new WeakReference<>(impl));
        }
    }
    return impl;
}

继续查看 createResourcesImpl 方法:

ResourcesManager.java

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key,
        @Nullable ApkAssetsSupplier apkSupplier) {
    // 先创建出了 
    final AssetManager assets = createAssetManager(key, apkSupplier);
    if (assets == null) {
        return null;
    }
    // ...
    // 将 AssetManager 对象作为参数传递给 ResourcesImpl 构造器
    final ResourcesImpl impl = new ResourcesImpl(assets, displayMetrics, config, daj);
    // ...
    return impl;
}

到这里先稍微总结下,Resource 的创建过程中会先创建出 AssetManager 并且传递给 ResourcesImpl,最后将 ResourcesImpl 再 set 给 Resources。实际上 Resources 和 ResourcesImpl 都是壳,真正做事情的是 AssetManager。

getDrawable()

熟悉了 Resources 的创建过程,就来跟踪下 getDrawable 看看其是如何查找资源的:

Resources.java

public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
        throws NotFoundException {
    return getDrawableForDensity(id, 0, theme);
}

public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {
    final TypedValue value = obtainTempTypedValue();
    try {
        // 在这里调用了 ResourcesImpl 的 getValueForDensity 方法
        final ResourcesImpl impl = mResourcesImpl;
        impl.getValueForDensity(id, density, value, true);
        return loadDrawable(value, id, density, theme);
    } finally {
        releaseTempTypedValue(value);
    }
}

继续查看 ResourcesImpl 的 getValueForDensity:

ResourcesImpl.java

void getValueForDensity(@AnyRes int id, int density, TypedValue outValue,
        boolean resolveRefs) throws NotFoundException {
    // 最终交给了真正做事情的 AssetManager
    boolean found = mAssets.getResourceValue(id, density, outValue, resolveRefs);
    if (found) {
        return;
    }
    throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}

看到这里就可以明白,如果想要加载皮肤包的资源,那就需要在 AssetManager 上做文章,本篇博客主要熟悉源码流程,思路在下一篇分享。

只需记住 Resources 中包含 ResourcesImpl,而 ResourcesImpl 中又包含着 AssetManager 即可。

最后

本篇主要分享了布局加载流程(好像跟换肤没啥关系)、View 创建过程(重点)、Resources 简单分析(重点)。


Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集https://qr18.cn/CgxrRy
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/422986.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

栈的实现及相关OJ题

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了 博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点!人生格言&#xff1a;当你的才华撑不起你的野心的时候,你就应该静下心来学习! 欢迎志同道合的朋友一起加油喔&#x1f9be;&am…

28岁,他是如何成为上市公司测试总监的

现在的大环境下&#xff0c;各行各业都开始内卷起来&#xff0c;测试也不例外&#xff0c;企业要求也越来越高&#xff0c;“会代码”逐渐成为测试工程师的一个标签。你要想拿到一个不错的薪资&#xff0c;必不可少的一个技能—自动化测试&#xff0c;自动化测试难吗&#xff1…

Oracle集合查询详解加练习题

#集合查询 概念&#xff1a;将不同的数据集合&#xff08;SQL查询语句&#xff09;按照集合的规则&#xff0c;拼接一个临时的&#xff0c;新的数据集合&#xff08;表&#xff09; 1.集合&#xff1a;并集、交集、差集 并集 union all 语法&#xff1a;select column1,COLUM…

「MongoDB」时序数据库和MongoDB第二部分-模式设计最佳实践

在上一篇博客文章时间序列数据与MongoDB&#xff1a;第一部分-简介中&#xff0c;我们介绍了时间序列数据的概念&#xff0c;然后介绍了一些可以用于帮助收集时间序列应用程序需求的发现问题。对这些问题的回答有助于指导支持大容量生产应用程序部署所需的模式和MongoDB数据库配…

基于深度学习PaddleOcr身份证识别

之前使用opencv机械学习处理图片&#xff0c;使用Testseract-OCR进行身份证和姓名识别&#xff0c;发现受背景图片的影响比较大&#xff0c;转PaddleOcr&#xff0c;识别成功率能达到使用要求。 PaddleOcr官网地址&#xff1a;飞桨PaddlePaddle-源于产业实践的开源深度学习平台…

Python爬虫-某跨境电商(AM)搜索热词

前言 本文是该专栏的第42篇,后面会持续分享python爬虫干货知识,记得关注。 关于某跨境电商(AM),本专栏前面有单独详细介绍过,获取配送地的cookie信息以及商品库存数据,感兴趣的同学可往前翻阅。 1. python爬虫|爬取某跨境电商AM的商品库存数据(Selenium实战) 2. Seleni…

偶数科技发布实时湖仓数据平台 Skylab 5.0

2023 年 4 月 11 日&#xff0c; 偶数发布了最新的实时湖仓数据平台 Skylab 5.0&#xff0c;平台各个组件进行了全面升级。新版 Skylab 的发布标志着偶数科技具有从数据存储、计算、管理到分析、应用和 AI 能力的完整的数据管理生态闭环&#xff0c;帮助用户实现批流一体、实时…

贴完车衣开车就走?

贴完车衣之后&#xff0c;你以为直接开走就好了吗&#xff1f; 大错特错&#xff01;&#xff01;&#xff01; 正确流程&#xff0c;记得收藏起来&#xff01; 1&#xff1a;膜开箱&#xff1a;这个当场开箱&#xff0c;防止偷梁换柱 2&#xff1a;装贴过程&#xff1a;确认施…

损失函数(Loss Function)一文详解-分类问题常见损失函数Python代码实现+计算原理解析

目录 前言 一、损失函数概述 二、损失函数分类 1.分类问题的损失函数 1.交叉熵损失函数&#xff08;Cross Entropy Loss&#xff09; 2.Hinge损失函数 3.余弦相似度损失函数&#xff08;Cosine Similarity Loss&#xff09; 4.指数损失函数&#xff08;Exponential Los…

Next.js Polygon, Solidity,The Graph,IPFS,Hardhat web3博客系统

参考 源文档The Complete Guide to Full Stack Web3 Development - DEV Community 源码&#xff0c;源文章里的github项目无法直接运行&#xff0c;经过修改后可mac中可用GitHub - daocodedao/web3-blog: https://linzhji.blog.csdn.net/article/details/130125634 框架 博客…

python pandas数据处理excel、csv列转行、行转列(具体示例)

一、数据处理需求 对Excel或CSV格式的数据,我们经常都是使用pandas库读取后转为DataFrame进行处理。有的时候我们需要对其中的数据进行行列转换,但是不是简单的行列转换,因为数据中有重复的数据属性。比如我们的数据在Excel中的格式如下: 那么,我们如何将上面的数据格式转…

2023爱分析·商业智能应用解决方案市场厂商评估报告:数聚股份

目录 1. 研究范围定义 2. 商业智能应用解决方案市场分析 3. 厂商评估&#xff1a;数聚股份 4. 入选证书 1. 研究范围定义 商业智能&#xff08;BI&#xff09;是在实现数据集成和统一管理的基础上&#xff0c;利用数据存储和处理、分析与展示等技术&#xff0c;满足企…

Java多线程案例-Java多线程(3)

各位观众老爷们, 创作不易, 多多支持&#x1f636;‍&#x1f32b;️&#x1f64f;&#x1f618; 字数11223, 时间:2023年4月16日11:19:58 状态: 精神恍恍惚惚, 想打游戏&#x1f975; 目录(点击传送) 单例模式 饿汉模式 懒汉模式 单线程版 多线程版 阻塞队列 什么是阻塞队…

助力工业物联网,工业大数据之ODS层及DWD层建表语法【七】

文章目录ODS层及DWD层构建01&#xff1a;课程回顾02&#xff1a;课程目标03&#xff1a;数仓分层回顾04&#xff1a;Hive建表语法05&#xff1a;Avro建表语法ODS层及DWD层构建 01&#xff1a;课程回顾 一站制造项目的数仓设计为几层以及每一层的功能是什么&#xff1f; ODS&am…

Pytorch-gpu的安装

1.先安装cuda和cudnn 推荐安装cuda11.3和cuda10.2&#xff0c;因为这两个版本用的多。 安装教程可以看我的另一篇文章&#xff1a; cuda和cudnn安装教程 2.安装对应版本的Pytorch Pytorch的版本需要和cuda的版本相对应。具体对应关系可以去官网查看。这里先附上一张对应关系…

openpnp - 顶部相机 - 辅助光(环形灯)的电路原理图

文章目录openpnp - 顶部相机 - 辅助光(环形灯)的电路原理图概述ENDopenpnp - 顶部相机 - 辅助光(环形灯)的电路原理图 概述 同学帮我做的简易灯板设计不太合理, 发热量极大. 想看看商用的环形灯电路啥样的, 如果有可能, 自己做块灯板, 塞进商用环形灯外壳中. 拆解了一个环形灯…

数据库备份

数据库备份&#xff0c;恢复实操 策略一&#xff1a;&#xff08;文件系统备份工具 cp&#xff09;&#xff08;适合小型数据库&#xff0c;是最可靠的&#xff09; 1、停止MySQL服务器。 2、直接复制整个数据库目录。注意&#xff1a;使用这种方法最好还原到相同版本服务器中&…

【图像分类】【深度学习】ViT算法Pytorch代码讲解

【图像分类】【深度学习】ViT算法Pytorch代码讲解 文章目录【图像分类】【深度学习】ViT算法Pytorch代码讲解前言ViT(Vision Transformer)讲解patch embeddingpositional embeddingTransformer EncoderEncoder BlockMulti-head attentionMLP Head完整代码总结前言 ViT是由谷歌…

Spring Boot+Vue实现Socket通知推送

目录 Spring Boot端 第一步&#xff0c;引入依赖 第二步&#xff0c;创建WebSocket配置类 第三步&#xff0c;创建WebSocket服务 第四步&#xff0c;创建Controller进行发送测试 Vue端 第一步&#xff0c;创建连接工具类 第二步&#xff0c;建立连接 ​编辑 第三步&a…

xxl-job-2.3.1 本地编译jar包并部署

参考网上其他文章&#xff0c;总结步骤 一、官网地址 分布式任务调度平台XXL-JOB 二、源码地址 github&#xff1a; GitHub - xuxueli/xxl-job: A distributed task scheduling framework.&#xff08;分布式任务调度平台XXL-JOB&#xff09; gitee: xxl-job: 一个分布…