IconCache类简介
分析IconCache源码之前,先了解下这个类在launcher中扮演了什么样的角色?从字面可以看出,它是一个缓存类,缓存了跟Icon相关的信息,实际它主要缓存了应用的桌面图标bitmap和label,除了memory缓存,还持久化到了数据库。
下面我们就具体看下源码实现。
Icon资源获取
每个应用Icon的图标资源是怎么获取到的?声明下,这里我们聊的是原生aosp launcher的实现,不是定制ROM中的主题功能,它们实现是不一样的。
原生launcher的图标资源是从对应应用中获取来的,就是原apk中的资源,看一个有代表性的实现:
public Drawable getFullResIcon(String packageName, int iconId) {
Resources resources;
try {
// 根据包名获取该应用的Resources对象
resources = mPackageManager.getResourcesForApplication(packageName);
} catch (PackageManager.NameNotFoundException e) {
resources = null;
}
if (resources != null) {
if (iconId != 0) {
// 根据icon资源id获取图标Drawable
return getFullResIcon(resources, iconId);
}
}
return getFullResDefaultActivityIcon();
}
private Drawable getFullResIcon(Resources resources, int iconId) {
Drawable d;
try {
// 根据icon资源id获取图标Drawable,这里还带了像素密度参数
d = resources.getDrawableForDensity(iconId, mIconDpi);
} catch (Resources.NotFoundException e) {
d = null;
}
return (d != null) ? d : getFullResDefaultActivityIcon();
}
从上面代码可以看出,如果存在Resources和iconId,通过上面的方法就可以获取到图标。如果没有iconId或者通过以上方法没有获取到图标呢?我们会退而求其次,用应用的图标而非入口组件本身的图标代替。
看下面这个方法是怎么只通过包名,加载图标的:
private CacheEntry getEntryForPackageLocked(String packageName, UserHandle user,
boolean useLowResIcon) {
Preconditions.assertWorkerThread();
ComponentKey cacheKey = getPackageKey(packageName, user);
CacheEntry entry = mCache.get(cacheKey);
if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
entry = new CacheEntry();
boolean entryUpdated = true;
// Check the DB first.
if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
try {
int flags = Process.myUserHandle().equals(user) ? 0 :
PackageManager.GET_UNINSTALLED_PACKAGES;
// 根据包名获取到PackageInfo对象
PackageInfo info = mPackageManager.getPackageInfo(packageName, flags);
// 得到ApplicationInfo
ApplicationInfo appInfo = info.applicationInfo;
......
LauncherIcons li = LauncherIcons.obtain(mContext);
BitmapInfo iconInfo = li.createBadgedIconBitmap(
//这里通过ApplicationInfo.loadIcon加载应用图标
appInfo.loadIcon(mPackageManager), user, appInfo.targetSdkVersion,
mInstantAppResolver.isInstantApp(appInfo));
li.recycle();
......
} catch (NameNotFoundException e) {
if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
entryUpdated = false;
}
}
......
}
return entry;
}
这里是通过PackageManager拿到对应包名的ApplicationInfo,进而加载Application定义的图标。
memory缓存
内存缓存到底是个什么样的存在,来看下它的庐山真面目。
private final HashMap<ComponentKey, CacheEntry> mCache =
new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
mCache是IconCache中的一个HashMap类型的私有变量,通过键值对的形式存储每个应用图标的信息。
了解下CacheEntry这个内部类:
public static class CacheEntry extends BitmapInfo {
public CharSequence title = "";
public CharSequence contentDescription = "";
public boolean isLowResIcon;
}
public class BitmapInfo {
public Bitmap icon;
public int color;
}
它继承了BitmapInfo,BitmapInfo定义了icon变量,用于存储Bitmap引用。
持久化
出于性能和用户体验的考虑,除了做了内存缓存,Icon也被持久化到了数据库,主要实现类是IconDB。
@Thunk final IconDB mIconDb;
mIconDb也作为实例变量被IconCache持有。
看下内部类IconDB的类继承关系:
private static final class IconDB extends SQLiteCacheHelper {
public abstract class SQLiteCacheHelper {
private final MySQLiteOpenHelper mOpenHelper;
private class MySQLiteOpenHelper extends NoLocaleSQLiteHelper {
......
}
}
public abstract class NoLocaleSQLiteHelper extends SQLiteOpenHelper {
IconDB应该算一个DAO帮组类,实际完成数据库创建是MySQLiteOpenHelper类完成的。
看一个IconCache中定义的插入数据的方法:
private void addIconToDB(ContentValues values, ComponentName key,
PackageInfo info, long userSerial) {
values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
values.put(IconDB.COLUMN_USER, userSerial);
values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime);
values.put(IconDB.COLUMN_VERSION, info.versionCode);
mIconDb.insertOrReplace(values);
}
Icon标准化处理
为了统一样式,launcher中定义了一个用于处理图标样式的帮助类LauncherIcons,在缓存图标之前,会通过该类进行标准化处理。
看下LauncherIcons中的一个public方法:
public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, int iconAppTargetSdk,
boolean isInstantApp) {
float[] scale = new float[1];
// 关键方法
icon = normalizeAndWrapToAdaptiveIcon(icon, iconAppTargetSdk, null, scale);
// 根据scale参数创建缩放处理的Bitmap
Bitmap bitmap = createIconBitmap(icon, scale[0]);
if (Utilities.ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
mCanvas.setBitmap(bitmap);
getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
mCanvas.setBitmap(null);
}
final Bitmap result;
......
......
result = bitmap;
return BitmapInfo.fromBitmap(result);
}
normalizeAndWrapToAdaptiveIcon是一个关键方法,接着看下方法实现:
private Drawable normalizeAndWrapToAdaptiveIcon(Drawable icon, int iconAppTargetSdk,
RectF outIconBounds, float[] outScale) {
float scale = 1f;
// O以上系统,apk的TargetSdk升级到O以上
if (Utilities.ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O) {
boolean[] outShape = new boolean[1];
if (mWrapperIcon == null) {
mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper)
.mutate();
}
AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
dr.setBounds(0, 0, 1, 1);
scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
// icon不是AdaptiveIconDrawable的,给原图标统一加上底板
if (Utilities.ATLEAST_OREO && !outShape[0] && !(icon instanceof AdaptiveIconDrawable)) {
FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
fsd.setDrawable(icon);
fsd.setScale(scale);
icon = dr;
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
}
} else {
// O以下系统,或者apk的TargetSdk未升级到O以上,计算原图片缩放参数,直接用于展示,不进行AdaptiveIconDrawable转换
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
}
outScale[0] = scale;
return icon;
}
上面有一个getNormalizer().getScale方法,用于计算图标缩放的值,保证图标统一的尺寸大小。
public IconNormalizer getNormalizer() {
if (mNormalizer == null) {
mNormalizer = new IconNormalizer(mContext);
}
return mNormalizer;
}
感兴趣的同学可以进一步研究下IconNormalizer类代码。
扩展
launcher如果需要支持主题图标,这里缓存的图标就不是来源于应用apk了,需要从主题包中加载,当然这里的缓存实现还是可以借鉴和重用的。
小结
以上就是个人目前所了解的IconCache的一些知识,有写的不当之处,欢饮指正。