作者
大家好,我叫;
目前主要负责国内相关事务开发和一些日常事务。
布景
最近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的计划,原因如下:
- 不需求保护皮肤包的apk,降低sdk的保护本钱和研制的接入本钱
- 需求支撑资源动态映射
- 事务只需求支撑图片替换
所以开发接入流程便是:
- 每个SDK版别定好换肤图片资源和称号输出到换肤阐明文档上(Android和iOS共用)
- 接入方依据换肤阐明文档上,出图切图,然后把切图资源放到规定的asset目录上面
- 母包选择对应的换肤SDK出包即可
原理剖析
LayoutInflater剖析
- LayoutInflater怎么实例化
咱们通常调用以下代码取得一个LayoutInflater 运用流程
LayoutInflater.from(context)
context一般传的是activity实例
没有找到inflater_service的name,再看看父类
要害点是getBaseContext()回来的实例,经过剖析上下文可知,getBaseContext()回来的是ContextWrapper的成员变量,且在attachBaseContext办法传进,如下图所示
由于Activity是在ActivityThread中经过反射实例的,所以在ActivityThread找到如下代码
经过以上可知appContext这个参数的实例是ContextImpl
依据上图追踪,LayoutInflater的实例生成下图所示
- View怎么经过LayoutInflater生成,如下图所示
经过对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,流程大约如下所示;
经过上面剖析,其内部的逻辑跟咱们换肤逻辑相同
2.反常处理
如果咱们在AppCompatActivity onCreate()之后设置LayoutInflaterCompat.setFactory2会抛出一下反常
经过代码剖析可知
LayoutInfalter调用过setFactory或许setFactory2的话,下一次调用的话就会跑出反常。
那么咱们在AppCompatActivity onCreate()之前设置LayoutInflaterCompat.setFactory2呢?,经过源码剖析可知
虽然这样不会跑出反常,但是就不会执行到AppCompatDelegateImpl的onCreateView办法,相当于AppCompatDelegateImpl设置的factory失效。所以咱们需求在咱们自己factory来做对AppCompatDelegateImpl的factory的处理。由于AppCompatDelegateImpl的下面的这个办法是public
所以咱们能够在咱们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实例的细节剖析
- 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来处理。
开发事务流程
大约需求处理的事务如下
1、在每个activity的oncreate办法调用SkinManager初始化办法。
2、SkinManager初始化办法注册咱们需求换肤的View。
3、在咱们自定义的Factory完成类oncreateview办法中模仿系统加载的机制来加载咱们的换肤View和获取对应的资源特点和值。
4、依据获取到的资源值动态映射加载外部换肤资源,原理便是经过获取资源的id,得到资源对应的资源string,再经过string查到加载外部资源的途径。
最终
以上是针对LayoutInflater换肤计划落地的研讨和考虑,期望对大家有协助,也欢迎一起讨论。