Android 换肤的思路

作者: zcwfeng | 来源:发表于2020-07-06 15:38 被阅读0次

    换肤思路:

    我们需要解决的几个问题

    1.什么时候换肤?
    

    xml加载前换肤,如果xml加载后换肤,用户将会看见换肤之前的色彩,用户体验不好。

    2.皮肤是什么?
    

    皮肤就是apk,是一个资源包,包含了颜色、图片等。

    3.什么样的控件应该进行换肤?
    

    包含背景图片的控件,例如textView文字颜色。

    4.皮肤与已安装的资源如何匹配?
    

    资源名字匹配

    思路解析

    首先换肤的基本思想是更换资源索引和路径(图片,颜色值,背景等等),需要注意就是规划好,比如颜色值 要提出来到color.xml 中,不要写死成“#xxxxxx”
    
    我们制作一个皮肤插件包,这个就由一个资源apk来承载,用到了系统application初始化时候的原理,会在LoadApk的时候加载Resources通过AssertManager进行资源的加载
    
    
    
    实现这个就需要在布局Xml加载之前setContentView()替换掉,这样用户体验比较好(之后替换也是可以但是会不自然发生切换)
    
    根据源码:需要在自己代码setContentView 之前,自己实现一个SkinFactory extends Factory2 。 并且把这个SkinFactory 设置
    类似,LayoutInflaterCompat.setFactory(getLayoutInflater(),skinFactory);
    
    这里需要注意,setFactory 之后会有一个标记 为 true,我们需要用反射吧这个true改成false,来避免只能设置一次
    
    整体思路:
    
    用一个SkinFactoryManager 管理类处理资源管理过滤。我们需要对要更换的view进行扫描记录,过滤和更换对应的属性和资源的路径等。
    
    1. 设置自己实现Factory2 的SkinFactory  
    2.我们需要 用一个数据结构记录下,我们应该换肤的View,然后过滤View的属性进行换肤替换
    
    List<我们要更换的View>
        List<(View 的属性)>
            LIst<Paie(属性,view)>
    
    3. 获取资源,通过SkinManager 加载apk 资源  
    4. 执行更换,通过记录的List<View> 循环View 设置颜色,背景等属性
    

    原理分析

    四个方面去分析原理

    1.UI布局流程分析
    2.LayoutInflate原理
    3.Android资源加载流程
    4 .插件化换肤原理分析
    

    UI 布局流程分析

    一般情况下我们会有这两个入口

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_mvp);
    

    ActivityThread------>main函数 是app启动过程,这个过程先忽略,

    我们入口从performLaunchActivity开始

    ->「起始点」
    我们跟踪到方法临时变量 window = r.mPendingRemoveWindow

        r.mPendingRemoveWindow  初始值 null
    
        继续搜索r.mPendingRemoveWindow = 找到赋值点
    
        r.mPendingRemoveWindow = r.window;
    
        继续搜索 r.window 赋值点,为null的不用看
    
        r.window = r.activity.getWindow();—————就是Activity上的属性mWindow
    
        目前位置看回到起始点,我们的赋值为空,只是拿到Activity的属性mWindow的引用
    
        随后我们会调用activity.attach
    
        Activity————mWindow = new PhoneWindow(this, window, activityConfigCallback);
    

    「总结点1:--------------- 层次Activity---> PhoneWindow」

        上面的setContentView 就是在PhoneWindow中实现的,所以看下PhoneWindow的源代码
    
        installDecor();———其实就是 mDecor = new DecorView  中间做了写操作
    
        查看 mDector 就是PhoneWindow的成员
    

    「总结点2: 层次 Activity——>PhoneWindow——->DecorView」

        installDector—> generateLayout
    
        看到源码注释:  Inflate the window decor 解析decor
    
        layoutResource 会有各种R.layout.xxxxx     这些其实都是AS中的模板等,还有一些其他的
    
        去Framework 源码搜索,screen_simplev 看下他的结构
    
    -> screen_simple.xml  这就是我们的root布局文件
    
        <LinearLayout     xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
    </LinearLayout>
    
        如果我们的布局文件选定,就会调用
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource)
    
        DectorVIew 就会调用 final View root = inflater.inflate(layoutResource, null);解析刚刚的布局
    
        再通过addView的方式把他加到DectorView
    

    「总结点3: 层次 Activity——>PhoneWindow——->DecorView()---> 我们的content的布局xml」

    PhoneWindow 再次 inflate ———> mLayoutInflater.inflate(layoutResID, mContentParent); 
    而这个mContentParent 就是我们的DectorView
    

    回到 PhoneWindow 创建generateLayout-》mContentParent

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    
    content 就是我们布局中的content。  所以  DectorView—mContentParent—FrameLayout
    
    最终调用了LayouInflater->inflate 方法
    
    
        根据Tag创建临时temp
            
        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    
    
    if (!attachToRoot) {
        // Set the layout params for temp if we are not
        // attaching. (If we are, we use addView, below)
        temp.setLayoutParams(params);
    }
    

    判断如果attchToRoot = false 我们的参数就会通过xml 获取属性写进去 如果true 就需要我们addView 手动将布局参数填写进去

    LayoutInflater继续往下看createViewFromTag ——> createView()
    看到是通过根据view name进行反射构造方法。

    继续看tryCreateView 在onCreateVIew 之前
    会有三个工厂,Factory2(包含父类的),Factory(无相关父类),Factory2 privateFactory. 如果工厂不为null后面的onCreateView就会被拦截
    

    ——————Hook点

    所以换肤的思路,一个是 实现Factory,对 onCrateView 提前进行拦截

    第二个思路,重写Inflate(会有一定的侵入性质)

    资源加载的原理

    apk 包 resource.asrc 二进制文件信息

    -> 入口:handleBindApplication

    看到一个关键信息

    mInstrumentation = new Instrumentation(); 创建了一个仪表盘
    
    
    初始化我们的Application
    new ContextImpl
    然后获取我们的资源
    LoadedApk->getResources-> getOrCreateResources——> createResourcesImpl(ResourceImpl)
    
    ->final AssetManager assets = createAssetManager(key);
    

    总结点1:加载的层次调用

    LoadedAPK
        Resources
            AssertManager
    
    
    根据mInstrumentation 信息调用Application初始化
    mInstrumentation.callApplicationOnCreate(app);

    相关文章

      网友评论

        本文标题:Android 换肤的思路

        本文链接:https://www.haomeiwen.com/subject/xqawqktx.html