美文网首页
Launcher3 中 IconCache 缓存逻辑

Launcher3 中 IconCache 缓存逻辑

作者: smart_dev | 来源:发表于2023-09-13 00:57 被阅读0次

    概述

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

    Launcher.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

    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. 加载与绑定桌面内容

      1. loadWorkspace

      2. sanitizeData

      3. bindWorkspace

      4. sendFirstScreenActiveInstallsBroadcast

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

      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

    4. 加载和绑定所有的Widgets

      1. load widgets

      2. bindWidgets

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

    IconCacheUpdateHandler.java

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

    调用流程

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

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

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

    举例说明过程

    updateIcons

    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

    
    /**
     * 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()

    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多种实现图标差异性。

    
    // 加载 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.首先看下构造方法
    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
    • 注意这里有一段代码是虚内存,使用技巧值得学习
     // 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()
    /**
     * @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
    /**
    * 在数据库和内存缓存中添加一个条目。 
    * @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 是可以定制图标样式的

    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

    // True is the widget support is disabled.
    public static final boolean GO_DISABLE_WIDGETS = true;
    
    • 当打开GO_DISABLE_WIDGETS = false ,会开启widget,同时在optionsview上会显示菜单, 如下图
    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

    相关文章

      网友评论

          本文标题:Launcher3 中 IconCache 缓存逻辑

          本文链接:https://www.haomeiwen.com/subject/joruvdtx.html