资讯动态

Android换肤方案:super.onCreate()源码解析与Factory2拦截机制

发布时间:2026/10/15 16:05:32 来源:尧图企业网站定制
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的installViewFactoryAppCompat自己的拦截机制会跟你的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继承自FragmentActivityFragmentActivity又继承自ComponentActivityComponentActivity继承自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返回了nullLayoutInflater会继续调用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 的 onCreateFragmentActivity.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) { // 收集需要换肤的属性 ListSkinAttr 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的销毁和重建。我试过几种数据结构最后发现最合适的是MapView, ListSkinAttr。View作为 key属性列表作为 value。这样既能快速查找又能快速遍历。public class SkinManager { private final MapView, ListSkinAttr skinViewMap new WeakHashMap(); public void addSkinView(View view, ListSkinAttr attrs) { skinViewMap.put(view, attrs); } public void applySkin() { for (Map.EntryView, ListSkinAttr entry : skinViewMap.entrySet()) { View view entry.getKey(); ListSkinAttr 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是包 IDTT是类型 IDEEEE是条目 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()之后设置Factory2AppCompatDelegate已经在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) { ListSkinAttr 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没有生效需要检查设置时机和设置对象。这个排查方法虽然简单但非常有效我每次遇到问题都是先用这招定位。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑