美文网首页
[MISC]关于Bitmap在Android平台的整理

[MISC]关于Bitmap在Android平台的整理

作者: panlinlin_js | 来源:发表于2019-04-02 18:20 被阅读0次

    前言

    最近看了一个问题,和 bitmap有关,由此回顾自己的工作经历,发现和图片打交道时间其实挺多的,包括曾经参与好几款的图库和修图app的开发,也主导过一款图库的bug fix,想想其实是可以做个 bitmap在 Android平台的整理。

    基础知识

    在谈 Android平台的 bitmap之前,我们先来回顾或学习下关于图片的一些基础知识。

    1、图片的存储

    回归到最原始状态,由冯诺伊曼集大成的提出二进制和现代电子计算机架构后,聪明的人类利用电子器件的物理特性,让计算机世界一直运行在二进制世界,且之后一直在蓬勃发展。而在二进制世界里,想要表达任何信息,就需要也只能使用0和1两个数字来表达(当然想要完整的构建一个二进制世界,逻辑运算必不可少,这里只讨论“静态”信息,不做过多拓展)0或1所占的空间为最小单位,8个bit对应的byte则在程度上更像是一个基本单位,当然还有KB,MB,GB,依次以1024的倍数拓展,正是这样宏观数量级的比特位构成了丰富多彩的计算机世界。

    而计算机能提供的信息形式其实是可以直接罗列出来的,无外乎:文字,图片,声音,视频。文字的话,对于美国人来说想要表达其实只要能表达26个字母再做组合就行了,所以最初的 ANSI码就是用8个bit位能表达的信息(28)的头127位就表达了包括常见的标点、数字、字母还分大小写。当然之后还有中国人的GB2312,用16个bit位,也就是两个byte来表达一个汉字,再到后来国际通用的utf-8。除了文字之外,声音和视频在这里不讨论,那图片呢,该怎么表达?

    图片其实可以看作是一个一个点密集的拼凑而成,也就是常称作的像素,所以粗暴点来看只要能表达像素也就可以表达图片了。事实也就是这么粗暴,图片就是用bit位表达的像素堆积而成,这应该也是 Bitmap名称的由来吧。那么表达一个像素需要多少几个bit位呢?我们先来看像素表达的信息有哪些,最直观的就是颜色,再来可能还有一个透明度。如果用RGB颜色模型来展现的话,需要一个R(red),一个G( green),一个B(blue)来混合组成一个颜色,如果红色按照程度分有256种的话,则需要8个bit位来表达一个红色,绿色和蓝色亦然,再加上256种透明度,所以想要高精度的表达一个像素,就需要四个字节。而我们常说的手机摄像头有一千万像素,两千万像素,也就是实实在在的有这么多像素,那么来简单算一下,一千万像素相当于10x1024x1024,每个像素所占空间有4个字节,也就是40x1024x1024个byte,约等于40MB,注意这里的40MB就是一张图片加载到内存里然后做渲染展示到屏幕上所需要的空间。

    一张图片40MB内存是非常惊人的,我们都知道Android平台的每个终端出厂都有设定允许运行app的最大堆内存。早期的Android机最大堆内存小的可怜,要是同时加载几张图片不就是直接爆了。至于怎么处理后面再来介绍。既然是图片的存储,除了在内存中,当然还有磁盘中的存储。

    关于图片在磁盘上的存储,我想大家都有了解。按照前面所说的,表达一张图片需要40MB,就算在磁盘上也是很大的压力,开玩笑,25张图片一个G,存不起啊。不过确实有这样的图片文件,就是windows平台的bmp格式的位图文件。但是我们平时使用的图片文件当然不会那么大,常见的图片文件jpeg,png,gif,svg等后缀结尾的文件。这些文件格式其实代表了不同的压缩算法,既然这么大,只能压缩啊。压缩的中心思想也是围绕像素展开,如果一张图片一行像素都一样,那显然不用存储每个像素,压缩算法不做展开,我也不熟,在这里稍微提一下,压缩算法分为有损压缩和无损压缩,我们常见的jpeg是有损压缩,png是无损。

    Bitmap in Android

    1、java层的bitmap

    前面说了这么多,下面我们来看下Bitmap在Android中的具体实现。首先看下Bitmap相关的Java类所在位置(注意,以下代码以Android O为标准):

    ~aosp/frameworks/base/graphics/java/android/graphics/Bitmap.java~
    

    与它同级的还有BitmapFactory.java BitmapRegionDecoder.java ColorSpace.java 都分在了/framework/base/graphics包下,注意这里的graphics我个人称作是狭义上的graphics,特指可绘制的素材和绘制相关的类组成的包,并不指Android平台或其它操作系统常说的graphics子系统,关于graphics subsystem我后面准备分几篇wiki尝试讲明白讲通透,敬请期待。

    其实关于Bitmap的类是不多的,上层开发常用的其实就是Bitmap 和 BitmapFactory,当然除此之外少不了的是各个开发者各种三方框架及各个厂商自己定义的工具类和解决方案。再回到Bitmap.java 首先看它的强关系,继承了谁,实现了谁:

    public final class Bitmap implements Parcelable {
    

    从实现了Parcelable接口来看,Bitmap已经明示了我们它会参与序列化传递,换句话说就是大概率会跨进程传递。再看Bitmap.java中包含的变量和方法:

    Bitmap.java的变量和方法

    总的来看,Bitmap.java中连带TAG共有154个方法和内部变量,除常量最靠前的mNativePtr变量看名字就知道是Bitmap连接JNI的桥梁,其实它就是实际的bitmap,native的bitmap之后会介绍到。继续看其它的变量和方法:
    除了一些看名字就知道意思的常用变量,有一个内部的联合体稍微提一下:


    Bitmap.Config

    这个Config联合体定义了配置图片解析时针对每个像素的存储配置,默认的是ARGB_8888也就是我们之前提到的在alpha, R, G, B四个通道用8个bit位来表示一个像素,另一个用的比较多的是RGB_565,意思是alpha通道不表示即图片不分透明度,然后RGB分别用5个6个5个bit位来表达,这样总共16个bit位占用两个字节,比ARGB_8888的方式节省一半的空间。ARGB_4444看起来是个比较好的折中方案,又能显示透明度又只占一半的空间,但是它现在已经被Deprecated了,原因是显示质量低。毕竟只用12位(RGB=4*3)在现代追求视网膜屏的大环境下来展示丰富多彩的颜色,确实有点太差了。至于其它的几种配置方式,不再多做介绍。除了这些,Bitmap.java对上层暴露的接口还包括压缩、复制,获取基本属性等。

    Bitmap.java更像是对一堆像素集做了一个封装,这堆像素集堆在一起自然有了自己的属性,例如长,宽,密度等。那从磁盘中把一个压缩文件(图片文件)解析的操作放在这样的一个抽象里好像有点不合适,所以decode bitmap的几种重载方法放到了BitmapFactory.java中,我们简单过一下:

    首先就如名字图片工厂一般,它的几乎所有方法都是decode 操作,包括从不同资源下decode bitmap:


    decode methods

    值得一提的是它一个静态内部类Options:


    Options

    这个Options设定了decode bitmap时你能配置的参数,比如inPreferredConfig就是配置我们之前讲到的像素存储方式,默认是ARGB_8888,这些Options的组合使用加上合适的像素存储格式,就能基础的展现一波小小的操作来应对这个难搞的Bitmap了。至于这里的小操作和全局的大操作该怎么做我们放在下一节图片的处理和优化来稍作展开,这里不多做解释。最后需要注意的是这里的decode方法全是耗时方法,想想也是,这可是动辄成百上千万的像素解析,不耗时才见了鬼了。

    介绍到这里,想必大家对图片本身和图片在Android上的封装有了简单的认识。但是这里有几个问题:图片既然这么耗内存,这些内存是怎么分配的?又是分配在哪呢?这样的大的内存该怎么跨进程传递?既然是关于Bitmap的所有,那Bitmap中像素点是怎么渲染到屏幕上的?和普通的GUI又有什么不同呢?来,我们继续看。

    2、Bitmap的内存追踪

    2.1 Bitmap内存的分配

    我们直接来看下,bitmap在关于内存的操作在代码中的实现。前面讲到,BitmapFactory是用来从资源里decode bitmap的方法类,那就从它下手我们来看看bitmap 被decode后内存到底是去了哪里。从最直观的decodeFile开始,可以发现decodeFile开了一个文件流,交给了decodeStream来处理:

    public static Bitmap decodeFile(String pathName, Options opts) {
    validate(opts);
    Bitmap bm = null;
    InputStream stream = null;
    try {
    stream = new FileInputStream(pathName);
    bm = decodeStream(stream, null, opts);
    } catch (Exception e) {
    //...
    }
    return bm;
    }
    

    而decodeStream的实现位decodeStreamInternal,那我们来看下decodeStreamInternal在干什么:

    private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
    byte [] tempStorage = null;
    if (opts != null) tempStorage = opts.inTempStorage;
    if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
    //放在了native处理,意料之中
    return nativeDecodeStream(is, tempStorage, outPadding, opts);
    }
    

    nativeDecodeStream方法毫无疑问的位于/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp,核心方法为doDecode:

    static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    

    //...这个方法真的很长,我们只看我们关心的部分。

    // 创建一个codec
    std::unique_ptr<SkAndroidCodec> codec(SkAndroidCodec::NewFromStream(
    streamDeleter.release(), &peeker));
    
    // 定义几个Allocator,然后根据条件选择对应的allocator赋值给decodeAllocator指针变量,值得注意的是BItmapFactory.h 中include的是
    "GraphicsJNI.h" 所以这里HeapAllocator位GraphicsJNI中的HeapAllocator
    HeapAllocator defaultAllocator;
    RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);
    ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
    //位于Skbitmap中的HeapAllocator和GraphicsJNI中同名,容易弄混,这个只在需要scale bitmap或是需要硬件支持时才会用到
    SkBitmap::HeapAllocator heapAllocator;
    SkBitmap::Allocator* decodeAllocator;
    if (javaBitmap != nullptr && willScale) {
    // 如果从java端传入了可重用的bitmap,那么使用如下的分配器
    decodeAllocator = &scaleCheckingAllocator;
    } else if (javaBitmap != nullptr) {
    decodeAllocator = &recyclingAllocator;
    } else if (willScale || isHardware) {
    decodeAllocator = &heapAllocator;
    } else {
    decodeAllocator = &defaultAllocator;
    }
    
    // 定义一个类型位skbitmap的bitmap变量,注意这里有个tryAllocPixels方法,开始分配内存,这一步完成后,allocator中的mStorage变量被赋值了给bitmap分配的内存了。
    SkBitmap decodingBitmap;
    if (!decodingBitmap.setInfo(bitmapInfo) ||
    !decodingBitmap.tryAllocPixels(decodeAllocator)) {
    return nullptr;
    }
    
    // 使用codec开始解码图片,注意这个skbitmap的getPixels方法,获取到的是pixel的地址,稍微留意下。
    SkCodec::Result result = codec->getAndroidPixels(decodeInfo, decodingBitmap.getPixels(),
    decodingBitmap.rowBytes(), &codecOptions);
    // 定义一个outputBitmmap用来存放处理后的bitmap
    SkBitmap outputBitmap;
    if (willScale) {
    //...如果是需要缩放图片的操作,单独处理
    SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
    canvas.scale(sx, sy);
    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
    } else {
    //如果不是的话直接置换
    outputBitmap.swap(decodingBitmap);
    }
    // 最后创建一个java bitmap返回,这其中会将bitmap包装成一个long型数值存在java端,这long型数值就是前面的mNativePtr
    return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
    bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
    }
    

    这个方法从上到下基本涵盖了decode的所有操作,而我们最关心的其实是
    tryAllocPixels的具体做了什么:

    bool SkBitmap::tryAllocPixels(Allocator* allocator) {
    HeapAllocator stdalloc;
    
    if (nullptr == allocator) {
    allocator = &stdalloc;
    }
    // 下面交给了allocator来处理,如果在Android O之前会处理color table变量
    #ifdef SK_SUPPORT_LEGACY_COLORTABLE
    return allocator->allocPixelRef(this, nullptr);
    #else
    return allocator->allocPixelRef(this);
    #endif
    }
    

    注意:这里传入的allocator不为空,传入的allocator位于GraphicsJNI中,使用这个allocator来做分配工作

    bool HeapAllocator::allocPixelRef(SkBitmap* bitmap) {
    mStorage = android::Bitmap::allocateHeapBitmap(bitmap);
    return !!mStorage;
    

    allocateHeapBitmap方法位于hwui lib库中,它的实现位于allocateBitmap方法:

    <Bitmap> allocateBitmap(SkBitmap* bitmap, AllocPixelRef alloc) {
    const SkImageInfo& info = bitmap->info();
    if (info.colorType() == kUnknown_SkColorType) {
    return nullptr;
    }
    size_t size;
    const size_t rowBytes = bitmap->rowBytes();
    if (!computeAllocationSize(rowBytes, bitmap->height(), &size)) {
    return nullptr;
    }
    // 在这里开始真正的分配内存工作,用的是alloc函数,也就是说原来Android
    // O是直接在native上为bitmap分配了内存
    auto wrapper = alloc(size, info, rowBytes);
    if (wrapper) {
    wrapper->getSkBitmap(bitmap);
    }
    return wrapper;
    }
    

    从上面的代码分析可以看出,一个bitmap在上层指定各种option后,传入native中,在Android O里直接在native中为bitmap分配了内存。也就是说老一套的在java heap中分配内存并且不好好处理会导致应用程序OOM的情况在O上不存在了,这个也是我在写这篇wiki才发现的。

    这里也提一下Android2.3.3 之后,Android O之前,这中间的很长时间里bitmap内存的分配情况,也就是目前最主流的Android 版本上bitmap内存的分配情况:在这些版本中以上的方法在doDecode之后会有所区别,默认的allocator是JavaPixelAllocator 是在java heap上为bitmap分配内存,上层应用时刻要关心bitmap 对象有没有垃圾回收器处理,必要时需要主动recycle。Android O上的改动说实话是做了一些图片三方处理框架如Fresco在做的事情,个人认为只会让不负责任的程序员更加不负责任。Android O之前关于Bitmap 在内存中的分配因为篇幅原因此处就不贴了,总体处理流程一致,allocator不同。

    2.2 bitmap内存的跨进程传递

    关于bitmap在内存这块的整理,还需要提到它在跨进程传递时的情况。前面已经提过Bitmap实现了Parcelable接口,那么说明bitmap是可以序列化传递的,我们首先看下中writeToParcel方法的实现:

    jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
    jlong bitmapHandle,boolean isMutable, jint density,jobject parcel) {
    //...省略一些基础属性的写入
    // 获取了一个匿名共享内存的FD
    android::status_t status;
    int fd = bitmapWrapper->bitmap().getAshmemFd();
    if (fd >= 0 && !isMutable && p->allowFds()) {
    // 将fd写入parcel中,在这一步会复制一个fd
    status = p->writeDupImmutableBlobFileDescriptor(fd);
    if (status) {
    return JNI_FALSE;
    }
    return JNI_TRUE;
    }
    // 如果bitmap不是写在匿名共享内存中的
    // parcel中写入一个blob data,这个data存放的是bitmap的内存数据
    bool mutableCopy = isMutable;
    //...
    size_t size = bitmap.getSize();
    android::Parcel::WritableBlob blob;
    status = p->writeBlob(size, mutableCopy, &blob);
    const void* pSrc = bitmap.getPixels();
    if (pSrc == NULL) {
    memset(blob.data(), 0, size);
    } else {
    //将bitmap的数据复制到blob data中
    memcpy(blob.data(), pSrc, size);
    }
    blob.release();
    return JNI_TRUE;
    }
    

    同样的我们来看下createFromParcel的实现

    static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) {
    android::Parcel* p = android::parcelForJavaObject(env, parcel);
    //...中间省略基础属性的获取
    //
    std::unique_ptr<SkBitmap> bitmap(new SkBitmap);
    // 读取bitmap blob的数据放在blob变量里面
    size_t size = bitmap->getSize();
    android::Parcel::ReadableBlob blob;
    android::status_t status = p->readBlob(size, &blob);
    
    sk_sp<Bitmap> nativeBitmap;
    //如果blob存放数据成功且大小大于匿名共享内存的规定尺寸
    if (blob.fd() >= 0 && (blob.isMutable() || !isMutable) && (size >= ASHMEM_BITMAP_MIN_SIZE)) {
    // Dup the file descriptor so we can keep a reference to it after the Parcel
    // is disposed.
    // 复制一个文件描述符
    int dupFd = dup(blob.fd());
    // bitmap映射到匿名共享内存然后创建一个native bitmap
    nativeBitmap = sk_sp<Bitmap>(GraphicsJNI::mapAshmemBitmap(env, bitmap.get(),
    dupFd, const_cast<void*>(blob.data()), size, !isMutable));
    
    // Clear the blob handle, don't release it.
    blob.clear();
    } else {
    // 否则新建一个bitmap 将数据复制到新的bitmap中
    nativeBitmap = Bitmap::allocateHeapBitmap(bitmap.get());
    memcpy(bitmap->getPixels(), blob.data(), size);
    
    // Release the blob handle.
    blob.release();
    }
    
    return createBitmap(env, nativeBitmap.release(),
    getPremulBitmapCreateFlags(isMutable), NULL, NULL, density);
    }
    

    从上面的代码分析我们可以看出,bitmap在跨进程传递时,如果之前是以匿名共享内存的形式存储,则在parcel中传递的是fd,如果是其他形式则在parcel中传递的就是实实在在的二进制数据。

    值得一提的是在传递fd时,写入parcel新复制了一个fd,读取parcel的时候同样复制了一个fd,所以每传递一个bitmap至少两个fd会被创建。之所以提到这个是因为我们有一个因为bitmap fd过多导致system server重启的问题。在那个问题中system_server充当中间人在app和systemUI中传递带有bitmap的notification,过多的bitmap传递直接使system_server的fd数量超过限制,从而导致系统重启,类似的问题可能会在其他地方复现,值得稍微留意一下。

    3、Bitmap的渲染

    说完bitmap的内存,我们来说说bitmap的渲染。view的渲染或是bitmap的渲染准备放在其他wiki上仔细分析,这里只简单提一下,不做太多拓展。首先了解一下skia图形库,在之前的很长一段时间,Android使用skia来绘制2d图形,openGL来绘制3d图形,但是skia作为一个向量绘图库,使用cpu来进行运算,性能瓶颈尤其明显。之后hwui逐渐取代skia开始为2d图形同样提供硬件支持,直至目前的app默认开启硬件加速,默认使用hwui绘制,skia只在其中做些少量的绘制工作。

    这样的话就变成上层app准备好的数据基本全交给hwui来处理,而hwui和openGL强相关,换句话说,bitmap的数据是交到openGL中做处理,所以bitmap和普通的GUI在渲染上的区别主要就是它们在openGL中的区别。

    事实上bitmap对应openGL中pixel data的概念,而普通的GUI则对应vertex的概念,当在做绘制时通过display list做缓存,最终通过surfaceflinger 和 hardware composer做合成显示到display上。这一块的东西非常多,在此点到为止,我们在这里只需要知道bitmap在渲染上也是和普通GUI有所区别即可。

    图片在应用程序中的处理和优化

    前面提到图片首先它吃内存,其次decode它本身就是耗时操作,再加上操作图片的应用程序不会有例外的都需要有很高的用户体验和顺滑度,最好来些酷炫的手势操作,批量处理,幻灯片什么的那哪能少,所以不好处理。如果说稳定性延伸到单个app,那么图片造成的问题可能是大头。

    但是前面在分析bitmap内存时发现如今的bitmap内存时分配到native中,java heap的大小限制对与bitmap来说忽然说没就没了,但是,我们该怎么优化仍是怎么优化。

    在这之前,仍然把一些前提讲清楚。就是图片虽然是有千万级像素,但是我们的手机屏幕以1080p=1920*1080举例来说,只有大概两百万的像素。也就是说你把一张千万级别的像素全部解析出来做整张图的预览,千万只能展示百万,本身就没意义。所以我们对图片的decode,通常都是两步,第一步在option中设置属性只获取bounds,获取到尺寸后计算一个合理的缩放倍数,再传入option 来decode一张合理大小的图片。

    在通用的图库开发中,我把图片按照尺寸分为微型图、小图、中图、大图、超大图五种。以屏幕宽度为标准,微型图占宽度的五分之一以下,小图五分之一到三分之一左右,中图三分之一到二分之一左右,大图通常是千万级像素的图片,超大图为超过五千万像素以上的图片且在手机上展示需要还原细节的图片。

    再对这些不同尺寸的图片的使用频率做量化,小图主要的预览图最常用到,中图作为相册封面、连拍预览等视图可以少量缓存,大图通常是在大图预览中才会遇到且消耗内存,最多缓存1-3张,超大图的缓存不做考虑。与此同时,不同尺寸的图片质量要求也不一样,小图使用RGB_565的格式即可,这样可以多缓存一倍的小图,大图通常需要ARGB_8888来还原图片细节,毕竟是在预览界面。

    这样的话我们就对图片大概分好了类,下面就是做缓存,通常缓存是两级缓存,也就是内存缓存和磁盘缓存。主要的做法是在内存中开辟一块限制大小的区域缓存一些使用频率高的小图,当然这些小图很多时候是通过裁剪获取到的,得来不易,所以肯定要在磁盘中缓存一份。应用需要展示图片的时候先在内存缓存中找,找不到再从磁盘中找解析后放在内存缓存中。耗时解析的操作可以自己封装线程池或是用AsyncTask,就不多说。

    然后就是一些小技巧,就是小图可以在快速切换展示大图的时候做大图的过度,超大图可以分块解析,实时回收内存达到手机也可以看世界地图的效果。有兴趣的同学请私聊。

    总结

    图片作为重要的应用广泛的多媒体介质,谈及它的方方面面均可以做延伸拓展,作为一个了解性的wiki,在这里很多方面没有多讲,其实有些地方连提都没提,比如颜色一些的基础知识,图片的编码流程分析等等。讲到的一些,一部分可以作为了解性阅读,一部分也确实会和稳定性工作有关联,希望能对看的人有些启发。

    相关文章

      网友评论

          本文标题:[MISC]关于Bitmap在Android平台的整理

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