作者

大家好,我叫;

目前主要负责国内相关事务开发和一些日常事务。

布景

最近SDK事务做了游戏内嵌视频直播的功用,由于不同研制游戏的界面风格不相同,所以咱们在SDK定好了换肤UI的规范,研制能够依据这套规范自行替换对应的皮肤UI资源,不需求修正SDK的内容,选择对应的换肤SDK版别出包即可完成皮肤替换。

计划调研

目前市面上换肤计划有Resource包装流和AssetManager替换流。

Resource包装流的原理大约如下:
1、创立新的Resrouce目标(署理的Resource)
2、替换系统Resource目标
3、运行时动态映射(原理相同资源在不同的资源表中的Type和Name相同)
4、xml布局解析阻拦(xml布局中的资源不能经过署理Resource加载,LayoutInflater)
优势:

支撑String/Layout

存在问题:

1.资源获取效率有影响

2.不支撑style、asset目录

3.Resource多出替换,Resource包装类代码量大

资源重定向:

支撑动态映射

AssetManager替换流的原理大约如下:
1、hook系统AssetMananger目标(系统资源途径及运用的资源途径 都添加到了AssetManager的Path当中)
2、编译期静态对齐(皮肤包中资源文件对应的id数值修正与运用程序中一致)
优势:

支撑style、asset目录,替换AM实例简洁

存在问题:

强依靠编译器资源id

资源重定向:

不支撑动态映射

计划选择

采取了Resource的LayoutInflater的计划,原因如下:

  1. 不需求保护皮肤包的apk,降低sdk的保护本钱和研制的接入本钱
  2. 需求支撑资源动态映射
  3. 事务只需求支撑图片替换

所以开发接入流程便是:

  1. 每个SDK版别定好换肤图片资源和称号输出到换肤阐明文档上(Android和iOS共用)
  2. 接入方依据换肤阐明文档上,出图切图,然后把切图资源放到规定的asset目录上面
  3. 母包选择对应的换肤SDK出包即可

原理剖析

LayoutInflater剖析

  1. LayoutInflater怎么实例化

咱们通常调用以下代码取得一个LayoutInflater 运用流程

LayoutInflater.from(context)

Android手游SDK换肤方案

context一般传的是activity实例

Android手游SDK换肤方案

没有找到inflater_service的name,再看看父类

Android手游SDK换肤方案

要害点是getBaseContext()回来的实例,经过剖析上下文可知,getBaseContext()回来的是ContextWrapper的成员变量,且在attachBaseContext办法传进,如下图所示

Android手游SDK换肤方案

由于Activity是在ActivityThread中经过反射实例的,所以在ActivityThread找到如下代码

Android手游SDK换肤方案

经过以上可知appContext这个参数的实例是ContextImpl

Android手游SDK换肤方案

依据上图追踪,LayoutInflater的实例生成下图所示

Android手游SDK换肤方案

  1. View怎么经过LayoutInflater生成,如下图所示

Android手游SDK换肤方案

经过对LayoutInflater这个类的剖析,咱们能够在蓝色部分设置factory的实例来阻拦系统View生成,所以咱们能够在factory对应实例的办法createView 经过类似于黄色部分的完成逻辑实例出View,再经过动态映射资源到达替换xml默认的资源

3.运用factory

所以咱们运用LayoutInflater的setFactory、setFactory2这两个办法在对应的实例做咱们事务的阻拦处理即可,这两个办法的功用基本是一致的,setFactory2是在SDK>=11以后引进的,所以咱们要依据SDK的版别去选择调用上述办法。 v4包下有个类LayoutInflaterCompat帮咱们完成了兼容性的操作,提供的办法为:

LayoutInflaterCompat
- setFactory(LayoutInflater inflater, 
             LayoutInflaterFactory factory)

AppCompatActivity冲突处理

1.内部运用了LayoutInflater的setFactory2,流程大约如下所示;

Android手游SDK换肤方案

经过上面剖析,其内部的逻辑跟咱们换肤逻辑相同

2.反常处理

如果咱们在AppCompatActivity onCreate()之后设置LayoutInflaterCompat.setFactory2会抛出一下反常

Android手游SDK换肤方案

经过代码剖析可知

Android手游SDK换肤方案

LayoutInfalter调用过setFactory或许setFactory2的话,下一次调用的话就会跑出反常。

那么咱们在AppCompatActivity onCreate()之前设置LayoutInflaterCompat.setFactory2呢?,经过源码剖析可知

Android手游SDK换肤方案

虽然这样不会跑出反常,但是就不会执行到AppCompatDelegateImpl的onCreateView办法,相当于AppCompatDelegateImpl设置的factory失效。所以咱们需求在咱们自己factory来做对AppCompatDelegateImpl的factory的处理。由于AppCompatDelegateImpl的下面的这个办法是public

Android手游SDK换肤方案

所以咱们能够在咱们factory处理逻辑调用AppCompatDelegate调用下面办法来处理解决上面的状况

View view = delegate.createView(parent, name, context, attrs);

所以处理处理逻辑大约如下所示

LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory()
{
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs)
    {
        //你能够在这里直接new自定义View
        //你能够在这里将系统类替换为自定义View
         //appcompat 创立view代码
        AppCompatDelegate delegate = getDelegate();
        View view = delegate.createView(parent, name, context, attrs);
        return view;
    }
});

弥补一点,由于咱们的事务涉及到切包,咱们无法确认游戏方的代码是否运用到了AppCompatActivity,一起涉及到support和androidx的兼容包,上面的

//appcompat 创立view代码
        AppCompatDelegate delegate = getDelegate();
        View view = delegate.createView(parent, name, context, attrs);

用了反射来处理这种兼容性问题。

所以处理方式应该是:

1、不考虑兼容AppCompatActivity:

只需求在Activity setContentView之前直接设置咱们自定义的factory。

2、考虑兼容AppCompatActivity:

在AppCompatActivity onCreate()之前调用咱们咱们自定义的factory,然后在咱们自定义的factory调用 AppCompatActivity的办法来兼容AppCompatActivity本来的处理。

xml->view实例的细节剖析

  1. xml中的系统View不需求前缀能加载到?

由于在PhoneLayoutInflater中生命了一个字段

  private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit.",
        "android.app."
    };

然后在onCreateView(String name, AttributeSet attrs)中先加上前缀去加载,如下图所示

for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
                // In this case we want to let the base class take a crack
                // at it.
            }
        }
        return super.onCreateView(name, attrs);

在父类中还有一个前缀是android.view.

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

2.自定义view为什么需求声明两个参数的结构办法 由于在LayoutInflater的createView(String name, String prefix, AttributeSet attrs)办法中,反射对应的Constructor是需求Context.class和AttributeSet.class的参数类型,否则会抛反常,要害代码


            Object lastContext = mConstructorArgs[0];
            if (mConstructorArgs[0] == null) {
                // Fill in the context if not already within inflation.
                mConstructorArgs[0] = mContext;
            }
            Object[] args = mConstructorArgs;
            args[1] = attrs;
            final View view = constructor.newInstance(args);

3.看源码过程中,view的parames是经过parentview的办法生成的,params = root.generateLayoutParams(attrs),然后经过解析xml的内容设置对应特点的值之前一向以为parames是在view本身的办法生成的,后来也了解了一下。一些屏幕适配计划,便是经过重写generateLayoutParams来处理。

开发事务流程

大约需求处理的事务如下

Android手游SDK换肤方案

1、在每个activity的oncreate办法调用SkinManager初始化办法。

2、SkinManager初始化办法注册咱们需求换肤的View。

3、在咱们自定义的Factory完成类oncreateview办法中模仿系统加载的机制来加载咱们的换肤View和获取对应的资源特点和值。

4、依据获取到的资源值动态映射加载外部换肤资源,原理便是经过获取资源的id,得到资源对应的资源string,再经过string查到加载外部资源的途径。

最终

以上是针对LayoutInflater换肤计划落地的研讨和考虑,期望对大家有协助,也欢迎一起讨论。