写代码时,我们一般会习以为常的按照惯例的方法走,最后项目也能够完成,顺利竣工。但是,有的朋友查看App内存消耗时,会发现“同样的应用产品,自己的App消耗的内存居然比别人多”。原因就在于,技术不断更新,做同样的事,由于方式不同,消耗的内存量也不同。在能完成同样一件事,用户界面体验相同时,我们应该采取更有效的方式(例如内存消耗更低)。下面还是举例,进行说明。
*****个人经验狭隘,难免有所疏漏,若有问题,恳请斧正!*****
1.单张图片的加载与内存消耗
“加载一张图片,完全不是事,不需要优化”这应该是很多初入开发的朋友的想法,没错,确实是“这都不是事儿”。 但是,应用中最常见的就是图片加载,一旦图片多了,积少成多,就可能出现OOM,所以我们应从小处入手,关注每一张图的优化。(在这里,我先说说单张图片的优化,多图加载将在后面论述中详细说明)
(1)加载的图片的大小不应超过展示图片的View的大小。举个例子,一张图片是1920px*1080px的,一个ImageView的大小是1280px*720px,当在此ImageView中加载这张图片时,实际显示的是1280px*720px的图片,而内存中却消耗了1920px*1080px的图片的大小。这样就相当于多做了无用功,浪费内存。而我们期望的是实际内存消耗的大小也应该是1280px*720px图片大小,此时我们就应该压缩图片大小后再显示。例如,我们可以按照谷歌官方推荐的方式压缩,我写了一个示例如下:
/** * Bitmap宽高压缩工具 * * @author ALion on 2016/11/17 18:46 */ public class BitmapUtil { /** * 不让工具类实例化 */ private BitmapUtil() { throw new UnsupportedOperationException("Do not need instantiate!"); } /** * <Enable>压缩Bitmap * @param res getResources() * @param resId 图片资源Id * @param reqWidth 压缩后的最大宽度 * @param reqHeight 压缩后的最大高度 * @return 压缩后的Bitmap */ public static Bitmap decodeBitmap(Resources res, int resId, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;//设置为true后,在解码时不会分配内存,返回null的Bitmap,但是可以拿到宽高 BitmapFactory.decodeResource(res, resId, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);//计算inSampleSize options.inJustDecodeBounds = false;//重置为false,解码后返回有效bitmap return BitmapFactory.decodeResource(res, resId, options); } /** * 计算inSampleSize * @param options 携带原始图片宽高的对象 * @param reqWidth 压缩后的最大宽度 * @param reqHeight 压缩后的最大高度 * @return inSampleSize */ private static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) { final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; //计算出压缩后不大于reqWidth、reqHeight的inSampleSize if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2;//得2的幂的数,是因为正真解码的时候,还是会自动向下处理,得到最靠近2的幂的数 } } return inSampleSize; } }
上述工具类,根据图片和View的大小对图片进行了压缩,这样就保证了图片不会做无意义的内存消耗。同时,你还可以修改工具类,改为传入Bitmap,而不是资源Id。
(2)注意图片的色彩格式。首先,我们理应知道到一下几点:
//A:透明度 R:红色 G:绿 B:蓝 //Bitmap.Config ARGB_8888:1像素=4字节,即A=8,R=8,G=8,B=8,那么一个像素点占8+8+8+8=32位 //Bitmap.Config ARGB_4444:1像素=2字节,即A=4,R=4,G=4,B=4,那么一个像素点占4+4+4+4=16位 //Bitmap.Config RGB_565: 1像素=2字节,即无A,R=5,G=6,B=5,那么一个像素点占5+6+5=16位 //Bitmap.Config ALPHA_8: 1像素=1字节,只有透明度,没有颜色。
不同的色彩格式,消耗的内存也不同,实际使用中应根据想要的效果来确定。例如,一张高清海报应该采用ARGB_8888,日常普通使用应采用ARGB_4444(有透明度,PNG)或者RGB_565(无透明度,JPG),ALPHA_8(只有透明度无颜色)使用很少见。(关于这点,第三方图片加载框架中很明显的例子就是Picasso和Glide,Picasso默认采用ARGB_8888,而Glide默认采用RGB_565,消耗的内存更小)
2.不需要时,就不传参
在使用一些方法函数时,我们常会传一些参数进去,其实有的时候,某些参数是无需(通常会去new一个对象传进去),传一个null既可。例如,我们在一个Canvas上面添加一张已画好的Bitmap,如下:
// Canvas canvas = new Canvas(resultBitmap); // canvas.drawBitmap(bitmap, 0, 0, new Paint()); Canvas canvas = new Canvas(resultBitmap); canvas.drawBitmap(bitmap, 0, 0, null);
在canvas调用drawaBitmap时,最后一个参数需要传一个Paint进去,但是实际上我们的Bitmap是一张已经画好的图,这时就可以直接传null,而不是像注释掉的代码一样new Paint()。
这里只提出一个例子,实际开发中还有很多地方要去注意,不做无意义的内存消耗。多注意你的代码是否需要某个参数,传进去是不是有意义?
3.实现同样的效果,应使用较轻量级的控件
开发中,要实现同样的一个效果,可能有很多种方式,那么又该如何选择呢?考虑到移动端内存的问题,当然是优选内存消耗较小的控件了。(同时,你还应该多关注网络上最新的实现方案)
举个例子,想要实现App中最常见的卡片式布局(底部有一排按钮,点击可以切换不同界面)。在过去,我们通常采用的实现方式是TableHost+Activity,而现在可以采用FragmentTableHost+Fragment来实现。Activity属于重量级控件,而Fragment相对属于轻量级,更有利于内存优化,减小内存的使用,因此应该更多的使用Fragment。(另外,你还可以采用ViewPager+Fragment+RadiuGroup,方式很多)
总结,开发中某些情况按照我们自己的思路,可能顺利的就实现了某个功能,但是我们不应只限于此,而应多思考是否有更好的方式来实现某个功能,也就是常说的“不仅要做到,还要做好!”。多去看,多去揣摩,多去思考,多去实践一番,去除不好的实现方式,得到更好的实现方式。