美文网首页
由一次引导页读取图片引发OOM后的思考

由一次引导页读取图片引发OOM后的思考

作者: Mr_zhaoF1 | 来源:发表于2017-11-23 19:42 被阅读0次

    一般我们做引导页的逻辑是:

    1.先判断是否第一次启动app,如果是,则进入功能使用导航(最简单的做法就是,左右滑动切换查看,滑动到最后一页点击按钮进入首页)。

    2.如果不是,则显示启动屏,2秒之后进入首页。

    如果有广告怎么办?广告从服务器拿,缓存到本地,没网的时候可以显示,或者使用webView来显示广告。自己写的话用ViewPager即可:  www.jianshu.com/p/adb21180862a

    但我在逛github时,发现了一个不错的控件:2000+star  使用也很方便

    地址:github.com/bingoogolapple/BGABanner-Android

    由于项目需要加载本地图片,需要在此处:

    BGABanner-Android 加载本地图片

    OK 在自己手机上测试非常流畅 在蒲公英上部署完版本后我就去忙别的事了  

    可是不一会项目经理说在他手机上应用打不开,直接崩溃  

    早期在构建项目时我引入了bugly,于是立刻看bugly后台,必须说腾讯出的bugly确实很好用  报错一目了然 还有解决方案

    OOM 内存溢出

    问题一目了然,加载图片时占用内存过多直接崩溃,发生OOM了,我的手机内存空间比较大,躲避了OOM,但内存小一些的手机就崩溃了,其实加载网络图片也容易发生OOM,不过在此处是另外一种情况,对于此,解决方式很多:

    1.适当调整图像大小。 

    我推荐使用 熊猫压缩 tinypng.com/  

    熊猫压缩网站

    直接在网站上压缩,方便快捷,图片不失真,效果显著,可一次上传多张图片

    2.采用合适的缓存策略。

    一般用于首次加载通过网络加载,获取图片,然后保存到内存和 SD 卡中。

    之后运行 APP 时,优先访问内存中的图片缓存。

    如果内存没有,则加载本地 SD 卡中的图片。

    具体的缓存策略可以是这样的:内存作为一级缓存,本地作为二级缓存,网络加载为最后。其中,内存使用 LruCache ,其内部通过 LinkedhashMap 来持有外界缓存对象的强引用;对于本地缓存,使用 DiskLruCache。加载图片的时候,首先使用 LRU 方式进行寻找,找不到指定内容,按照三级缓存的方式,进行本地搜索,还没有就网络加载。

    代码:www.jianshu.com/p/05132e3b7320

    3.采用低内存占用量的编码方式

    比如Bitmap.Config.ARGB_4444比Bitmap.Config.ARGB_8888更省内存。

    1920*1200的图片:

    ARGB_8888:1920*1200*4/1024/1024=8.79MB

    ARGB_4444,RGB565:1920*1200*2/1024/1024=4.39MB

    但是色彩质量会下降。

    4.及时回收Bitmap

    一般而言,回收bitmap内存可以用到以下代码

    if(!bitmapObject.isRecyled()) {  // Bitmap对象没有被回收

    bitmapObject.recycle();  // 释放

    System.gc();  // 提醒系统及时回收

    }

    bitmap.recycle()方法用于回收该bitmap所占用的内存,接着将bitmap置空,最后,别忘了用System.gc()调用一下系统的垃圾回收器。

    在这里要声明一下,bitmap可以有多个(以为着可以有多个if语句),但System.gc()最好只有一个(所以我将它写在了if语句外),因为System.gc()

    每次调用都要将整个内存扫描一遍,因而如果多次调用的话会影响程序运行的速度。为了程序的效率,我将它放在了所有回收语句之后,

    这样已经起到了它的效果,还节约的时间。

    回收bitmap已经知道了,那么“及时”怎么理解呢?

    根据我的实际经验,bitmap发挥作用的地方要么在View里,要么在Activity里(当然肯定有其他区域,但是原理都是类似的),

    回收bitmap的地方最好写在这些区域刚刚不使用bitmap了的时刻。

    比如说View如果使用了bitmap,就应该在这个View不再绘制了的时候回收,或者是在跳转到的下一个区域的代码中回收;

    再比如说SurfaceView,就应该在onSurfaceDestroyed这个方法中回收;

    同理,如果Activity使用了bitmap,就可以在onStop或者onDestroy方法中回收......

    结合以上的共同点,“及时回收”的原理就是在使用了bitmap的区域结束时或结束后回收。

    5 在manifest文件application节点加入android:largeHeap=“true”

    设置largeHeap的确可以增加内存的申请量。但不是系统有多少内存就可以申请多少,而是由dalvik.vm.heapsize限制。

    但是作为程序员的我们应该努力减少内存的使用,尽量想回收和复用的方法,而不是想方设法增大内存。当内存很大的时候,每次gc的时间也会长一些,性能会下降的。

    6 使用绘制背景或者Drawable代替图片

    以上都是一些常用的解决方案,回到项目中我发现,此前为了减小apk大小,在mipmap中对于引导图的资源图片,我只用了一套。而且图片大小我之前已经压缩过,每张都仅仅在200K左右,这是为什么呢?

    碰巧看到了这篇测试研究:关于Android中图片大小、内存占用与drawable文件夹关系的研究与分析    mp.weixin.qq.com/s/7I8JcjzUzDl46cfAe8yS4Q

    从上面的测试结果,我们可以得出如下结论:

    1.同一张图片,放在不同目录下,会生成不同大小的Bitmap

    2.Bitmap的长度和宽度越大,占用的内存就越大

    3.图片在硬盘上占用的大小,与在内存中占用的大小完全不一样

    我们以放在drawable文件夹下面的图片为例,加载到内存之后,2160*3840大小的Bitmap占用的内存为

    2160  3840  4 = 3317,7600 byte = 3,2400kb = 31.640625 M

    所以drawable文件夹下的App内存占用 = 原始内存8.31M+图片内存31.64M= 39.95M ,与实际内存占用39.88M存在0.1755%的误差,在误差范围之内。

    先简单解释一下上面的计算公式,长*宽是图片的像素总数,乘以4则是因为一个像素占用A、R、G、B四个通道,每个通道占用8位,所以描述一个像素需要32位即4个字节。

    一个颜色通道需要8位描述,2^8=256,所以每个颜色通道就有256种状态。如果把彩色图转化成灰阶图的话,也有256种状态分割从白色到黑色之间的过渡颜色。

    当然,也并不是所有格式的图片每个像素占用4字节,这和图片在加载时设置的Bitmap.Config有关,默认的是Bitmap.Config.ARGB_8888,其他类型如下:

    Bitmap.Config.ALPHA_8 此时图片只有alpha值,没有RGB值,1个像素占用一个字节

    Bitmap.Config.ARGB_4444 一个像素占用2个字节,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各占4个bites共16bites,即2个字节

    Bitmap.Config.ARGB_8888 一个像素占用4个字节,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各占8个bites,共32bites,即4个字节。这是一种高质量的图片格式,在电脑上普通采用。它也是Android手机上一个Bitmap的默认格式。

    Bitmap.Config.RGB_565 一个像素占用2个字节,没有alpha(A)值,即不支持透明和半透明,Red(R)值占5个bites ,Green(G)值占6个bites ,Blue(B)值占5个bites,共16bites,即2个字节。对于没有透明和半透明颜色的图片来说,该格式的图片能够达到比较的呈现效果,相对于ARGB_8888来说也能减少一半的内存开销。因此它是一个不错的选择。

    那么为啥在硬盘上存储只需要77.11k,放到内存里面就需要30多M呢?

    存放在硬盘上的图片文件,会根据各自的压缩规则进行压缩,比如Jpeg这种有损压缩的图片格式,最常使用可变字长编码的哈弗曼编码,会使用哈弗曼树,也就是最优二叉树,根据某些数据出现的频率对数据段编码,从而减少占用的硬盘大小。

    比如说“10111”这个序列在图片的二进制数据中出现的概率最大,那我们可以用“01”来代替这一段数据,原来5位的数据,用2位就可以表示了,这就是压缩率60%。当然这只是打个比方,在实际操作中需要考虑“异前缀原则”等编码的基本原则。

    而如果把图像读取到内存中就不一样了,因为我们需要每一个像素都能在屏幕上显示,所以会把每个像素点都加载至内存中,不会对相同像素进行压缩或者是替换,所以你也应该能明白前面提到的Bitmap占用内存大小的计算公式的由来了。

    说到这里,其实后两个结论已经解释清楚了,那么为什么“同一张图片,放在不同目录下,会生成不同大小的Bitmap”呢?

    如果你真的看懂了我之前写的文章,那么这个问题应该不算问题。

    我的测试设备为锤子T1,10801960,xxhdpi,所以说,如果把这张放置在xxhdpi的话,应该不会对图像进行放缩,也就是原始大小,所以我们在前面得到drawable-xxhdpi文件夹下,图片大小为720  1280是完全可以理解的,就是图片本身的大小。

    当图片放置在drawable-hdpi中时,图片大小为1440 * 2560,长宽变为原来的两倍,这是因为不同分辨率之间的倍数关系导致的,来一张图

    我们可以很明显的看到xxhdpi是hdpi的2倍,所以如果单独放置在某个drawable文件夹,手机会自动根据当前的屏幕密度对图片进行放缩。

    比如上面,当把图片放置在xxxhdpi里面的时候,在xxhdpi的设备上,图片长 = 720  (3/4) = 540,图片宽 = 1280  (3/4) = 960,这与上面的测试结果是完全一致的。

    至于为什么在前面的测试中,drawable和drawable-mdpi是一样的大小,是因为drawable-mdpi是系统默认的像素密度,其他像素密度都以它为基数,当只在drawable中存在图片时,如果使用该图片,那么将按照drawable-mdpi的放缩比例进行放缩。

    结论

    从上面的测试我们可以得出以下几个结论:

    当图片放置在不同drawable文件夹中,且只有这一张图片时,运行设备会根据自身的屏幕密度,对图片进行放缩,放缩比例符合前面图上的规则

    图片文件的大小与在内存中占用的大小没关系,内存中实际占用大小与图片分辨率、像素显示参数有关

    所以,在一个App里面使用一套UI理论上应该是没有问题的,但是要注意

    最好使用较高分辨率的切图,并且放置在正确的drawable文件夹中,比如按照xxhdpi的分辨率进行切图,放置在drawable-xxhdpi中

    对于可以使用.9格式的图片,最好使用.9,减少资源大小

    如果有条件,最好提供多套UI切图。如果只有一套切图,系统需要对图片进行压缩,会进行大量运算,影响设备性能。同时,在某些情况下,系统对图片的压缩会可能会出现锯齿,造成信息的丢失

    如果是多套切图的话,最好不要直接用工具按照比例放缩,这样小图标会丢失一些细节。当然,这部分是美工来做的,可以让她参考这篇文章利用PS CS6的新功能保持ICON细节饱满完美

    思考一下,如果把一个本来应该放在drawable-xxhdpi里面的图片放在了drawable文件夹中会出现什么问题呢?

    在xxhdpi设备上,图片会被放大3倍,图片内存占用就会变为原来的9倍!

    希望这篇文章对你有所帮助,就写到这吧,我去吃鸡了!大吉大利!~

    相关文章

      网友评论

          本文标题:由一次引导页读取图片引发OOM后的思考

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