美文网首页
App换肤流程

App换肤流程

作者: 我要离开浪浪山 | 来源:发表于2023-04-15 14:08 被阅读0次

    一、前言:

    参考:https://github.com/ximsfei/Android-skin-support#toc14

    换肤流程.png

    换肤的思路: (观察者模式、aop、Hook技术)

      1. 知道xml的View 怎么解析的?? ---》
      1. 如何拦截系统的创建流程? setFactory2 可以拦截 --- aop的思路去实现
      1. 拦截后怎么做?? 重写系统的创建过程的代码(复制)
      1. 收集View以及属性,每个Activity的View 及属性都需要收集
      1. 创建皮肤包 -- apk(只有资源文件的皮肤包)
      1. 如何使用??只用插件的res(插件的 java、res)

    1. 系统的资源是如何加载的? Resources、Assetmanager
    2. 通过Hook技术,创建一个Assetmanager 专门加载皮肤包的资源
    3. 通过 反射 addAssetPath 方法放入皮肤包的路径 从而得到 加载皮肤包资源的 Assetmanager
    4. 首先通过 app的资源id --》 找到 app的资源name --》 皮肤包的资源id

    注意:

    • Hook技术 --- 反射、动态代理的使用
    • 通过反射、动态代理等技术 改变代码的原有流程

    二、布局创建流程

    1、布局加载思路

    1、Activity类setContentView()->调用到AppCompatDelegateImpl类的setContentView()方法;
    2、setContentView()方法中 调用 LayoutInflater.from(mContext).inflate(resId,contentParent)加载布局;
    3、点击inflate方法跳转到LayoutInflater.java类中的inflate()方法
    4、可以看到inflate()方法中有 XmlResouerceparser解析布局的方法,再通过inflate()解析;
    5、在这个inflate()方法中root view 通过createViewfromTag()创建,子view通过rInflateChildren()方法创建;
    6、点击进入createViewFromTag()方法,里面通过name.indexOf('.')来说明是否是自定义View,系统 的view加上前缀;
    7、点你里面的onCreateView()方法,会加上 “android.view”的前缀;
    8、一直往下点击onCreateView(),可以看到使用全类名,反射获取对象,获得对应控件;
    9、在第6步的时候,view有可能不为null,看到上方调用了 tryCreateView()方法,后面的view不为null,不往下执行了。
    10、在tryCreateView()方法中,mFactory2方法,用它进行换肤;

    2、正常view的流程

    1、特别注意:在第6步的时候,正常流程,不是直接往下执行的,先执行了tryCreateView()方法;
    2、Activity中点击onCreate()->AppCompatActivity.java类中(新版本在initDelegate()方法中调用了delete.installViewFactory()方法),delete.installViewFactory()
    3、进入AppCompatDelegateImpl.java中,查看installViewFactory()方法的实现,看到里创建了mFactory2类;

    4、在onCreateView()方法中,会创建AppCompateViewInflater();
    5、在AppCompatViewInflater类中,会创建TextView、ImageView等控件;

    //tryCreateView
     public final View tryCreateView(@Nullable View parent, @NonNull String name,
            @NonNull Context context,
            @NonNull AttributeSet attrs) {
            if (name.equals(TAG_1995)) {
                // Let's party like it's 1995!
                return new BlinkLayout(context, 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;
        }
    
    //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;
    
            // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
            // by using the parent's context
            if (inheritContext && parent != null) {
                context = parent.getContext();
            }
            if (readAndroidTheme || readAppTheme) {
                // We then apply the theme on the context, if specified
                context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
            }
            if (wrapContext) {
                context = TintContextWrapper.wrap(context);
            }
    
            View view = null;
    
            // We need to 'inject' our tint aware Views in place of the standard framework versions
            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;
                case "Spinner":
                    view = createSpinner(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "ImageButton":
                    view = createImageButton(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "CheckBox":
                    view = createCheckBox(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "RadioButton":
                    view = createRadioButton(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "CheckedTextView":
                    view = createCheckedTextView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "AutoCompleteTextView":
                    view = createAutoCompleteTextView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "MultiAutoCompleteTextView":
                    view = createMultiAutoCompleteTextView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "RatingBar":
                    view = createRatingBar(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "SeekBar":
                    view = createSeekBar(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "ToggleButton":
                    view = createToggleButton(context, attrs);
                    verifyNotNull(view, name);
                    break;
                default:
                    // The fallback that allows extending class to take over view inflation
                    // for other tags. Note that we don't check that the result is not-null.
                    // That allows the custom inflater path to fall back on the default one
                    // later in this method.
                    view = createView(context, name, attrs);
            }
    
            if (view == null && originalContext != context) {
                // If the original context does not equal our themed context, then we need to manually
                // inflate it using the name so that android:theme takes effect.
                view = createViewFromTag(context, name, attrs);
            }
    
            if (view != null) {
                // If we have created a view, check its android:onClick
                checkOnClickListener(view, attrs);
                backportAccessibilityAttributes(context, view, attrs);
            }
    
            return view;
        }
    

    三、换肤

    1、整体流程(获取拦截)

    1、换肤使用Application.ActivityLifecycleCallbacks 接口,来实现换肤,对每个Activity或者Fragment更改view;
    2、ActivityLifecycleCallbacks是在super.onCreate()方法时调用;
    3、点击进入Application类,看到ActivityLifecycleCallbacks的onActivitypreCreated()方法;
    4、在Activity.java中dispatchActivityPreCreate()方法中,调用Application.ActivityLifecycleCallbacks类的onActivityPreCreated()方法;

    ab41cb9cef9212ca1a9977557787e85.png

    2、怎么写代码

    public class ApplicationActivityLifecycle implements Application.ActivityLifecycleCallbacks {
    
        private Observable mObserable;
        private ArrayMap<Activity, SkinLayoutInflaterFactory> mLayoutInflaterFactories = new
                ArrayMap<>();
    
        public ApplicationActivityLifecycle(Observable observable) {
            mObserable = observable;
        }
    
        @Override
        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            /**
             *  更新状态栏
             */
            SkinThemeUtils.updateStatusBarColor(activity);
    
            /**
             *  更新布局视图
             */
            //获得Activity的布局加载器
            LayoutInflater layoutInflater = activity.getLayoutInflater();
    
            try {
                //Android 布局加载器 使用 mFactorySet 标记是否设置过Factory
                //如设置过抛出一次
                //设置 mFactorySet 标签为false
                Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
                field.setAccessible(true);
                field.setBoolean(layoutInflater, false);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            //使用factory2 设置布局加载工程
            SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory
                    (activity);
            LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory);
            mLayoutInflaterFactories.put(activity, skinLayoutInflaterFactory);
    
            mObserable.addObserver(skinLayoutInflaterFactory);
        }
    
        @Override
        public void onActivityStarted(Activity activity) {
    
        }
    
        @Override
        public void onActivityResumed(Activity activity) {
    
        }
    
        @Override
        public void onActivityPaused(Activity activity) {
    
        }
    
        @Override
        public void onActivityStopped(Activity activity) {
    
        }
    
        @Override
        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
    
        }
    
        @Override
        public void onActivityDestroyed(Activity activity) {
            SkinLayoutInflaterFactory observer = mLayoutInflaterFactories.remove(activity);
            SkinManager.getInstance().deleteObserver(observer);
        }
    }
    
    /**
     * 用来接管系统的view的生产过程
     */
    public class SkinLayoutInflaterFactory implements LayoutInflater.Factory2, Observer {
    
        private static final String[] mClassPrefixList = {
                "android.widget.",
                "android.webkit.",
                "android.app.",
                "android.view."
        };
    
        //记录对应VIEW的构造函数
        private static final Class<?>[] mConstructorSignature = new Class[]{
                Context.class, AttributeSet.class};
    
        private static final HashMap<String, Constructor<? extends View>> mConstructorMap =
                new HashMap<String, Constructor<? extends View>>();
    
        // 当选择新皮肤后需要替换View与之对应的属性
        // 页面属性管理器
        private SkinAttribute skinAttribute;
        // 用于获取窗口的状态框的信息
        private Activity activity;
    
        public SkinLayoutInflaterFactory(Activity activity) {
            this.activity = activity;
            skinAttribute = new SkinAttribute();
        }
    
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            //换肤就是在需要时候替换 View的属性(src、background等)
            //所以这里创建 View,从而修改View属性
            View view = createSDKView(name, context, attrs);
            if (null == view) {
                view = createView(name, context, attrs);
            }
            //这就是我们加入的逻辑
            if (null != view) {
                //加载属性
                skinAttribute.look(view, attrs);
            }
            return view;
        }
    
    
        private View createSDKView(String name, Context context, AttributeSet
                attrs) {
            //如果包含 . 则不是SDK中的view 可能是自定义view包括support库中的View
            if (-1 != name.indexOf('.')) {
                return null;
            }
            //不包含就要在解析的 节点 name前,拼上: android.widget. 等尝试去反射
            for (int i = 0; i < mClassPrefixList.length; i++) {
                View view = createView(mClassPrefixList[i] + name, context, attrs);
                if (view != null) {
                    return view;
                }
            }
            return null;
        }
    
        private View createView(String name, Context context, AttributeSet
                attrs) {
            Constructor<? extends View> constructor = findConstructor(context, name);
            try {
                return constructor.newInstance(context, attrs);
            } catch (Exception e) {
            }
            return null;
        }
    
    
        private Constructor<? extends View> findConstructor(Context context, String name) {
            Constructor<? extends View> constructor = mConstructorMap.get(name);
            if (constructor == null) {
                try {
                    Class<? extends View> clazz = context.getClassLoader().loadClass
                            (name).asSubclass(View.class);
                    constructor = clazz.getConstructor(mConstructorSignature);
                    mConstructorMap.put(name, constructor);
                } catch (Exception e) {
                }
            }
            return constructor;
        }
    
    
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }
    
        //如果有人发送通知,这里就会执行
        @Override
        public void update(Observable o, Object arg) {
            SkinThemeUtils.updateStatusBarColor(activity);
            skinAttribute.applySkin();
        }
    }
    

    四、更换资源

    1、流程思路

    1、Resourse Assermanager->Context
    2、ActivityThread.java类中 preformLaunchActivity()方法中,创建ContextImpl类
    3、ContextImpl类中创建createActivityCotext()-》new ContextImpl()
    4、然后cotext.serResourse()加载资源,比如:getResDir()
    5、ContextImpl类中调用createBaseTokenResourse()
    6、进入ResourcesManager.java里面创建了resourcesKey,下面会调用createResources()方法;
    7、在createResources()方法中调用findOrCreateResourcesImplForKeyLocked()方法
    8、进入findCreateResourcesImplForKeyLocaked()方法中创建createResourcesImpl(key)
    9、点击createResourcesImp方法,可以看到里面创建了AssetManager类;
    10、进入AssetManager类,看到builder.addApkAssets()

    • AssertManager 加载资源 --》 资源路径 --》 默认传入的资源路径 key.mResDir,app下面的res(改成皮肤包的资源路径 ---Resources AssertManager 皮肤包的)
    • Hook的思路:不能改变原有的资源加载,单独创建一个AssertManager--> 专门加载皮肤包的资源

    首先通过 app的资源id --》 找到 app的资源name --》 皮肤包的资源id

    // app的resId
    String resName=mAppResources.getResourceEntryName(resId); // 通过app的resId 找到 resName
    String resType=mAppResources.getResourceTypeName(resId);// 通过app的resId 找到 类型,layout、drawable
    // 获取对应皮肤包的资源Id
    int skinId=mSkinResources.getIdentifier(resName,resType,mSkinPkgName);
    
    public class SkinManager extends Observable {
    
        private volatile static SkinManager instance;
        /**
         * Activity生命周期回调
         */
        private ApplicationActivityLifecycle skinActivityLifecycle;
        private Application mContext;
    
        /**
         * 初始化 必须在Application中先进行初始化
         */
        public static void init(Application application) {
            if (instance == null) {
                synchronized (SkinManager.class) {
                    if (instance == null) {
                        instance = new SkinManager(application);
                    }
                }
            }
        }
    
        private SkinManager(Application application) {
            mContext = application;
            //共享首选项 用于记录当前使用的皮肤
            SkinPreference.init(application);
            //资源管理类 用于从 app/皮肤 中加载资源
            SkinResources.init(application);
            //注册Activity生命周期,并设置被观察者
            skinActivityLifecycle = new ApplicationActivityLifecycle(this);
            application.registerActivityLifecycleCallbacks(skinActivityLifecycle);
            //加载上次使用保存的皮肤
            loadSkin(SkinPreference.getInstance().getSkin());
        }
    
        public static SkinManager getInstance() {
            return instance;
        }
    
        /**
         * 记载皮肤并应用
         *
         * @param skinPath 皮肤路径 如果为空则使用默认皮肤
         */
        public void loadSkin(String skinPath) {
            if (TextUtils.isEmpty(skinPath)) {
                //还原默认皮肤
                SkinPreference.getInstance().reset();
                SkinResources.getInstance().reset();
            } else {
                try {
                    //反射创建AssetManager 与 Resource
                    AssetManager assetManager = AssetManager.class.newInstance();
                    //资源路径设置 目录或压缩包
                    Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",
                            String.class);
                    addAssetPath.invoke(assetManager, skinPath);
    
                    //宿主app的 resources;
                    Resources appResource = mContext.getResources();
                    //根据当前的设备显示器信息 与 配置(横竖屏、语言等) 创建Resources
                    Resources skinResource = new Resources(assetManager, appResource.getDisplayMetrics(),
                            appResource.getConfiguration());
    
                    //获取外部Apk(皮肤包) 包名
                    PackageManager mPm = mContext.getPackageManager();
                    PackageInfo info = mPm.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
                    String packageName = info.packageName;
                    SkinResources.getInstance().applySkin(skinResource, packageName);
    
                    //记录路径
                    SkinPreference.getInstance().setSkin(skinPath);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            //通知采集的View 更新皮肤
            //被观察者改变 通知所有观察者
            setChanged();
            notifyObservers(null);
        }
    
    }
    

    四、手动换肤的一个小例子

    package com.leo.lsn2;
    
    import android.content.Context;
    import android.os.Bundle;
    import android.text.TextUtils;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    
    import androidx.annotation.NonNull;
    import androidx.annotation.Nullable;
    import androidx.appcompat.app.AppCompatActivity;
    import androidx.core.view.LayoutInflaterCompat;
    
    public class Factory2Activity extends AppCompatActivity {
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            // 必须在 super 之前调用
            LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
                @Nullable
                @Override
                public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context,
                                         @NonNull AttributeSet attrs) {
    //                if (TextUtils.equals(name, "TextView")) {
    //                    Button btn = new Button(Factory2Activity.this);
    //                    btn.setText("我是一个按钮");
    //                    return btn;
    //                }
    
                    return null;
                }
    
                @Nullable
                @Override
                public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                    return null;
                }
            });
            super.onCreate(savedInstanceState);
    
            // 在super之后调用,反射  设置mFactorySet = false;
    
            setContentView(R.layout.activity_factory2);
    
            TextView tv = findViewById(R.id.tv);
            Log.e("leo", "tv: " + tv);
    
            TextView tv2 = new TextView(this);
            Log.e("leo", "tv2: " + tv2);
        }
    }
    

    相关文章

      网友评论

          本文标题:App换肤流程

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