上一篇完成了换肤框架的基本搭建,这一次 我们继续补完上一次遗留的一些可以完善的部分
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中保存皮肤文件路径的keypublic 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;// 皮肤文件状态OKpublic 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.skinpublic 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;}// 检查是否是apkString 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的所有换肤的viewpublic 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中添加如下代码
@Overrideprotected 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();// 遍历存储的所有需要换肤的Activityfor (ISkinChangeListener key : keys) {List<SkinView> skinViews = mAllSkinViewsInActivity.get(key);// 更新所有Activity中的viewfor (SkinView skinView : skinViews) {skinView.applySkin();}// 通知Activitykey.changeSkin(mSkinResource);}}
至此 换肤框架的搭建就告一段落了,写的比较乱,读者见谅 完整的代码如下,本节主要重点还是分析内存泄漏
https://github.com/caihuijian/learn_darren_eassy_joke