上一篇完成了换肤框架的基本搭建,这一次 我们继续补完上一次遗留的一些可以完善的部分
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