Introduce trimMemory to AppIconCacheManager

This CL adds a method to trim lrucache size dynamically based on the
level of onLowMemory to avoid calling evictAll to frequently, and to make cache resize more reasonable.

Bug: 259630755
Test: atest AppIconCacheManagerTest -c

Change-Id: I1b9692a0fad8c13722da4906cf844232707e74b2
This commit is contained in:
Shen Lin
2022-11-18 16:47:58 +08:00
parent 140a69e94f
commit c301e09c19
2 changed files with 95 additions and 1 deletions

View File

@@ -22,13 +22,16 @@ import android.os.UserHandle;
import android.util.Log;
import android.util.LruCache;
import androidx.annotation.VisibleForTesting;
/**
* Cache app icon for management.
*/
public class AppIconCacheManager {
private static final String TAG = "AppIconCacheManager";
private static final float CACHE_RATIO = 0.1f;
private static final int MAX_CACHE_SIZE_IN_KB = getMaxCacheInKb();
@VisibleForTesting
static final int MAX_CACHE_SIZE_IN_KB = getMaxCacheInKb();
private static final String DELIMITER = ":";
private static AppIconCacheManager sAppIconCacheManager;
private final LruCache<String, Drawable> mDrawableCache;
@@ -109,4 +112,25 @@ public class AppIconCacheManager {
private static int getMaxCacheInKb() {
return Math.round(CACHE_RATIO * Runtime.getRuntime().maxMemory() / 1024);
}
/**
* Clears as much memory as possible.
*
* @see android.content.ComponentCallbacks2#onTrimMemory(int)
*/
public void trimMemory(int level) {
if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// Time to clear everything
if (sAppIconCacheManager != null) {
sAppIconCacheManager.mDrawableCache.trimToSize(0);
}
} else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
|| level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
// Tough time but still affordable, clear half of the cache
if (sAppIconCacheManager != null) {
final int maxSize = sAppIconCacheManager.mDrawableCache.maxSize();
sAppIconCacheManager.mDrawableCache.trimToSize(maxSize / 2);
}
}
}
}

View File

@@ -34,11 +34,21 @@ import org.robolectric.RobolectricTestRunner;
public class AppIconCacheManagerTest {
private static final String APP_PACKAGE_NAME = "com.test.app";
private static final String APP_PACKAGE_NAME1 = "com.test.app1";
private static final String APP_PACKAGE_NAME2 = "com.test.app2";
private static final String APP_PACKAGE_NAME3 = "com.test.app3";
private static final int APP_UID = 9999;
@Mock
private Drawable mIcon;
@Mock
private Drawable mIcon1;
@Mock
private Drawable mIcon2;
@Mock
private Drawable mIcon3;
private AppIconCacheManager mAppIconCacheManager;
@Before
@@ -48,6 +58,29 @@ public class AppIconCacheManagerTest {
doReturn(10).when(mIcon).getIntrinsicHeight();
doReturn(10).when(mIcon).getIntrinsicWidth();
doReturn(mIcon).when(mIcon).mutate();
// Algorithm for trim memory test:
// The real maxsize is defined by AppIconCacheManager.MAX_CACHE_SIZE_IN_KB, and the size
// of each element is calculated as following:
// n * n * 4 / 1024
// In the testcase, we want to mock the maxsize of LruCache is 3, so the formula calculating
// the size of each element will be like:
// n * n * 4 / 1024 = maxsize / 3
// Thus, n = square_root(maxsize / 3 * 1024 / 4), which can be used as an icon size.
final int iconSize =
(int) Math.sqrt(AppIconCacheManager.MAX_CACHE_SIZE_IN_KB / 3f * 1024f / 4f);
doReturn(iconSize).when(mIcon1).getIntrinsicHeight();
doReturn(iconSize).when(mIcon1).getIntrinsicWidth();
doReturn(mIcon1).when(mIcon1).mutate();
doReturn(iconSize).when(mIcon2).getIntrinsicHeight();
doReturn(iconSize).when(mIcon2).getIntrinsicWidth();
doReturn(mIcon2).when(mIcon2).mutate();
doReturn(iconSize).when(mIcon3).getIntrinsicHeight();
doReturn(iconSize).when(mIcon3).getIntrinsicWidth();
doReturn(mIcon3).when(mIcon3).mutate();
}
@After
@@ -106,4 +139,41 @@ public class AppIconCacheManagerTest {
assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME, APP_UID)).isNull();
}
@Test
public void trimMemory_levelSatisfied_shouldNotCacheIcon() {
mAppIconCacheManager.put(APP_PACKAGE_NAME1, APP_UID, mIcon1);
mAppIconCacheManager.put(APP_PACKAGE_NAME2, APP_UID, mIcon2);
mAppIconCacheManager.put(APP_PACKAGE_NAME3, APP_UID, mIcon3);
// Expected to trim size to 0
final int level = android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND;
mAppIconCacheManager.trimMemory(level);
// None of the elements should be cached
assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME1, APP_UID)).isNull();
assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME2, APP_UID)).isNull();
assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME3, APP_UID)).isNull();
}
@Test
public void trimMemory_levelSatisfied_shouldCacheAtLeastHalf() {
mAppIconCacheManager.put(APP_PACKAGE_NAME1, APP_UID, mIcon1);
mAppIconCacheManager.put(APP_PACKAGE_NAME2, APP_UID, mIcon2);
mAppIconCacheManager.put(APP_PACKAGE_NAME3, APP_UID, mIcon3);
// Get the last element
mAppIconCacheManager.get(APP_PACKAGE_NAME1, APP_UID);
// Expected to trim size to half of it, which is int( 3 / 2 ) = 1
final int level = android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
mAppIconCacheManager.trimMemory(level);
// There should be only one cached element, which is the last recently used one
assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME1, APP_UID)).isNotNull();
assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME2, APP_UID)).isNull();
assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME3, APP_UID)).isNull();
}
}