background

Make a summary of picture optimization. The main record about the implementation of the train of thought and code, said wrong place, also please big guy pointed out.

Image memory usage calculation

A formula to calculate

Image memory = Image quality * width * height

  • Image quality: This parameter is used by defaultARGB_8888As the data format for pixels, so that each pixel is4 ByteThe size of the.
  • Image width: Refers to the width of the Bitmap that is actually loaded into memory.

Note: When loading resource images under the RES directory, the system will perform a resolution conversion according to the different directories where the images are stored, and the conversion rule is as follows:

Height of the new figure = Height of the original figure * (DPI of the device/DPI of the directory) Width of the new figure = width of the original figure * (DPI of the device/DPI of the directory)

Equipment dpi

  • Enter the adb shell
  • View the current DPI: VM Density
  • Custom DPI: VM Density XXX
  • Reset Operation: VM Density reset

example

For example, we usually put a slice file under XHDPI and have a 100×100 slice file.

  • On a device with a DPI of 320, the occupied memory is: 100 x 100 x 4=40000B (about 39KB)
  • On a device whose DPI is 480, the memory usage is 100 x(480/320) x 100 x(480/320)X 4 = 90000 B (87 KB)

Therefore, for the cutting map resources, they need to be placed in the appropriate DPI directory.

Different RES directories have different memory usage.

Check out this article: How to calculate the memory size of an image on Android

Analyze Bitmap objects in memory

tool

Memory Profiler+Bitmap Preview

  • Premise:Mobile devices are below 8.0This preview function is relatively convenient.
  • Reason: From Android 3.0 to 7.0, Bitmap objects and pixel data are put into Java heap. After Android 8.0, Bitmap is put into Native.
  • Operation: Dump the Memory snapshot of the running app. Hprof file through the Memory Profiler of the AS. Select the Bitmap object in memory, and a Bitmap Preview will appear on the right, which can be used to Preview the current image.
  • Analysis: Generally, we mainly analyze the Bitmap with the Top N memory usage to see if it can be optimized. It is convenient to use with Bitmap Preview.

MAT+GIMP

  • When analyzing Android Memory with MAT(Memory Analyzer Tool), the original mBuffer data of Bitmap is exported and opened with GIMP software to restore the original image.
  • Use the tutorial for reference: Android MAT, GIMP check memory usage

Single image memory optimization

Local image

Scenario: The current test device is XXXHDPI. A 300×300 cut map is placed under the xhdPI directory of the main project. Set SRC to this map in the ImageView XML file.

Results:

  • Image memory usage is too large: 300x2x300x2x4=1440000 (1.37MB)
  • The InfalTE time for the ImageView and the overall layout XML will be longerBecause this process gets the Resource#getDrawable, it is done on the main thread.

Optimization:

  • The cut diagram must be stored in the correct DPI directory.
  • Self-implementation of asynchronous loading, in the background thread load image resources, get the Bitmap object, and then post to the main thread for ImageView Settings.
  • Or directlyUse Fresco for loadingFresco loads local image resources, which are parsed in IO threads and then posted to the main thread for display by default.

The image source file size should be close to the target ImageVIew

Scenario: The Bitmap width is much larger than the actual View width. A bit of a waste of memory resources.

Optimization:

  • Be careful to set the View to the appropriate size
  • For local maps, be careful not to place the wrong DPI directory. (For example, the bitmap loaded on the XXHDPI device will be multiplied by the width and height of the corresponding coefficient if the cut map is placed in the HDPI directory)
  • For network diagrams, you can get images of the appropriate width and height (server support is required),Faster downloads to image resources, it can also beSave bandwidth
  • forA larger image detectionWhen the Bitmap is too large for the View, prompt the developer to optimize.

Reduce the size of the pixels

Scenario: The system defaults to ARGB_8888, so each pixel is 4 bytes in size. By changing this format, you can change how much memory each pixel takes up.

Optimization: can be replaced with RGB_565, ARGB_4444, etc. Reduce the size of each pixel, thereby reducing the memory taken up by the entire image.

Lower resolution

  • inSampleSize

When loading the images, set BitmapFactory. Options. After inSampleSize Bitmap width is smaller inSampleSize times higher, so the actual loading pictures of memory, Will shrink to inSampleSize* inSampleSize by one.

  • Fresco#ResizeOptions

Resize does not change the original image, it only changes the size of the image in memory before decoding. When creating the ImageRequest, provide a ResizeOptions, specifying the corresponding width and height.

Uri uri = "file:///mnt/sdcard/MyApp/myfile.jpg"; int width = 50, height = 50; ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri) .setResizeOptions(new ResizeOptions(width, // Set resizeOptions.build (); PipelineDraweeController controller = Fresco.newDraweeControllerBuilder() .setOldController(mDraweeView.getController())  .setImageRequest(request) .build(); mSimpleDraweeView.setController(controller);Copy the code

A larger image detection

Epic

Epic is a Java Method-granular runtime AOP Hook framework at the virtual machine level. It can intercept almost any Java method call within the process, and can be used to implement AOP programming, runtime staking, performance analysis, security auditing, and so on.

ImageView large image detection

ImageView setImageDrawable(drawable)

Implementation: custom BigSizeImageHook class, Hook ImageView setImageDrawable method, parsing parameters, you can get bitmap width and view width and height, compare the size relationship between the two. When the size of the Bitmap is n times that of the View, a log message is displayed.

class BigSizeImageHook : XC_MethodHook() {private val TAG = "ImageHook" private val max_threshold = 2 Override fun afterHookedMethod(param: MethodHookParam?) override fun afterHookedMethod(MethodHookParam?) { super.afterHookedMethod(param) param ? : ThisObject as imageView val drawable = imageView.drawable checkBitmap(imageView, drawable) } private fun checkBitmap(view: View, drawable: Drawable?) { if (drawable is BitmapDrawable) { val bitmap = drawable.bitmap val viewWidth = imageView.width val viewHeight = Imageview. height if (viewWidth > 0 && viewHeight > 0) {// If the width of the image is more than 2 times that of the view, If (bitmap.width >= viewWidth * max_threshold && bitmap.height >= viewHeight * max_threshold) {wran(imageView, Bitmap)}} else {// When width and height equals 0, the ImageView has not been drawn yet, Using ViewTreeObserver for listening to wide high information imageView. ViewTreeObserver. AddOnPreDrawListener (object: ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { if (imageView.width > 0 && imageView.height > 0) { if (bitmap.width >= viewWidth * max_threshold && bitmap.height >= viewHeight * max_threshold) { wran(imageView, Bitmap)} imageView. ViewTreeObserver. RemoveOnPreDrawListener return true (this)}}}}}}) / / output log information related to private fun wran(imageView: ImageView, bitmap: Bitmap) { val warnInfo = "Bitmap size too large, " + "view size : (${imageView.width},${imageView.height}), " + "bitmap size:(${bitmap.width},${bitmap.height}), " + "view id:${getId(imageView)}, " + "bitmap id:${bitmap.density}" Log.d(TAG, $warnInfo)} private fun getId(view: view?): String? {view?: return "no-id" return if (view.id == View.NO_ID) "no-id" else view.resources.getResourceName(view.id) } }Copy the code

Hook the setImageDrawable method of the ImageView so that every time an ImageView calls setImageDrawable, it will go into the BigSizeImageHook logic.

DexposedBridge.hookAllConstructors(ImageView::class.java, object : XC_MethodHook() {
    override fun afterHookedMethod(param: MethodHookParam?) {
        super.afterHookedMethod(param)
        DexposedBridge.findAndHookMethod(
            ImageView::class.java,
            "setImageDrawable",
            Drawable::class.java,
            ImageHook()
        )
    }
})
Copy the code
Fresco big picture detection

The detection logic above doesn’t work well with SimpleDraweeView, so you’ll need to re-implement it yourself. In fact, it is to find a reasonable hook point, can get both bitmap and view.

Hook: GenericDraweeHierarchy setImage(Drawable Drawable, float Progress, Boolean immediate), The resulting Bitmap is set to SimpleDraweeView using this method. Drawable retrieves the bitmap, and drawable callback retrieves the simpleDraweeView.

//GenericDraweeHierarchy class setImage(Drawable Drawable, float progress, Boolean immediate)Copy the code

Implementation: custom BigSizeFrescoImageHook class, Hook GenericDraweeHierarchy setImage method, parsing parameters, get bitmap width height and view width height, compare the size relationship between the two. When the size of the Bitmap is n times that of the View, a log message is displayed.

class BigSizeFrescoImageHook : XC_MethodHook() {private val TAG = "bigsize" private val max_threshold = 2 Override Fun afterHookedMethod(param: MethodHookParam?) override Fun afterHookedMethod(MethodHookParam?) { super.afterHookedMethod(param) Log.d(TAG, "afterHookedMethod: ") param ? : If (param.thisObject is GenericDraweeHierarchy){val Hierarchy =param.thisObject as GenericDraweeHierarchy val bitmapDrawable =param.args[0] as BitmapDrawable val simpleDraweeView = hierarchy. TopLevelDrawable. Callback as SimpleDraweeView / / detection logic is the same as the one above checkBitmap (SimpleDraweeView bitmapDrawable)}} } / / use DexposedBridge.findAndHookMethod(GenericDraweeHierarchy::class.java,"setImage",Drawable::class.java,Float::class.java,Bo olean::class.java,BigSizeFrescoImageHook())Copy the code

Duplicate image detection

Parse the hprof file

Dump the memory snapshot. Hprof file when the current APP is running, and then analyze the memory snapshot based on the open source framework com.squareup.haha:haha to obtain all bitmaps in the memory.

Code implementation can refer to: hprof_bitmap_dump

Implementation approach
  1. Reads the hprof file in the specified directory
  2. Get the ClassObj object from the class name Android.graphics.bitmap
  3. Gets a list of instances of the Bitmap
  4. The for loop iterates through each bitmap to read information such as width, height, and pixel data.
  5. According to width, height, pixel data, output the corresponding image to the local disk
Specific code
Public static void main(String[] args) {public static void main(String[] args) {final File hprofFile = new File(args[0]); Key is the MD5 value of the corresponding image, and value is the buffer data of the corresponding image. HashMap<String, String> hashMap = new HashMap<>(); Final HprofBuffer buffer = new MemoryMappedFileBuffer(hprofFile); final HprofParser parser = new HprofParser(buffer); final Snapshot snapshot = parser.parse(); Final ClassObj bitmapClass = snapshot.findClass(" Android.graphics.bitmap "); / / get the Bitmap instance number final int bitmapCount = bitmapClass. GetInstanceCount (); System.out.println("Found bitmap instances: " + bitmapCount); / / to obtain a List of Bitmap Instance final List < Instance > bitmapInstances = bitmapClass. GetInstancesList (); Int n = 0; int n = 0; for (Instance bitmapInstance : bitmapInstances) { if (bitmapInstance instanceof ClassInstance) { int width = 0; int height = 0; byte[] data = null; String md5=""; String id=""; final ClassInstance bitmapObj = (ClassInstance) bitmapInstance; Final List< classinstance. FieldValue> values = bitmapobj.getValues (); FieldValue FieldValue: (ClassInstance.FieldValue FieldValue: Values) {if ("mWidth".equals(fieldValue.getField().getName())) {// Width = (Integer) fieldValue.getValue(); } else if ("mHeight".equals(fieldValue.getField().getName())) {// Image height = (Integer) fieldValue.getValue(); } else if ("mBuffer".equals(fieldValue.getField().getName())) { ArrayInstance ArrayInstance = (ArrayInstance) fieldValue.getValue(); Object[] boxedBytes = arrayInstance.getValues(); data = new byte[boxedBytes.length]; for (int i = 0; i < data.length; i++) { data[i] = (Byte) boxedBytes[i]; } // Calculate the md5 value of the image md5= md5util.getMD5 (data); // Object Id Id = integer.tohexString ((int) bitmapobj.getid ()); System.out.println("Bitmap #" + n +" :"+ width +" x" + height+" id :"+id); // check whether the image repeats if (! hashMap.containsKey(md5)) { hashMap.put(md5,id); } else {system.out. println(" id: "+id: "+ hashmap.get (md5)); } // According to width, height, pixel data, BufferedImage Image = new BufferedImage(width, height, buffereDimage.type_4byte_abgr); for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { int offset = 4 * (row * width + col); int byte3 = 0xff & data[offset++]; int byte2 = 0xff & data[offset++]; int byte1 = 0xff & data[offset++]; int byte0 = 0xff & data[offset++]; int alpha = byte0; int red = byte1; int green = byte2; int blue = byte3; int pixel = (alpha << 24) | (blue << 16) | (green << 8) | red; image.setRGB(col, row, pixel); } } final OutputStream inb = new FileOutputStream("bitmap-0x" + Integer.toHexString((int) bitmapObj.getId()) + ".png"); final ImageWriter wrt = ImageIO.getImageWritersByFormatName("png").next(); final ImageOutputStream imageOutput = ImageIO.createImageOutputStream(inb); wrt.setOutput(imageOutput); wrt.write(image); inb.close(); n++; } } } catch (IOException e) { e.printStackTrace(); }}Copy the code
Compare two bitmaps to see if they are the same

Comparison field: mBuffer in Bitmap. If the MD5 values of the two bytes [] are the same, the two bitmaps can be considered to be the same.

Algorithm implementation: made a HashMap to determine whether the repetition, key is the MD5 value of the corresponding image, value is the buffer data of the corresponding image.

Key is the MD5 value of the corresponding image, and value is the buffer data of the corresponding image. HashMap<String, String> hashMap = new HashMap<>(); Static class Md5Util {private static MessageDigest md5; static { try { md5 = MessageDigest.getInstance("MD5"); } catch (Exception e) { throw new RuntimeException(e); } } public static String getMd5(byte[] bs) { StringBuilder sb = new StringBuilder(40); for (byte x : bs) { if ((x & 0xff) >> 4 == 0) { sb.append("0").append(Integer.toHexString(x & 0xff)); } else { sb.append(Integer.toHexString(x & 0xff)); } } return sb.toString(); }}Copy the code

Image cache Management

By default, Fresco has three levels of cache: Bitmap cache + undecoded image cache + hard disk cache.

  • BitmapCache: storageBitmapobject
  • EncodeCacheUndecoded image cache: Stores images in the original compressed format. Images accessed from this cache need to be decoded before they can be used.
  • DiskCacheDisk cache: Stores the undecoded original compression format of the picture, before using the same need to be decoded and other processing.
  • During Fresco initialization, there are a lot of configurable options, and you can change the configuration as needed.
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context) . SetBitmapMemoryCacheParamsSupplier (bitmapCacheParamsSupplier) / / custom memory cache configuration parameters. SetDownsampleEnabled (true) / / whether to open the picture down sampling . SetEncodedMemoryCacheParamsSupplier (encodedCacheParamsSupplier) / / custom not decoded image cache configuration . SetExecutorSupplier (executorSupplier). / / a custom thread pool providers setImageCacheStatsTracker (imageCacheStatsTracker) / / pictures of statistics can be used to cache events SetMainDiskCacheConfig (mainDiskCacheConfig)// Customize disk cache configuration parameters . SetMemoryTrimmableRegistry (memoryTrimmableRegistry) / / memory changes to monitor the registry, Objects that need to listen for memory changes on the system need to be added to the.setrequestListeners // listeners to the requests . SetSmallImageDiskCacheConfig (smallImageDiskCacheConfig) / / disk cache configuration. The build (); Fresco.initialize(context, config);Copy the code

Configure two disk caches

By default, there is only one fresco disk cache, using MainDiskCache. According to the LRU Cache principle, when the Cache is full, the earliest access data needs to be deleted.

Optimization: Configure two disk caches, one for caching large images and one for caching small images. This way, small files are not removed from the cache because of frequent changes to large files.

val IMAGE_PIPELINE_MAIN_CACHE_DIR = "fresco_cache_big" val IMAGE_PIPELINE_SMALL_CACHE_DIR = "fresco_cache_small" /** * */ fun configDiskCache(context: context, builder: ImagePipelineConfig. Builder) {Builder. SetMainDiskCacheConfig (. / / a larger cache DiskCacheConfig newBuilder (context) .setBaseDirectoryPath(context.externalCacheDir) .setBaseDirectoryName(IMAGE_PIPELINE_MAIN_CACHE_DIR) .build() ) SetSmallImageDiskCacheConfig (. / / insets cache DiskCacheConfig newBuilder (context) .setBaseDirectoryPath(context.externalCacheDir) .setBaseDirectoryName(IMAGE_PIPELINE_SMALL_CACHE_DIR) .build() ) }Copy the code

When constructing the image request, use setCacheChoice to specify the type of cache.

val request = ImageRequestBuilder. NewBuilderWithSource (Uri. Parse (" https://img95.699pic.com/photo/40011/0709.jpg_wh860.jpg ")) .setCacheChoice(ImageRequest.CacheChoice.SMALL) .build() mBinding.draweeView.setImageRequest(request)Copy the code

Set different cache sizes depending on the device

Disk cache

Define a set of default values in the DiskCacheConfig.Builder class.

MainCache is 40MB Max by default if the phone has enough disk space. If the disk space is low, MainCache has a maximum of 10MB. For very low disk controls, MainCache has a maximum of 2MB.


private long mMaxCacheSize = 40 * ByteConstants.MB;
private long mMaxCacheSizeOnLowDiskSpace = 10 * ByteConstants.MB;
private long mMaxCacheSizeOnVeryLowDiskSpace = 2 * ByteConstants.MB;
Copy the code

Memory cache

MemoryCacheParams, a memory cache configuration class.

Fresco “has two layers of memory cache, so the corresponding default configuration has two implementations (DefaultBitmapMemoryCacheParamsSupplier and DefaultEncodedMemoryCacheParamsSupplier).

The default Fresco cache size is based on the current operating memory of the application. For phones with 64MB or more of operating memory (which is already common), the default Fresco cache size is maxMemory / 4

public class DefaultBitmapMemoryCacheParamsSupplier implements Supplier<MemoryCacheParams> { @Override public MemoryCacheParams get() { return new MemoryCacheParams( getMaxCacheSize(), MAX_CACHE_ENTRIES, MAX_EVICTION_QUEUE_SIZE, MAX_EVICTION_QUEUE_ENTRIES, MAX_CACHE_ENTRY_SIZE, PARAMS_CHECK_INTERVAL_MS); } private int getMaxCacheSize() { final int maxMemory = Math.min(mActivityManager.getMemoryClass() * ByteConstants.MB, Integer.MAX_VALUE); if (maxMemory < 32 * ByteConstants.MB) { return 4 * ByteConstants.MB; } else if (maxMemory < 64 * ByteConstants.MB) { return 6 * ByteConstants.MB; } else { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { return 8 * ByteConstants.MB; } else { return maxMemory / 4; }}}}Copy the code

Through setBitmapMemoryCacheParamsSupplier and setEncodedMemoryCacheParamsSupplier to customize two memory cache configuration.

/ / private fun configMemoryCache(builder: ImagePipelineConfig.Builder) { builder.setBitmapMemoryCacheParamsSupplier(object : Supplier<MemoryCacheParams> { override fun get(): MemoryCacheParams {val cacheSize = 50 * byteconstants. MB return MemoryCacheParams(cacheSize,// Maximum size of a total image in the memory cache, in bytes MAX_VALUE,// The maximum number of images in the memory cache Runtime.getruntime ().maxMemory().toint ()/8,// The maximum size, in bytes, of total images in the memory cache that are ready to be cleared but not yet deleted Int.MAX_VALUE,// The maximum number of images in the memory cache to be cleared in.max_value // the maximum size of a single image in the memory cache)}})Copy the code

Optimization point: The size of disk cache and memory cache can be appropriately increased according to the situation of mobile devices. With more cache space to hold more elements, it is theoretically possible to increase the cache hit ratio and thus speed up image loading.

According to the system state to release the corresponding memory

onTrimMemory

OnTrimMemory onTrimMemory is an API provided by the system. Its main function is to remind developers that when memory is insufficient, they can release memory by processing some resources, so as to avoid being killed by the Android system.

When onTrimMemory gets tight, we can clean up the Fresco image cache, freeing up some memory. When we come back, the memory cache has been removed, but we can also request reloads from disk cache or network.

MemoryTrimmableRegistry

MemoryTrimmableRegistry, a memory modulator provided by Fresco. RegisterMemoryTrimmable can be called to register various classes that implement the MemoryTrimmable interface. MemoryTrimmableRegistry can notify registered MemoryTrimmable that memory usage needs to be adjusted accordingly.

MemoryTrimmable, the default Fresco cache management class, already implements this interface and can automatically resize the cache based on the MemoryTrimType

public interface MemoryTrimmableRegistry {
  void registerMemoryTrimmable(MemoryTrimmable trimmable);
  void unregisterMemoryTrimmable(MemoryTrimmable trimmable);
}
Copy the code

Custom MemoryTrimmableRegistry

Default: the default implementation class NoOpMemoryTrimmableRegistry, there is no any processing.

Optimization: We can implement the MemoryTrimmableRegistry interface ourselves, using a list to hold registered MemoryTrimmable objects. When memory is low, the list is traversed to notify the registered MemoryTrimmable object and the corresponding trim method is invoked.

  • Background process, and the current phone memory is tight, trimType is set toMemoryTrimType.OnAppBackgroundedIn this case, Fresco reclaims all the memory cache
  • The foreground process, but the current phone memory is tight, trimType is set toMemoryTrimType.OnSystemLowMemoryWhileAppInForegroundIn this case, Fresco reclaims half of the memory cache, freeing up memory stress.
/ * * * custom memory controller * / object CustomMemoryTrimmableRegistry: MemoryTrimmableRegistry { private val TAG = "CustomMemoryTrimmableRegistry" val list = CopyOnWriteArrayList<MemoryTrimmable>() fun init(application: Application) {/ / register callback, listening onTrimMemory Application. RegisterComponentCallbacks (object: ComponentCallbacks2 { override fun onConfigurationChanged(newConfig: Configuration) { } override fun onLowMemory() { } override fun onTrimMemory(level: Int) { dispatchTrim(level) } }) } override fun registerMemoryTrimmable(trimmable: MemoryTrimmable?) { list.add(trimmable) } override fun unregisterMemoryTrimmable(trimmable: MemoryTrimmable?) { list.remove(trimmable) } private fun dispatchTrim(level: Int) { var trimType: MemoryTrimType? = null if (level > ComponentCallbacks2. TRIM_MEMORY_BACKGROUND) {/ / daemons trimType = MemoryTrimType. OnAppBackgrounded} TrimType = elseIf (level > ComponentCallbacks2.trim_memory_running_low) {elseIf (level > ComponentCallbacks2.trim_memory_running_low) MemoryTrimType.OnSystemLowMemoryWhileAppInForeground } trimType ? : return Log.d(TAG, "onTrimMemory trimType:$trimType") list.forEach { it.trim(trimType) } } }Copy the code

Manual Cache management

By default, Fresco has three levels of cache: Bitmap cache + undecoded image cache + hard disk cache. There are two levels of memory cache, BitmapCache and EncodingCache, which occupy a certain amount of memory. Some memory is not released until onTrimMemory notifies it that it is out of memory.

  • Optimization:Manually add or remove caches when appropriate.

Image preloading

  • This can be done for some scenarios, such as image list streaming scenarios. Generally, the list data is obtained through the network request first, and then the data source is set to RecyclerView, ViewHolder sets the image URL to the corresponding SimpleDraweeView for display, and SimpleDraweeView loads the network image at this time.
  • Optimization: Network data can come back afterTry preloading the first few partial images into BitmapCacheIn the. In this way, when loading, the Bitmap can be directly obtained from the Bitmap cache, and the user can see the image more quickly.
Fresco.getImagePipeline().prefetchToBitmapCache()
Copy the code

Remove the specified image cache (memory + disk)

  • For example, you can only keep sliding backwards. Therefore, when we switch to the next card, the BitmapCache corresponding to the image of the previous card can be removed, freeing up memory resources in time.
Fresco.getImagePipeline().evictFromMemoryCache(imageUrl)
Copy the code

Remove expired image cache (disk)

  • For example, after the startup, you can clear the disk cache generated 24 hours ago to reduce storage space usage.
  • You can implement this logic using WorkManager.WorkManager is the recommended task scheduler for the Android platform for handling deferred work while ensuring that it is executed.
/** * Fresco disk cache, clean task. * define a new Worker, call FileCache clearOldEntries (long cacheExpirationMs) clear disk cache expiration time of the image. */ class FrescoCacheCleanWorker(context: context, params: WorkerParameters) : Worker(context, params) {private val TAG="FrescoCacheCleanWorker" 24 HOURS of private val cacheExpirationMs = TimeUnit. MILLISECONDS. Convert (24, TimeUnit. HOURS) override fun doWork () : Result { Fresco.getImagePipelineFactory().mainFileCache.clearOldEntries(cacheExpirationMs) return Result.success() } }Copy the code

Custom cache algorithm

The cache hit ratio has something to do with the elimination algorithm that the cache uses.

The Fresco cache algorithm uses LRU by default. When the cache space is full, objects that have not been accessed for the most recent time are cleared.

A custom implementation of CountingMemoryCache interface, replacing the default LRUCache, using other caching algorithms, to manage cache-related logic. (Here to provide ideas, because I have not been specific practice, theoretically feasible)

Common cache elimination algorithms

Indicators to monitor

Cache hit ratio

  • Cache hit ratio is a very important problem in cache, it is an important index to measure the effectiveness of cache. The higher the hit ratio, the higher the cache utilization. The ability to read data directly from the cache improves performance.
  • Memory cache hit ratio and disk cache hit ratio
  • Calculation method: Hits/(Hits + misses)
  • How to do it: Fresco provides oneImageCacheStatsTrackerCan implement ImageCacheStatsTracker. In this class, each cached event has a callback notification, based on which you can count and count the cache.
object FrescoImageCacheTracker : ImageCacheStatsTracker {private Val TAG = "FrescoImageCacheTracker" /** * memoryCacheHitCount: Number of hits * MemoryCacheMissCount: MemoryCacheHitCount /(memoryCacheHitCount+memoryCacheMissCount) */ private var memoryCacheHitCount = Private var memoryCacheMissCount = AtomicInteger() Private var memoryCacheMissCount = AtomicInteger()  CacheKey?) { memoryCacheHitCount.incrementAndGet() Log.d(TAG, "onBitmapCacheHit , CacheKey :$cacheKey")} override fun onbitmapCacheemiss (cacheKey: $cacheKey? { memoryCacheMissCount.incrementAndGet() Log.d(TAG, "onBitmapCacheMiss , Override fun onDiskCacheHit(cacheKey: cacheKey?) {log. d(TAG, "onDiskCacheHit, cacheKey:$cacheKey")} override fun onDiskCacheMiss(cacheKey: $cacheKey?) { Log.d(TAG, "onDiskCacheMiss , cacheKey:$cacheKey") } }Copy the code

Statistics the memory used by Fresco

Generally, we will monitor the overall memory of the App. At this time, we can also report Fresco memory usage data to help us troubleshoot problems.

/** * FrescoMemoryStat {bitmap+encode val memorySize: String get() { return ( ImagePipelineFactory.getInstance().bitmapMemoryCache.sizeInBytes + ImagePipelineFactory. GetInstance (). EncodedMemoryCache. SizeInBytes) byteToString ()} / / take up disk size: mainCache + smallCache val diskCache: String get() { return ( ImagePipelineFactory.getInstance().mainFileCache.size + ImagePipelineFactory.getInstance().smallImageFileCache.size ).byteToString() } }Copy the code

Image loading success rate statistics

Fresco provides global listeners to listen to each step of the image loading process, and multiple custom RequestListeners can be set up when Fresco is configured.

In Fresco, Producer represents a step in the picture loading process, such as network loading, decoding, and so on. Each Producer has a specific name, so we only need to parse the events of the Producer we are interested in in the callback.

  • Success rate = Number of successes /(Number of successes + Number of failures)
  • Implementation: InheritanceBaseRequestListenerThrough theonRequestSuccessandonRequestFailureDo the counting and finally figure out the result.
  • When onRequestFailure (image loading failure) occurs, you can obtain the throwable corresponding to the loading failure and report it to the server for later repair.
/ * * * picture loading rate statistics * success = number of successful/count + failure (success) * / object FrescoImageSuccessStatListener: BaseRequestListener() { private val TAG="FrescoRequestListener" private val successCount = AtomicInteger() private val failCount = AtomicInteger() val succssRatio: Float get() { return successCount.get().toFloat() / (successCount.get() + failCount.get()).apply { Log.d(TAG, "successCount:$successCount,failCount:$failCount ,ratio:$this") } } override fun requiresExtraMap(requestId: String?) : Boolean { return true } override fun onRequestSuccess(request: ImageRequest? , requestId: String? , isPrefetch: Boolean) { Log.d(TAG, "onRequestSuccess,requestId:$requestId,isPrefetch:$isPrefetch ") successCount.incrementAndGet() } override fun onRequestFailure( request: ImageRequest? , requestId: String? , throwable: Throwable? , isPrefetch: Boolean ) { Log.d(TAG, "onRequestFailure,requestId:$requestId,isPrefetch:$isPrefetch,throwable:$throwable,request:$request") failCount.incrementAndGet() } override fun onRequestCancellation(requestId: String?) { Log.d(TAG, "onRequestCancellation,requestId:$requestId") } }Copy the code

Network picture loading speed statistics

By default, the image was obtained from the network layer flow is HttpUrlConnectionNetworkFetcher. In the process, will automatically mark some point in time, the corresponding time stamps recorded in HttpUrlConnectionNetworkFetchState, data will be filled into the ExtraMap. (Using Okhttp as the network layer for fresco, the corresponding classes would be OkHttpNetworkFetcher and OkHttpNetworkFetchState.)

Through a simple log printing, you can see, NetworkFetchProducer trigger onProducerFinishWithSuccess callback, can fill extraMap data back.

ExtraMap :{queue_time=193, total_time=358, image_size=201925, Fetch_time =165 fetch_time=165 fetch_time=165 fetch_time=165Copy the code

public class HttpUrlConnectionNetworkFetcher @Override public Map<String, String> getExtraMap( HttpUrlConnectionNetworkFetchState fetchState, int byteSize) { Map<String, String> extraMap = new HashMap<>(4); extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime)); extraMap.put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime)); extraMap.put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime)); extraMap.put(IMAGE_SIZE, Integer.toString(byteSize)); return extraMap; } } public static class HttpUrlConnectionNetworkFetchState extends FetchState { private long submitTime; private long responseTime; private long fetchCompleteTime; public HttpUrlConnectionNetworkFetchState( Consumer<EncodedImage> consumer, ProducerContext producerContext) { super(consumer, producerContext); }}Copy the code

Since data can get to, that is the custom FrescoNetWorkImageListener to obtain the data needed, these fields can then be reported to the server. For a simple implementation, see the following.

/ * * * network image monitoring * / object FrescoNetWorkImageListener: BaseRequestListener() { private val TAG = "FrescoNetWorkImageListener" override fun onProducerFinishWithSuccess( requestId: String? , producerName: String? , extraMap: MutableMap<String, String>? ) {the d (TAG, "onProducerFinishWithSuccess, requestId: $requestId, producerName: $producerName, extraMap:$extraMap" ) if (producerName == NetworkFetchProducer.PRODUCER_NAME && extraMap ! = null) {//NetworkFetchProducer, Fetch_time = extraMap["queue_time"] val fetch_time = extraMap["fetch_time"] val total_time = ExtraMap ["total_time"] val image_size = extraMap["image_size"] Override Fun requiresExtraMap(requestId: String? : Boolean { return true } }Copy the code