红橙Darren视频笔记 换肤框架4 换肤的功能完善 内存泄漏分析

上一篇完成了换肤框架的基本搭建,这一次 我们继续补完上一次遗留的一些可以完善的部分

1.完善换肤

1.1退出后再进入应用 不会丢失上一次保存的皮肤
基本原理:将上一次切换的皮肤path保存在SharedPreference中,下一次进入应用读取该数值
同时在BaseSkinActivity创建view时应该加载一下皮肤

            public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                //在这里拦截view创建
                //可以在这里进行换肤
                Log.e(TAG, "onCreateView: 拦截了view " + name);
                // 换肤框架从这里开始搭建
                // 1.创建View 目的是替换原先的view的一些属性
                // createView走的代码流程和AppCompatDelegateImpl createView源码没有二致
                // 即先走源码的流程 让源码帮我们创建好view 我们再检查这些view中的属性 看是否需要换肤
                View view = createView(parent, name, context, attrs);

                // 在拦截后与返回前进行所有可以进行换肤view的存储
                // 2.解析属性  src  textColor  background textHintColor TODO 自定义属性先不考虑
                if (view != null) {
                    // 获取当前activity中一个view中所有换肤的属性
                    List<SkinAttr> skinAttrs = SkinAttrSupport.getSkinAttrs(context, attrs);
                    // 创建skinView  skinView中可能包含多个需要换肤的属性
                    SkinView skinView = new SkinView(view, skinAttrs);
                    // 3.交给SkinManager统一存储管理
                    managerSkinView(skinView);
                    // 4.换肤 !!!!!!多了这一步!!!!!!!
                    skinView.applySkin();
                }

                return view;
            }

1.2.换肤时如果已经换肤 不再调用换肤的一系列接口 避免无效调用
1.3.切换皮肤前判断是否是有效切换
比如 当前如果已经是那个皮肤 则不应该调用任何切换的代码
比如 切换皮肤前应该判断皮肤包是否存在 是否格式正确(可以获取包名)
上面提到的几点 实现如下

/**
 * Created by hjcai on 2021/4/21.
 */
public class SkinConfig {
    // SharedPreferences xml文件的文件名
    public static final String SHARED_PREFERENCE_FILE_NAME_SKIN = "skinInfo";
    // SharedPreferences中保存皮肤文件路径的key
    public static final String SKIN_PATH_NAME_KEY = "skinPath";

    // 不需要改变任何东西
    public static final int SKIN_CHANGE_NOTHING = -1;

    // 换肤成功
    public static final int SKIN_CHANGE_SUCCESS = 1;

    // 皮肤文件不存在
    public static final int SKIN_FILE_NO_EXIST = -2;

    // 皮肤文件有错误可能不是一个apk文件
    public static final int SKIN_FILE_ERROR = -3;

    // 皮肤文件状态OK
    public static final int SKIN_FILE_OK = 2;

}
/**
 * Created by hjcai on 2021/4/19.
 */
public class SkinUtil {
    private static SkinUtil mInstance;
    private final WeakReference<Context> contextWeakRef;

    private SkinUtil(Context context) {
        contextWeakRef = new WeakReference<>(context.getApplicationContext());
    }

    public static SkinUtil getInstance(Context context) {
        if (mInstance == null) {
            synchronized (SkinUtil.class) {
                if (mInstance == null) {
                    mInstance = new SkinUtil(context);
                }
            }
        }
        return mInstance;
    }

    // return /storage/emulated/0/light.skin
    public String getLightSkinPath() {
        return Environment.getExternalStorageDirectory().getAbsolutePath()
                + File.separator + "light.skin";
    }

    // 保存当前皮肤路径
    public void saveSkinPath(String skinPath) {
        contextWeakRef.get().getSharedPreferences(SkinConfig.SHARED_PREFERENCE_FILE_NAME_SKIN, Context.MODE_PRIVATE)
                .edit().putString(SkinConfig.SKIN_PATH_NAME_KEY, skinPath).apply();
    }

    // 清空皮肤路径
    public void clearSkinInfo() {
        saveSkinPath("");
    }

    public String getSkinPathFromSP() {
        return contextWeakRef.get().getSharedPreferences(SkinConfig.SHARED_PREFERENCE_FILE_NAME_SKIN, Context.MODE_PRIVATE)
                .getString(SkinConfig.SKIN_PATH_NAME_KEY, "");
    }

    // 检查皮肤有效性
    public int checkSkinFileByPath(String skinPath) {
        File file = new File(skinPath);

        if (!file.exists()) {
            // 不存在,清空皮肤
            SkinUtil.getInstance(contextWeakRef.get()).clearSkinInfo();
            return SkinConfig.SKIN_FILE_NO_EXIST;
        }

        // 检查是否是apk
        String packageName = Objects.requireNonNull(contextWeakRef.get().getPackageManager().getPackageArchiveInfo(
                skinPath, PackageManager.GET_ACTIVITIES)).packageName;

        if (TextUtils.isEmpty(packageName)) {
            SkinUtil.getInstance(contextWeakRef.get()).clearSkinInfo();
            return SkinConfig.SKIN_FILE_ERROR;
        }
        return SkinConfig.SKIN_FILE_OK;
    }
}

上述工具类提供检查皮肤有效性 以及 保存皮肤路径到SharePreference以及从SP读取皮肤路径的方法

public class SkinManager {
...
    public void init(Context context) {
        mContextWeakRef = new WeakReference<>(context);

        // 每一次打开应用都会到这里来,防止皮肤被任意删除 检查皮肤有效性
        String currentSkinPath = SkinUtil.getInstance(context).getSkinPathFromSP();
        int checkRes = SkinUtil.getInstance(context).checkSkinFileByPath(currentSkinPath);
        if (checkRes != SkinConfig.SKIN_FILE_OK && checkRes != SkinConfig.SKIN_FILE_ERROR) {
            return;
        }

        // 最好校验签名  增量更新再说
        // 做一些初始化的工作
        mSkinResource = new SkinResource(context, currentSkinPath);
    }
    
    public int loadSkin(String skinPath) {
        // 1.加载皮肤前检查有效性
        if (SkinUtil.getInstance(mContextWeakRef.get()).checkSkinFileByPath(skinPath) != SkinConfig.SKIN_FILE_OK) {
            return SkinUtil.getInstance(mContextWeakRef.get()).checkSkinFileByPath(skinPath);
        }

        // 2.当前皮肤如果一样不要换
        String currentSkinPath = SkinUtil.getInstance(mContextWeakRef.get()).getSkinPathFromSP();
        if (skinPath.equals(currentSkinPath)) {
            return SkinConfig.SKIN_CHANGE_NOTHING;
        }

        // 3.初始化资源管理并换肤
        mSkinResource = new SkinResource(mContextWeakRef.get(), skinPath);
        changeSkin();
        // 4.保存皮肤的状态
        saveSkinStatus(skinPath);

        return SkinConfig.SKIN_CHANGE_SUCCESS;
    }
    
    private void saveSkinStatus(String skinPath) {
        SkinUtil.getInstance(mContextWeakRef.get()).saveSkinPath(skinPath);
    }
    
    // 恢复默认皮肤
    public int restoreDefault() {
        // 判断当前SP如果没有存储皮肤 什么都不做
        String currentSkinPath = SkinUtil.getInstance(mContextWeakRef.get()).getSkinPathFromSP();

        if (TextUtils.isEmpty(currentSkinPath)) {
            return SkinConfig.SKIN_CHANGE_NOTHING;
        }

        // 当前手机运行的app的路径apk路径
        String skinPath = mContextWeakRef.get().getPackageResourcePath();
        // 初始化资源管理
        mSkinResource = new SkinResource(mContextWeakRef.get(), skinPath);

        // 改变皮肤
        changeSkin();

        // 把皮肤信息清空
        SkinUtil.getInstance(mContextWeakRef.get()).clearSkinInfo();
        return SkinConfig.SKIN_CHANGE_SUCCESS;
    }    
...    
}

2.内存泄漏的分析与完善

2.1Android Studio 4.1.1的内存分析工具使用

点击Android studio左下角的Profiler出现如图的效果
在这里插入图片描述
点击+ 选择设备 选择想查看的app
在这里插入图片描述
完了之后如下
在这里插入图片描述
点击memory部分 如下
在这里插入图片描述
在这里插入图片描述
我们点击导出堆栈 发现左边多出一个堆栈结果
在这里插入图片描述
例如我输入
baseSkin
在这里插入图片描述
如图只有一个BaseSkinActivity实例 说明没有内存泄漏

其实有更简便的方式看是否内存泄漏
在这里插入图片描述
如图 即选中show activity/fragments leaks, Android studio 则会智能的过滤出内存泄漏的activity/fragment
**注意:**我们分析内存泄漏时 假设从activity A->B->C->D
我们分析时要退回到A 再GC 然后还要等一段时间 因为GC不是即时的,等一段时间如果发现BCD的实例仍然没有释放,则说明存在内存泄漏

2.2 具体分析项目中的内存泄漏

    // 缓存当前activity的所有换肤的view
    public void cache(ISkinChangeListener skinChangeListener, List<SkinView> skinViews) {
        mAllSkinViewsInActivity.put(skinChangeListener, skinViews);
    }

之前我们的代码只在activity创建的时候缓存了Activity的实例 却没有提供方法将其remove 自然会造成内存泄漏
比如我们在切换皮肤界面写一个button自己跳转自己的activity ,退出到最开始的界面 然后调用GC 发现不管多久 内存中始终存在多个BaseSkinActivity对象 勾选show activity/fragments leaks后就会发现BaseSkinActivity已经导致泄漏了
因此我们需要提供unCache方法 并在activity销毁时调用

    public void unCache(ISkinChangeListener skinChangeListener) {
        mAllSkinViewsInActivity.remove(skinChangeListener);
    }

然后再BaseSkinActivity中添加如下代码

    @Override
    protected void onDestroy() {// 避免内存泄漏
        super.onDestroy();
        SkinManager.getInstance().unCache(this);
    }

3.自定义view 如何切换皮肤

如今 我们的换肤仅仅适用于Android原生的view 如果是自定义的view换肤则可以通过换肤的时候,由SkinManager通知给Activity 各个Activity在各自的回调中处理自定义view的换肤
原理也很简单 即,让Activity实现接口

public interface ISkinChangeListener {
    void changeSkin(SkinResource skinResource);
}

而SkinManager则存储了这些接口(实际是Activity对象)

private final Map<ISkinChangeListener, List<SkinView>> mAllSkinViewsInActivity = new HashMap<>();

最后在SkinManager中发生皮肤切换的时候 通知各Activity

    private void changeSkin() {
        Set<ISkinChangeListener> keys = mAllSkinViewsInActivity.keySet();
        // 遍历存储的所有需要换肤的Activity
        for (ISkinChangeListener key : keys) {
            List<SkinView> skinViews = mAllSkinViewsInActivity.get(key);
            // 更新所有Activity中的view
            for (SkinView skinView : skinViews) {
                skinView.applySkin();
            }
            // 通知Activity
            key.changeSkin(mSkinResource);
        }
    }

至此 换肤框架的搭建就告一段落了,写的比较乱,读者见谅 完整的代码如下,本节主要重点还是分析内存泄漏
https://github.com/caihuijian/learn_darren_eassy_joke

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值