美文网首页
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