一、背景

在Android开发中,任何一个APP都离不开图片的加载和显示问题。这里的图片来源分为三种:项目图片资源文件(一般为res/drawable目录下的图片文件)、手机本地图片文件、网络图片资源等。图片的显示我们一般采用ImageView作为载体,通过ImageView的相应API即可设置其显示的图片内容。

我们知道:如果是需要展示项目中的图片资源文件,我们只需要调用ImageView的setImageResource(int id)方法并传入该图片资源的id(一般为R.drawable.xxx)即可。但是如果是需要展示手机本地的某张图片或者网络上的某个图片资源,又该怎么办呢?——问题A

为了回答问题A,我们先思考一个更深的问题B:Android中是如何将某一张图片的内容加载到内存中继而由ImageView显示的呢?

我们知道:如果我们想通过TextView展示一个本地txt文件的内容,我们只需要由该文件创建并包装一个输入流对象。通过该输入流对象即可得到一个代表该文件内容的字符串对象,再将该字符串对象交由TextView展示即可。换句话说,这个txt文件的内容在内存中的表达形式就是这个字符串对象。

类推一下,虽然图片文件也是文件,但是我们显然不可能对图片文件也采用这种方式:即通过该图片建立并包装一个输入流对象再获取一个字符串对象。毕竟无论如何我们都无法将某个图片的内容表示为一个字符串对象(细想一下就知道了,你能通过一段话100%准确地描述一张图片吗?显然不现实)。那么,这就引入了问题C:既然字符串对象不行,那么我们该以哪种对象来在内存中表示某个图片的内容呢?答案就是:Bitmap对象!

二、基本概述

Bitmap,即位图。它本质上就是一张图片的内容在内存中的表达形式。那么,Bitmap是通过什么方式表示一张图片的内容呢?

Bitmap原理:从纯数学的角度,任何一个面都由无数个点组成。但是对于图片而言,我们没必要用无数个点来表示这个图片,毕竟单独一个微小的点人类肉眼是看不清的。换句话说,由于人类肉眼的能力有限,我们只需要将一张图片表示为 有限但足够多的点即可。点的数量不能无限,因为无限的点信息量太大无法存储;但是点的数量也必须足够多,否则视觉上无法形成连贯性。这里的点就是像素。比如说,某个1080*640的图片,这里的像素总数即为1080X640个。

将图片内容表示为有限但足够多的像素的集合,这个“无限→有限”的思想极其迷人。所以,我们只需要将每个像素的信息存储起来,就意味着将整个图片的内容进行了表达。

像素信息:每个像素的信息,无非就是ARGB四个通道的值。其中,A代表透明度,RGB代表红绿蓝三种颜色通道值。每个通道的值范围在0~255之间,即有256个值,刚好可以通过一个字节(8bit)进行表示。所以,每个通道值由一个字节表示,四个字节表示一个像素信息,这似乎是最好的像素信息表示方案。

但是这里忽略了两个现实的需求问题:

①在实际需求中,我们真的需要这么多数量的颜色吗?上述方案是256X256X256种。有的时候,我们并不需要这么丰富的颜色数量,所以可以适当减少表示每个颜色通道的bit位数。这么做的好处是节省空间。也就是说,每个颜色通道都采用8bit来表示是代表全部颜色值的集合;而我们可以采用少于8bit的表示方式,尽管这会缺失一部分颜色值,但是只要颜色够用即可,并且这还可以节省内存空间。

②我们真的需要透明度值吗?如果我们需要某个图片作为背景或者图标,这个图片透明度A通道值是必要的。但是如果我们只是普通的图片展示,比如拍摄的照片,透明度值毫无意义。细想一下,你希望你手机自拍的照片透明或者半透明吗?hell no! 因此,透明度这个通道值是否有必要表示也是根据需求自由变化的。

具体每个像素点存储ARGB值的方案介绍,后面会详细介绍。

总结:Bitmap对象本质是一张图片的内容在内存中的表达形式。它将图片的内容看做是由存储数据的有限个像素点组成;每个像素点存储该像素点位置的ARGB值。每个像素点的ARGB值确定下来,这张图片的内容就相应地确定下来了。

现在回答一下问题A和问题B:Android就是将所有的图片资源(无论是何种来源)的内容以Bitmap对象的形式加载到内存中,再通过ImageView的setImageBitmap(Bitmap b)方法即可展示该Bitmap对象所表示的图片内容。

三、详细介绍

1、Bitmap.Config

Config是Bitmap的一个枚举内部类,它表示的就是每个像素点对ARGB通道值的存储方案。取值有以下四种:

ARGB_8888:这种方案就是上面所说的每个通道值采8bit来表示,每个像素点需要4字节的内存空间来存储数据。该方案图片质量是最高的,但是占用的内存也是最大的

ARGB_4444:这种方案每个通道都是4位,每个像素占用2个字节,图片的失真比较严重。一般不用这种方案。

RGB_565:这种方案RGB通道值分别占5、6、5位,但是没有存储A通道值,所以不支持透明度。每个像素点占用2字节,是ARGB_8888方案的一半。

ALPHA_8:这种方案不支持颜色值,只存储透明度A通道值,使用场景特殊,比如设置遮盖效果等。

比较分析:一般我们在ARGB_8888方式和RGB_565方式中进行选取:不需要设置透明度时,比如拍摄的照片等,RGB_565是个节省内存空间的不错的选择;既要设置透明度,对图片质量要求又高,就用ARGB_8888。

2、Bitmap的压缩存储

Bitmap是图片内容在内存中的表示形式,那么如果想要将Bitmap对象进行持久化存储为一张本地图片,需要对Bitmap对象表示的内容进行压缩存储。根据不同的压缩算法可以得到不同的图片压缩格式(简称为图片格式),比如GIF、JPEG、BMP、PNG和WebP等。这些图片的(压缩)格式可以通过图片文件的后缀名看出。

换句话说:Bitmap是图片在内存中的表示,GIF、JPEG、BMP、PNG和WebP等格式图片是持久化存储后的图片。内存中的Bitmap到磁盘上的GIF、JPEG、BMP、PNG和WebP等格式图片经过了”压缩”过程,磁盘上的GIF、JPEG、BMP、PNG和WebP等格式图片到内存中的Bitmap经过了“解压缩”的过程。

那么,为什么不直接将Bitmap对象进行持久化存储而是要对Bitmap对象进行压缩存储呢?这么做依据的思想是:当图片持久化保存在磁盘上时,我们应该尽可能以最小的体积来保存同一张图片的内容,这样有利于节省磁盘空间;而当图片加载到内存中以显示的时候,应该将磁盘上压缩存储的图片内容完整地展开。前者即为压缩过程,目的是节省磁盘空间;后者即为解压缩过程,目的是在内存中展示图片的完整内容。

3、有损压缩和无损压缩

Bitmap压缩存储时的算法有很多种,但是整体可分为两类:有损压缩和无损压缩。

①有损压缩
有损压缩的基本依据是:人的眼睛对光线的敏感度远高于对颜色的敏感度,光线对景物的作用比颜色的作用更为重要。有损压缩的原理是:保持颜色的逐渐变化,删除图像中颜色的突然变化。生物学中的大量实验证明,人类大脑会自发地利用与附近最接近的颜色来填补所丢失的颜色。有损压缩的具体实现方法就是删除图像中景物边缘的某些颜色部分。当在屏幕上看这幅图时,大脑会利用在景物上看到的颜色填补所丢失的颜色部分。利用有损压缩技术,某些数据被有意地删除了,并且在图片重新加载至内存中时这些数据也不会还原,因此被称为是“有损”的。有损压缩技术可以灵活地设置压缩率。
无可否认,利用有损压缩技术可以在位图持久化存储的过程中大大地压缩图片的存储大小,但是会影响图像质量,这一点在压缩率很高时尤其明显。所以需要选择恰当的压缩率。
②无损压缩
无损压缩的基本原理是:相同的颜色信息只需保存一次。具体过程是:首先会确定图像中哪些区域是相同的,哪些是不同的。包括了重复数据的区域就可以被压缩,只需要记录该区域的起始点即可。
从本质上看,无损压缩的方法通过删除一些重复数据,也能在位图持久化存储的过程中减少要在磁盘上保存的图片大小。但是,如果将该图片重新读取到内存中,重复数据会被还原。因此,无损压缩的方法并不能减少图片的内存占用量,如果要减少图片占用内存的容量,就必须使用有损压缩方法。
无损压缩方法的优点是能够比较好地保存图像的质量,但是相对来说这种方法的压缩率比较低。
对比分析:有损压缩压缩率高而且可以灵活设置压缩率,并且删除的数据不可还原,因此可以减少图片的内存占用,但是对图片质量会有一定程度的影响;无损压缩可以很好地保存图片质量,也能保证一定的压缩率虽然没有有损压缩那么高,并且无损压缩删除的数据在重新加载至内存时会被还原,因此不可以减少图片的内存占用。
 
4、位深与色深
我们知道了图片在内存中和在磁盘上的两种不同的表示形式:前者为Bitmap,后者为各种压缩格式。这里介绍一下位深与色深的概念:
①色深
色深指的是每一个像素点用多少bit来存储ARGB值,属于图片自身的一种属性。色深可以用来衡量一张图片的色彩处理能力(即色彩丰富程度)。
典型的色深是8-bit、16-bit、24-bit和32-bit等。
上述的Bitmap.Config参数的值指的就是色深。比如ARGB_8888方式的色深为32位,RGB_565方式的色深是16位。
 
②位深
位深指的是在对Bitmap进行压缩存储时存储每个像素所用的bit数,主要用于存储。由于是“压缩”存储,所以位深一般小于或等于色深 。
举个例子:某张图片100像素*100像素 色深32位(ARGB_8888),保存时位深度为24位,那么: 
该图片在内存中所占大小为:100 * 100 * (32 / 8) Byte 
在文件中所占大小为 100 * 100 * ( 24/ 8 ) * 压缩率 Byte
 
5、常见的压缩格式
Bitmap的压缩格式就是最终持久化存储得到的图片格式,一般由后缀名即可看出该图片采用了何种压缩方式。不同的压缩方式的压缩算法不一样。常见的主要有:
①Gif 
Gif是一种基于LZW算法的无损压缩格式,其压缩率一般在50%左右。Gif可插入多帧,从而实现动画效果。因此Gif图片分为静态GIF和动画GIF两种GIF格式。
由于Gif以8位颜色压缩存储单个位图,所以它最多只能用256种颜色来表现物体,对于色彩复杂的物体它就力不从心了。因此Gif不适合用于色彩非常丰富的图片的压缩存储,比如拍摄的真彩图片等。
 
②BMP
BMP是标准图形格式,它是包括Windows在内多种操作系统图像展现的终极形式。其本质就是Bitmap对象直接持久化保存的位图文件格式,由于没有进行压缩存储,因此体积非常大,故而不适合在网络上传输。同时也是因为这种格式是对Bitmap对象的直接存储而没有进行压缩,因此我们在讨论压缩格式时往往忽略这一种。
 
③PNG
PNG格式本身的设计目的是替代GIF格式,所以它与GIF 有更多相似的地方。PNG格式也属于无损压缩,其位深为32位,也就是说它支持所有的颜色类型。
同样是无损压缩,PNG的压缩率高于Gif格式,而且PNG支持的颜色数量也远高于Gif,因此:如果是对静态图片进行无损压缩,优先使用PNG取代Gif,因为PNG压缩率高、色彩好;但是PNG不支持动画效果。所以Gif仍然有用武之地。
PNG缺点是:由于是无损压缩,因此PNG文件的体积往往比较大。如果在项目中多处使用PNG图片文件,那么在APP瘦身时需要对PNG文件进行优化以减少APP体积大小。具体做法后面会详细介绍。
 

④JPEG

JPEG是一种有损压缩格式,JPEG图片以24位颜色压缩存储单个位图。也就是说,JPEG不支持透明通道。JPEG也不支持多帧动画。
因为是有损压缩,所以需要注意控制压缩率以免图片质量太差。
JPG和JPEG没有区别,全名、正式扩展名是JPEG。但因DOS、Windows95等早期系统采用的8.3命名规则只支持最长3字符的扩展名,为了兼容采用了.jpg。也因历史习惯和兼容性的考虑,.jpg目前更流行。
JPEG2000作为JPEG的升级版,其压缩率比JPEG高约30%左右,同时支持有损和无损压缩。JPEG2000格式有一个极其重要的特征在于它能实现渐进传输,即先传输图像的轮廓,然后逐步传输数据,不断提高图像质量,让图像由朦胧到清晰显示。此外,JPEG2000还支持所谓的“感兴趣区域”特性,也就是可以任意指定影像上感兴趣区域的压缩质量;另外,JPEG2000还可以选择指定的部分先解压缩来加载到内存中。JPEG2000和JPEG相比优势明显,且向下兼容,因此可取代传统的JPEG格式。
 
 ⑤WebP
WebP 是 Google 在 2010 年发布的图片格式,希望以更高的压缩率替代 JPEG。它用 VP8 视频帧内编码作为其算法基础,取得了不错的压缩效果。WebP支持有损和无损压缩、支持完整的透明通道、也支持多帧动画,并且没有版权问题,是一种非常理想的图片格式。WebP支持动图,基本取代gif。
WebP不仅集成了PNG、JPEG和Gif的所有功能,而且相同质量的无损压缩WebP图片体积比PNG小大约26%;如果是有损压缩,相同质量的WebP图片体积比JPEG小25%-34%。
很多人会认为,既然WebP功能完善、压缩率更高,那直接用WebP取代上述所有的图片压缩格式不就行了吗?其实不然,WebP也有其缺点:我们知道JPEG是有损压缩而PNG是无损压缩,所以JPEG的压缩率高于PNG;但是有损压缩的算法决定了其压缩时间一定是高于无损压缩的,也就是说JPEG的压缩时间高于PNG。而WebP无论是无损还是有损压缩,压缩率都分别高于PNG和JPEG;与其相对应的是其压缩时间也比它们长的多。经测试,WebP图片的编码时间比JPEG长8倍。可以看出,时间和空间是一对矛盾;如果想要节省更多的空间,必然要付出额外的时间;如果想要节省时间,那么必然要付出空间的代价。这取决于我们在实际中对于时空不同的需求程度来做出选择。
不管怎么说,WebP还是一种强大的、理想的图片压缩格式,并且借由 Google 在网络世界的影响力,WebP 在几年的时间内已经得到了广泛的应用。看看你手机里的 App:微博、微信、QQ、淘宝等等,每个 App 里都有 WebP 的身影。
另外,WebP是Android4.0才引入的一种图片压缩格式,如果想要在Android4.0以前的版本支持WebP格式的图片,那么需要借助于第三方库来支持WebP格式图片,例如:webp-android-backport函数库,该开源项目在GitHub地址为:https://github.com/alexey-pelykh/webp-android-backport  当然考虑到一般的Android开发中只需要向下兼容到Android4.0即可,所以也可以忽略这个问题。
 
目前来说,以上所述的五种压缩格式,Android操作系统都提供了原生支持;但是在上层能直接调用的编码方式只有 JPEG、PNG、WebP 这三种。具体的,可以查看Bitmap类的枚举内部类CompressFormat类的枚举值来获取上层能调用的图片编码方式。你会发现枚举值也是JPEG、PNG和WEBP三种。
如果我们想要在应用层使用Gif格式图片,需要自行引入第三方函数库来提供对Gif格式图片的支持。不过一般我们用WebP取代Gif。
因此,我们只需要比较分析PNG、JPEG、WebP这三种压缩格式即可。
 
比较分析:
①对于摄影类等真彩图片:因为我们对这类色彩丰富的图片的透明度没有要求(一般默认为不透明),可以采用JPEG有损压缩格式,因为JPEG本身就不支持透明度,而且因为是有损压缩,所以尽管会牺牲一丢丢照片的质量但是可以大大减少体积。如果非要采用PNG格式,那么首先因为PNG支持透明度通道,所以明明不必要的透明度值却会被存储;其次因为是无损压缩,所以压缩率不会很高从而导致保存的图片非常大!综上比较,建议采用JPEG格式,不要用PNG格式。
JPEG格式可以与Bitmap.Config参数值为RGB_565搭配使用,这是一个理想的设置。
 
②对于logo图标、背景图等图片:这类图片的特点是往往是有大块的颜色相同的区域,这与无损压缩的思路不谋而合(即删除重复数据)。而且这类图片对透明度是有要求的,因此可以采用PNG无损压缩格式;尽管使用PNG格式会让图片有点大,但是可以在后续进行PNG图片优化以对APP体积进行瘦身。如果非要采用JPEG格式,那么由于有损压缩的原理(利用人脑的自动补全机制),可能会随机地丢失一些线条导致最终的图片完全不是想要的效果。综上比较,建议使用PNG格式,不要用JPEG格式。
PNG格式可以与Bitmap.Config参数值为ARGB_8888搭配使用,这是一个理想的设置。
当然,以上两种情况,我们都可以使用WebP取代PNG或JPEG,如果我们想要这么做的话。如果你的项目中对空间的需求程度更高,你完全有理由这么做。但是如果你对空间需求程度还OK,你也可以选择分情况使用PNG或JPEG格式。
 
6、图片优化
图片优化属于Android性能优化的一种,这里主要是针对PNG图片的大小进行优化,毕竟PNG这种无损压缩格式往往会导致图片都比较大。除非你的项目已经全面支持了WebP格式,否则对PNG格式图片的优化都会是你必须考虑的一点,这有利于减少APP的体积大小。
对PNG图片进行优化的思想是:减少PNG图片的体积,常用方式有:
①无损压缩工具ImageOptim
ImageOptim是一种无损压缩工具,所以你不用担心利用该工具对PNG图片进行压缩后图片质量会受影响。它的压缩原理是:优化PNG压缩参数,移除冗余元数据以及非必需的颜色配置文件等,在不牺牲图片质量的前提下,既减少了PNG图片的大小,又提高了其加载的速度。
ImageOptim工具的网址为:https://imageoptim.com
 
②有损压缩工具ImageAlpha
ImageAlpha与ImageOptim是同一个作者,不过ImageAlpha属于有损压缩,因此图片质量会受到影响。所以使用ImageAlpha对PNG图片进行压缩后,必须让设计师检视一下优化后的PNG图片,以免影响APP的视觉效果。但是ImageAlpha的优点是可以极大减少PNG图片的体积大小。
ImageAlpha工具的网址为:https://pngmini.com
 
③有损压缩TinyPNG
前面两个工具是应用程序,TinyPNG是一个Web站点。你可以上传原PNG图片,它对PNG图片压缩后你就可以下载优化后的结果了。因为TinyPNG也是有损压缩,所以优缺点同②
TinyPNG的网址为:https://tinypng.com
以上方案都属于对PNG图片进行二次压缩(有的是有损有的是无损),我们需要在图片质量和图片大小这对矛盾中根据实际情况进行选择。
 
④PNG/JPEG转换为WebP
如果不想对PNG图片进行二次压缩,可以考虑直接将其替换为WebP格式的图片。另外,我们对JPEG格式的图片也可以这么替换。毕竟WebP无论是与PNG还是与JPEG格式想比,压缩后体积大小都小很多。WebP转换工具有:
智图,这是一个图片优化平台,地址为:https://zhitu.isux.us
iSparta,这是一个针对PNG图片的二次压缩和格式转换工具,地址为:https://isparta.github.io
 
⑤使用NinePatch格式的PNG图
.9.png图片格式简称为NinaPatch图,本质上仍然是PNG格式图片。不过它的优点是体积小、拉伸不变形,能够很好地适配Android各种机型。我们可以利用Android Studio提供的功能,右键一张PNG图片点击“create 9=Patch File”即可完成转换。
总结:无论是二次压缩还是格式转换,无论是有损二次压缩还是无损二次压缩,我们都需要根据实际需求进行方案和工具的选择。
 
我们已经知道了Android中图片内存中的表示形式(Bitmap)和磁盘上的表示形式(各种压缩格式),以及二者的关系(压缩和解压缩的过程)。下面具体看看Bitmap的使用方式和注意事项,毕竟磁盘上存储的图片终究还是要加载到内存中以Bitmap的形式进行展示的。
 
四、Bitmap的简单使用
先看看Bitmap的简单使用方式:
1、Bitmap的加载方法
Bitmap的工厂类BitmapFactory提供了四类静态方法用于加载Bitmap对象:decodeFile、decodeResource、decodeStream、decodeByteArray。
分别代表从本地图片文件、项目资源文件、流对象(可以是网络输入流对象或本地文件输入流对象)、字节序列中加载一个Bitmap对象。
之前讲的图片的三个来源,都可以找到对应的decodeXXXX方法来获取该图片对应的Bitmap对象。
举个例子,假设需要通过网络请求一张图片资源并展示:先处理该网络请求并得到返回结果的输入流对象;依据该流对象调用decodeStream方法得到一个Bitmap对象,该对象即表示这张图片的内容;最后通过ImageView的setImageBitmap()方法显示该图片即可。
 
2、Bitmap的压缩存储方法
Bitmap的压缩存储与Bitmap的加载是相反的过程,通过compress()方法来实现,该方法原型为:
compress(Bitmap.CompressFormat format, int quality, OutputStream stream)
format参数表示压缩存储的格式,可选为PNG、JPEG和WEBP;quality表示压缩率,取值在0~100之间,100表示未压缩,30表示压缩为原大小的30%,但是该参数在format值为PNG时无效,因为PNG属于无损压缩无法设置压缩率;stream就是希望输出到某个位置的输出流对象,比如某个文件的输出流对象。
通过compress()方法可以将Bitmap按照指定的格式和压缩率(非PNG格式时)压缩存储到指定的位置。
 
3、BitmapFactory.Options类
BitmapFactory是Bitmap的工厂类,通过BitmapFactory的静态方法来创建Bitmap对象;BitmapFactory.Options类代表对Bitmap对象的属性设置(配置)。一般情况下,我们调用decodeXXXX方法时不需要传递一个BitmapFactory.Options对象作为参数,因此此时是利用默认的配置信息来创建Bitmap对象。如果需要对创建的Bitmap对象进行自定义的配置,那么就需要给decodeXXXX方法传递一个BitmapFactory.Options对象,该对象包含了对Bitmap对象的配置信息。
通过BitmapFactory.Options类的构造器创建BitmapFactory.Options对象。该对象包含的Bitmap配置信息即为该对象的各属性值,主要有:

介绍一下比较不好理解的属性:

①inJustDecodeBounds:这个属性表示是否只扫描轮廓,默认为false。如果该属性为true,decodeXXXX方法不会返回一个Bitmap对象(即不会为Bitmap分配内存)而是返回null。那如果decodeXXXX方法不再分配内存以创建一个Bitmap对象,那么还有什么用呢?答案就是:扫描轮廓。

BitmapFactory.Options对象的outWidth和outHeight属性分别代表Bitmap对象的宽和高,但是这两个属性在Bitmap对象未创建之前显然默认为0,默认只有在Bitmap对象创建后才能被赋予正确的值。而当inJustDecodeBounds属性为true,虽然不会分配内存创建Bitmap对象,但是会扫描轮廓来给outWidth和outHeight属性赋值,就相当于绕过了Bitmap对象创建的这一步提前获取到Bitmap对象的宽高值。那这个属性到底有啥用呢?具体用处体现在Bitmap的采样率计算中,后面会详细介绍。

②inSample:这个表示Bitmap的采样率,默认为1。比如说有一张图片是2048像素X1024像素,那么默认情况下该图片加载到内存中的Bitmap对象尺寸也是2048像素X1024像素。如果采用的是ARGB_8888方式,那么该Bitmap对象加载所消耗的内存为2048X1024X4/1024/1024=8M。这只是一张图片消耗的内存,如果当前活动需要加载几张甚至几十张图片,那么会导致严重的OOM错误。

OOM错误:尽管Android设备内存大小可能达到好几个G(比如4G),但是Andorid中每个应用其运行内存都有一个阈值,超过这个阈值就会引发out of memory即OOM错误(内存溢出错误)。因为现在市场上流行的手机设备其操作系统都是在Andori原生操作系统基础上的拓展,所以不同的设备环境中这个内存阈值不一样。可以通过以下方法获取到当前应用所分配的内存阈值大小,单位为字节: Runtime.getRuntime().maxMemory();

尽管我们确实可以通过设置来修改这个阈值大小以提高应用的最大分配内存(具体方式是在在Manifest中设置android.largeHeap="true"),但是需要注意的是:内存是一种很宝贵的资源,不加考虑地无脑给每个应用提高最大分配内存是一个糟糕的选择。因为手机总内存相比较每个应用默认的最大分配内存虽然高很多,但是手机中的应用数量是非常多的,每个应用都修改其运行内存阈值为几百MB甚至一个G,这很严重影响手机性能!另外,如果应用的最大分配内存很高,这意味着其垃圾回收工作也会变得更加耗时,这也会影响应用和手机的性能。所以,这个方案需要慎重考虑不能滥用。

关于这个方案的理解可以参考一位大神的解释:“在一些特殊的情景下,你可以通过在manifest的application标签下添加largeHeap=true的属性来为应用声明一个更大的heap空间。然后,你可以通过getLargeMemoryClass()来获取到这个更大的heap size阈值。然而,声明得到更大Heap阈值的本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用)。不要轻易的因为你需要使用更多的内存而去请求一个大的Heap Size。只有当你清楚的知道哪里会使用大量的内存并且知道为什么这些内存必须被保留时才去使用large heap。因此请谨慎使用large heap属性。使用额外的内存空间会影响系统整体的用户体验,并且会使得每次gc的运行时间更长。在任务切换时,系统的性能会大打折扣。另外, large heap并不一定能够获取到更大的heap。在某些有严格限制的机器上,large heap的大小和通常的heap size是一样的。因此即使你申请了large heap,你还是应该通过执行getMemoryClass()来检查实际获取到的heap大小。”

综上,我们已经知道了Bitmap的加载是一个很耗内存的操作,特别是在大位图的情况下。这很容易引发OOM错误,而我们又不能轻易地通过修改或提供应用的内存阈值来避免这个错误。那么我们该怎么做呢?答案就是:利用这里所说的采样率属性来创建一个原Bitmap的子采样版本。这也是官方推荐的对于大位图加载的OOM问题的解决方案。其具体思想为:比如还是那张尺寸为2048像素X1024像素图片,在inSample值默认为1的情况下,我们现在已经知道它加载到内存中默认是一个2048像素X1024像素大位图了。我们可以将inSample设置为2,那么该图片加载到内存中的位图宽高都会变成原宽高的1/2,即1024像素X512像素。进一步,如果inSample值设置为4,那么位图尺寸会变成512像素X256像素,这个时候该位图所消耗的内存(假设还是ARGB_8888方式)为512X256X4/1024/1024=0.5M,可以看出从8M到0.5M,这极大的节省了内存资源从而避免了OOM错误。

切记:官方对于inSample值的要求是,必须为2的幂,比如2、4、8...等整数值。

这里会有两个疑问:第一:通过设置inSample属性值来创建一个原大位图的子采样版本的方式来降低内存消耗,听不上确实很不错。但是这不会导致图片严重失真吗?毕竟你丢失了那么多像素点,这意味着你丢失了很多颜色信息。对这个疑问的解释是:尽管在采样的过程确实会丢失很多像素点,但是原位图的尺寸也在减小,其像素密度是不变的。比如说如果inSample值为2,那么子采样版本的像素点数量是原来的1/4,但是子采样版本的显示尺寸(区域面积)也会变成原来的1/4,这样的话像素密码是不变的因此图片不用担心严重失真问题。第二:inSample值如何选取才是最佳?这其实取决于ImageView的尺寸,具体采样率的计算方式后面会详细介绍。

③inPreferredConfig:该属性指定Bitmap的色深值,该属性类型为Bitmap.Config值。

例如你可以指定某图片加载为Bitmap对象的色深模式为ARGB_8888,即:options.inPreferredConfig=Bitmap.Config.ARGB_8888;

④isMutable:该属性表示通过decodeXXXX方法创建的Bitmap对象其代表的图片内容是否允许被外部修改,比如利用Canvas重新绘制其内容等。默认为false,即不允许被外部操作修改。

利用这些属性定制BitmapFactory.Options对象,从而灵活地按照自己的需求配置创建的Bitmap对象。

五、Bitmap的进阶使用

1、高效地加载大位图

上面刚说了大位图加载时的OOM问题,解决方式是通过inSample属性创建一个原位图的子采样版本以减低内存。那么这里的采样率inSample值如何选取最好呢?这里我们利用官方推荐的采样率最佳计算方式:基本步骤就是:①获取位图原尺寸 ②获取ImageView即最终图片显示的尺寸  ③依据两种尺寸计算采样率(或缩放比例)。

public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 位图的原宽高通过options对象获取
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2;
final int halfWidth = width / 2;
//当要显示的目标大小和图像的实际大小比较接近时,会产生没必要的采样,先除以2再判断以防止过度采样
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
} return inSampleSize;
}

依据上面的最佳采样率计算方法,进一步可以封装出利用最佳采样率创建子采样版本再创建位图对象的方法,这里以从项目图片资源文件加载Bitmap对象为例:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
//因为inJustDecodeBounds为true,所以不会创建Bitmap对象只会扫描轮廓从而给options对象的宽高属性赋值
BitmapFactory.decodeResource(res, resId, options); // 计算最佳采样率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 记得将inJustDecodeBounds属性设置回false值
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}

2、Bitmap加载时的异步问题

由于图片的来源有三种,如果是项目图片资源文件的加载,一般采取了子采样版本加载方案后不会导致ANR问题,毕竟每张图加载消耗的内存不会很大了。但是对于本地图片文件和网络图片资源,由于分别涉及到文件读取和网络请求,所以属于耗时操作。为了避免ANR的产生,必须将图片加载为Bitmap对象的过程放入工作线程中;获取到Bitmap对象后再回到UI线程设置ImageView的显示。举个例子,如果采用AsyncTask作为我们的异步处理方案,那么代码如下:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final ImageView iv;
private int id = 0; public BitmapWorkerTask(ImageView imageView) {
iv = imageView;
} // Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
id = params[0];
//假设ImageView尺寸为500X500,为了方便还是以项目资源文件的加载方式为例,因为这可以复用上面封装的方法
return decodeSampledBitmapFromResource(getResources(), id, 500, 500);
} @Override
protected void onPostExecute(Bitmap bitmap) {
iv.setImageBitmap(bitmap);
}
}

该方案中,doInBackground方法执行在子线程,用来处理 ”图片文件读取操作+Bitmap对象的高效加载操作” 或 ”网络请求图片资源操作+Bimap对象的高效加载操作”等两种情形下的耗时操作。onPostExecute方法执行在UI线程,用于设置ImageView的显示内容。看上去这个方案很完美,但是有一个很隐晦的严重问题:

由当前活动启动了BitmapWorkerTask任务后:当我们退出当前活动时,由于异步任务只依赖于UI线程所以BitmapWorkerTask任务会继续执行。正常的操作是遍历当前活动实例的对象图来释放各对象的内存以销毁该活动,但是由于当前活动实例的ImageView引用被BitmapWorkerTask对象持有,而且还是强引用关系。这会导致Activity实例无法被销毁,引发内存泄露问题。内存泄露问题会进一步导致内存溢出错误。

为了解决这个问题,我们只需要让BitmapWorkerTask类持有ImageView的弱引用即可。这样当活动退出时,BitmapWorkerTask对象由于持有的是ImageView的弱引用,所以ImageView对象会被回收,继而Activity实例得到销毁,从而避免了内存泄露问题。具体修改后的代码如下:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0; public BitmapWorkerTask(ImageView imageView) {
// 用弱引用来关联这个imageview!弱引用是避免android 在各种callback回调里发生内存泄露的最佳方法!
//而软引用则是做缓存的最佳方法 两者不要搞混了!
imageViewReference = new WeakReference<ImageView>(imageView);
} // Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100);
} @Override
protected void onPostExecute(Bitmap bitmap) {
//当后台线程结束后 先看看ImageView对象是否被回收:如果被回收就什么也不做,等着系统回收他的资源
//如果ImageView对象没被回收的话,设置其显示内容即可
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}

拓展:①WeakReference是弱引用,其中保存的对象实例可以被GC回收掉。这个类通常用于在某处保存对象引用,而又不干扰该对象被GC回收,可以用于避免内存泄露。②SoftReference是软引用,它保存的对象实例,不会被GC轻易回收,除非JVM即将OutOfMemory,否则不会被GC回收。这个特性使得它非常适合用于设计Cache缓存。缓存可以省去重复加载的操作,而且缓存属于内存因此读取数据非常快,所以我们自然不希望缓存内容被GC轻易地回收掉;但是因为缓存本质上就是一种内存资源,所以在内存紧张时我们需要能释放一部分缓存空间来避免OOM错误。综上,软引用非常适合用于设计缓存Cache。但是,这只是早些时候的缓存设计思想,比如在Android2.3版本之前。在Android2.3版本之后,JVM的垃圾收集器开始更积极地回收软引用对象,这使得原本的缓存设计思想失效了。因为如果使用软引用来实现缓存,那么动不动缓存对象就被GC回收掉实在是无法接受。所以,Android2.3之后对于缓存的设计使用的是强引用关系(也就是普通对象引用关系)。很多人会问这样不会由于强引用的缓存对象无法被回收从而导致OOM错误吗?确实会这样,但是我们只需要给缓存设置一个合理的阈值就好了。将缓存大小控制在这个阈值范围内,就不会引发OOM错误了。

3、列表加载Bitmap时的图片显示错乱问题

我们已经知道了如何高效地加载位图以避免OOM错误,还知道了如何合理地利用异步机制来避免Bitmap加载时的ANR问题和内存泄露问题。现在考虑另一种常见的Bitmap加载问题:当我们使用列表,如ListView、GridView和RecyclerView等来加载多个Bitmap时,可能会产生图片显示错乱的问题。先看一下该问题产生的原因。以ListView为例:

①ListView为了提高列表展示内容在滚动时的流畅性,使用了一种item复用机制,即:在屏幕中显示的每个ListView的item对应的布局只有在第一次的时候被加载,然后缓存在convertView里面,之后滑动改变ListView时调用的getView就会复用缓存在converView中的布局和控件,所以可以使得ListView变得流畅(因为不用重复加载布局)。

②每个Item中的ImageView加载图片时往往都是异步操作,比如在子线程中进行图片资源的网络请求再加载为一个Bitmap对象最后回到UI线程设置该item的ImageView的显示内容。

③ 听上去①是一种非常合理有效的提高列表展示流畅性的机制,②看起来也是图片加载时很常见的一个异步操作啊。其实①和②本身都没有问题,但是①+②+用户滑动列表=图片显示错乱!具体而言:当我们在其中一个itemA加载图片A的时候,由于加载过程是异步操作需要耗费一定的时间,那么有可能图片A未被加载完该itemA就“滚出去了”,这个itemA可能被当做缓存应用到另一个列表项itemB中,这个时候刚好图片A加载完成显示在itemB中(因为ImageView对象在缓存中被复用了),原本itemB该显示图片B,现在显示图片A。这只是最简单的一种情况,当滑动频繁时这种图片显示错乱问题会愈加严重,甚至让人毫无头绪。

那么如何解决这种图片显示错乱问题呢?解决思路其实非常简单:在图片A被加载到ImageView之前做一个判断,判断该ImageView对象是否还是对应的是itemA,如果是则将图片加载到ImageView当中;如果不是则放弃加载(因为itemB已经启动了图片B的加载,所以不用担心控件出现空白的情况)。

那么新的问题出现了,如何判断ImageView对象对应的item已经改变了?我们可以采取下面的方式:

①在每次getView的复用布局控件时,对会被复用的控件设置一个标签(在这里就是对ImageView设置标签)。标签内容必须可以标识不同的item!这里使用图片的url作为标签内容,然后再异步加载图片。

②在图片下载完成后要加载到ImageView之前做判断,判断该ImageView的标签内容是否和图片的url一样:如果一样说明ImageView没有被复用,可以将图片加载到ImageView当中;如果不一样,说明ListView发生了滑动,导致其他item调用了getView从而将该ImageView的标签改变,此时放弃图片的加载(尽管图片已经被下载成功了)。

总结:解决ListView异步加载Bitmap时的图片错乱问题的方式是:为被复用的控件对象(即ImageView对象)设置标签来标识item,异步任务结束后要将图片加载到ImageView时取出标签值进行比对是否一致:如果一致意味着没有发生滑动,正常加载图片;如果不一样意味着发生了滑动,取消加载。

4、Android中的Bitmap缓存策略

如果只是加载若干张图片,上述的Bitmap使用方式已经绝对够用了;但是如果在应用中需要频繁地加载大量的图片,特别是有些图片会被重复加载时,这个时候利用缓存策略可以很好地提高图片的加载速度。比如说有几张图片被重复加载的频率很高,那么可以在缓存中保留这几张图片的Bitmap对象;后续如果需要加载这些图片,则不需要花费很多时间去重新在网络上获取并加载这些图片的Bitmap对象,只需要直接向缓存中获取之前保留下来的Bitmap对象即可。

Android中对Bitmap的缓存策略分为两种:

  • 内存缓存:图像存储在设备内存中,因此访问速度非常快。事实上,比图像解码过程要快得多,所以将图像存储在这里是让app更快更稳定的一个好主意。内存缓存的唯一缺点是:它只存活于app的生命周期,这意味着一旦app被Android操作系统内存管理器关闭或杀死(全部或部分),那么储存在那里的所有图像都将丢失。由于内存缓存本质上就是一种内存资源,所以切记:内存缓存必须设置一个最大可用的内存量。否则可能会导致臭名昭著的outOfMemoryError。
  • 磁盘缓存:图像存储在设备的物理存储器上(磁盘)。磁盘缓存本质上就是设备SD卡上的某个目录。只要app不被卸载,其磁盘缓存可以一直安全地存储图片,只要有足够的磁盘空间即可。缺点是,磁盘读取和写入操作可能会很慢,而且总是比访问内存缓存慢。由于这个原因,因此所有的磁盘操作必须在工作线程执行,UI线程之外。否则,app会冻结,并导致ANR警报。

在实际使用中,我们不需要强行二选一,可以二者都使用,毕竟各有优势。所以Android中完整的图片缓存策略为:先尝试在内存缓存中查找Bitmap对象,如果有直接加载使用;如果没有,再尝试在磁盘缓存中查找图片文件是否存在,如果有将其加载至内存使用;如果还是没有,则老老实实发送网络请求获取图片资源并加载使用。需要注意的是,后面两种情况下的操作都必须使用异步机制以避免ANR的发生。

Android中通过LruCache实现内存缓存,通过DiskLruCache实现磁盘缓存,它们采用的都是LRU(Least Recently Used)最近最少使用算法来移除缓存中的最近不常访问的内容(变相地保留了最近经常访问的内容)。

①内存缓存LruCache

LruCache原理:LruCache底层是使用LinkedHashMap来实现的,所以LruCache也是一个泛型类。在图片缓存中,其键类型是字符串,值类型为Bitmap。利用LinkedHashMap的accessOrder属性可以实现LRU算法。accessOrder属性决定了LinkedHashMap的链表顺序:accessOrder为true则以访问顺序维护链表,即被访问过的元素会安排到链表的尾部;accessorder为false则以插入的顺序维护链表。

而LruCache利用的正是accessOrder为true的LinkedHashMap来实现LRU算法的。具体表现为:

1° put:通过LinkedHashMap的put方法来实现元素的插入,插入的过程还是要先寻找有没有相同的key的数据,如果有则替换掉旧值,并且将该节点移到链表的尾部。这可以保证最近经常访问的内容集中保存在链表尾部,最近不常访问的内存集中保存在链表头部位置。在插入后如果缓存大小超过了设定的最大缓存大小(阈值),则将LinkedHashMap头部的节点(最近不常访问的内容)删除,直到size小于maxSize。

2° get:通过LinkedHashMap的get方法来实现元素的访问,由于accessOrder为true,因此被访问到的元素会被调整到链表的尾部,因此不常被访问的元素就会留到链表的头部,当触发清理缓存时不常被访问的元素就会被删除,这里是实现LRU最关键的地方。

3° remove:通过LinkedHashMap的remove方法来实现元素的移除。

3° size:LruCache中很重要的两个成员变量size和maxSize,因为清理缓存的是在size>maxSize时触发的,因此在初始化的时候要传入maxSize定义缓存的大小,然后重写sizeOf方法,因为LruCache是通过sizeOf方法来计算每个元素的大小。这里我们是使用LruCache来缓存图片,所以sizeOf方法需要计算Bitmap的大小并返回。

LruCache对其缓存对象采用的是强引用关系,采用maxSize来控制缓存空间大小以避免OOM错误。而且LruCache类在Android SDK中已经提供了,在实际使用中我们只需要完成以下几步即可:

  • 设计LruCache的最大缓存大小:一般是通过计算当前可用的内存大小继而来获取到应该设置的缓存大小
  • 创建LruCache对象:传入最大缓存大小的参数,同时重写sizeOf方法来设置存在LruCache里的每个对象的大小
  • 封装对LruCache的数据访问和添加操作并对外提供接口以供调用

具体代码参考如下:

//初始化LruCache对象
public void initLruCache()
{
//获取当前进程的可用内存,转换成KB单位
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//分配缓存的大小
int maxSize = maxMemory / 8;
//创建LruCache对象并重写sizeOf方法
lruCache = new LruCache<String, Bitmap>(maxSize)
{
@Override
protected int sizeOf(String key, Bitmap value) {
// TODO Auto-generated method stub
return value.getWidth() * value.getHeight() / 1024;
}
};
} /**
* 封装将图片存入缓存的方法
* @param key 图片的url转化成的key
* @param bitmap对象
*/
private void addBitmapToMemoryCache(String key, Bitmap bitmap)
{
if(getBitmapFromMemoryCache(key) == null)
{
mLruCache.put(key, bitmap);
}
} //封装从LruCache中访问数据的方法
private Bitmap getBitmapFromMemoryCache(String key)
{
return mLruCache.get(key);
} /**
* 因为外界一般获取到的是url而不是key,因此为了方便再做一层封装
* @param url http url
* @return bitmap
*/
private Bitmap loadBitmapFromMemoryCache(String url)
{
final String key = hashKeyFromUrl(url);
return getBitmapFromMemoryCache(key);
}

②磁盘缓存DiskLruCache

由于DiskLruCache并不属于Android SDK的一部分,需要自行设计。与LruCache实现LRU算法的思路基本上是一致的,但是有很多不一样的地方:LruCache是内存缓存,其键对应的值类型直接为Bitmap;而DiskLruCache是磁盘缓存,所以其键对应的值类型应该是一个代表图片文件的类。其次,前者访问或添加元素时,查找成功可以直接使用该Bitmap对象;后者访问或添加元素时,查找到指定图片文件后还需要通过文件的读取和Bitmap的加载过程才能使用。另外,前者是在内存中的数据读写操作所以不需要异步;后者涉及到文件操作必须开启子线程实现异步处理。

具体DiskLruCache的设计方案和使用方式可以参考这篇博客:https://www.jianshu.com/p/765640fe474a

有了LruCache类和DiskLruCache类,可以实现完整的Android图片二级缓存策略:在具体的图片加载时:先尝试在LruCache中查找Bitmap对象,如果有直接拿来使用。如果没有再尝试在DiskLruCache中查找图片文件,如果有将其加载为Bitmap对象再使用,并将其添加至LruCache中;如果没有查找到指定的图片文件,则发送网络请求获取图片资源并加载为Bitmap对象再使用,并将其添加DiskLruCache中。

5、Bitmap内存管理

Android设备的内存包括本机Native内存和Dalvik(类似于JVM虚拟机)堆内存两部分。在Android 2.3.3(API级别10)及更低版本中,位图的支持像素数据存储在Native内存中。它与位图本身是分开的,Bitmap对象本身存储在Dalvik堆中。Native内存中的像素数据不会以可预测的方式释放,可能导致应用程序短暂超出其内存限制并崩溃。从Android 3.0(API级别11)到Android 7.1(API级别25),像素数据与相关Bitmap对象一起存储在Dalvik堆上,一起交由Dalvik虚拟机的垃圾收集器来进行回收,因此比较安全。

①在Android2.3.3版本之前:

在Bitmap对象不再使用并希望将其销毁时,Bitmap对象自身由于保存在Dalvik堆中,所以其自身会由GC自动回收;但是由于Bitmap的像素数据保存在native内存中,所以必须由开发者手动调用Bitmap的recycle()方法来回收这些像素数据占用的内存空间。

②在Android2.3.3版本之后:

由于Bitmap对象和其像素数据一起保存在Dalvik堆上,所以在其需要回收时只要将Bitmap引用置为null 就行了,不需要如此麻烦的手动释放内存操作。

当然,一般我们在实际开发中往往向下兼容到Android4.0版本,所以你懂得。

③在Android3.0以后的版本,还提供了一个很好用的参数,叫options.inBitmap。如果你使用了这个属性,那么在调用decodeXXXX方法时会直接复用 inBitmap 所引用的那块内存。大家都知道,很多时候ui卡顿是因为gc 操作过多而造成的。使用这个属性能避免频繁的内存的申请和释放。带来的好处就是gc操作的数量减少,这样cpu会有更多的时间执行ui线程,界面会流畅很多,同时还能节省大量内存。简单地说,就是内存空间被各个Bitmap对象复用以避免频繁的内存申请和释放操作。

需要注意的是,如果要使用这个属性,必须将BitmapFactory.Options的isMutable属性值设置为true,否则无法使用这个属性。

具体使用方式参考如下代码:

final BitmapFactory.Options options = new BitmapFactory.Options();
//size必须为1 否则是使用inBitmap属性会报异常
options.inSampleSize = 1;
//这个属性一定要在用在src Bitmap decode的时候 不然你再使用哪个inBitmap属性去decode时候会在c++层面报异常
//BitmapFactory: Unable to reuse an immutable bitmap as an image decoder target.
options.inMutable = true;
inBitmap2 = BitmapFactory.decodeFile(path1,options);
iv.setImageBitmap(inBitmap2);
//将inBitmap属性代表的引用指向inBitmap2对象所在的内存空间,即可复用这块内存区域
options.inBitmap = inBitmap2;
//由于启用了inBitmap属性,所以后续的Bitmap加载不会申请新的内存空间而是直接复用inBitmap属性值指向的内存空间
iv2.setImageBitmap(BitmapFactory.decodeFile(path2,options));
iv3.setImageBitmap(BitmapFactory.decodeFile(path3,options));
iv4.setImageBitmap(BitmapFactory.decodeFile(path4,options));

补充:Android4.4以前,你要使用这个属性,那么要求复用内存空间的Bitmap对象大小必须一样;但是Android4.4 以后只要求后续复用内存空间的Bitmap对象大小比inBitmap指向的内存空间要小就可以使用这个属性了。另外,如果你不同的imageview 使用的scaletype 不同,但是你这些不同的imageview的bitmap在加载是如果都是引用的同一个inBitmap的话,

这些图片会相互影响。综上,使用inBitmap这个属性的时候 一定要小心小心再小心。

六、开源框架

我们现在已经知道了,Android图片加载的知识点和注意事项实在太多了:单个的位图加载我们要考虑Bitmap加载的OOM问题、异步处理问题和内存泄露问题;列表加载位图要考虑显示错乱问题;频繁大量的位图加载时我们要考虑二级缓存策略;我们还有考虑不同版本下的Bitmap内存管理问题,在这部分最后我们介绍了Bitmap内存复用方式,我们需要小心使用这种方式。

那么,能不能有一种方式让我们省去这么多繁琐的细节,方便我们对图片进行加载呢?答案就是:利用已有的成熟的图片加载和缓存开源框架!比如square公司的Picasso框架、Google公司的Glide框架和Facebook公司的Fresco框架等。特别是Fresco框架,提供了三级缓存策略,非常的专业。根据APP对图片显示和缓存的需求从低到高排序,我们可以采用的方案依次为:Bitmapfun、Picasso、Android-Universal-Image-Loader、Glide、Fresco。

这些框架可以方便我们实现对网络图片的加载和缓存操作。具体不再赘述。

Android Bitmap(位图)详解的更多相关文章

  1. Android之canvas详解

    首先说一下canvas类: Class Overview The Canvas class holds the "draw" calls. To draw something, y ...

  2. 【转】Android Canvas绘图详解(图文)

    转自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2012/1212/703.html Android Canvas绘图详解(图文) 泡 ...

  3. android屏幕适配详解

    android屏幕适配详解 官方地址:http://developer.android.com/guide/practices/screens_support.html 一.关于布局适配建议 1.不要 ...

  4. android:ToolBar详解

    android:ToolBar详解(手把手教程) 泡在网上的日子 发表于 2014-11-18 12:49 第 124857 次阅读 ToolBar 42 来源 http://blog.mosil.b ...

  5. Android 核心分析 之八Android 启动过程详解

    Android 启动过程详解 Android从Linux系统启动有4个步骤: (1) init进程启动 (2) Native服务启动 (3) System Server,Android服务启动 (4) ...

  6. Android GLSurfaceView用法详解(二)

    输入如何处理       若是开发一个交互型的应用(如游戏),通常需要子类化 GLSurfaceView,由此可以获取输入事件.下面有个例子: java代码: package eoe.ClearTes ...

  7. Android编译过程详解(一)

    Android编译过程详解(一) 注:本文转载自Android编译过程详解(一):http://www.cnblogs.com/mr-raptor/archive/2012/06/07/2540359 ...

  8. Android.mk文件详解(转)

    源:Android.mk文件详解 从对Makefile一无所知开始,折腾了一个多星期,终于对Android.mk有了一个全面些的了解.了解了标准的Makefile后,发现Android.mk其实是把真 ...

  9. Android Studio 插件开发详解四:填坑

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/78265540 本文出自[赵彦军的博客] 在前面我介绍了插件开发的基本流程 [And ...

  10. Android Studio 插件开发详解三:翻译插件实战

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/78113868 本文出自[赵彦军的博客] 一:概述 如果不了解插件开发基础的同学可以 ...

随机推荐

  1. Early Media and Music on Hold

    Early media refers to any media that is played to the initial caller’s phone before the remote party ...

  2. ACM学习历程——HDU4814 Golden Radio Base(数学递推) (12年成都区域赛)

    Description Golden ratio base (GRB) is a non-integer positional numeral system that uses the golden ...

  3. BZOJ3127:[USACO2013OPEN]Yin and Yang

    浅谈树分治:https://www.cnblogs.com/AKMer/p/10014803.html 题目传送门:https://www.lydsy.com/JudgeOnline/problem. ...

  4. poj2228Naptime——环形DP

    题目:http://poj.org/problem?id=2228 dp[i][j][0/1]表示前i小时中第j小时睡(1)或不睡(0)的最优值: 注意第一个小时,若睡则对最终取结果有要求,即第n个小 ...

  5. ZigBee简介

    前言 目前,中国大力推广的物联网是zigbee 应用的主战场,物联网通过智能感知.识别技术与普适计算(我还特意申请了个域名psjs.vip).泛在网络的融合应用,被称为继计算机.互联网之后世界信息产业 ...

  6. rmmod: chdir(/lib/modules): No such file or directory

    内核版本:linux3.4.20 交叉编译器:arm-linux-gcc 4.3.3 busybox :  busybox 1.20 问题: 使用rmmod会出现 rmmod : chdir(/lib ...

  7. exsi thick convert to thin

    http://gaoming.blog.51cto.com/822334/1176139

  8. xgene:之illumina,,ion-torrent

    illumina技术: 工具:flowcell(流动池):8通道,每个通道都有 2种DNA引物 种在玻璃表面(用共价键连到Flowcell上),这引物和文库中的接头互补    Flowcell:8个l ...

  9. Primer回顾 标准库类型

    string类型的输入操作符: 1.读取并忽略开头所有的空白字符(如空格,换行符,制表符). 2.读取字符直至再次遇到空白字符,读取终止.   用getline读取整行文本 getline.接受两个参 ...

  10. c++控制台 设置字体颜色

    一种方法是直接在程序上方栏杆点右键,然后属性处设置 优点是设置后一劳永逸,不需要像后面方法那样要自己把设置写入程序代码内 缺点是,一旦设置了就不能再改变了,程序从头到尾都是那种设置. 第二种方法是使用 ...