Launcher3 中 IconCache 缓存逻辑

概述

我们先看下IconCache的初始化过程,接着看下IconCache核心数据结构、算法,最后介绍与之关联的几个类。

Launcher.java

java 复制代码
public class Launcher extends StatefulActivity<LauncherState> implements ... {
    ...
    public static final String TAG = "Launcher";
    private LauncherModel mModel;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        LauncherAppState app = LauncherAppState.getInstance(this);
        mOldConfig = new Configuration(getResources().getConfiguration());
        mModel = app.getModel();
        ...
       }
   }
  • 这个类是Launcher的主入口,即 MainActivity。onCreate()做了许多界面和管理器的初始化。

  • 这里我们关注是初始化了 LauncherAppStateLauncherModel

LauncherAppState.java

java 复制代码
public class LauncherAppState {
    // 注释1
    // We do not need any synchronization for this variable as its only written on UI thread.
    public static final MainThreadInitializedObject<LauncherAppState> INSTANCE =
            new MainThreadInitializedObject<>(LauncherAppState::new);

    private final Context mContext;
    private final LauncherModel mModel;
    private final IconProvider mIconProvider;
    private final IconCache mIconCache;
    private final DatabaseWidgetPreviewLoader mWidgetCache;
    private final InvariantDeviceProfile mInvariantDeviceProfile;
    private final RunnableList mOnTerminateCallback = new RunnableList();

    public static LauncherAppState getInstance(final Context context) {
        return INSTANCE.get(context);
    }

    public LauncherAppState(Context context) {
        // 注释2
        this(context, LauncherFiles.APP_ICONS_DB);
        ...
    }

   public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
       ...
       // 注释3
       mIconCache = new IconCache(mContext, mInvariantDeviceProfile,
       iconCacheFileName, mIconProvider);
       ...
   }
}
  • 注释1:LauncherAppState 是单例,且限定在主线程上初始化

  • 注释2 注释3 传入数据库名字 app_icon.db,进而初始化 IconCache

  • mModel 即数据管理器,用于维护启动器的内存状态。预计静态中应该只有一个LauncherModel对象。还提供用于更新 Launcher 的数据库状态的 API。

  • mIconCache应用程序icon和title的缓存,图标可以由任何线程创建。

  • mWidgetCache 存储widget预览信息的数据库

LoaderTask.java

是有数据管理类LauncherModel来调用的,其核心是Run方法。

主要分为四大步骤,并开启事务机制来管理

加载与绑定桌面内容

  1. loadWorkspace

  2. sanitizeData

  3. bindWorkspace

  4. sendFirstScreenActiveInstallsBroadcast

加载和绑定所有的应用图标和信息

  1. loadAllApps

  2. bindAllApps

  3. update icon cache 对应图标缓存逻辑类 LauncherActivityCachingLogic

  4. save shortcuts in icon cache

这一步实际是在第一步的,对应的图标缓存逻辑类 ShortcutCachingLogic
3.

加载和绑定所有DeepShortcuts

  1. loadDeepShortcuts

  2. bindDeepShortcuts

  3. save deep shortcuts in icon cache 对应的图标缓存逻辑类 ShortcutCachingLogic

加载和绑定所有的Widgets

  1. load widgets

  2. bindWidgets

  3. save widgets in icon cache 对应的图标缓存逻辑类 ComponentWithIconCachingLogic

IconCacheUpdateHandler.java

IconCacheUpdateHandler扫描到所有应用后,会开启一个线程 SerializedIconUpdateTask进行更新图标操作,把图标缓存到内存和数据库里。

调用流程

  • 在上面LoaderTask过程中更新图标用的是**IconCacheUpdateHandler.updateIcons()**,

  • 这是个工具类,处理更新图标缓存, 处理业务与IconCache的连接

  • 内部类 SerializedIconUpdateTask 序列化图标更新任务,即将这些图标信息存储或者更新到数据库中

举例说明过程

updateIcons

java 复制代码
public <T> void updateIcons(List<T> apps, CachingLogic<T> cachingLogic,
        OnUpdateCallback onUpdateCallback) {
    // Filter the list per user
    HashMap<UserHandle, HashMap<ComponentName, T>> userComponentMap = new HashMap<>();
    int count = apps.size();
    for (int i = 0; i < count; i++) {
        T app = apps.get(i);
        UserHandle userHandle = cachingLogic.getUser(app);
        HashMap<ComponentName, T> componentMap = userComponentMap.get(userHandle);
        if (componentMap == null) {
            componentMap = new HashMap<>();
            userComponentMap.put(userHandle, componentMap);
        }
        componentMap.put(cachingLogic.getComponent(app), app);
    }

    for (Entry<UserHandle, HashMap<ComponentName, T>> entry : userComponentMap.entrySet()) {
        updateIconsPerUser(entry.getKey(), entry.getValue(), cachingLogic, onUpdateCallback);
    }

    // From now on, clear every valid item from the global valid map.
    mFilterMode = MODE_CLEAR_VALID_ITEMS;
}
  • 这里有两个Map,按照用户维度来分组组件

    • 按照用户维度来分组组件 HashMap<UserHandle, HashMap<ComponentName, T>> userComponentMap;

    • 按照组件不同分组 HashMap<ComponentName, T> componentMap = userComponentMap.get(userHandle);

updateIconsPerUser

java 复制代码
/**
 * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
 * the DB and are updated.
 * @return The set of packages for which icons have updated.
 */
@SuppressWarnings("unchecked")
private <T> void updateIconsPerUser(UserHandle user, HashMap<ComponentName, T> componentMap,
        CachingLogic<T> cachingLogic, OnUpdateCallback onUpdateCallback) {
    Set<String> ignorePackages = mPackagesToIgnore.get(user);
    if (ignorePackages == null) {
        ignorePackages = Collections.emptySet();
    }
    long userSerial = mIconCache.getSerialNumberForUser(user);
    Log.d(TAG, "updateIconsPerUser: userSerial = " + userSerial + " ,componentMap =" + componentMap.size());

    Stack<T> appsToUpdate = new Stack<>();
    try (Cursor c = mIconCache.mIconDb.query(
            new String[]{IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT,
                    IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION,
                    IconDB.COLUMN_SYSTEM_STATE},
            IconDB.COLUMN_USER + " = ? ",
            new String[]{Long.toString(userSerial)})) {

        final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT);
        final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED);
        final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION);
        final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID);
        final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE);

        Log.d(TAG, "updateIconsPerUser: 111");
        while (c.moveToNext()) {
            Log.d(TAG, "updateIconsPerUser: 222");
            ...
        }
    } catch (SQLiteException e) {
        Log.d(TAG, "Error reading icon cache", e);
        // Continue updating whatever we have read so far
    }

    // Insert remaining apps.
    if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) {
        Stack<T> appsToAdd = new Stack<>();
        appsToAdd.addAll(componentMap.values());
        Log.d(TAG, "SerializedIconUpdateTask appsToAdd = " + appsToAdd.size() + ", appsToUpdate = "+ appsToUpdate.size());
        new SerializedIconUpdateTask(userSerial, user, appsToAdd, appsToUpdate, cachingLogic,
                onUpdateCallback).scheduleNext();
    }
}
  • 为什么要删除操作?setIgnorePackages

SerializedIconUpdateTask.run()

java 复制代码
private class SerializedIconUpdateTask<T> implements Runnable {
    ....
    @Override
    public void run() {
       ...
       if (!mAppsToAdd.isEmpty()) {
            T app = mAppsToAdd.pop();
            PackageInfo info = mPkgInfoMap.get(mCachingLogic.getComponent(app).getPackageName());
            // We do not check the mPkgInfoMap when generating the mAppsToAdd. Although every
            // app should have package info, this is not guaranteed by the api
            if (info != null) {
                mIconCache.addIconToDBAndMemCache(app, mCachingLogic, info,
                        mUserSerial, false /*replace existing*/);
            }

            if (!mAppsToAdd.isEmpty()) {
                scheduleNext();
            }
        }
    }

    public void scheduleNext() {
        mIconCache.mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN,
                SystemClock.uptimeMillis() + 1);
    }
}

IconCache.java

核心思想:针对每类图标提供通用的HashMap内存缓存 + 数据库缓存,同时通过CachingLogic多种实现图标差异性。

java 复制代码
// 加载 shortcut 图标
private synchronized <T extends ItemInfoWithIcon> void getShortcutIcon(T info, ShortcutInfo si,
        boolean useBadged, @NonNull Predicate<T> fallbackIconCheck) {
    BitmapInfo bitmapInfo;
    if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
        bitmapInfo = cacheLocked(ShortcutKey.fromInfo(si).componentName, si.getUserHandle(),
                () -> si, mShortcutCachingLogic, false, false).bitmap;
    } else {
        // If caching is disabled, load the full icon
        bitmapInfo = mShortcutCachingLogic.loadIcon(mContext, si);
    }
    if (bitmapInfo.isNullOrLowRes()) {
        bitmapInfo = getDefaultIcon(si.getUserHandle());
    }

    if (isDefaultIcon(bitmapInfo, si.getUserHandle()) && fallbackIconCheck.test(info)) {
        return;
    }
    info.bitmap = bitmapInfo;
    if (useBadged) {
        BitmapInfo badgeInfo = getShortcutInfoBadge(si);
        try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
            info.bitmap = li.badgeBitmap(info.bitmap.icon, badgeInfo);
        }
    }
}

/**
 * 加载 Widget 图标
 */
public synchronized String getTitleNoCache(ComponentWithLabel info) {
    CacheEntry entry = cacheLocked(info.getComponent(), info.getUser(), () -> info,
            mComponentWithLabelCachingLogic, false /* usePackageIcon */,
            true /* useLowResIcon */);
    return Utilities.trim(entry.title);
}

BaseIconCache.java

1.首先看下构造方法
java 复制代码
public abstract class BaseIconCache {
    ....
    private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
    private final Map<ComponentKey, CacheEntry> mCache;
    ...
    public BaseIconCache(Context context, String dbFileName, Looper bgLooper,
        int iconDpi, int iconPixelSize, boolean inMemoryCache) {
        ...
        if (inMemoryCache) {
            mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
        } else {
            // Use a dummy cache
            mCache = new AbstractMap<ComponentKey, CacheEntry>() {
                @Override
                public Set<Entry<ComponentKey, CacheEntry>> entrySet() {
                    return Collections.emptySet();
                }

                @Override
                public CacheEntry put(ComponentKey key, CacheEntry value) {
                    return value;
                }
            };
        }
        ...
    }

}

// 缓存key, 组成: 组件名 和 用户UserHandle
public class ComponentKey {

    public final ComponentName componentName;
    public final UserHandle user;

    private final int mHashCode;

    public ComponentKey(ComponentName componentName, UserHandle user) {
        if (componentName == null || user == null) {
            throw new NullPointerException();
        }
        this.componentName = componentName;
        this.user = user;
        mHashCode = Arrays.hashCode(new Object[] {componentName, user});
    }
    ...
}

// 缓存Value,组成:图标 + title + contentDesc
public static class CacheEntry {

    @NonNull
    public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
    public CharSequence title = "";
    public CharSequence contentDescription = "";
}
  • 缓存数据结构:Map<ComponentKey, CacheEntry>
  • 缓存集合初始大小为50
  • ComponentKey:缓存key, 组成: 组件名(pkg+cls) 和 用户UserHandle
  • CacheEntry:缓存Value,组成:图标 + title + contentDesc
  • 注意这里有一段代码是虚内存,使用技巧值得学习
java 复制代码
 // Use a dummy cache
            mCache = new AbstractMap<ComponentKey, CacheEntry>() {
                @Override
                public Set<Entry<ComponentKey, CacheEntry>> entrySet() {
                    return Collections.emptySet();
                }

                @Override
                public CacheEntry put(ComponentKey key, CacheEntry value) {
                    return value;
                }
            };
2.继续看另一个重要方法 cacheLocked()
java 复制代码
/**
 * @param  componentName  组件名
 * @param  user 用户
 * @param  infoProvider 组件信息提供者
 * @param  cachingLogic  对应的缓存逻辑处理类
 * @param  usePackageIcon 是否使用pkg的icon
 * @param  useLowResIcon  是否使用默认的空图标
 */
protected <T> CacheEntry cacheLocked(
        @NonNull ComponentName componentName, @NonNull UserHandle user,
        @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
        boolean usePackageIcon, boolean useLowResIcon) {
    assertWorkerThread();
    // 1.生成缓存key
    ComponentKey cacheKey = new ComponentKey(componentName, user);
    // 2.尝试根据key,从缓存中取
    CacheEntry entry = mCache.get(cacheKey);
    // 3.尚未缓存 或者 缓存了但是缓存的是空的默认图标,此时去缓存
    if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
        entry = new CacheEntry();
        //4.如果对应的缓存逻辑控制类 允许添加到内存缓存中,即存入mCache,但此时value未赋值
        if (cachingLogic.addToMemCache()) { 
            mCache.put(cacheKey, entry);
        }

        // Check the DB first.
        T object = null;
        boolean providerFetchedOnce = false;

        // 4.首先查看数据库是否存在
        // 如果数据存在,取出来赋值给entry
        // 如果数据库不存在,加载默认空图标、pkg图标、或者 cachingLogic.loadIcon
        if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
            object = infoProvider.get();
            providerFetchedOnce = true;

            if (object != null) { // 4.1如果信息提供者不为空,直接去对应的缓存控制逻辑取图标
                entry.bitmap = cachingLogic.loadIcon(mContext, object);
            } else { // 4.2如果提供者是空的,返回默认的或者使用pkg的图标
                if (usePackageIcon) {
                    CacheEntry packageEntry = getEntryForPackageLocked(
                            componentName.getPackageName(), user, false);
                    if (packageEntry != null) {
                        if (DEBUG) Log.d(TAG, "using package default icon for " +
                                componentName.toShortString());
                        entry.bitmap = packageEntry.bitmap;
                        entry.title = packageEntry.title;
                        entry.contentDescription = packageEntry.contentDescription;
                    }
                }
                // 如果pkg依然为空,使用默认的空白图标
                if (entry.bitmap == null) {
                    if (DEBUG) Log.d(TAG, "using default icon for " +
                            componentName.toShortString());
                    entry.bitmap = getDefaultIcon(user);
                }
            }
        }

        // 5.检查并对entry的title和desc继续赋值
        if (TextUtils.isEmpty(entry.title)) {
            if (object == null && !providerFetchedOnce) {
                object = infoProvider.get();
                providerFetchedOnce = true;
            }
            if (object != null) {
                entry.title = cachingLogic.getLabel(object);
                entry.contentDescription = mPackageManager.getUserBadgedLabel(
                        cachingLogic.getDescription(object, entry.title), user);
            }
        }
    }
    return entry; // 返回缓存的Value,及CacheEntry
}

补充说明两点

  • getEntryFromDB 从数据库中查询目标 Entry

  • getEntryForPackageLocked 与上面这个方法类似,唯一多的逻辑是当从packagemanger查询到应用图标会存入到数据库

3.方法addIconToDBAndMemCache
java 复制代码
/**
* 在数据库和内存缓存中添加一个条目。 
* @param replaceExisting 如果为真,它会重新创建位图,即使它已经存在于内存中。
* 这在以前的位图是使用旧数据创建时很有用。
*/
public synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic,
        PackageInfo info, long userSerial, boolean replaceExisting) {
    UserHandle user = cachingLogic.getUser(object);
    ComponentName componentName = cachingLogic.getComponent(object);

    final ComponentKey key = new ComponentKey(componentName, user);
    CacheEntry entry = null;
    if (!replaceExisting) {
        entry = mCache.get(key);
        // We can't reuse the entry if the high-res icon is not present.
        if (entry == null || entry.bitmap.isNullOrLowRes()) {
            entry = null;
        }
    }
    // 新加载图标
    if (entry == null) {
        entry = new CacheEntry();
        entry.bitmap = cachingLogic.loadIcon(mContext, object);
    }

    // 无法从 cachingLogic 加载图标,这意味着已加载替代图标(例如后备图标、默认图标)。
    // 所以我们放在这里,因为缓存空条目没有意义。
    if (entry.bitmap.isNullOrLowRes()) return;
    entry.title = cachingLogic.getLabel(object);
    entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
    // 是否需要添加到内存中
    if (cachingLogic.addToMemCache()) mCache.put(key, entry);

    ContentValues values = newContentValues(entry.bitmap, entry.title.toString(),
            componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList));
    // 添加到数据库
    addIconToDB(values, componentName, info, userSerial,
            cachingLogic.getLastUpdatedTime(object, info));
}

IconDB

  • 类路径:com.android.launcher3.icons.cache.BaseIconCache.IconDB
  • db数据库名:app_icons.db
  • table表名:icons

某个手机数据库表示例

CachingLogic.java 系列

  • LauncherActivityCachingLogic 用于allApp的图标缓存

  • ShortcutCachingLogic 用于shortcut的图标缓存

  • ComponentWithIconCachingLogic 用于widget的图标缓存

  • 其中 loadIcon 是可以定制图标样式的

java 复制代码
public class ShortcutCachingLogic implements CachingLogic<ShortcutInfo> {

    private static final String TAG = "ShortcutCachingLogic";

    // 根据shortcutInfo获取组件
    @Override
    public ComponentName getComponent(ShortcutInfo info) {
        return ShortcutKey.fromInfo(info).componentName;
    }

   ...

    @NonNull
    @Override
    public BitmapInfo loadIcon(Context context, ShortcutInfo info) {
        try (LauncherIcons li = LauncherIcons.obtain(context)) {
            Drawable unbadgedDrawable = ShortcutCachingLogic.getIcon(
                    context, info, LauncherAppState.getIDP(context).fillResIconDpi);
            if (unbadgedDrawable == null) return BitmapInfo.LOW_RES_INFO;
            return new BitmapInfo(li.createScaledBitmapWithoutShadow(
                    unbadgedDrawable, 0), Themes.getColorAccent(context));
        }
    }

    @Override
    public boolean addToMemCache() {
        return false;// 表示不缓存到内存中
    }

    /**
     * Similar to {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)} with additional
     * Launcher specific checks
     */
    public static Drawable getIcon(Context context, ShortcutInfo shortcutInfo, int density) {
        if (GO_DISABLE_WIDGETS) { // 开关控制是否允许有shortcut
            return null;
        }
        try {// 从LauncherApps中查询图标
            return context.getSystemService(LauncherApps.class)
                    .getShortcutIconDrawable(shortcutInfo, density);
        } catch (SecurityException | IllegalStateException e) {
            Log.e(TAG, "Failed to get shortcut icon", e);
            return null;
        }
    }
}

WidgetsModel.java

java 复制代码
// True is the widget support is disabled.
public static final boolean GO_DISABLE_WIDGETS = true;
  • 当打开_GO_DISABLE_WIDGETS = false ,会开启widget,同时在optionsview上会显示菜单, 如下图_
java 复制代码
OptionsPopupView.java
public static WidgetsFullSheet openWidgets(Launcher launcher) {
    if (launcher.getPackageManager().isSafeMode()) {
        Toast.makeText(launcher, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show();
        return null;
    } else {
        return WidgetsFullSheet.show(launcher, true /* animated */);
    }
}
  • 异常情况默认图标兜底 makeDefaultIcon
    com.android.launcher3.icons.BaseIconFactory#getFullResDefaultActivityIcon

👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀

相关推荐
你的小1026 分钟前
JavaWeb项目-----博客系统
android
风和先行1 小时前
adb 命令查看设备存储占用情况
android·adb
AaVictory.2 小时前
Android 开发 Java中 list实现 按照时间格式 yyyy-MM-dd HH:mm 顺序
android·java·list
似霰3 小时前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder
大风起兮云飞扬丶3 小时前
Android——网络请求
android
干一行,爱一行3 小时前
android camera data -> surface 显示
android
断墨先生3 小时前
uniapp—android原生插件开发(3Android真机调试)
android·uni-app
无极程序员5 小时前
PHP常量
android·ide·android studio
萌面小侠Plus6 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机
慢慢成长的码农6 小时前
Android Profiler 内存分析
android