Android通过Hook技术实现一键换肤

目录

1.什么是一键换肤

2.界面上那些东西可以换肤 

3.利用Hook实现一键换肤

4.Android创建视图源码分析

4.1.自定义Activity设置要显示的布局文件xml

4.2.调用兼容AppCompatActivity代理类AppCompatDelegate实现xml布局到View视图的转换 

4.3.AppCompatActivity定义实现不同版本AppCompatDelegate实现类

4.4.setContentView()方法最终调用代理类AppCompatDelegateImplV7中的方法

4.5.在LayoutInflater中实现基于XmlPullParser解析xml布局文件

4.6.xml解析调用createViewFromTag()通过控件名称和属性集合创建视图

4.7.createViewFromTag()真正执行解析xml布局文件以后创建View视图

4.8.mFactory2来源(调试发现最终调用的是AppCompatActivity代理类的onCreateView()方法)

4.9.AppCompatViewInflater的createView()方法真正创建视图(AppCompatDelegateImplV9.onCreateView()调用AppCompatViewInflater.createView())

5.Resources/AssetManager

6.实现关键类SkinFactory,SkinEngine

6.1SkinFactory实现Factory2接口,完成视图的创建,视图的缓存,全部视图的换肤操作;

6.2SkinEngine加载插件apk的对应的Resources,(Resources用插件apk的资源)实现更换皮肤的各种方法(例如:getColor等)

6.3实例使用

6.4主app和插件app设置需要换肤资源用相同的名称

7.显示效果

8.注意事项


实现换肤的方案:

a.静态修改theme主题方式

设置多套皮肤的theme;

styles.xml
<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="defaultcolor">@color/defalultcolor</item>                                      
    </style>

    <style name="NewTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
        <item name="defaultcolor">@color/newcolor</item>
    </style>
</resources>

声明属性需要动态替换的样式属性

attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="defaultcolor" format="reference|color" />
</resources>

为控件设置属性样式?attr/defaultcolor

<TextView
        android:layout_marginTop="20dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="这是第一个fragment"
        android:textColor="?attr/defaultcolor"
        android:textSize="36dp" />

动态设置主题

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if( 1 ==  1){
            setTheme(R.style.AppTheme);
        }else {
            setTheme(R.style.NewTheme);
        }
        setContentView(R.layout.activity_main);
}

缺点是设置主题在setContentView之前,设置以后需要重启Activity;

b.动态修改样式(应用内/插件化-皮肤apk),不需要重启Activity

应用内

采用通过相同名称+不同皮肤后缀来区分不同皮肤,如实现黑白皮肤,文本颜色item_text_color有一套默认皮肤,一套黑色皮肤定义资源item_text_color,item_text_color_black;

String resName = mOutResource.getResourceEntryName(resId);
        int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);

resName+"_"+不同皮肤后缀

缺点:应用内多套皮肤时可能导致安装包过大;

插件化-皮肤apk

皮肤apk和主apk有相同皮肤资源名称,获取皮肤apk下的资源名称,需要和主apk下资源名称一致,设置在皮肤apk下皮肤资源 ;

String resName = mOutResource.getResourceEntryName(resId);
        int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);

mOutResource皮肤apk资源Resources;

动态修改视图皮肤需要解决两个问题:

1).缓存获取全部需要换肤的视图,换肤(资源)时执行换肤操作;

2).获取Resources资源(插件apk资源Resources),动态设置皮肤;

1.什么是一键换肤

所谓”一键换肤“就是通过一个接口调用,实现app范围内所有资源文件替换,包括文本,颜色,图片,动画等;

2.界面上那些东西可以换肤 

例如TextView文字颜色,字体大小,ImageView的background等等;

res目录所有的资源几乎都可以替换,具体如下:

动画
背景图片
字体
字体颜色
字体大小
音频
视频

3.利用Hook实现一键换肤

什么是hook
如题,我是用hook实现一键换肤。那么什么是hook?
hook,钩子. 安卓中的hook技术,其实是一个抽象概念:对系统源码的代码逻辑进行"劫持",插入自己的逻辑,然后放行。注意:hook可能频繁使用java反射机制···

"一键换肤"中的hook思路

  1. "劫持"系统创建View的过程,我们自己来创建View
    系统原本自己存在创建View的逻辑,我们要了解这部分代码,以便为我所用.
  2. 收集我们需要换肤的View(用自定义view属性来标记一个view是否支持一键换肤),保存到变量中
    劫持了 系统创建view的逻辑之后,我们要把支持换肤的这些view保存起来
  3. 加载外部资源包,调用接口进行换肤
    外部资源包,是.apk后缀的一个文件,是通过gradle打包形成的。里面包含需要换肤的资源文件,但是必须保证,要换的资源文件,和原工程里面的文件名完全相同.

4.Android创建视图源码分析

自己定义Activity继承自兼容AppCompatActivity

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

创建Activity经常在onCreate()方法中调用setContentView(R.layout.xxx);设置要显示视图,那么如何将我们创建的xml布局文件转换为要显示View视图呢?

源码执行流程:

4.1.自定义Activity设置要显示的布局文件xml

setContentView(R.layout.activity_main);

4.2.调用兼容AppCompatActivity代理类AppCompatDelegate实现xml布局到View视图的转换 

由于Android版本比较分散,需要兼容各个版本Android系统,AppCompatActivity定义实现不同版本AppCompatDelegate实现类;

@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

4.3.AppCompatActivity定义实现不同版本AppCompatDelegate实现类

private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        final int sdk = Build.VERSION.SDK_INT;
        if (sdk >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (sdk >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (sdk >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV7(context, window, callback);
        }
    }

4.4.setContentView()方法最终调用代理类AppCompatDelegateImplV7中的方法

通过如下实现将layout的xml布局文件转换为View视图添加到父视图上contentParent;

LayoutInflater.from(mContext).inflate(resId, contentParent);

4.5.在LayoutInflater中实现基于XmlPullParser解析xml布局文件

4.6.xml解析调用createViewFromTag()通过控件名称和属性集合创建视图

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            View result = root;
                //找到跟节点
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }
                //获取跟节点名字
                final String name = parser.getName();
                //创建xml的layout布局文件跟视图
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                //递归调用创建子视图
                rInflateChildren(parser, temp, attrs, true);
                    // 添加到xml布局文件视图到父视图
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
                    //将xml布局的跟视图添加做为父视图
                     if (root == null || !attachToRoot) {
                        result = temp;
                    }
            ...部分源码

            return result;
        }
    }

//创建xml布局文件的跟视图

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

//递归创建xml布局文件的子视图,最后调用createViewFromTag()方法创建视图

rInflateChildren(parser, temp, attrs, true);

createViewFromTag(parent, name, context, attrs);

4.7.createViewFromTag()真正执行解析xml布局文件以后创建View视图

最终调用mFactory2实现视图创建

view = mFactory2.onCreateView(parent, name, context, attrs);

if(view==null)

通过反射创建View(例如自定义View)

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {}

* @param parent:创建视图的父View
* @param name:xml标签使用的视图名称(例如:<TextView/>)
* @param context:上下文
* @param attrs:需要创建的xml标签的属性集;(例如:android:textColor="")
* @param ignoreThemeAttr:true忽略{android:theme}属性为被解析的视图,false相反;

4.8.mFactory2来源(调试发现最终调用的是AppCompatActivity代理类的onCreateView()方法)

AppCompatActivity设置LayoutInflater工厂

 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
    }

代理类AppCompatDelegateImplV7实现installViewFactory()方法

AppCompatDelegate(Activity代理类)
@Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory(layoutInflater, this);
        } else {
            if (!(LayoutInflaterCompat.getFactory(layoutInflater)
                    instanceof AppCompatDelegateImplV7)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }
LayoutInflaterCompat是LayoutInflater兼容类
    //LayoutInflater不同版本的实现
    static final LayoutInflaterCompatImpl IMPL;
    static {
        final int version = Build.VERSION.SDK_INT;
        if (version >= 21) {
            IMPL = new LayoutInflaterCompatImplV21();
        } else if (version >= 11) {
            IMPL = new LayoutInflaterCompatImplV11();
        } else {
            IMPL = new LayoutInflaterCompatImplBase();
        }
    }

public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
        IMPL.setFactory(inflater, factory);
    }

static class LayoutInflaterCompatImplV11 extends LayoutInflaterCompatImplBase {
        @Override
        public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory) {
            LayoutInflaterCompatHC.setFactory(layoutInflater, factory);
        }
    }

FactoryWrapperHC实现了Factory2接口,包裹LayoutInflaterFactory接口,当调用Factroy2. 
onCreateView方法时最后调用LayoutInflaterFactory(AppCompatDelegateImplV9).onCreateView()方法(class 
AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
        implements MenuBuilder.Callback, LayoutInflaterFactory),
AppCompatDelegateImplV9实现LayoutInflaterFactory接口
static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
        final LayoutInflater.Factory2 factory2 = factory != null
                ? new FactoryWrapperHC(factory) : null;
        //设置包裹LayoutInflaterFactory类做为factory2
        inflater.setFactory2(factory2);

        final LayoutInflater.Factory f = inflater.getFactory();
        if (f instanceof LayoutInflater.Factory2) {
            // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
            // We will now try and force set the merged factory to mFactory2
            forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
        } else {
            // Else, we will force set the original wrapped Factory2
            forceSetFactory2(inflater, factory2);
        }
    }

static class FactoryWrapperHC extends LayoutInflaterCompatBase.FactoryWrapper
            implements LayoutInflater.Factory2 {

        FactoryWrapperHC(LayoutInflaterFactory delegateFactory) {
            super(delegateFactory);
        }

        @Override
        public View onCreateView(View parent, String name, Context context,
                AttributeSet attributeSet) {
            return mDelegateFactory.onCreateView(parent, name, context, attributeSet);
        }
    }

LayoutInfalter设置factory2
   /**
     * Like {@link #setFactory}, but allows you to set a {@link Factory2}
     * interface.
     */
    public void setFactory2(Factory2 factory) {
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }





LayoutInflaterCompat.setFactory(layoutInflater, this);

将代理类AppCompatDelegateImplV7的this设置为mFactory2;

AppCompatDelegateImplV7实现了LayoutInflaterFactory接口,mFactory2调用onCreateView()方法最终调用实现Factory2接口FactoryWrapperHC类,FactoryWrapperHC包裹类LayoutInflaterFactory,最后调用LayoutInflaterFactory(AppCompatDelegateImplV7).onCreateView()方法创建视图;

4.9.AppCompatViewInflater的createView()方法真正创建视图(AppCompatDelegateImplV9.onCreateView()调用AppCompatViewInflater.createView())

 @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        final boolean isPre21 = Build.VERSION.SDK_INT < 21;

        if (mAppCompatViewInflater == null) {
            mAppCompatViewInflater = new AppCompatViewInflater();
        }

        // We only want the View to inherit its context if we're running pre-v21
        final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);

        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
                true, /* Read read app:theme as a fallback at all times for legacy reasons */
                VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
        );
    }

AppCompatViewInflater

public 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 = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            case "EditText":
                view = new AppCompatEditText(context, attrs);
                break;
            case "Spinner":
                view = new AppCompatSpinner(context, attrs);
                break;
            case "ImageButton":
                view = new AppCompatImageButton(context, attrs);
                break;
            case "CheckBox":
                view = new AppCompatCheckBox(context, attrs);
                break;
            case "RadioButton":
                view = new AppCompatRadioButton(context, attrs);
                break;
            case "CheckedTextView":
                view = new AppCompatCheckedTextView(context, attrs);
                break;
            case "AutoCompleteTextView":
                view = new AppCompatAutoCompleteTextView(context, attrs);
                break;
            case "MultiAutoCompleteTextView":
                view = new AppCompatMultiAutoCompleteTextView(context, attrs);
                break;
            case "RatingBar":
                view = new AppCompatRatingBar(context, attrs);
                break;
            case "SeekBar":
                view = new AppCompatSeekBar(context, attrs);
                break;
        }

        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 it's android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

创建Android的原生控件都是以AppCompat开头的兼容类例如:AppCompatSeekBar;

整个流程分析完成,我们发现最终调用mFactory2.onCreateView()和反射的方式创建视图;

就可以实现Factory2接口 ,给LayoutInflater重新设置实现自己逻辑Factory2(创建View视图,缓存视图和视图属性便于动态调用修改视图属性-文字颜色,字体大小,背景等)

5.Resources/AssetManager

调用AssetManager.addAssetPath("插件apk路径")指定要加载的皮肤apk,创建执行插件apk的Resources以便获取资源(图片,颜色,尺寸等);

需要动态设置皮肤资源就需要获取相关资源类(颜色,字体,背景等等),AssetManager.addAssetPath(String path)可以实现调用指定资源包;

Android中与资源相关的类主要有Resources、ResourcesImpl、ResourcesManager、Java层的AssetManager和Native层的AssetManager;

Resources:提供了大多数与应用开发直接相关的加载资源的方法,比如getColor(int resId)等。实现上是通过AssetManager来加载资源并进行解析。

AssetManager:Java层的AssetManager是对Native层AssetManager的封装,为上层提供了加载资源的方法

加载资源包:一般而言,一个App至少会引用两个资源包:系统资源包和App的资源包。一个AssetManager对象可加载多个资源包。App在创建AssetManager的时候,会先加载系统资源包,再加载App资源包。这样,通过一个AssetManager对象就可以访问系统资源包和App资源包的资源了。

AssetManager.addAssetPath(String path)是一个@hide方法,调用这个方法可以加载指定的资源包。

访问资源:由于资源id包含了package id的信息,AssetManager通过解析资源id,即可知道从哪个资源包来加载资源。

 

6.实现关键类SkinFactory,SkinEngine

6.1SkinFactory实现Factory2接口,完成视图的创建,视图的缓存,全部视图的换肤操作;

/**
 * 自定义Factory2,目的是拿到需要换肤的View和View样式
 */
public class SkinFactory implements LayoutInflater.Factory2 {

    //预定义一个委托类,他负责按照系统原有逻辑来创建View
    private AppCompatDelegate mDelegate;
    //缓存要换肤的View
    private List<SkinView> lisCacheSkinViews = new ArrayList<>();

    /**
     * 外部提供一个Set方法
     * @param mDelegate
     */
    public void setDelegate(AppCompatDelegate mDelegate) {
        this.mDelegate = mDelegate;
    }

    /**
     *
     * @param parent
     * @param name
     * @param context
     * @param attrs
     * @return
     */
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        //关键点1:执行系统代码里的创建View的过程,我们只是想加入自己的思想,并不是全盘接管
        //系统创建出来的时候有可能为空,你问为啥?请全文搜索 “标记标记,因为” 你会找到你要的答案
        View view  = mDelegate.createView(parent, name, context, attrs);
        if(view == null){//万一系统创建出来是空,那我们来补救
            if(-1 == name.indexOf('.')){ //不包含,说明不带包名,那么我门帮他加上包名
                view = createViewByPrefix(context, name, prefixs, attrs);
            }else {
                view = createViewByPrefix(context, name, null, attrs);
            }
        }

        //关键点2:收集需要换肤的View
        collectSkinView(context, attrs, view);

        return view;
    }

    /**
     * 收集换肤的控件
     * 收集的方式是:通过自定义isSupport,从创建出来的很多View中,找到支持换肤的那些,保存到map中
     * @param context
     * @param attrs
     * @param view
     */
    private void collectSkinView(Context context, AttributeSet attrs, View view){
        //获取我们自己定义的属性
        TypedArray  typedArray = context.obtainStyledAttributes(attrs, R.styleable.Skinable);
        boolean isSupport = typedArray.getBoolean(R.styleable.Skinable_isSupport, false);
        if(isSupport){//找到支持换肤的View
            final int Len = attrs.getAttributeCount();
            HashMap<String,  String>  attrMap = new HashMap<>();
            for(int i=0; i< Len; i++){//遍历所有属性
                String attrName = attrs.getAttributeName(i);
                String attrValue = attrs.getAttributeValue(i);
                attrMap.put(attrName, attrValue);//全部存起来
            }

            SkinView skinView = new SkinView();
            skinView.view = view;
            skinView.attrsMap = attrMap;
            //将可换肤的View放到listCacheSkinView中
            lisCacheSkinViews.add(skinView);
        }
    }

    /**
     * 公开对外的换肤接口
     */
    public void changeSkin(){
        for(SkinView skinView  : lisCacheSkinViews){
            skinView.changeSkin();
        }
    }

    /**
     * Factory2是继承Factory的,所以这次主要重写Factory2的onCreatteView逻辑,就不必理会Factory的重写方法
     * @param name
     * @param context
     * @param attrs
     * @return
     */
    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }

    /**
     * 所谓hook,要懂源码,懂了之后再劫持系统逻辑,加入自己的逻辑。
     * 那么,既然懂了,系统的有些代码,直接拿过来用,也无可厚非。
     */
    //*******************************下面一大片,都是从源码里面抄过来的,并不是我自主设计******************************
    // 你问我抄的哪里的?到 AppCompatViewInflater类源码里面去搜索:view = createViewFromTag(context, name, attrs);
    static final Class<?>[] mConstructorSignature = new Class[]{Context.class, AttributeSet.class};//
    final Object[] mConstructorArgs = new Object[2];//View的构造函数的2个"实"参对象
    private static final HashMap<String, Constructor<? extends View>> sConstructorMap = new HashMap<String, Constructor<? extends View>>();//用映射,将View的反射构造函数都存起来
    static final String[] prefixs = new String[]{//安卓里面控件的包名,就这么3种,这个变量是为了下面代码里,反射创建类的class而预备的
            "android.widget.",
            "android.view.",
            "android.webkit."
    };

    private final View createViewByPrefix(Context context, String name, String[] prefixs, AttributeSet attrs){
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        Class<? extends View> clazz = null;

        if(constructor == null){
            try {
                if(prefixs != null && prefixs.length > 0){
                    for(String prefix : prefixs){
                        clazz = context.getClassLoader().loadClass(prefix != null ? (prefix+name) : name).asSubclass(View.class);
                        if(clazz != null) break;
                    }
                }else {
                    if(clazz  == null){
                        clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
                    }
                }

                if(clazz == null){
                    return null;
                }
                //拿到构造方法
                constructor = clazz.getConstructor(mConstructorSignature);

            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                return null;
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
                return null;
            }
            constructor.setAccessible(true);
            //然后缓存起来,下次再用,就直接从内存中去取
            sConstructorMap.put(name, constructor);
        }
       Object[] args = mConstructorArgs;
        try {
            args[0] = context;
            args[1] = attrs;
            //通过反射创建View对象
            //执行构造函数,拿到View对下
            final View view = constructor.newInstance(args);
            return view;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }finally {
            args[0] = null;
            args[1] = null;
        }
        return null;
    }

    static class  SkinView{
        View view;
        HashMap<String, String>  attrsMap;

        public void changeSkin(){
            if (!TextUtils.isEmpty(attrsMap.get("background"))) {//属性名,例如,这个background,text,textColor....
                int bgId = Integer.parseInt(attrsMap.get("background").substring(1));//属性值,R.id.XXX ,int类型,
                // 这个值,在app的一次运行中,不会发生变化
                String attrType = view.getResources().getResourceTypeName(bgId); // 属性类别:比如 drawable ,color
                if (TextUtils.equals(attrType, "drawable")) {//区分drawable和color
                    view.setBackgroundDrawable(SkinEngine.getInstance().getDrawable(bgId));//加载外部资源管理器,拿到外部资源的drawable
                } else if (TextUtils.equals(attrType, "color")) {
                    view.setBackgroundColor(SkinEngine.getInstance().getColor(bgId));
                }
            }

            if (view instanceof TextView) {
                if (!TextUtils.isEmpty(attrsMap.get("textColor"))) {
                    int textColorId = Integer.parseInt(attrsMap.get("textColor").substring(1));
                    ((TextView) view).setTextColor(SkinEngine.getInstance().getColor(textColorId));
                }

                if (!TextUtils.isEmpty(attrsMap.get("textSize"))) {
                    int textSizeId = Integer.valueOf(attrsMap.get("textSize").substring(1));
                    ((TextView) view).setTextSize(SkinEngine.getInstance().getTextSize(textSizeId));
                }
            }



//            //那么如果是自定义组件呢
//            if (view instanceof ZeroView) {
//                //那么这样一个对象,要换肤,就要写针对性的方法了,每一个控件需要用什么样的方式去换,尤其是那种,自定义的属性,怎么去set,
//                // 这就对开发人员要求比较高了,而且这个换肤接口还要暴露给 自定义View的开发人员,他们去定义
//                // ....
//            }
        }

    }








}

6.2SkinEngine加载插件apk的对应的Resources,(Resources用插件apk的资源)实现更换皮肤的各种方法(例如:getColor等)

/**
 *  单例
 */

public class SkinEngine {
    private final static SkinEngine instance = new SkinEngine();

    public static SkinEngine getInstance(){
        return instance;
    }

    public void init(Context context){
        mContext = context.getApplicationContext();
        //使用application的目的是,如果万一传进来的是Activity对象
        //那么它被静态对象instance所持有,这个Activity就无法释放了
    }

    private Resources mOutResource; //资源管理器
    private Context mContext;  //上下文
    private String mOutPkgName; //外部资源包packageName

    //加载外部资源包
    public void load(final String path){    //path  是外部传入的apk文件名
        File file = new File(path);
        if(!file.exists()){
            return;
        }
        //取得PackageManager引用
        PackageManager mPm = mContext.getPackageManager();
        //“检索在包归档文件中定义的应用程序包的总体信息”,说人话,外界传入了一个apk的文件路径,这个方法,拿到这个apk的包信息,这个包信息包含什么?
        PackageInfo mInfo = mPm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
        mOutPkgName = mInfo.packageName; //先把包名存起来
        AssetManager assetManager;//资源管理器
        //关键技术点3 通过反射获取AssetManager 用来加载外面的资源包
        try {
            //反射创建AssetManager对象,为何要反射?使用反射,是因为他这个类内部的addAssetPath方法是hide状态
            assetManager = AssetManager.class.newInstance();
            //addAssetPath方法可以加载外部的资源包
            //为什么要反射执行这个方法?因为它是hide的,不直接对外开放,只能反射调用
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, path); //反射执行方法
            mOutResource = new Resources(assetManager,  //资源管理器
                    mContext.getResources().getDisplayMetrics(),    //屏幕参数
                    mContext.getResources().getConfiguration());    //资源配置
            //最终创建出一个 "外部资源包"mOutResource ,它的存在,就是要让我们的app有能力加载外部的资源文件
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    /**
     * 提供外部资源包里面的颜色
     * @param resId
     * @return
     */
    public int getColor(int resId) {
        if (mOutResource == null) {
            return resId;
        }
        String resName = mContext.getResources().getResourceEntryName(resId);
        int outResId = mOutResource.getIdentifier(resName, "color", mOutPkgName);
        if (outResId == 0) {
            return resId;
        }
        return mOutResource.getColor(outResId);
    }

    /**
     * 提供外部资源包里的图片资源
     * @param resId
     * @return
     */
    public Drawable getDrawable(int resId) {//获取图片
        if (mOutResource == null) {
            return ContextCompat.getDrawable(mContext, resId);
        }
        String resName = mContext.getResources().getResourceEntryName(resId);
        int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);
        if (outResId == 0) {
            return ContextCompat.getDrawable(mContext, resId);
        }
        return mOutResource.getDrawable(outResId);
    }

    /**
     * 提供外部资源包里的字体大小
     * @param resId
     * @return
     */
    public float getTextSize(int resId) {//获取字体大小
        if (mOutResource == null) {
            return mContext.getResources().getDimension(resId);
        }
        String resName = mContext.getResources().getResourceEntryName(resId);
        int outResId = mOutResource.getIdentifier(resName, "textSize", mOutPkgName);
        if (outResId == 0) {
            return mContext.getResources().getDimension(resId);
        }
        return mOutResource.getDimension(outResId);
    }

    //..... 这里还可以提供外部资源包里的String,font等等等,只不过要手动写代码来实现getXX方法









}

注意事项:

通过资源id找到资源名称,mContext是当前主app的上下文;

String resName = mContext.getResources().getResourceEntryName(resId);

mOutResource指的是插件apk的Resources获取和主app相同名称的资源替换显示;
int outResId = mOutResource.getIdentifier(resName, "textSize", mOutPkgName);

6.3实例使用

初始化"换肤引擎"指定当前上下文

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //初始化换肤引擎
        SkinEngine.getInstance().init(this);
    }
}

在基类Activity为LayoutInflator注入Facotry2类,其他Activity实现基类;

/**
 * Activity积类
 */

public class BaseActivity extends AppCompatActivity {

    private boolean ifAllowChangeSkin=true;
    private SkinFactory mSkinFactory;
    private String mCurrentSkin;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        // TODO: 关键点1:hook(劫持)系统创建view的过程
        if (ifAllowChangeSkin) {
            mSkinFactory = new SkinFactory();
            mSkinFactory.setDelegate(getDelegate());
            LayoutInflater layoutInflater = LayoutInflater.from(this);
            layoutInflater.setFactory2(mSkinFactory);//劫持系统源码逻辑
        }
        super.onCreate(savedInstanceState);

    }
}

在设置界面调用换肤操作, changeSkin("apk插件的名字"),具体apk插件存在哪和如何放入存储卡加载插件apk,下面仅仅是插件apk下载完成以后实现的方法;

changeSkin("skinmodule-debug.apk");
protected void changeSkin(String path) {
        if (ifAllowChangeSkin) {
            File skinFile = new File(Environment.getExternalStorageDirectory(), path);
            SkinEngine.getInstance().load(skinFile.getAbsolutePath());//加载外部资源包
            mSkinFactory.changeSkin();//执行换肤操作
            mCurrentSkin = path;
        }
    }

6.4主app和插件app设置需要换肤资源用相同的名称

6.5自定义isSupport属性设置View是否支持换肤

<resources><!--TODO: 关键技术点2 通过自定义属性来标识哪些view支持换肤-->
    <declare-styleable name="Skinable">
        <!--TODO: isSupport=true标识当前控件支持换肤-->
        <attr name="isSupport" format="boolean" />
    </declare-styleable>
</resources>

<TextView
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:text="第一个"
            android:id="@+id/tv_one"
            android:gravity="center"
            android:textSize="@dimen/tabsize"

            app:isSupport="true"

            />

7.显示效果

以下效果实现动态替换字体大小,背景颜色;

已经创建的Activity界面可能需要监听器,监听换肤操作执行换肤,新创建的页面将使用插件apk皮肤资源;

8.注意事项

a.skinmodule创建module只需要管理assets和res下的资源即可,需要替换资源名称要和app一致;

b.皮肤包 skin_plugin module的gradle sdk版本最好和app module的保持完全一致,否则无法保证不会出现奇葩问题;

c.skinmodule生成的具体保存位置自己定义,只需要创建插件Resources时执行插件路径apk即可;

 

 

参考:

https://www.jianshu.com/p/4c8d46f58c4f

https://github.com/18598925736/HookSkinDemoFromHank

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章