插件化换肤,resource 资源加载流程 ,Activity实例创建启动后界面如何生成

(activiy 或者 Service, application )
Context.getResources 获取的Resource,是从对应的ContextImpl 的mResource 字段
调用流程如下:
在这里插入图片描述

也就是说 无论是哪个Context Context.getResources 获取的Resource对象 是 ResourceImpl ,
是 从 Assetmanager.addAssetPath()
把当前资源设置进去这个AssetManager,然后通过AssetManager去创建这个 ResourceImpl ,
而 Resource.getXXX,获取资源 是 利用 ResourceImpl 中 asset,获取的资源

获取文本的时候:
在这里插入图片描述
在这里插入图片描述
获取颜色值:
在这里插入图片描述
在这里插入图片描述

获取raw文件资源跟 assets 文件资源有点区别,android会为raw资源生成的id,这意味着很容易通过id 获取资源,而在
assets文件的资源不会只能通过AssetsManager 去访问,访问速度会慢

那么我们获取插件资源就有两种方案

第一种:插件跟宿主各自的resource
利用 AssetManager的addAssetPath 把插件路径传入。
以插件的 AssetManager 创作属于插件的 resources。

/**
 * Author :
 * Description : 插件跟宿主apk 各自的 resource
 */
public class ResourcePluginManager {

    public static Resources pluginResources;

    public static void preloadResource(Context context, String apkFilePath) {
        try {

            //反射调用AssetManager的addAssetPath方法把插件路径传入
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPathMethod = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPathMethod.setAccessible(true);
            addAssetPathMethod.invoke(assetManager,apkFilePath);

            //以插件的 AssetManager 创作属于插件的 resources。
            //这里的resource的后面两个参数,一般跟宿主的配置一样就可以了,根据当前的设备显示器信息 与 配置(横竖屏、语言等) 创建
            pluginResources = new Resources(assetManager,context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());

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

    //resource getXXX 都是需要通过资源的id, 先通过该方法获取插件资源的id。
    public static int  getId(String name,String type,String packageName){
        if(pluginResources==null){
            return 0;
        }
        return pluginResources.getIdentifier(name,type,packageName);
    }

    public static String getString(String name, String packageName){
        int stringId=getId(name,"string",packageName);
        if(stringId==0){
            return null;
        }
        return pluginResources.getString(stringId);
    }

    public static Drawable getDrawable(String name,String packageName){
        int imgId=getId(name,"mipmap",packageName);
        if (imgId==0){
            imgId=getId(name,"drawable",packageName);
        }
        if (imgId==0){
            return null;
        }
        return pluginResources.getDrawable(imgId);
    }

    public static Integer getColor(String name,String packageName){
        int colorId=getId(name,"color",packageName);
        if (colorId==0){
            return null;
        }
        return pluginResources.getColor(colorId);
    }


    public static XmlResourceParser getLayout(String name, String packageName){
        int layoutId=getId(name,"layout",packageName);
        if (layoutId==0){
            return null;
        }
        return pluginResources.getLayout(layoutId);
    }


    public static XmlResourceParser getXml(String name, String packageName){
        int xmlId=getId(name,"xml",packageName);
        if (xmlId==0){
            return null;
        }
        return pluginResources.getXml(xmlId);
    }
}

第二种,把插件的resource 和宿主的resource 合并成一个allResource。然后反射替换宿主原有的Resource对象

package com.example.pluginlibrary;

import android.app.Application;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.util.Log;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class PluginAndHostResource {

    public static Resources allResources;

    public static void init(Application application) {

        try {

            //hook  Context 中Resource对象
            // 先创建AssetManager
            Class<? extends AssetManager> AssetManagerClass = AssetManager.class;
            AssetManager assetManager = AssetManagerClass.newInstance();
            // 将插件资源和宿主资源通过 addAssetPath方法添加进去
            Method addAssetPathMethod = AssetManagerClass.getDeclaredMethod("addAssetPath", String.class);
            addAssetPathMethod.setAccessible(true);
            String hostResourcePath = application.getBaseContext().getPackageResourcePath();
            //加入宿主的路径资源
            int result_1 = (int) addAssetPathMethod.invoke(assetManager, hostResourcePath);
            //加入插件的路径资源
            int result_2 = (int) addAssetPathMethod.invoke(assetManager, "/sdcard/plugin.apk");
            // 接下来创建,合并资源后的Resource
            allResources = new Resources(assetManager, application.getBaseContext().getResources().getDisplayMetrics(), application.getBaseContext().getResources().getConfiguration());

            // 替换 宿主当前 Context 中mResources对象,
            Class<?> contextImplClass = application.getBaseContext().getClass();
            Field resourcesField1 = contextImplClass.getDeclaredField("mResources");
            resourcesField1.setAccessible(true);
            resourcesField1.set(application.getBaseContext(), allResources);


            //这是最主要的需要替换的,如果不支持插件运行时更新,只留这一个就可以了
            Object mPackageInfo = RefInvoke.getFieldObject(application.getBaseContext(), "mPackageInfo");
            RefInvoke.setFieldObject(mPackageInfo, "mResources", allResources);

            //需要清理mTheme对象,否则通过inflate方式加载资源会报错
            //如果是activity动态加载插件,则需要把activity的mTheme对象也设置为null
            RefInvoke.setFieldObject(application.getBaseContext(), "mTheme", null);

            
//            //校验是否能获取到插件的字符串
//            String pluginName = allResources.getString(allResources.getIdentifier("plugin", "string", "com.example.myapplication"));
//            Log.d("zjs", "pluginName :" + pluginName);



        }catch (Exception e){
            Log.d("zjs", "MyApplication", e);
        }

    }
}

由于经常需要用到反射我们直接封装成工具

package com.example.pluginlibrary;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class RefInvoke {

    //无参
    public static Object createObject(String className) {
        Class[] pareTyples = new Class[]{};
        Object[] pareVaules = new Object[]{};

        try {
            Class r = Class.forName(className);
            return createObject(r, pareTyples, pareVaules);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        return null;
    }

    //无参
    public static Object createObject(Class clazz) {
        Class[] pareTyple = new Class[]{};
        Object[] pareVaules = new Object[]{};

        return createObject(clazz, pareTyple, pareVaules);
    }

    //一个参数
    public static Object createObject(String className, Class pareTyple, Object pareVaule) {
        Class[] pareTyples = new Class[]{ pareTyple };
        Object[] pareVaules = new Object[]{ pareVaule };

        try {
            Class r = Class.forName(className);
            return createObject(r, pareTyples, pareVaules);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        return null;
    }

    //一个参数
    public static Object createObject(Class clazz, Class pareTyple, Object pareVaule) {
        Class[] pareTyples = new Class[]{ pareTyple };
        Object[] pareVaules = new Object[]{ pareVaule };

        return createObject(clazz, pareTyples, pareVaules);
    }

    //多个参数
    public static Object createObject(String className, Class[] pareTyples, Object[] pareVaules) {
        try {
            Class r = Class.forName(className);
            return createObject(r, pareTyples, pareVaules);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        return null;
    }

    //多个参数
    public static Object createObject(Class clazz, Class[] pareTyples, Object[] pareVaules) {
        try {
            Constructor ctor = clazz.getDeclaredConstructor(pareTyples);
            ctor.setAccessible(true);
            return ctor.newInstance(pareVaules);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }



    //多个参数
    public static Object invokeInstanceMethod(Object obj, String methodName, Class[] pareTyples, Object[] pareVaules) {
        if (obj == null)
            return null;

        try {
            //调用一个private方法
            Method method = obj.getClass().getDeclaredMethod(methodName, pareTyples); //在指定类中获取指定的方法
            method.setAccessible(true);
            return method.invoke(obj, pareVaules);

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

        return null;
    }

    //一个参数
    public static Object invokeInstanceMethod(Object obj, String methodName, Class pareTyple, Object pareVaule) {
        Class[] pareTyples = {pareTyple};
        Object[] pareVaules = {pareVaule};

        return invokeInstanceMethod(obj, methodName, pareTyples, pareVaules);
    }

    //无参
    public static Object invokeInstanceMethod(Object obj, String methodName) {
        Class[] pareTyples = new Class[]{};
        Object[] pareVaules = new Object[]{};

        return invokeInstanceMethod(obj, methodName, pareTyples, pareVaules);
    }




    //无参
    public static Object invokeStaticMethod(String className, String method_name) {
        Class[] pareTyples = new Class[]{};
        Object[] pareVaules = new Object[]{};

        return invokeStaticMethod(className, method_name, pareTyples, pareVaules);
    }

    //一个参数
    public static Object invokeStaticMethod(String className, String method_name, Class pareTyple, Object pareVaule) {
        Class[] pareTyples = new Class[]{pareTyple};
        Object[] pareVaules = new Object[]{pareVaule};

        return invokeStaticMethod(className, method_name, pareTyples, pareVaules);
    }

    //多个参数
    public static Object invokeStaticMethod(String className, String method_name, Class[] pareTyples, Object[] pareVaules) {
        try {
            Class obj_class = Class.forName(className);
            return invokeStaticMethod(obj_class, method_name, pareTyples, pareVaules);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    //无参
    public static Object invokeStaticMethod(Class clazz, String method_name) {
        Class[] pareTyples = new Class[]{};
        Object[] pareVaules = new Object[]{};

        return invokeStaticMethod(clazz, method_name, pareTyples, pareVaules);
    }

    //一个参数
    public static Object invokeStaticMethod(Class clazz, String method_name, Class classType, Object pareVaule) {
        Class[] classTypes = new Class[]{classType};
        Object[] pareVaules = new Object[]{pareVaule};

        return invokeStaticMethod(clazz, method_name, classTypes, pareVaules);
    }

    //多个参数
    public static Object invokeStaticMethod(Class clazz, String method_name, Class[] pareTyples, Object[] pareVaules) {
        try {
            Method method = clazz.getDeclaredMethod(method_name, pareTyples);
            method.setAccessible(true);
            return method.invoke(null, pareVaules);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }



    //简写版本
    public static Object getFieldObject(Object obj, String filedName) {
        return getFieldObject(obj.getClass(), obj, filedName);
    }

    public static Object getFieldObject(String className, Object obj, String filedName) {
        try {
            Class obj_class = Class.forName(className);
            return getFieldObject(obj_class, obj, filedName);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static Object getFieldObject(Class clazz, Object obj, String filedName) {
        try {
            Field field = clazz.getDeclaredField(filedName);
            field.setAccessible(true);
            return field.get(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    //简写版本
    public static void setFieldObject(Object obj, String filedName, Object filedVaule) {
        setFieldObject(obj.getClass(), obj, filedName, filedVaule);
    }

    public static void setFieldObject(Class clazz, Object obj, String filedName, Object filedVaule) {
        try {
            Field field = clazz.getDeclaredField(filedName);
            field.setAccessible(true);
            field.set(obj, filedVaule);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void setFieldObject(String className, Object obj, String filedName, Object filedVaule) {
        try {
            Class obj_class = Class.forName(className);
            setFieldObject(obj_class, obj, filedName, filedVaule);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }




    public static Object getStaticFieldObject(String className, String filedName) {
        return getFieldObject(className, null, filedName);
    }

    public static Object getStaticFieldObject(Class clazz, String filedName) {
        return getFieldObject(clazz, null, filedName);
    }

    public static void setStaticFieldObject(String classname, String filedName, Object filedVaule) {
        setFieldObject(classname, null, filedName, filedVaule);
    }

    public static void setStaticFieldObject(Class clazz, String filedName, Object filedVaule) {
        setFieldObject(clazz, null, filedName, filedVaule);
    }
}

使用如下 :

    在宿主中获取插件的字符串,包名是宿主的
   String pluginName =  getResources().getString(getResources().getIdentifier("pluginName", "string", "com.example.myapplication"));

第二种资源 有 个缺点就是 存在宿主与插件的资源冲突。也就是会存在宿主和插件的资源名称一致对应的id也一致,
这个时候你获取到的资源是宿主的,相当于相同的资源id下插件的东西没放进来allResource。
解决方法是 通过 通过aapt修改插件资源的id。这里就自行百度了。

接下来我们来学习,当Activity启动后,界面如何生成。

下面的源码分析都是基于 android x 1.2.0下

Activity 在被创建实例的时候 会创建一个 Window 并与此进行绑定,这个Window是抽象类, 具体实现是PhoneWindow。
接着,在Activity的onResume中,Activity的内容将开始渲染到Window上面,然后开始绘制直到我们可以看见。

我们从 oncreate 的 setContentView(R.layout.activity_main); 入手

在这里插入图片描述

Activity 的 setContentView方法,在androidx中是交给 AppCompatDelegateImpl 处理的
也就是把layout id 传递过来
在这里插入图片描述

首先进入一个很重要的方法

ensureSubDecor();
在这里插入图片描述
这个方法进去了
createSubDecor()
在这里插入图片描述
在这个方法中 ,864 mWindow.getDecorView(); 内部主要做了两件事情

通过generateDecor(-1); 创建decorview的对象
mContentParent = generateLayout(mDecor);
创建 decorview 内部的 contentparent布局对象

也就是 mWindow.getDecorView(); 为当前window(Activity绑定的)创建了 DecorView view,内部有个 contentparent。
接在 createSubDecor() 中创建完 DecorView 后 再根据不同条件创建了 subdecor 对象
这个 subdecor其实是一个 toolbar + framelayout 布局。 其中 framelayout 是id 为R.id.content.
然后在 1001 行 mWindow.setContentView(subDecor);
也就是把这个 subdecor 对象add进入 DecorVIew中的 contentparent中去。

所以我们回到最开始的 setContentView
在这里插入图片描述

先是调用了 ensureSubDecor();这个方法你可以简单理解为。
先是window 创建了DecorView ,内部有个 contentparent,然后在创建了个subdecor 对象
这个 subdecor其实是一个 toolbar + framelayout 布局。 其中 framelayout 是id 为R.id.content.
接着把 subdecor 对象add进入 DecorVIew中的 contentparent中去
也就是
在这里插入图片描述
所以 694 行
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
先获取了 subdecor 里面的 id 为 android.R.id.content 。
最关键的就是 696行
LayoutInflater.from(mContext).inflate(resId, contentParent);
这里 mContext 上下文 也就是 这个 Activity 的 LayoutInflater ,然后调用 inflate方法。
以我们的闯进来 布局id ,以subdecor 里面的Framelayout 为父布局。
那么也就是说我们的layout 布局其实就是 放在在 subdecor 的Framelayout里面
在这里插入图片描述
所以说 我们想获得activity绑定的这个layout view
我们就可以这么做
在这里插入图片描述
LayoutInflater.from(mContext).inflate(resId, contentParent)。
先简单说下
这个类LayoutInflater的inflate方法能够把指定的layout xml 布局里面的各种标签名 通过反射 创造 各种view对象然后放在 父容器 contentParent 中 。

举个例子
在这里插入图片描述
在这里插入图片描述
这里 Main2Activity 里 setContentView(R.layout.activity_main2);
LayoutInflater.from(mContext).inflate(resId, contentParent)。
这里 mContext 上下文 也就是 这个 Main2Activity 的 LayoutInflater ,调用 inflate方法。
一开始 他是先把 顶层的父布局给 反射成一个view
也就是根据
androidx.constraintlayout.widget.ConstraintLayout 这个全路径类名 通过反射获取实例对象。然后
放在 subdecor 的Framelayout 里面。
接着。它在去遍历 里面的子类。
不过这次的 父容器 不在是 subdecor 的Framelayout 而是以 ConstraintLayout 作为父容器
然后一样 以标签 反射创造 ImageView 实例 放入 ConstraintLayout 中 。

注意一个点,在使用inflate这个函数的时候,有个 3个参数的构造方法

View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)

第三个参数attachToRoot是个boolean参数,表示当前 布局的顶层布局 是否要作为 root 根布局的子view 如果true 那么 当前view 作为 root 的子view ,如果false 当前view 作为root 。并且当前view 设置root 的LayoutParams

从上面的例子来看
root 就是 subdecor 里面的Framelayout, 当前 布局的顶层布局 就是 ConstraintLayout

我们来跟踪看下里面是干了什么
LayoutInflater.from(mContext).inflate(resId, contentParent)。

sdk 30
在这里插入图片描述
538 行 通过 res.getLayout(resource) 根据 资源layout id 创作 XmlResourceParser ,layoutRes 转换成 parser 解析器
然后调用 inflate(parser, root, attachToRoot)。 把解析器和 父view 传进去 。

这里贴出 inflate 代码

   public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
           **//解释 1 。 通过 解析器 parser 获取 当前 标签的下 所有的 属性 AttributeSet** 
            final AttributeSet attrs = Xml.asAttributeSet(parser);

            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                advanceToRootNode(parser);
            **//解释 2 。 通过 解析器 parser 获取 当前 标签的 命名,也就是布局的根布局 也就是类似上面的ConstraintLayout** 
                final String name = parser.getName();

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

    **//解释 3 。 如果这个标签的命名 是 <merge />。 并且 root == null  或者 当前view 又想充当root ,那么肯定会抛出异常。
    //从这里我们也可以看到merge 标签不能当初root,调用 rInflate 表示他只能充当子view
                if (TAG_MERGE.equals(name)) {
                    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 {
                    // Temp is the root view that was found in the xml
                    **//解释 4  根据标签的名字 属性,父容器 ,调用 createViewFromTag 创作view 对象。这里创作出来的就是 根布局 也就是 ConstraintLayout**
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
          //解释 5   就像注意点说的 ,第三个参数attachToRoot是个boolean参数,表示子view  是否要作为 root根布局的子view  
          // 如果是 作为   root根布局的子view  ,如果不是  子view  设置 root  的 LayoutParams
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

          //解释 6 调用 rInflateChildren方法 就是去遍历当前布局的 子view ,当这个时候 父容器就是  ConstraintLayout,而不是 subdecor 的Framelayout
                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                     //解释 7 如果root 不等于 空,并且 attachToRoot ==true 表示要作为root下 的子view ,那么 root  addView
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                       //解释 8 如果root等于 空,或者 attachToRoot ==flase  不做为root的子view ,那么 当前view 就是  root 
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(
                        getParserStateDescription(inflaterContext, attrs)
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

总结下上面的解释点

最先 根据 XmlPullParser getName 获取 顶层布局 的命名 以及根据 XmlPullParser 获取 改标签的所有的属性 AttributeSet
调用 createViewFromTag 去创作 顶层 view 。
然后以这个 顶层view 作为 parent 在调用 rInflate 去创作子view ,我们看下 rInflate

在这里插入图片描述

1009 通过 while 循环遍历 当前 XmlPullParser next() 。也就是一个个去遍历
然后1106 依旧是是利用 XmlPullParser getName() 去获取当前 标签的名字
最终在1121 依旧还是通过 createViewFromTag(parent, name, context, attrs) 去创作view
创作完后 1125 viewGroup.addView
当然如果这个子view 还有他的子view 那么已这个 子view 为父容器 继续调用 rInflateChildren。

所以说 都是通过 layout 的解析器 XmlPullParser 去获取 标签名字,然后 调用 createViewFromTag 去创作view 然后加入到父容器当中 。

好那么我们最终看 createViewFromTag 是如何 根据标签名字 创作view 的

在这里插入图片描述

从上面解释可以看到 ,最终都是来到
我们看 createView(context, name, prefix, attrs)。这个方法。只是 prefix 差别而已
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
(这里代码是android sdk 30)
这样整条链路我们也都基本屡清楚了。接下来重点关注上面说的 很重要的 点 tryCreateView
通过标签获取调用 createView前会先调用 tryCreateView 去获取view 如果获取不到 才去 createView。

贴出 tryCreateView 代码
在这里插入图片描述
代码很简单 就是一个个去排查 mFactory2 或者 mFactory或者 mPrivateFactory去调用 onCreateView 能不能创作view
我们这里只看 mFactory2 就好 。
Factory2 是个 LayoutInflater 类 里 内部 定义的接口 ,方法只有一个就是onCreateView

在这里插入图片描述
并且 LayoutInflater 类有这个接口对象,
在这里插入图片描述
LayoutInflater 设置这个 接口 目的 就是 外部人员 设置一个 Factory2 对象进来。
这样子,根据上面的流程我们知道,当 通过标签 创作view 调用的 createViewFromTag 的时候,就会优先
进入Factory2 的 onCreateView 。只有 创作不出来 才 后需调用createView。
这里就是非常有用的点 ,也是插件化换肤一个非常关键的点。
我们想下,如果我们 自己 实现一个 LayoutInflater.Factory2接口 ,然后重写 onCreateView方法
onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs)
这个方法参数有 当前标签名字,有当前标签的属性。
我们模仿源码 createView 利用 name 去 反射 创作构造函数 创建view ,并且获取 当前 view的属性 attrs 。对需要的属性 改变他的属性值,从插件资源包中获取资源 设置进去,最终 onCreateView 中返回这个view 。不就实现了 插件化换肤。

Activity实例创建启动后界面如何生成总结

XML转化为View转化为主要是通过LayoutInflator来完成的,将标签名转化为View的名称,XML中的各种属性转化为AttributeSet对象,然后通过反射生成View对象。
这个过程中存在一些耗时操作,比如解析XML的IO操作,通过反射生成View等,我们可以通过多种方式优化这个过程,比如将反向的耗时转移到编译期。

View添加到页面上,主要经过了这么几个过程:
1.启动activity
2.activity的attatch 中 为当前上下文也就是 activity 创建 PhoneWindow
3.activity 的 setContentView,先 创建 DecorView 内部有个contentParent ,创建subDecor 放入 contentParent 中 ,将layoutId通过LayoutInflator转化为View 放入subDecor中的 android.R.id.content Framelayout布局中
4.ActivityThread 调用 handleResumeActivity 方法,在这个方法中

    //注释1
    r.window = r.activity.getWindow();
    View decor = r.window.getDecorView();
    decor.setVisibility(View.INVISIBLE);
    ViewManager wm = a.getWindowManager();
    WindowManager.LayoutParams l = r.window.getAttributes();
    ...
    //注释2
    wm.addView(decor, l);
    ...

通过WindowManager把DecorView 给添加进去
5.最后会调到ViewRootImpl中注释3处,这里才是真正的通过WMS在屏幕上开辟一个窗口,到这一步我们的View也就可以显示到屏幕上了

#ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            //记录DecorView
            mView = view;
            //省略
            //开启View的三大流程(measure、layout、draw)
            requestLayout();
            try {
                //添加到WindowManagerService里,这里是真正添加window到底层
                //这里的返回值判断window是否成功添加,权限判断等。
                //比如用Application的context开启dialog,这里会添加不成功
                // 注释3
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                        mTempInsets);
                setFrame(mTmpFrame);
            } catch (RemoteException e) {
            }
            //省略
            //输入事件接收
        }
    }
}

可以看出,当我们打开一个Activity时,界面真正可见是在onResume之后。

插件话方案总结

说白了 就是 Activity 这个Context 设置的布局 会利用当前 Context 下的 LayoutInflater 一个个根据标签名字去反射view 对象。而每一个view 在被创建前 会经过 LayoutInflater.Factory2 的 onCreateView 方法, 利用 LayoutInflater.Factory2 的 onCreateView 方法中,我们自己手动创建layout 中的每一个 view ,并对这个view的属性 设置插件包的资源,不让系统源码 createView 帮我们创建。

我们在自己手动创建每一个view 的时候, 学习下 别人 源码 createView 是怎么 创建的 ,createView 会有个对构造函数的缓存,利用 sConstructorMap 对每个类的构造函数 进行缓存 做了优化 ,防止每次都根据name 来反射创作view 很耗性能 。所以我们在 LayoutInflater.Factory2 的 onCreateView 根据 name 来反射创作view的时候也要学下人家这么做缓存。

最后我们把 自定义的 LayoutInflater.Factory2 自定义出来后,如何 设置进去呢?
利用的就是 LayoutInflater setFactory

 LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory()
        {
            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs)
            {
                 View  view 
                return  view  ;
            }
        });

其实在我们还没设置这个Factory 进去的时候,不同的android 版本 源码里面 他们内部其实有的有设置进去过
在android x 版本上 Activity都继承于AppCompatActivity,而在AppCompatActivity中,实际上也调用了setFactory方法。人家也想把自己自定义 的 Factory设置进去。
所以这个时候出现了一些冲突。只能设置一个。谁先设置算谁的,后面的就不生效。

所以说 如果你想 以你的生效的话,你想为当前Activity 设置一个Factory 自己去创建 当前 Activity 绑定的layout 中view。
你得抢先一步 在 Activity 的 onCreate 调用 super.onCreate(savedInstanceState); 前先设置进去
在这里插入图片描述

这里有个很关键的问题 ,虽然以你的为主了,但是 源码的Factory 却不生效了,怎么办呢,这是会出问题的,怎么个出问题法,可以看看鸿洋大神写的

Android 探究 LayoutInflater setFactory

解决就是 你可以把 源码的Factory做的事情搬到你的Factory里面来。

好了下面我们来做一个最最简单的插件换肤实例。
在做之前,学习下属性使用。
从上面我们知道,在
onCreateView 这个方法有个参数 AttributeSet,这个代表当前view的所有属性。
我们可以对当前view 遍历他的所有属性,你就知道怎么使用了

  public class Factory2 implements LayoutInflater.Factory2 {

        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            //TODO 接管View的创建

            for (int i = 0; i < attrs.getAttributeCount(); i++) {
                //获取到属性的name,value
                String attrName = attrs.getAttributeName(i);   //属性名字 比如 text ,background
                String attrValue = attrs.getAttributeValue(i);  //属性值

                if (attrValue.contains("@")) {//如果属性值是以@引用的
                    int resId = Integer.parseInt(attrValue.substring(1));//获取该属性值的资源id 比如 @string/app_name 这个id号,@drawable/ic_launcher_background这个id号
                    //通过该属性值的id获取 改属性值的类型,比如说这个属性值是drawable,或者string
                    String typeName = context.getResources().getResourceTypeName(resId);
                    //通过该属性值的id获取该属性值的名称或者文件名。比如这里是@string/app_name,那么这个entryName就是app_name
                    //如果这里是个@drawable/ic_launcher_background"。那么这个entryName就是ic_launcher_background
                    String entryName = context.getResources().getResourceEntryName(resId);

                    Log.d("attrs@", " attrName " + attrName + " attrValue " + attrValue + " resId " + resId + " typeName " + typeName + " entryName " + entryName);
                }

                if (attrValue.startsWith("?")) {
                    int attrId = Integer.parseInt(attrValue.substring(1));
                    String typeName = context.getResources().getResourceTypeName(attrId);
                    String entryName = context.getResources().getResourceEntryName(attrId);
                    Log.d("attrs?", " attrName " + attrName + " attrValue " + attrValue + " attrId " + attrId + " typeName " + typeName + " entryName " + entryName);
                }

            }
            return null;
        }

        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }

    }

打印日志如下:
在这里插入图片描述
在这里插入图片描述

注意这里 属性的写法有这3种形式
#ffff
?attr/xxx
@string/xxx

我们上面对 ?和@可以做处理。但是对那种直接写死值的没办法。所有在代码中不要固定写死资源写法。

好了 我们可以开始动手了

我们要做的是:
我们在宿主activity布局 里面有一个 ImageView 和一个自定义 TextView ,和一个按钮。
我们点击个按钮。 ImageView 的背景图 从 插件里面拿一张文件名跟宿主的 all_background 图片文件
并且 DiyText 这个text 从插件中获取 字符串命名跟宿主一样 app_name 字符串文字。

在这里插入图片描述

我们的思路:
我们在onCreateView中创建每一个view的时候,我们把 有 background 或者有 text 的属性 的view 用List 保存下来。并且保存该view 属性 background 或 text 的 attrName typeName entryName 。
然后点击按钮的时候 ,我们从保存的view 中根据 attrName typeName entryName 从插件资源包中获取对应的资源,设置进去这个view
(所以说做插件包的时候,宿主和插件资源的命名一致好对应。)

我们先制造两个 bean 叫 SkinView ,SkinViewAttr

public class SkinView {

    private View view;//每个需要保存的view
    private ArrayList<SkinViewAttr> attrs;//该view中的需要替换的属性

    public SkinView(View view, ArrayList<SkinViewAttr> attrs) {
        this.view = view;
        this.attrs = attrs;

    }

    public View getView() {
        return view;
    }

    public void setView(View view) {
        this.view = view;
    }

    public ArrayList<SkinViewAttr> getAttrs() {
        return attrs;
    }

    public void setAttrs(ArrayList<SkinViewAttr> attrs) {
        this.attrs = attrs;
    }

    @Override
    public String toString() {
        return "SkinView{" +
                "view=" + view.getClass().getName() +
                ", attrs=" + attrs +
                '}';
    }
}


public class SkinViewAttr {

    private String attrName;

    private String typeName;

    private String entryName;

    public SkinViewAttr(String attrName, String typeName, String entryName) {
        this.attrName = attrName;
        this.typeName = typeName;
        this.entryName = entryName;
    }

    public String getAttrName() {
        return attrName;
    }

    public void setAttrName(String attrName) {
        this.attrName = attrName;
    }

    public String getTypeName() {
        return typeName;
    }

    public void setTypeName(String typeName) {
        this.typeName = typeName;
    }

    public String getEntryName() {
        return entryName;
    }

    public void setEntryName(String entryName) {
        this.entryName = entryName;
    }

    @Override
    public String toString() {
        return "SkinViewAttr{" +
                "attrName='" + attrName + '\'' +
                ", typeName='" + typeName + '\'' +
                ", entryName='" + entryName + '\'' +
                '}';
    }
}


然后我们创建自定义 Factory2。这个 Factory2 的很多 逻辑 基本都从上面sdk 30源码 createView搬过来的,除了 过滤器操作 没有搬 ,看懂上面的源码注释 就很容易入手 ,我们只是多加了 对创建的每个view 的属性进行判断是否需要更换 需要就 保存下来 ,然后 等需要更换的时候,就对这些view 进行属性值更换 。

package com.example.myapplication.pluginskin;

import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;


import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;

import com.example.myapplication.ResourcePluginManager;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;


public class Factory2 implements LayoutInflater.Factory2 {

    //对每个类的构造函数进行缓存
    private static final HashMap<String, Constructor<? extends View>> sConstructorMap =
            new HashMap<String, Constructor<? extends View>>();

    private static final ClassLoader BOOT_CLASS_LOADER = LayoutInflater.class.getClassLoader();


    //宿主的context
    private static Context mContext;

    private static final Class<?>[] mConstructorSignature = new Class[]{
            Context.class, AttributeSet.class};

    final Object[] mConstructorArgs = new Object[2];

    //上面的所有的属性都是跟源码createView sdk 30 一模一样的


    //这里这个list 是因为不清楚不全的view前缀是什么,所以会一个个去创作直到找到
    private static final String[] mClassPrefixList = {
            "android.widget.",
            "android.webkit.",
            "android.app.",
            "android.view."
    };


    //需要更换属性的viewlist
    private ArrayList<SkinView> viewItems = new ArrayList<>();

    //我们需要改动的属性List
    private static final String[] attrList = new String[]{
            "background", "text"};


    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        //TODO 接管View的创建
        View view;
        mContext = context;

        //这里的意思是,如果这个标签名字没有写全比如 直接就是TextView 按道理
        //应该是 android.view.TextView。这个方法是判断如果没有点也就是没有写全 就先调用onCreateView
        //这个onCreateView方法没什么 他只是根据这个name 加上  prefix
        // 然后最还是调用createView(context, name, prefix, attrs)
        if (-1 == name.indexOf('.')) {
            view = addNameToCreateView(context, name, attrs);
        } else {
            //写全了的全路径类名则直接创作view,所以这里的prefix传进去的是null
            view = createView(context, name, null, attrs);
        }

        return view;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }


    private View addNameToCreateView(Context context, String name, AttributeSet attrs) {
        for (int i = 0; i < mClassPrefixList.length; i++) {
            View view = createView(context, name, mClassPrefixList[i], attrs);
            if (view != null) {
                return view;
            }
        }
        return null;
    }

    public final View createView(Context viewContext, String name,
                                 String prefix, AttributeSet attrs) {
        View view = null;

        try {

            //从 sConstructorMap 获取 构造函数,也就是这里 利用 sConstructorMap 对每个类的构造函数 进行缓存 做了优化 ,防止每次都根据name 来反射创作view 很耗性能 。
            Constructor<? extends View> constructor = sConstructorMap.get(name);
            //如果缓存中有,会调用 verifyClassLoader 这个方法 对 这个类的 classloader 进行验证
            //verifyClassLoader方法会 验证这个类的 classloader是否 是context 上下文双亲委派链classloader们中其中一个classloader
            //如果验证是true 就没关系,如果验证是false 那么这里会  sConstructorMap.remove
            if (constructor != null && !verifyClassLoader(constructor)) {
                constructor = null;
                sConstructorMap.remove(name);
            }


            Class<? extends View> clazz = null;
            String finalName = null;


            //如果这个constructor == null,那么就需要去反射
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                //这里就是根据name来获取class ,这里根据 prefix 和 name ,也就是得类写全名了来创建
                //这里可以用mContext.getClassLoader(),就是因为上面的 verifyClassLoader方法已经保证了 类的classloder已经
                //属性context.getClassLoader()父类链路调用的其中一个了

                finalName = prefix != null ? (prefix + name) : name;

                clazz = Class.forName(finalName, false,
                        mContext.getClassLoader()).asSubclass(View.class);

                Log.d("Factory2ClassName", finalName);


                //如果没有过滤器,则 Constructor 创作

                constructor = clazz.getConstructor(mConstructorSignature);

                constructor.setAccessible(true);
                //把 Constructor放进去sConstructorMap中
                sConstructorMap.put(name, constructor);
            }

            mConstructorArgs[0] = viewContext;
            Object[] args = mConstructorArgs;
            //这里args数组有了两个,就是 createView(@NonNull Context viewContext, @NonNull String name,@Nullable String prefix, @Nullable AttributeSet attrs)
            //中的 viewContext,attrs
            args[1] = attrs;

            //根据args参数创造实例view
            view = constructor.newInstance(args);


            if (view != null) {
                passSkinViewAttr(view, viewContext, attrs);
            }


        } catch (Exception e) {
            Log.d("Factory2Exception", "Exception", e);
        }

        return view;
    }


    //对创作的每个view 进行判断,view里面有需要替换的属性有的话把这个view 给保存下来
    private void passSkinViewAttr(View view, Context context, AttributeSet attributeSet) {

        ArrayList<SkinViewAttr> viewAttrs = new ArrayList<>();

        //对该view所有的属性进行遍历查找,如果有attrList中的符合,就保存这个属性
        for (int i = 0; i < attributeSet.getAttributeCount(); i++) {
            //获取到属性的name
            String attrName = attributeSet.getAttributeName(i);

            for (int j = 0; j < attrList.length; j++) {

                if (attrName.equals(attrList[j])) {

                    String attrValue = attributeSet.getAttributeValue(i);
                    //这里我们只对属性值是@写法的做操作
                    if (attrValue.contains("@")) {
                        int id = Integer.parseInt(attrValue.substring(1));
                        String typeName = context.getResources().getResourceTypeName(id);
                        String entryName = context.getResources().getResourceEntryName(id);
                        SkinViewAttr attr = new SkinViewAttr(attrName, typeName, entryName);
                        viewAttrs.add(attr);
                    }

                }
            }
        }
        //如果查询所有的属性后 viewAttrs 数据不等于空,这个view是需要保存的
        if (!viewAttrs.isEmpty()) {
            SkinView skinView = new SkinView(view, viewAttrs);
            viewItems.add(skinView);
            Log.d("Factory2SkinView", skinView.toString());
        }
    }


    //校验操作 ,根据双亲委派机制, 查询view 的classloader 是否属于 context的 ClassLoader其中一个
    private final boolean verifyClassLoader(Constructor<? extends View> constructor) {
        final ClassLoader constructorLoader = constructor.getDeclaringClass().getClassLoader();
        if (constructorLoader == BOOT_CLASS_LOADER) {
            return true;
        }
        ClassLoader cl = mContext.getClassLoader();
        do {
            if (constructorLoader == cl) {
                return true;
            }
            cl = cl.getParent();
        } while (cl != null);
        return false;
    }


    public void changeSkin() {
        for (int i = 0; i < viewItems.size(); i++) {

            List<SkinViewAttr> skinViewAttrList = viewItems.get(i).getAttrs();

            for (SkinViewAttr skinViewAttr : skinViewAttrList) {

                for (int j = 0; j < attrList.length; j++) {

                    if (skinViewAttr.getAttrName().equals(attrList[j])) {

                        if ("drawable".equals(skinViewAttr.getTypeName())) {
                            Drawable drawable = ResourcePluginManager.getDrawable(skinViewAttr.getEntryName(), "com.example.plugin");
                            ImageView imageView = (ImageView) viewItems.get(i).getView();
                            imageView.setBackground(drawable);
                        }

                        if ("string".equals(skinViewAttr.getTypeName())) {
                            String s = ResourcePluginManager.getString(skinViewAttr.getEntryName(), "com.example.plugin");
                            TextView textView = (TextView) viewItems.get(i).getView();
                            textView.setText(s);
                        }
                    }
                }
            }
        }

        updateStatusBarColor();
    }


    //下面是改变 状态栏等颜色。
    //做法其实也还是根据插件包中的颜色命名获取颜色值 然后设置进去
    //对 状态对应哪个不清楚的 这个看看这篇文章
    // https://blog.csdn.net/smartzzg/article/details/104788412 其中colorPrimaryDark跟statusBarColor是对应一样的,只是版本差异
    //在状态烂主题这一块在不同版本上源码写法都不大相同,对应的名字也是不大相同 ,需要注意
    public static void updateStatusBarColor() {

        AppCompatActivity activity = (AppCompatActivity) mContext;

        int colorPrimaryDarkColor = ResourcePluginManager.getColor("colorPrimaryDarkColor", "com.example.plugin");
        if (colorPrimaryDarkColor != 0) {
            activity.getWindow().setStatusBarColor(colorPrimaryDarkColor);
        }

        int colorPrimaryColor = ResourcePluginManager.getColor("colorPrimaryColor", "com.example.plugin");
        if (colorPrimaryColor != 0) {
            ColorDrawable drawable = new ColorDrawable(colorPrimaryColor);
            ActionBar actionBar = activity.getDelegate().getSupportActionBar();
            actionBar.setBackgroundDrawable(drawable);
        }

        int navigationColorColor = ResourcePluginManager.getColor("navigationColorColor", "com.example.plugin");
        if (navigationColorColor != 0) {
            activity.getWindow().setNavigationBarColor(navigationColorColor);

        }
    }


}

使用如下

public class Main2Activity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        Factory2 factory2 = new Factory2();

        LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), factory2);

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        ResourcePluginManager.init(this,"/sdcard/plugin.apk");
        Button button = findViewById(R.id.change);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                factory2.changeSkin();
            }
        });
    }
}

最后我们看下效果
在这里插入图片描述

很显然,imageview 已经从插件资源包中获取了图片。自定义textview也从插件资源包获取了文字,并且也从插件资源包获取颜色资源 设置状态栏和actionbar 颜色。

我们通过日志来看下 ,对当前activity 反射创建的view 都有哪些
在这里插入图片描述
日志打印的就是我们这个activity创建的所有view
箭头标志的就是 我们需要修改的 view

我们再来看看。我们保存下来需要修改的view都有哪些
在这里插入图片描述
看这里已经保存了需要更换的view ,已经他的内部需要更换的属性。

好了我们的最简单的换肤实例就到此结束了。基本原理大致就是如此,剩下就是慢慢去打磨,如果要完全实现估计会出现一些细节sdk版本差异导致等问题。可以去GitHub看看一些demo。

猜你喜欢

转载自blog.csdn.net/weixin_43836998/article/details/114575917
今日推荐