从源码角度上分析常用的换肤原理(二)

1.原理简述

上篇说道外置资源文件的生成,可以和原来自带的资源文件使用两套Resources进行替换,对于新打开的页面可以根据标签对每个View进行判断并赋值,之前的页面会有无法更新的情况,并且对每个View都重写或主动调用相关的设置属性的方法会很麻烦。

那么就得从根源上去处理View属性的创建和缓存的逻辑,就是说,从View的创建上入手,获取到相关的属性,用缓存存储相关页面的View属性信息,更换主题的时候通知所有的缓存中的View即可,当然这些View也要及时清除,避免泄露。

而这些都是基于本篇的主角LayoutInflater去处理的

1.LayoutInflater

我们平时用LayoutInflater最多的场合是列表视图中,使用

  View view = LayoutInflater.from(context).inflate(resId,null)

这个方式去获取一个View的实例,其实Activity的setContentView也是这么处理的

@Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }

setContentView方法中会查找到DecordView中id为R.id.content的容器,并把当前的resIdcontentParent作为参数传递,这里就相当于创建一个View同时把这个View添加到contentParent这个容器中来

而且这个方法我们常用的有两种重载方式

  public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }
    
 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                  + Integer.toHexString(resource) + ")");
        }

        View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
        if (view != null) {
            return view;
        }
        XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

我们常用的传null的方式其实最终调用了inflate(resource, null, false)这个重载方法
然后在这个方法里去创建当前指定的布局id的解析器XmlResourceParser,并把传参的另外两个root,root!=null,传到下一个方法中去

 private static final String TAG_MERGE = "merge";
 private static final String TAG_INCLUDE = "include";

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            final Context inflaterContext = mContext;
            //获取到当前的AttributeSet接口
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;
            try {
                advanceToRootNode(parser);
                final String name = parser.getName();
                if (TAG_MERGE.equals(name)) { //merge标签
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                	//实例化一个View
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    ViewGroup.LayoutParams params = null;
                    if (root != null) {      
                    	//调用ViewGroup的方法生成一个默认的LayoutParams参数                 
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) { //如果为ture,则给View设置这个LayoutParams
                            temp.setLayoutParams(params);
                        }
                    }        
                    //把当前的View作为父容器,遍历里面的所有子View
                    rInflateChildren(parser, temp, attrs, true);
                    if (root != null && attachToRoot) {
                    	//如果父容器不为空,同时attach为true,则添加到父容器里
                        root.addView(temp, params);
                    }
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {           
                throw ie;
            } catch (Exception e) {  
                throw ie;
            } finally {     
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
            }
            return result;
        }
    }

这里会通过Xml.asAttributeSet(parser)方法生成当前的AttributeSet对象,我们常用的AttributeSet并不是字面意义上的集合,而是一个接口,方法调用也是适用传入内部的Parser去处理的,比如XmlPullAttributes

class XmlPullAttributes implements AttributeSet {
    @UnsupportedAppUsage
    public XmlPullAttributes(XmlPullParser parser) {
        mParser = parser;
    }
    public int getAttributeCount() {
        return mParser.getAttributeCount();
    }
    public String getAttributeNamespace (int index) {
        return mParser.getAttributeNamespace(index);
    }
    public String getAttributeName(int index) {
        return mParser.getAttributeName(index);
    }
    public String getAttributeValue(int index) {
        return mParser.getAttributeValue(index);
    }
    public String getAttributeValue(String namespace, String name) {
        return mParser.getAttributeValue(namespace, name);
    }
    public boolean getAttributeBooleanValue(String namespace, String attribute,
            boolean defaultValue) {
        return XmlUtils.convertValueToBoolean(
            getAttributeValue(namespace, attribute), defaultValue);
    }
	......
 }

使用的传入的Parser去获取相应的属性,也就是说获取的属性会根据当前解析到的标签而变化,相当的灵活,这也是后面为什么这个属性Xml.asAttributeSet(parser)只初始化一次,但当解析不同的标签的View时,能正确获取到相关属性的原因。

然后创建View的方法createViewFromTag

 private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
        return createViewFromTag(parent, name, context, attrs, false);
    }
    @UnsupportedAppUsage
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
    	......
        try {
            View view = tryCreateView(parent, name, context, attrs);
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }
            return view;
        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            throw ie;
        } catch (Exception e) {
            throw ie;
        }
    }

这里分两步去创建
1.通过tryCreateView创建,如果创建成功直接返回
2.第一步创建失败,那么使用默认的onCreateView或者createView方法进行创建,这两个方法差异判断是是否包含包名分隔符.。一般系统自带的如TextView直接书写就可以,而自定义的View一般都需要制定完整的路径,比如<com.test.CusView ... /> ,这里就是根据这两种情况分别处理;其实也很简单,系统的会自动拼接前缀

 protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return createView(name, "android.view.", attrs);
    }

 public final View createView(@NonNull Context viewContext, @NonNull String name,
            @Nullable String prefix, @Nullable AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Objects.requireNonNull(viewContext);
        Objects.requireNonNull(name);
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;
        ......
        try {
            if (constructor == null) {
            	//反射后强转为View
                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);
            } 
			//mConstructorArgs是一个有两个值的Object集合,初始都为空
            Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = viewContext;
            Object[] args = mConstructorArgs;
            //把attrs赋值给args
            args[1] = attrs;
			......
            try {
            	//这里的args里有两个值,第一个是context,第二个是attrs
                final View view = constructor.newInstance(args);
             	......
                return view;
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        } catch (NoSuchMethodException e) {
            throw ie;
        } catch (ClassCastException e) { 
            throw ie;
        } catch (ClassNotFoundException e) {   
            throw e;
        } catch (Exception e) {
            throw ie;
        } finally {
        }
    }

而最终createView会通过反射创建出真正的View实例,这个是在第一步创建失败的情况,那么再看下第一步的处理

  public final View tryCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context,
        @NonNull AttributeSet attrs) {
 		.......
        View view;
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }
        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        return view;
    }

也很简单,只是把创建View的过程交付给Factory接口去处理,可以通过setFactory方法设置处理的工厂,Factory2相比于Factory多了一个当前的父容器parent; 我们通常修改的setFactory2方法

 public void setFactory2(Factory2 factory) {
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }

可以看出有一个mFactorySet标签,默认是false,设置过一次就变成true;也就是说这里只能设置一次,如果想要覆盖之前设置的怎么办,那就只能反射修改mFactorySetfalse就可以了。
同时也可以看出,只要设置了mFactory2,如果mFactory为空则赋值为mFactory2,如果不为空,那么就把这两个进行合并,生成一个FactoryMerger的工厂,这里面的前两个参数传的是当前要设置的factory,第三个参数传的是mFactory,而最后一个是之前的mFactory2,初始默认是应该是空的。

 private static class FactoryMerger implements Factory2 {
        private final Factory mF1, mF2;
        private final Factory2 mF12, mF22;

        FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) {
            mF1 = f1;
            mF2 = f2;
            mF12 = f12;
            mF22 = f22;
        }

        @Nullable
        public View onCreateView(@NonNull String name, @NonNull Context context,
                @NonNull AttributeSet attrs) {
            View v = mF1.onCreateView(name, context, attrs);
            if (v != null) return v;
            return mF2.onCreateView(name, context, attrs);
        }

        @Nullable
        public View onCreateView(@Nullable View parent, @NonNull String name,
                @NonNull Context context, @NonNull AttributeSet attrs) {
            View v = mF12 != null ? mF12.onCreateView(parent, name, context, attrs)
                    : mF1.onCreateView(name, context, attrs);
            if (v != null) return v;
            return mF22 != null ? mF22.onCreateView(parent, name, context, attrs)
                    : mF2.onCreateView(name, context, attrs);
        }
    }

其实Activity在创建的时候已经添加了一个默认的Factory

//androidx.appcompat.app.AppCompatActivity
 protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        super.onCreate(savedInstanceState);
    }

//androidx.appcompat.app.AppCompatDelegateImpl
 @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            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");
            }
        }
    }

AppCompatActivityonCreate方法中会通过installViewFactory设置一个默认的Factory2

//AppCompatDelegateImpl
class AppCompatDelegateImpl extends AppCompatDelegate
        implements MenuBuilder.Callback, LayoutInflater.Factory2 {
		 @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
       	......
        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,IS_PRE_LOLLIPOP,true,VectorEnabledTintResources.shouldBeUsed() 
        );
    }
}


//AppCompatViewInflater
public class AppCompatViewInflater {
 final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        final Context originalContext = context;
 		......
        View view = null;
        switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageView":
                view = createImageView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Button":
                view = createButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "EditText":
                view = createEditText(context, attrs);
                verifyNotNull(view, name);
                break;
           	 .......
            default:
           view = createView(context, name, attrs);
        }
        if (view == null && originalContext != context) {
            view = createViewFromTag(context, name, attrs);
        }
        if (view != null) {
            checkOnClickListener(view, attrs);
        }
        return view;
    }

 	@NonNull
    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
        return new AppCompatTextView(context, attrs);
    }

    @NonNull
    protected AppCompatImageView createImageView(Context context, AttributeSet attrs) {
        return new AppCompatImageView(context, attrs);
    }
    .......
}

可以看出,AppCompatActivity中其实是重写了Factory把之前控件替换成AppCompat相关的View, 那么我们通过setFactory也是可以监听到所有的View的创建过程的,而创建过程中我们是可以获取到相关的attrs属性的,比如background,textColor

换句话说,自定义Factory2可以接管View的创建过程,并且每个View布局中定义的属性都是可以拿到的,那么通过这个设置就可以获取每个页面所对应的需要更换主题的控件的集合,可以统一管理,根据不同资源进行匹配的方法也只需要写一套即可;

  fun setDefaultFactorys(activity: Activity) {
        try {
            val name = activity.javaClass.name
            val factory = SkinLayoutFactory(name)
            val filed = LayoutInflater::class.java.getDeclaredField("mFactorySet")
            filed.isAccessible = true

            val inflater = LayoutInflater.from(activity)

            filed.set(inflater, false)
            inflater.factory2 = factory

        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

class SkinLayoutFactory constructor(val activityName: String) : LayoutInflater.Factory2 {
    private val constructorMap: HashMap<String, Constructor<*>> = HashMap()
    private val constructorSignature = arrayOf(
        Context::class.java, AttributeSet::class.java
    )
    private val prefixList = arrayOf("android.view.", "android.widget.")

    override fun onCreateView(
        parent: View?,
        name: String,
        context: Context,
        attrs: AttributeSet
    ): View? {
        return onCreateView(name, context, attrs)
    }

    override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
        if (name.indexOf(".") != -1) {
            return createView(context, name, attrs)
        } else {
            return createInnerView(context, name, attrs)
        }
    }

    fun createInnerView(context: Context, name: String, attrs: AttributeSet?): View? {
        var view: View? = null
        for (i in prefixList.indices) {
            val realViewName = "${prefixList[i]}$name"
            view = createView(context, realViewName, attrs)
            if (view != null) return view
        }
        return null
    }

    fun createView(context: Context, name: String, attrs: AttributeSet?): View? {
        try {
            var consturctorUse: Constructor<*>? = null
            if (constructorMap.containsKey(name)) {
                consturctorUse = constructorMap[name]
            } else {
                val clazz = Class.forName(name)

                if (clazz != null) {
                    consturctorUse = clazz.getConstructor(*constructorSignature)
                    if (consturctorUse != null) {
                        consturctorUse.isAccessible = true
                        constructorMap[name] = consturctorUse
                    } else {
                        throw IllegalArgumentException("构造函数缺失")
                    }
                }
            }
            consturctorUse?.apply {
                val view = consturctorUse!!.newInstance(context, attrs) as View
                SkinManager.getInstance().doSaveSkinItem(activityName, view, attrs)
                return view
            }

        } catch (e: Exception) {
        }
        return null
    }
}

这里很简单的使用了一个反射的方式修改mFactorySet属性为false,并自定义了一个Factory2的实现类,然后把这个Factory设置给当前的LayoutInflater,为了方便处理,在每个需要进行主题替换的Activity的setContentView前进行修改,通过自定义的Factory进行View属性的收集,然后在Activity的onDestroy方法中进行数据清除。

这个可以通过我们常用Application.ActivityLifecycleCallbacks进行注册处理,比如在onActivityCreated中进行上面的setDefaultFactorys方法的调用;经常有人觉得onActivityCreatedonCreate方法执行完后调用,这时候已经执行完setContentView,会有问题;其实不然,看下这个方法回调的时机

//ndroid.app.Activity
protected void onCreate(@Nullable Bundle savedInstanceState) {
        ......
        dispatchActivityCreated(savedInstanceState);
       ......
    }

 private void dispatchActivityCreated(@Nullable Bundle savedInstanceState) {
        getApplication().dispatchActivityCreated(this, savedInstanceState);
        ......
    }

这里的getApplication().dispatchActivityCreated就是这个回调的时间点

可以看出这是在父类Activity中的onCreate进行处理的,我们通常都是重写并调用super.onCreate(),这个时候已经完成onActivityCreated的回调了,也就保证了在setContentView之前进行调用;

ActivityLifecycleCallbacks中的回调基本都是在父类方法中进行的,我们子类重写这个方法调用super的时候已经完成了

然后根据我们自定义的Factory,我们可以捕获到我们定义的View的信息

    private val defaultRef = arrayOf(
        "textColor",
        "background"
    )

fun doSaveSkinItem(activityName: String, view: View, attr: AttributeSet?) {
        var skinStore: SkinStore? = null
        attr?.apply {
            for (i in 0 until attributeCount) {
                val name = attr.getAttributeName(i)
                val value = attr.getAttributeValue(i)
                if (defaultRef.contains(name)) {
                    if (value.startsWith("@")) {
                        //测试只考虑@标示符
                        val resId = value.substring(1).toInt()
                        if (resId != 0) {
                            if (skinStore == null) {
                                skinStore = SkinStore(view, name, resId)
                            } else {
                                skinStore!!.addNesSkinItem(name, resId)
                            }
                        }
                    }
                }
            }
            skinStore?.apply {
                addTo(activityName, skinStore!!)
            }
        }
    }

比如我们可以根据属性进行筛选,这里通过getAttributeValue获取的都是字符串,上面我们获取到@resId资源的时候,需要截取除了首位之外的字符;注意打包之后,我们获取的资源属性都变成了id匹配的,比如上面的可能变成@1234567的资源id,只需要截取就可以转成真正的映射id了

那么这时候,我们已经保存了相关的View,相关的属性名,已经当前属性匹配的资源id了,这里是原应用内的。

根据上篇的新建的Resources资源配置

fun findResIntValue(originId: Int): Int {
        val resName = appResource!!.getResourceEntryName(originId)
        val resType = appResource!!.getResourceTypeName(originId)
        if (useOrigin) return getResourceIntValue(resType, originId, appResource!!)

        if (skinResouce != null && skinPkgName != null) {
            val skinId = skinResouce!!.getIdentifier(resName, resType, skinPkgName)
            val colorValue = getResourceIntValue(resType, skinId, skinResouce!!)
            if (colorValue != Int.MAX_VALUE) return colorValue
        }
        return Int.MAX_VALUE
    }

fun getResourceIntValue(name: String, colorId: Int, resources: Resources): Int {
        try {
            when (name) {
                "color" -> {
                    val color = resources.getColor(colorId)
                    return color
                }
            }
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
        return -1
    }

我们可以通过刚刚保存的id使用getResourceEntryName查找出对应的资源声明字符串,比如aaa.png(这个查找出的.png后缀是没有的,这里只是提示完整资源)

也通过getResourceTypeName获取到对应的资源类型,比如color,drawable
根据这些属性我们就可以从自定义的Resources去查找到指定的资源了;

注意如果是应用内自带的不需要进行进行额外查找,如果是下载的需要更新的主题包,那么需要通过getIdentifier查找到该主题资源包中对应的资源id,然后重复上面的操作即可;而查找资源库中对应名称的资源id使用需要传递指定的资源名和资源类型,比如上面的aaadrawable

比如上面例子通过color这个type,然后根据传入的resource去查找对应的id对应的属性

3.总结

那么换肤的原理步骤可以简单分为下面的(这里以资源包apk为例)
1. 新建一个资源工程, 准备好需要替换的资源,名称声明和应用内需要替换的名称和类型保持一致,只是资源内容不一样,直接打包成apk就可以
2. 自定义Factory2去接管View的创建过程,可以获取到页面内所有的View的属性,记录下所需要替换资源的View以及所对应的属性,比如src,background,可以根据tag区分需要更新的View
3. 通过反射的方式创建指定路径的AssetManager(就是上面生成的apk的路径),并以此生成对应的主题资源包Resources
4. 依据保存的资源属性,去查找主题包Resources中对应的资源id,并根据资源类型刷新所保存的所有需要替换资源的View即可
5. 上述方法只适用于布局中生成的View,对于通过new XView(...)方法生成的则需要额外进行存储,原理其实差不多,这里就不多描述了

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值