Android性能优化-实践篇-Frida监测Bitmap

151 阅读3分钟

系列文章

1、Android性能优化-Frida工具篇

2、Android性能优化-实践篇:Frida监测Binder调用

3、Android性能优化-实践篇-Frida监测Bitmap

一、前言

对于现代应用来说,图片内容是必不可少的,但是由于图片的显示性质,在Android上面需要占用大量的Native内存,并且这部分很难感知,往往是出现OOM之后,才回过头审视这部分。

在Android上面,图片基本都是以Bitmap的形式加载,不管是本地图片,还是网络图片,都会通过某种形式转换成Bitmap来进行展示,以图片A为例,宽高为1080x1920,带透明度,Bitmap占用内存可以达到7.5M(1080x1920x4),如果size更大则占用更大。

所以对于Bitmap的监测是很重要的一件事情,避免内存水位过高,甚至发生OOM。

二、使用Frida监控Bitmap

2.1 分析

实际上目前已经有一些监控Bitmap的手段,但是往往需要集成到App中,且使用过于复杂。

我们依然可以使用Frida来进行Bitmap的监控。

我们知道Frida可以用来Hook Java层方法,那么能不能找一个Bitmap在Java层的统一入口?回忆一下,Bitmap是如何从Resource里面加载的,往往是通过BitmapFactory来进行decode。实际上BitmapFactory提供了多个decode方法,decodeFile/decodeStream/decodeResource,即便是Glide、Fressco这种三方框架,最终也是通过这些方法来实现。

所以我们考虑Hook BitmapFactory的decode这一系列方法。

  • 1、添加方法hook
  • 2、拿到Bitmap对象
  • 3、进行大小判定,如果大于某个阈值(1024x1024),就将此对象打印出来
  • 4、打印对应堆栈

到这里我们发现,打印堆栈并不能解决问题,有一些是从网络加载的图片,堆栈并不清楚,那么有没有更好的办法?

实际上在Frida里面,这里有了Java的运行环境,我们可以模拟实现Bitmap的文件写入,也就是:将异常的图片写入到系统中,然后通过adb pull出来,这样就拿到了异常图片本身,拿到之后我们可以容易得判断此图片是否合理,以及云端是否配置了异常的图片。

2.2 实现

代码如下:

Java.perform(function () {
    const Bitmap = Java.use('android.graphics.Bitmap');
    var Throwable = Java.use("java.lang.Throwable");
    var Environment = Java.use('android.os.Environment');
    var File = Java.use('java.io.File');
    var FileOutputStream = Java.use('java.io.FileOutputStream');
    var CompressFormat = Java.use('android.graphics.Bitmap$CompressFormat');
    var BitmapFactory = Java.use('android.graphics.BitmapFactory');
    // Hook主要解码方法
    var decodeMethods = [
        'decodeFile',
        'decodeStream',
        'decodeByteArray',
        'decodeResource'
    ];
    var totalSize = 0;
    var mb = 1024 * 1024;
    var lastIndex = 0;
    decodeMethods.forEach(function (method) {
        BitmapFactory[method].overloads.forEach(function (overload) {
            overload.implementation = function () {
                var bitmap = this[method].apply(this, arguments);
                if (bitmap !== null) {
                    var size = bitmap.getByteCount();
                    if (size > 200 * 1024) {
                        console.log('[Bitmap Constructor]');
                        console.log('  ➤ Width  : ' + bitmap.getWidth());
                        console.log('  ➤ Height : ' + bitmap.getHeight());
                        console.log('  ➤ Size: ' + size);
                        console.log('  ➤ Thread       : ' + Java.use('java.lang.Thread').currentThread().getName());
                        var stack = Java.use("android.util.Log").getStackTraceString(
                            Throwable.$new()
                        );
                        console.log(stack);
                        save(bitmap);
                    }
                    totalSize += size;
                    if (totalSize % mb != lastIndex) {
                        console.log(`Bitmap total size : ${(totalSize / mb).toFixed(2)} Mb`);
                    }
                }
                return bitmap;
            };
        });
    });

    function save(bitmap) {
        try {
            // 准备保存路径(SD卡下载目录)
            var downloadsDir = Environment.getExternalStoragePublicDirectory(
                "Download"
            );
            var filename = 'frida_bitmap_' + Date.now() + '.png';
            var outputFile = File.$new(downloadsDir, filename);
            // 保存Bitmap为PNG
            var fos = FileOutputStream.$new(outputFile);
            bitmap.compress(CompressFormat.PNG.value, 100, fos);
            fos.close();
            console.log("[+] Bitmap saved to: " + outputFile.getAbsolutePath());
        } catch (e) {
            console.log("[-] Error saving bitmap: " + e);
        }
    }
});

效果如下:

f0e6df4b-2de0-46e7-a989-28751628d6bd.png

将其pull出来之后,可以直接在本地电脑上查看:

图片.png

三、总结

至此,我们可以对自己的应用进行Bitmap监控,如果发现有异常,定位解决即可,此方案也是仅用于本地监测,如果要监测线上异常,需要考虑别的实现。