1. 换肤方案背后的核心逻辑
做 Android 换肤这个方向,绕不开的一个话题就是super.onCreate()里到底发生了什么。很多人第一次接触换肤,思路很直接:在setContentView之前把Factory2塞进去,拦截View的创建过程,然后对每个View做属性替换。这个思路本身没问题,但如果你不理解super.onCreate()内部的执行链路,就会在后续遇到一堆莫名其妙的问题——比如为什么有的View拦截不到、为什么AppCompatViewInflater会跟你抢活、为什么换肤之后Fragment里的控件没反应。
我自己在这个坑里来回折腾过好几轮,从最开始照抄网上的方案,到后来把ActivityThread到PhoneWindow再到LayoutInflater这条链路完整跟了一遍,才算真正把换肤这件事的底层逻辑理顺。这篇文章就围绕super.onCreate()的源码展开,把换肤方案里最核心的那几个环节拆开讲清楚。
1.1 换肤的本质是什么
换肤的本质,说白了就是在运行时替换View的属性值。一个TextView的文字颜色、一个LinearLayout的背景、一个ImageView的src,这些属性在 XML 里写死之后,正常情况下是不会变的。换肤要做的事情,就是在不重新创建Activity的前提下,把这些已经设置好的属性值换成另一套。
那问题来了:怎么找到这些View?怎么知道哪些属性需要换?怎么保证换完之后还能换回来?
这三个问题对应了换肤方案的三个核心模块:View 的收集、属性的记录、资源的映射。而super.onCreate()这个环节,恰好是解决第一个问题的关键节点。因为setContentView就是在super.onCreate()之后调用的,如果你能在setContentView之前把LayoutInflater的Factory2替换掉,那么后续所有通过 XML 解析出来的View都会经过你的手。
注意:
Factory2的替换时机非常关键,必须在super.onCreate()之前完成,否则AppCompatDelegate会抢先一步把自己的Factory2设置进去,你的拦截就会失效。
1.2 为什么选择 Factory2 而不是其他方案
换肤的拦截方案有好几种,常见的有:
- 自定义
LayoutInflater.Factory:只能拦截View的创建,拿不到AttributeSet的完整信息。 - 自定义
LayoutInflater.Factory2:可以拿到AttributeSet,能读取android:background、android:textColor这些属性的资源 ID。 LayoutInflaterCompat.setFactory2:兼容性封装,内部还是走Factory2。AppCompatDelegate的installViewFactory:AppCompat自己的拦截机制,会跟你的Factory2冲突。
从实操角度来看,Factory2是最合适的切入点。原因很简单:它既能拦截View的创建,又能拿到AttributeSet,而AttributeSet里包含了 XML 中写的所有属性值。你可以通过AttributeSet.getAttributeNameResource()拿到属性的资源 ID,再通过getAttributeResourceValue()拿到属性值的资源 ID。有了这两个 ID,你就能判断这个属性是不是需要换肤的属性。
但这里有个坑:Factory2只能拦截通过LayoutInflater解析 XML 创建的View。如果你在代码里直接new TextView(context),那Factory2是拦不住的。所以换肤方案通常还需要配合一个手动注册的机制,让开发者把动态创建的View也注册进来。
1.3 super.onCreate() 在换肤链路中的位置
super.onCreate()是Activity生命周期里最早被调用的几个方法之一。在它内部,Activity会完成一系列初始化工作,包括Window的创建、LayoutInflater的初始化、AppCompatDelegate的安装等。如果你把换肤的Factory2替换放在super.onCreate()之后,就会遇到两个问题:
第一,AppCompatDelegate已经在super.onCreate()里通过installViewFactory()设置了自己的Factory2。你再设置一次,要么覆盖掉它导致AppCompat的兼容性处理失效,要么被它覆盖导致你的拦截失效。
第二,setContentView通常在super.onCreate()之后立即调用,如果你在setContentView之后才替换Factory2,那当前Activity的布局已经解析完了,拦截不到任何View。
所以正确的做法是:在super.onCreate()之前完成Factory2的替换。但这里又有一个矛盾:LayoutInflater是在super.onCreate()里才初始化的,你在super.onCreate()之前拿不到LayoutInflater实例。
这个矛盾怎么解决?答案是:通过LayoutInflater.from(context)提前获取一个LayoutInflater实例,然后对它设置Factory2。因为LayoutInflater在Activity和Context层面是共享的,你提前设置的Factory2会在后续setContentView时生效。
@Override protected void onCreate(Bundle savedInstanceState) { // 在 super.onCreate() 之前替换 Factory2 LayoutInflater inflater = LayoutInflater.from(this); inflater.setFactory2(new SkinFactory2()); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }这段代码看起来简单,但里面的门道不少。接下来我会把super.onCreate()的源码拆开,看看它到底做了什么,以及这些操作对换肤方案有什么影响。
2. super.onCreate() 源码逐层拆解
要理解super.onCreate()对换肤的影响,最直接的方式就是跟源码。这里以AppCompatActivity为例,因为大部分换肤方案都是在AppCompatActivity的基础上做的。AppCompatActivity继承自FragmentActivity,FragmentActivity又继承自ComponentActivity,ComponentActivity继承自Activity。所以super.onCreate()实际上会依次调用FragmentActivity.onCreate()、ComponentActivity.onCreate()、Activity.onCreate()。
2.1 Activity.onCreate() 的核心工作
Activity.onCreate()的源码在不同版本上略有差异,但核心工作基本一致。它主要做了这几件事:
- 调用
getWindow()获取Window对象,如果为null则抛出异常。 - 调用
Window.setCallback(this),把Activity设置为Window的回调。 - 调用
Window.setWindowManager(),初始化WindowManager。 - 调用
Window.setGravity()、Window.setSoftInputMode()等,根据Activity的主题设置窗口属性。 - 调用
Window.getDecorView(),触发DecorView的创建。
这里最关键的是Window的创建。Activity的Window是在attach()方法里创建的,具体是在ActivityThread.performLaunchActivity()中调用activity.attach()时完成的。attach()里会创建一个PhoneWindow对象,并把它赋值给Activity的mWindow字段。
// Activity.attach() 中的关键代码 mWindow = new PhoneWindow(this, window, activityConfigCallback); mWindow.setWindowControllerCallback(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this);注意最后一行:mWindow.getLayoutInflater().setPrivateFactory(this)。这里把Activity自己设置成了LayoutInflater的PrivateFactory。PrivateFactory是LayoutInflater的一个内部接口,它的优先级低于Factory2,但高于默认的View创建逻辑。它的作用是让Activity能够拦截一些特殊的View创建,比如Fragment的标签。
这个PrivateFactory对换肤方案有影响吗?有,但不大。因为Factory2的优先级高于PrivateFactory,所以你的Factory2会先被调用。但如果你的Factory2返回了null,LayoutInflater会继续调用PrivateFactory,这时候Activity的onCreateView()就会被触发。
2.2 PhoneWindow 与 LayoutInflater 的关系
PhoneWindow是Window的唯一实现类。它在Activity的attach()方法中被创建,并在Activity.onCreate()中被初始化。PhoneWindow内部持有一个LayoutInflater对象,这个对象是在PhoneWindow的构造函数里通过LayoutInflater.from(context)获取的。
// PhoneWindow 构造函数中的关键代码 mLayoutInflater = LayoutInflater.from(context);LayoutInflater.from(context)内部会调用context.getSystemService(LAYOUT_INFLATER_SERVICE)。对于Activity来说,这个调用最终会走到ContextThemeWrapper.getSystemService(),然后返回一个PhoneLayoutInflater实例。
PhoneLayoutInflater是LayoutInflater的子类,它重写了onCreateView()方法,在创建View时会优先尝试创建android.widget.、android.webkit.、android.app.前缀的View。这个机制对换肤方案的影响是:你的Factory2会在PhoneLayoutInflater.onCreateView()之前被调用,所以你可以拦截到所有通过 XML 创建的View。
但这里有一个细节需要注意:PhoneWindow的LayoutInflater和Activity的LayoutInflater是同一个实例吗?答案是:在大多数情况下是同一个实例。因为PhoneWindow的构造函数里调用LayoutInflater.from(context)时,context就是Activity本身,而LayoutInflater.from()内部会缓存实例。所以你在super.onCreate()之前通过LayoutInflater.from(this)获取的LayoutInflater,和PhoneWindow内部使用的是同一个对象。
这就解释了为什么在super.onCreate()之前设置Factory2是有效的:因为你操作的就是PhoneWindow后续要用的那个LayoutInflater。
2.3 AppCompatDelegate 的 installViewFactory
如果你用的是AppCompatActivity,那super.onCreate()还会触发AppCompatDelegate的初始化。AppCompatDelegate是在AppCompatActivity的attachBaseContext()或onCreate()中被创建的,具体取决于版本。在onCreate()中,AppCompatDelegate会调用installViewFactory(),把自己的Factory2设置到LayoutInflater上。
// AppCompatDelegateImpl.installViewFactory() 中的关键代码 LayoutInflater layoutInflater = LayoutInflater.from(context); if (layoutInflater.getFactory() == null) { LayoutInflaterCompat.setFactory2(layoutInflater, this); }注意这里的判断条件:layoutInflater.getFactory() == null。也就是说,如果LayoutInflater已经有Factory了,AppCompatDelegate就不会再设置自己的Factory2。这个判断条件对换肤方案来说是一个机会:如果你在super.onCreate()之前设置了自己的Factory2,那么AppCompatDelegate就不会再设置它的Factory2,你的拦截就会生效。
但这里有一个问题:AppCompatDelegate的Factory2不仅仅是为了创建AppCompat系列的View,它还负责处理一些兼容性逻辑,比如VectorDrawable的加载、TextView的allCaps属性等。如果你直接覆盖了它的Factory2,这些兼容性逻辑就会失效。
所以更稳妥的做法是:在你的Factory2中,先调用AppCompatDelegate的createView(),拿到它创建的View,然后再对这个View做换肤处理。这样既能保证AppCompat的兼容性,又能实现换肤。
public class SkinFactory2 implements LayoutInflater.Factory2 { private AppCompatDelegate delegate; public SkinFactory2(AppCompatDelegate delegate) { this.delegate = delegate; } @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { // 先让 AppCompatDelegate 创建 View View view = delegate.createView(parent, name, context, attrs); if (view == null) { // 如果 AppCompatDelegate 创建失败,走默认逻辑 view = createViewFromTag(context, name, attrs); } // 对 View 做换肤处理 if (view != null) { SkinManager.getInstance().registerView(view, attrs); } return view; } }这段代码的关键在于:不要试图完全替代AppCompatDelegate,而是把它当作一个前置处理器。这样你的换肤逻辑就变成了一个后置处理器,在View创建完成之后再做属性替换。
2.4 FragmentActivity 的 onCreate
FragmentActivity.onCreate()主要做了两件事:恢复Fragment的状态、初始化FragmentController。它对换肤方案的影响主要体现在Fragment中的View上。
Fragment的View是通过Fragment.onCreateView()创建的,而Fragment.onCreateView()通常会调用LayoutInflater.from(context).inflate()。这个LayoutInflater是从Fragment的context获取的,而Fragment的context就是宿主Activity。所以你在Activity中设置的Factory2同样会对Fragment的View生效。
但这里有一个坑:Fragment的View创建时机可能晚于Activity的setContentView()。如果你在Activity的onCreate()中设置Factory2,而Fragment是在onCreate()之后才添加的,那Fragment的View创建时Factory2已经生效了,没问题。但如果Fragment是在Activity的onCreate()之前就存在的(比如通过FragmentManager恢复的),那Fragment的View创建可能会在Factory2设置之前发生,导致拦截失效。
解决这个问题的办法是:在Application级别设置Factory2。通过registerActivityLifecycleCallbacks()监听Activity的创建,在每个Activity的onCreate()之前设置Factory2。这样无论Fragment什么时候创建,都能被拦截到。
3. 换肤方案的完整实现链路
理解了super.onCreate()的源码之后,换肤方案的实现链路就清晰了。整个链路可以分成四个阶段:拦截阶段、收集阶段、记录阶段、替换阶段。每个阶段都有对应的核心类和关键方法。
3.1 拦截阶段:Factory2 的设置时机
拦截阶段的核心任务是:在LayoutInflater解析 XML 之前,把自定义的Factory2设置进去。这个时机必须早于setContentView(),最好早于super.onCreate()。
具体的设置方式有三种:
- 在
Activity.onCreate()中设置:适用于单个Activity的换肤,但需要在每个Activity中重复代码。 - 在
Application中通过registerActivityLifecycleCallbacks()设置:适用于全局换肤,代码集中,维护方便。 - 在
BaseActivity中设置:适用于有统一基类的项目,代码复用性好。
我个人的建议是:用Application级别的ActivityLifecycleCallbacks来设置。这样无论Activity有没有继承基类,都能被拦截到。具体实现如下:
public class SkinApplication extends Application { @Override public void onCreate() { super.onCreate(); registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { // 在 Activity 的 onCreate() 之前设置 Factory2 LayoutInflater inflater = LayoutInflater.from(activity); if (inflater.getFactory2() == null) { inflater.setFactory2(new SkinFactory2()); } } // 其他生命周期方法省略 }); } }这里有一个细节:onActivityCreated()是在Activity.onCreate()之后调用的,还是在之前?答案是:在Activity.onCreate()之后。因为ActivityLifecycleCallbacks的onActivityCreated()是在ActivityThread.performLaunchActivity()中调用的,而performLaunchActivity()会先调用activity.onCreate(),再调用onActivityCreated()。
所以如果你在onActivityCreated()中设置Factory2,实际上已经晚于Activity.onCreate()了。但为什么还能生效?因为setContentView()通常是在Activity.onCreate()中调用的,而onActivityCreated()是在onCreate()之后调用的,所以setContentView()已经执行完了,Factory2设置得太晚了。
那怎么办?答案是:在onActivityCreated()中设置Factory2只对后续的setContentView()生效,对当前的setContentView()无效。所以如果你要在Application级别设置Factory2,必须确保setContentView()不在Activity.onCreate()中调用,而是在onResume()或更晚的时机调用。但这显然不现实。
所以更靠谱的做法是:在BaseActivity的onCreate()中,在super.onCreate()之前设置Factory2。这样既能保证时机正确,又能通过基类统一管理。
public abstract class BaseActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { // 在 super.onCreate() 之前设置 Factory2 LayoutInflater inflater = LayoutInflater.from(this); if (inflater.getFactory2() == null) { inflater.setFactory2(new SkinFactory2()); } super.onCreate(savedInstanceState); } }3.2 收集阶段:View 与属性的关联
收集阶段的核心任务是:在Factory2.onCreateView()中,把创建的View和它的属性关联起来。具体来说,就是读取AttributeSet中的属性值,判断哪些属性需要换肤,然后把View和这些属性的资源 ID 保存起来。
public class SkinFactory2 implements LayoutInflater.Factory2 { @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { View view = createView(context, name, attrs); if (view != null) { // 收集需要换肤的属性 List<SkinAttr> skinAttrs = new ArrayList<>(); for (int i = 0; i < attrs.getAttributeCount(); i++) { String attrName = attrs.getAttributeName(i); String attrValue = attrs.getAttributeValue(i); if (isSkinAttr(attrName, attrValue)) { int resId = Integer.parseInt(attrValue.substring(1)); skinAttrs.add(new SkinAttr(attrName, resId)); } } if (!skinAttrs.isEmpty()) { SkinManager.getInstance().addSkinView(view, skinAttrs); } } return view; } }这段代码的关键在于isSkinAttr()方法。它需要判断两个条件:属性名是否是换肤支持的属性(如background、textColor、src等),属性值是否是一个资源引用(以@开头)。
这里有一个坑:attrs.getAttributeValue(i)返回的值可能是@2131234567这样的字符串,也可能是#FF0000这样的颜色值,还可能是?2130771968这样的主题属性引用。你需要根据前缀来判断类型,只处理@开头的资源引用。
另一个坑是:attrs.getAttributeName(i)返回的属性名可能是background,也可能是android:background。你需要统一处理,去掉android:前缀。
3.3 记录阶段:SkinManager 的数据结构
记录阶段的核心任务是:把收集到的View和属性保存到一个全局的数据结构中,以便后续换肤时使用。这个数据结构需要满足几个要求:
- 能够快速查找某个
View对应的所有属性。 - 能够快速遍历所有需要换肤的
View。 - 能够支持
View的销毁和重建。
我试过几种数据结构,最后发现最合适的是Map<View, List<SkinAttr>>。View作为 key,属性列表作为 value。这样既能快速查找,又能快速遍历。
public class SkinManager { private final Map<View, List<SkinAttr>> skinViewMap = new WeakHashMap<>(); public void addSkinView(View view, List<SkinAttr> attrs) { skinViewMap.put(view, attrs); } public void applySkin() { for (Map.Entry<View, List<SkinAttr>> entry : skinViewMap.entrySet()) { View view = entry.getKey(); List<SkinAttr> attrs = entry.getValue(); for (SkinAttr attr : attrs) { attr.apply(view); } } } }这里用WeakHashMap而不是HashMap,是为了避免内存泄漏。因为View可能被销毁,如果SkinManager持有View的强引用,就会导致View无法被回收。
但WeakHashMap也有一个问题:如果View被回收了,SkinManager中的记录也会自动消失。这在大多数情况下是好事,但如果你需要在View重建后恢复换肤状态,就需要额外的机制来保存换肤状态。
3.4 替换阶段:资源映射与属性应用
替换阶段的核心任务是:根据新的皮肤包,找到对应的资源 ID,然后把View的属性值替换掉。这个阶段的关键是资源映射:如何根据旧的资源 ID 找到新的资源 ID。
Android 的资源 ID 是一个 32 位的整数,格式是0xPPTTEEEE,其中PP是包 ID,TT是类型 ID,EEEE是条目 ID。换肤时,你需要根据旧的资源 ID 找到对应的资源名称,然后在新的皮肤包中找到同名的资源 ID。
public class SkinAttr { private String attrName; private int resId; public void apply(View view) { // 根据旧的资源 ID 找到资源名称 String resName = getResourceName(resId); // 在新的皮肤包中找到同名的资源 ID int newResId = getSkinResourceId(resName); if (newResId == 0) { return; } // 根据属性名应用新的资源 if ("background".equals(attrName)) { view.setBackgroundResource(newResId); } else if ("textColor".equals(attrName)) { ((TextView) view).setTextColor(getColor(newResId)); } else if ("src".equals(attrName)) { ((ImageView) view).setImageResource(newResId); } } }这里的关键是getResourceName()和getSkinResourceId()两个方法。getResourceName()通过Resources.getResourceName(resId)获取资源名称,格式是包名:类型/条目名。getSkinResourceId()通过SkinResources.getIdentifier()获取新皮肤包中的资源 ID。
private String getResourceName(int resId) { return context.getResources().getResourceName(resId); } private int getSkinResourceId(String resName) { // resName 格式:com.example:color/primary String[] parts = resName.split(":"); String type = parts[1].split("/")[0]; String name = parts[1].split("/")[1]; return skinResources.getIdentifier(name, type, skinPackageName); }这里有一个性能问题:每次换肤时都要遍历所有View,对每个属性都做一次资源查找。如果View数量很多,这个过程会比较耗时。优化的办法是:在收集阶段就把资源名称缓存起来,避免在替换阶段重复查找。
4. 常见问题与排查技巧实录
换肤方案在实操中会遇到各种各样的问题,有些是源码层面的,有些是使用层面的。我把这些问题整理成了一张速查表,方便你遇到问题时快速定位。
4.1 Factory2 设置无效的排查思路
这是最常见的问题:明明设置了Factory2,但onCreateView()就是不被调用。排查思路如下:
| 排查项 | 可能原因 | 解决方法 |
|---|---|---|
| 设置时机 | 在super.onCreate()之后设置 | 移到super.onCreate()之前 |
| 设置对象 | 对不同的LayoutInflater实例设置 | 用LayoutInflater.from(this)获取实例 |
| 被覆盖 | AppCompatDelegate覆盖了你的Factory2 | 在super.onCreate()之前设置,或包装AppCompatDelegate |
| 布局解析方式 | 用View.inflate()而不是LayoutInflater.inflate() | 统一用LayoutInflater.inflate() |
| 动态创建 | 代码中直接new View() | 手动注册到SkinManager |
这里重点说一下AppCompatDelegate覆盖的问题。如果你在super.onCreate()之后设置Factory2,AppCompatDelegate已经在super.onCreate()中通过installViewFactory()设置了自己的Factory2。这时候你再设置,会覆盖掉AppCompatDelegate的Factory2,导致AppCompat的兼容性处理失效。但如果你在super.onCreate()之前设置,AppCompatDelegate的installViewFactory()会因为getFactory() != null而跳过设置,你的Factory2就会生效。
4.2 换肤后部分 View 不生效的原因
换肤后部分View不生效,通常有以下几个原因:
View是动态创建的:Factory2只能拦截 XML 创建的View,动态创建的View需要手动注册。- 属性不在支持列表中:
SkinAttr只处理了background、textColor、src等常见属性,其他属性需要手动扩展。 - 资源 ID 找不到:新皮肤包中没有对应的资源,或者资源名称不一致。
View被回收:WeakHashMap中的View被回收后,换肤记录丢失。
针对动态创建的View,我通常会在BaseActivity中提供一个registerSkinView()方法,让开发者手动注册:
public void registerSkinView(View view, int... resIds) { List<SkinAttr> attrs = new ArrayList<>(); for (int resId : resIds) { String resName = getResources().getResourceName(resId); String type = resName.split("/")[0].split(":")[1]; attrs.add(new SkinAttr(type, resId)); } SkinManager.getInstance().addSkinView(view, attrs); }4.3 内存泄漏与性能优化
换肤方案中最容易出问题的地方就是内存泄漏。因为SkinManager是全局单例,如果它持有View的强引用,就会导致Activity无法被回收。用WeakHashMap可以解决这个问题,但也会带来另一个问题:View被回收后,换肤记录丢失,下次换肤时这个View就不会被更新。
解决这个矛盾的办法是:在View被回收时,主动从SkinManager中移除记录。可以通过View.addOnAttachStateChangeListener()监听View的销毁:
view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) {} @Override public void onViewDetachedFromWindow(View v) { SkinManager.getInstance().removeSkinView(v); } });性能优化方面,主要有两个方向:减少遍历次数和缓存资源查找结果。减少遍历次数可以通过只遍历当前可见的View来实现,缓存资源查找结果可以通过SparseIntArray来保存旧的资源 ID 和新的资源 ID 的映射关系。
4.4 换肤方案的扩展思路
基础的换肤方案只能替换颜色和图片资源,但实际项目中可能需要更复杂的换肤效果,比如:
- 字体换肤:替换
TextView的typeface。 - 圆角换肤:替换
ShapeDrawable的圆角半径。 - 主题换肤:替换整个
Activity的主题。 - 局部换肤:只替换某个
Fragment或某个ViewGroup中的View。
这些扩展思路的实现方式各不相同,但核心逻辑是一样的:在Factory2中拦截View的创建,记录需要换肤的属性,在换肤时替换属性值。区别只在于属性的类型和替换的方式。
比如字体换肤,你需要在SkinAttr中增加对typeface属性的处理:
if ("typeface".equals(attrName)) { Typeface typeface = Typeface.createFromAsset(skinContext.getAssets(), "fonts/skin.ttf"); ((TextView) view).setTypeface(typeface); }圆角换肤则需要替换View的background,但background是一个Drawable,你需要根据新的资源 ID 创建一个新的Drawable,然后设置给View。
5. 从源码到落地的经验总结
把super.onCreate()的源码跟完之后,我对换肤方案的理解上了一个台阶。以前只知道“要在setContentView之前设置Factory2”,但不知道为什么。现在知道了:因为LayoutInflater是在Activity.attach()中创建的,PhoneWindow和Activity共享同一个LayoutInflater实例,而AppCompatDelegate会在super.onCreate()中尝试设置自己的Factory2,所以你必须抢在它之前设置。
这个链路理顺之后,很多之前觉得莫名其妙的问题都有了合理的解释。比如为什么Fragment中的View有时候拦截不到——因为Fragment的View创建时机可能晚于Factory2的设置时机。为什么动态创建的View拦截不到——因为Factory2只对LayoutInflater.inflate()生效。
我在实际项目中踩过最大的一个坑是:在BaseActivity的onCreate()中设置Factory2,但有些Activity没有继承BaseActivity,导致这些Activity的换肤失效。后来改成在Application中通过ActivityLifecycleCallbacks设置,虽然时机上晚了一点,但配合setContentView的延迟调用,也能覆盖大部分场景。
最后分享一个小技巧:如果你不确定Factory2是否设置成功,可以在onCreateView()中打一行日志,看看有没有被调用。如果日志没有输出,那说明Factory2没有生效,需要检查设置时机和设置对象。这个排查方法虽然简单,但非常有效,我每次遇到问题都是先用这招定位。