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

news/2024/11/16 16:54:25/

前言

继上次 WebView 干货分享后,本次将分享下自己在探索学习 App 换肤功能过程中的相关知识,着重分享换肤的原理以及实现思路。

由于篇幅原因分为两篇博客,本文主要分析了 Android 布局加载流程,下一篇将具体讲解插件化换肤实现思路,以及对主流换肤框架的详细分析。

PS:博客中展示源码部分均已进行省略只展示与本文相关的重要代码 (省略部分用 // … 表示)

分析

一般的换肤功能大概是这样的:在 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 {// 通过反射创建 Activityjava.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 的 mWindowManagermWindowManager = 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) {// 初始化 mDecormDecor = generateDecor(-1);// ...} else {mDecor.setWindow(this);}if (mContentParent == null) {// 初始化 mContentParentmContentParent = generateLayout(mDecor);// ...}
}

mDecor 初始化代码:

PhoneWindow.java

protected DecorView generateDecor(int featureId) {// ...// 直接 new DecorView 进行初始化// 注意第三个参数 this,将当前 PhoneWindow 对象赋值给 DecorView 中的 mWindowreturn 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_simpleif (...){}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" /><FrameLayoutandroid: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 解析成 Viewfinal 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 赋值给 resultView result = root;// 将解析器推进到第一个START_TAG 也就是根ViewadvanceToRootNode(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 不为 nullparams = 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 尝试创建 ViewView 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 创建 Viewview = mFactory2.onCreateView(parent, name, context, attrs);} else if (mFactory != null) {  // 第二选择 mFactory 创建 Viewview = mFactory.onCreateView(name, context, attrs);} else { // 都创建失败返回 nullview = null;}// ...return view;
}

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

LayoutInflate.java

public interface Factory {@NullableView onCreateView(@NonNull String name, @NonNull Context context,@NonNull AttributeSet attrs);
}public interface Factory2 extends Factory {@NullableView 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. 用于反射创建 Viewreturn 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 {// ...}// ...// 通过反射获取的构造方法创建 Viewfinal 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": // 根据名字 创建出 Viewview = createTextView(context, attrs);verifyNotNull(view, name);break;case "ImageView":view = createImageView(context, attrs);verifyNotNull(view, name);break;case "Button":// 还有很多不贴了// ...default:// 没有列举出来的 走 createView 方法 返回的 nullview = 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) {// ...// 创建出 contextContextImpl 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 {// 最终交给了真正做事情的 AssetManagerboolean 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://0a.fit/dNHYY
Android 车载学习指南https://0a.fit/jdVoy
Android Framework核心知识点笔记https://0a.fit/acnLL
Android 音视频学习笔记https://0a.fit/BzPVh
Jetpack全家桶(含Compose)https://0a.fit/GQJSl
Kotlin 入门到精进https://0a.fit/kdfWR
Flutter 基础到进阶实战https://0a.fit/xvcHV
Android 八大知识体系https://0a.fit/mieWJ
Android 中高级面试题锦https://0a.fit/YXwVq


http://www.ppmy.cn/news/7725.html

相关文章

pure pursuit纯跟踪

Pure Pursuit是一种几何追踪方法,速度越小,performance越好; :汽车前轮转角 L:前后轮轴距(车长) R:转弯半径 将车辆模型简化为自行车模型(这里默认左轮和右轮的旋转是一致的)!!! bicycle model: pure pursuit建立于自行车模型和阿克曼小车模型的基础上,goal point为距离后…

linux的权限

前言 学习权限我们先理解一下xshell 我们使用Xshell的存在的意义 帮助进行命令行传递和返回结构保护操作系统 linux的权限 (1)权限的概念 限制人的&#xff0c;访问的对象可能没有这种“属性” 权限&#xff1a;一件事情是否运行被谁“做” 权限 人 事物属性 (2)linux的上…

【STM32F4系列】【HAL库】【自制库】模拟IIC主机

介绍 本项目是利用GPIO模拟I2C的主机 网上常见的是模拟I2C主机 本项目是作为一个两个单片机之间低速通信的用法 协议介绍请看,传送门 模拟从机请看这里 主机 功能描述 I2C按字节(Byte)读写I2C读写寄存器I2C连续读写 编程思路解析 主机是时钟信号的发起方,起始和中止信号…

什么样的GPU云计算平台的是好的平台

GPU云计算平台产品解析。通过不同平台不同阶段进行产品分析。究竟我们是需要更便宜还是需要更好用。 第一阶段分为以下几个模块 GPU 选型、 环境选型、 启动实例、 关闭实例。 第二阶段分为以下几个模块 实例关闭策略、无卡模式启动、实例状态监控、提供对外接口、云文件管…

终极.NET混淆器丨.NET Reactor产品介绍

无与伦比的 .NET 代码保护系统&#xff0c;可完全阻止任何人反编译您的代码。 产品优势 01、混淆技术 .NET Reactor通过向 .NET 程序集添加不同的保护层来防止逆向工程。除了标准的混淆技术之外&#xff0c;它还包括NecroBit、虚拟化、x86代码生成或防篡改等特殊功能。NET Re…

Vue--》超详细教程——vite脚手架的搭建与使用

目录 vite 创建 vite 项目 目录文件的构成 vite项目的运行流程 开发者工具安装 vite vue官方提供了两种快速创建工程化的SPA项目的方式&#xff0c;一种是基于 vue-cli 创建的SPA项目&#xff0c;另一种就是基于 vite 创建的SPA项目。两者的区别如下&#xff1a; 说明v…

Linux——磁盘在网络中共享

实现计算服务器挂载存储服务器磁盘 方法一&#xff1a;通过启动nfs服务实现挂载 流程可参考&#xff1a;在linux挂载另一台服务器的磁盘 启动nfs服务参考&#xff1a;Linux 环境下 NFS 服务安装及配置使用 避免存储server的非root用户访问共享文件夹 在存储server上多设置一…

2023春招面试:消息中间件面试题整理

RabbitMQ如何确保消息发送 &#xff1f; 消息接收&#xff1f; 开启生产者确认机制&#xff0c;确保生产者的消息能到达队列&#xff08;config机制保证消息正确到达交换机、return机制保证消息正确到达队列&#xff09;开启持久化功能&#xff0c;确保消息未消费前在队列中不会…