Android 插件化换肤实现(系列 1、2原理篇、3实现篇)

19年做过华为音乐的插件换肤,这次自己又写了个简单demo例子,就当技术总结。之前分析过换肤的原理1、2,今天来看看怎么实现。之前的原理篇地址:
源码学习《3》Layout.xml 的解析和 xml 标签生成 View 对象的过程(App 换肤原理 1)
源码学习《4》Launcher 启动 app 和 apk 资源的加载流程 (App 换肤原理 2)
在对原理的理解的基础上,开始实现换肤的功能。

                                       

分析:插件化换肤其实就是用我们宿主 app 去加载插件 apk 中的资源文件,简单来说就是我们拿到每个Activity中的需要换肤的View对象,然后对View对象进行set值,这个值肯定是插件 apk 中的资源值,所以我们有一下问题:

  1. 如何拿到一个新的Activity中的需要换肤的所有View对象?
  2. 如何解析插件 apk 中的资源?

这两个问题其实就是我们 app 换肤1、2的学习,当然我们了解了,这两个问题的原理,今天就说说如何通过源码的学习去解决我们的换肤问题。

目录:

  1. 解析插件 apk 中的资源
  2. 拿到换肤的所有 View 对象、并给View设置资源
  3. 架构设计、介绍
  4. 内存优化、检测内存泄漏

1.解析插件 apk 中的资源

解析apk资源首先我们要加载插件apk,创建插件 apk 的 Resources对象。

    public void parserApk(String path) {
        if (null == mContext) {
            return;
        }
        AssetManager assets = null;
        try {
            assets = AssetManager.class.newInstance();
            Method m = AssetManager.class.getMethod("addAssetPath",String.class);
            m.invoke(assets,path);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        mResources = new Resources(assets, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
    }

就是这么简单直接反射创建对象、调用方法。得到Resources对象,等待使用。

2.拿到换肤的所有 View 对象、并给View设置资源

这一步比较关键涉及到 不同 android 版本的兼容问题,我们还是从源码的创建View入手,因为源码的实现对于兼容性是最稳定的。源码学习《3》Layout.xml 的解析和 xml 标签生成 View 对象的过程(App 换肤原理 1) 看完这个源码我们知道,我们要在Activity 的 onCreate 方法调用surper onCreate之前去设置一个 Factory2。

  @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }

这样的话,当LayoutInflater 回调时候回调的就是我们自定义的 Factory2,这样在 onCreateView() 方法内我们就能拿到我们需要换肤的所有 View 对象,保存起来然后换肤。

   @Override
    public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        // 这里模仿源码的创建流程,构造我们的View创建流程
        View view = createView(parent, name, context, attrs);
        if (null == view && -1 != name.indexOf('.')) {
            // 自定义view
            view = createView(context, name, null, attrs);
        }
        boolean enable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
        if (enable) {
            // 需要换肤的view
            SkinViewManager.getInstance().addSkinView(view, attrs, mSkinView);
        }
        return view;
    }

要想做到兼容行最好的方式就是 根据源码创建 View 对象,复制源码的 AppCompatViewInflater 对象到我们自己代码。AppCompatViewInflater 主要是创建 AppCompatTextView、AppCompatImageView、LinearLayout等View对象,但是对于自定义View 和 系统的 com.xxx.xxx.View 却不是走这里,从源码中可知,这部分走的是反射,故也需要复制修改到我们的代码中。
得到View之后 到 SkinViewManager 中封装并且保存View对象,

    private Map<String, SkinView> views = new HashMap<>();

    public void addSkinView(View view, AttributeSet attr, SkinView skinView) {
        if (null != view) {
            // 封装 skin view
            skinView.addSkinView(view,attr);
            views.put(skinView.getId(),skinView);
        }
    }

3.架构设计、介绍

对于架构设计主要是向外面页面暴露 换肤、解析apk、是否换肤的接口。换肤功能实现部分自行管理,不用暴露给外面页面。

主要是在我们得到View之后怎么对View进行换肤、保存。换肤由于我们的换肤的VIew的类型有 TextView、ImageView、ViewGroup (LinearLayout)、自定义View 等。为了让业务分离,使用了 策略 + 责任连 模式进行架构 ,做到接口分离、并且减少代码 if else 的判断。容易阅读,提高代码编辑效率。

4.内存优化、检测内存泄漏

实现换肤之后,做了个内存的检测,出现了内存的泄漏,通过简单的内存检测命令配合 AS 的 profile 进行跟踪发现异常,具体不讲解使用方式对于内存检查不熟悉的看之前的博客:Android studio + MAT内存分析优化 一 

adb shell dumpsys meminfo -d xxx

gc之后发现了 4 个Activity ,说明内存有泄漏,然后在通过as 的 profile 检测

发现是MainActivity泄漏,继续跟踪发现是我们保存到SkinViewManager中HashMap保存 的View 持有了Context 对象导致 Activity 不能销毁。也就是当我们保存了 每个页面的 换肤View 到集合中后,由于HashMap 被SkinViewManager 持有,而且SkinVIewManager 是个单例,HashMap 就不能被释放,HashMap 又持有 VIew ,View 又持有Context,所以最终导致 Activity 泄漏,不能被回收。泄漏顺序:
 SkinVIewManager --> HashMap --> View --> Context --> Activity
因此要解决这个问题,当我们 activity 退出之后,需要把 SkinViewManager 中保存的当前的activity 的View remove 掉。让当前的View 脱离Gc root 树,当再次内存GC 时候就被回收了。就是把上面链接从View断开
SkinVIewManager --> HashMap --断开> View --> Context --> Activity   
解决代码:

 private Map<String, SkinView> views = new HashMap<>();

@Override
    protected void onDestroy() {
        super.onDestroy();
        SkinViewManager.getInstance().clearViews(this.toString());
    }

  public void clearViews(String id){
        // id 是activity 的id
        views.remove(id);
    }

再次检测:

未发现内存泄漏的现象。
由于篇幅过长问题,没能把每一部分都详细的介绍到位,自己动手实现,印象会非常深刻。遇到问题、解决问题、思考问题、没部分知识虽然小,感觉很简单但是一做就出先bug,因为源码的能量、超乎想像。

 

王项雨 CSDN认证博客专家 FuckCode
Fucking source code
已标记关键词 清除标记
相关推荐
目前这方面的软件很多,但大部分都是收费的,不收费大部分又换的不全,对于一个学生来说花钱买是有些奢侈了,所以我一直就想做一个换肤软件提供给学生,让他们做课程设计或毕业设计时能轻易给自己软件美界面。 但是一直苦于时间有限。工作太忙有时只能在周末或晚上写上两行代码。现在终于成形了本打算开源,但是有些地方还不完善(现只支持VC MFC, Windows Type: Dialog, SDI),所以现在只讲下原理,提供部分源码供感兴趣的人研究。现在发出来与大家共享。 现在商业的换肤软件大部分都是采用的Hook技术(呵呵,猜的,也许采用的更高深的技术)。Hook窗体消息,对窗体消息进行截获最终换成自已的处理方式。所以本人写的SkinMaster也是采用了同样的技术原理。说很简单但做起来有些困难。下面是我做Skin时遇到的问题及处理方式。 1.对于Windows基本控件进行Hook则可完成绘制。 2.对于菜单会制则有些麻烦,程序运行时窗体菜单WM_MEASUREITEM只运行一次,所以会出现在动态换另一套皮肤时菜单项大小不会跟据皮肤改变,解决方法是所有菜单你要动态生成。 3.主窗体的绘制,没啥太深技术就是要处理大量的消息。 4.滚动条的绘制,滚动条全靠Hook消息就没办法完成了,这个东西微软做的不像基本控件那样工作,还要对滚动条的API进行Hook。 先写这些,有时间我会把更详细的方法给大家写出来。下面程序中TestSkin程序提供源码,并完成了按钮等控件的换肤
©️2020 CSDN 皮肤主题: 黑客帝国 设计师:白松林 返回首页