Andorid性能优化(一) 之 如何给App进行内存优化

1 前言

Android系统为每个应用进程都分配一个有封顶的堆内存值,当应用内存占用过高到没有足够的内存来提供给新对象分配并且垃圾回收机制也已经没有空间可回收时就会OOM。当一个应用内存占用过高会使一些性能差的手机系统内存紧缺,使得整体系统卡顿。而且应用内存占用过高后,一旦退到后台后,就会容易被系统杀死,这点我们在前面《Android进程回收机制和保活方案》中有介绍过,这时一旦你需要进行一些后台工作时就会很被动。除此以外,若比起竞品中内存使用占用过高就会处于劣势,也会容易引起用户反感,从而弃之。今天这篇文章我们就来看看导致应用内存占用过高的情况和解决办法。

2 相关概念

在Android中进程的内存占用按从大到小可以分为:VSS >= RSS >= PSS >= USS。可以通过adb命令:adb shell procrank来查看系统中所应用的VSS、RSS、PSS 和USS的值情况,如下图。

VSS(Virtual Set Size)表示进程总共可访问的内存大小。它包括了分配但尚未使用的虚拟内存所有共享库所占用内存进程本身占用内存

RSSResident Set Size表示进程实际使用的物理内存大小。它包括了所有共享库所占用内存假如有3个进程使用同一个共享库占用了30M内存,这里的值是30M) 和 进程本身占用内存

PSSProportional Set Size表示进程实际使用的物理内存大小。它包括了按进程比例平分的共享库所占用的内存假如有3个进程使用同一个共享库占用了30M内存,这里的值是10M) 和 进程本身占用内存

USSUnique Set Size表示进程独自占用的物理内存大小。它仅包括进程本身占用内存

一般情况下,反映进程内存占用情况我们会选择查看PSS的值。在系统中应用管理或第三方工具中,要查看一个进程的内存使用情况都是用PSS来表示的。也可以通过adb命令:adb shell dumpsys meminfo XXX (XXX表示进程名)来查看进程的PSS值和组成部分,如下图。

要想知道一台手机给一个进程实际上能分配到多大的堆内存,可以使用代码来查看:

ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
int memory = am.getMemoryClass();
int largeMemory = am.getLargeMemoryClass();

其中,getMemoryClass()方法返回的是标准的内存封顶值;而getLargeMemoryClass()方法返回的是最大的内存封顶值,要想你的App内存封顶值能使用到最大,可以在AndroidManifest中的Application中添加 android:largeHeap=“true”。例如笔者手上的一台小米6,手机本身是6GB的内存,而使用上面的代码后输出的结果是:256M和512M。

 

3 内存优化方案

3.1 解决内存泄漏

一个应用中若存在大量的内存泄漏是使应用内存占用升高的主要原因之一。处理好内存泄漏就是对内存优化最为立竿见影的方法。关于内存泄漏场景和定位内存泄漏方法可看后面两篇文章《Andorid性能优化(二) 之 内存泄漏场景介绍》《Andorid性能优化(三) 之 如何定位内存泄漏》。

3.2 避免代码量过多

代码量多的应用,除了影响到安装包和安装后的占用空间外,其实还会对内存占用影响。我们从上面通过adb命令获得进程PSS值和组成部分中可以看到,有.so mmap和.dex mmap,它们就是分别对应Native层和Java层代码量所占用的内存。因为使用代码本身也占用内存,Android会把进程所使用的代码也算入进程所使用的内存,也就是说,你的应用功能所需要的Java代码量很多的话,.dex mmap就会越大。所以我们在日常开发版本迭代中,如若明确不需要的功能不要因为舍不得而不忍心移除,一个应用中代码能简便简。还有避免使用过多的第三方库从而导致安装包大小变大和因代码量过多导致内存占用高。

3.3 高效使用Bitmap

3.3.1 使用采样率高效加载图片

通常情况下,内存占用高的是使用Bitmap造成的,特别现在手机分辨率越来越大,在Android中加载一张图片它所占用的内存可能会在几M到几十M不等。很多时候界面中需要显示的图片大小并没有源图片尺寸那么大,这时若加载源图片就会产生浪费内存,所以合理加载Bitmap是非常重要的。

在Android中通过使用BitmapFactory类提供了四类方法:decodeFile、decodeResurce、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象。而采用BitmapFactory.Options对象的inSampleSize参数设置采样率可以将图片的加载进行缩放,从而可高效地加载所需尺寸的Bitmap。

情况1,当inSampleSize == 1时:采样后的图片大小为图片原始大小。

情况2,当inSampleSize < 1时:其作用相当于1,即无缩放效果。

情况3,当inSampleSize > 1时:比如值是2,那么采样后的图片其宽/高均为原图大小的1/2,而像素为原图的1/4,(缩放比率为1 / ( inSamplesize 2 )),假设采用的是ARGB8888格式存储的话,占用内存大小为原图的1/4。例如,一张1024 *1024像素的图片来说,采用ARGB8888格式存储(8个bit等于1byte,所以这里是4byte),它占有的内存为1024*1024*4,即4MB,如果inSampleSize为2,那么采样后的图片其内存占用人有512*512*4,即1MB。

建议:最新的官方文档中指出,inSampleSize的取值应该总是2的指数,比如1、2、4、8、16等等。如果传入不为2指数,系统会向下取整并选择一个最接近2的指数来代替,比如3,会用2来代替,但是经过验证发现这个结论并非在所有的Android版本中都成立,因此把它当成一个开发建议即可。

封装方法代码如下:

public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
    final BitmapFactory.Options options = new BitmapFactory.Options();

    // inJustDecodeBounds为true时,是轻量级的加载,BitmapFactory只会解析图片的原始宽/高信息,并不会去真正地加载图片
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // 计算 inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    if (reqWidth == 0 || reqHeight == 0) {
        return 1;
    }
    // outWidth 和 outHeight 表示原图大小
    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;

        while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
}

调用代码:

Bitmap bitmapObj = decodeSampledBitmapFromResource(getResources(), R.mipmap.myImage, 100, 100);
mImageView.setImageBitmap(bitmapObj);

当Bitmap对象使用完后,一定要进行资源回收,如代码:

if (!bitmapObj.isRecycled()) {
    bitmapObj.recycle();
    System.gc();
}

3.3.2 Bitmap格式存储类型选择

在不考虑透明度的情况下,一个像素点的颜色在计算机中的表示方法有以下3种:

浮点数编码就是RGB(1.0, 1.0, 1.0),每个颜色值各占1个float,其取值范围都是0.0~1.0。

24位整数编码就是RGB(255, 255, 255),每个颜色值各占8 bit(2^8),其取值范围都是0~255。

16位整数编码就是RGB(31, 63, 31),第1和第3个颜色值各占5 bit,其取值范围都是0~31;第2个颜色值占6 bit,其取值范围是0~63。

在Java中,float和int类型的变量都是占32 bit,short和char类型的变量都是占16 bit,可得结论:

浮点数编码中一个像素的颜色内存占用量是3个float类型,大小是 32 * 3 = 96 bit,即12个byte(8 个bit  = 1个byte);

24位整数编码中一个像素的颜色内存占用量仅用一个int类型即可,大小是 32个bit,即4个byte(高 8 bit 空着不用,低 24 bit 用于表示颜色);

16位整数编码中一个像素的颜色内存占用量仅用一个short类型即可,大小是 16个bit,即2个byte。

所以不必再考虑浮点数法编码,采用整数法编码的颜色值是可以大大节省内存的,在Android中获取Bitmap的时候一般也是采用整数编码。其中RGB编码格式有:

RGB565(short)R、G、B分别占5bit、6bit、5bit,取值为:RGB(31, 63, 31)。同理RGB555(short)R、G、B 就是各点5bit(剩下1bit不用),取值为:RGB(31, 31, 31)。

ARGB4444(short):A、R、G、B各点4bit,取值为:RGB(15, 15, 15)(使用它的话图片质量太差,已经不推荐使用了)

ARGB8888(int):A、R、G、B各点8bit,,取值为:RGB(255, 255, 255)

由此可得出计算一个长和宽都是1024的Btimap所占用的内存应该是(8个bit = 1个byte):

RGB565:        1024 * 1024 * 2 = 2M

ARGB4444:  1024 * 1024 * 2 = 2M

ARGB8888:  1024 * 1024 * 4 = 4M

所以我们在平时使用Bitmap时,如果需要展示的界面效果要求不是特别地高和不需要考虑透明时,就可以考虑使用RGB565格式存储类型,这样会比使用ARGB8888省下一半的内存占用。

3.3.3 能不使用Bitmap就尽量不使用

有些时候,我们需求中可能只需要比较简单渐变色或图形,那么不使用Bitmap,改用Android提供的Drawable也是可以的,Drawable常被用来作为View的背景使用。它一般都是通过XML来定义。又或者可以通过自义View的onDraw来绘制实现。经验证,无论使用Drawable还是自定义View的onDraw,它们所占用的内存都是非常小的,这样就可以大大地省下了使用Bitmap的内存了。

有时候在不得不用图时,先要考虑一下能否使用尺寸尽量小的图或使用.9图,因为前面也提到过,图片的尺寸大小和内存占用是成正比的。

3.4 使用临时进程

在文章的开始时,我们有提到过,Android系统为每个应用进程都分配一个有封顶的堆内存值,我们唤此值为Heap Size。Heap Size等于未被使用的堆内存Free Size + 当前实际已分配的堆内存Allocated Size。一般情况下,系统不会一开始就分配给一个进程一个封顶值,现假设封顶值为120M。目前应用中只使用到10M,即Allocated Size是10M,系统可能就只分配给你的应用的Heap Size是15M,当你的应用在使用过程中使用到的内存分配变成了50M,那么系统可能就会将Heap Size涨到了60M。当然实际多少是根据系统计算,我在这只是假设一个值。不管怎样,能看出来系统会随着你的应用的实际需求Allocated Size来增加Heap Size值。但是值得注意的是,当你应用中触发了GC后,Allocated Size得到了下降后,Heap Size并不会随着Allocated Size而下降

得出结论

如果应用中逻辑执行过程中使用了大量的内存,即使运行完之后已经释放了中间过程的内存,内存在短时间内仍然会高企不下,这是由Android的内存调度机制决定的,而且这段短时间可能是几十秒到几百分钟不等。

解决方案

一般在大多数应用中,都会存在一个前台进程后台进程,前台进程专门负责UI的展示和用户交互,后台进程一般用于后台计算、轮询、保活等操作。前台进程在用户退出界面后,不用担心Heap Size是否高低放心死去。而后台进程的进程优先级并不高,如若还存在Heap Size很高的话,就会很容易被系统回收杀死。所以我们在架构层面可以考虑引进临时进程,临时进程只负责做一些一次性性质并且高耗内存的逻辑处理,这样就能有效控制前台进程和后台进程的Heap Size。临时进程做完事情后如果有返回结果可将结果通过AIDL传递到前台进程或后台进程,然后自杀掉自己就完事了,只要不超出封顶的Heap Size值,是完全不是担心内存问题。

实施注意

1.分析是否一次性性质逻辑

何为一次性性质逻辑?一般地,不需要直接输出结果的逻辑就是一次性性质逻辑,例如下载文件并保存起来、做一些复杂的逻辑处理后将其结果保存到SharedPreferences中,等。那么哪些为非一次性性质逻辑?一般地,需要直接输出结果的逻辑,例如通过一系列计算后,将其结果返回到外部进行使用,又例如内存缓存性质的逻辑,像从数据库读取一些数据缓存到内存中,供外部进行快速读取,等。

我们在分析是否一次性性质逻辑时,要完整地分析出所实现的功能,不能放过任何一个细节,并确保不存在与原有进程的互相依赖关系。

2.改造非一次性性质逻辑

像通过计算后返回结果的情况,我们完全可以通过改造它将其返回结果通过AIDL的callbakc的接口方式来实现像一次性性质逻辑一样放在临时进程中去处理。但要注意分析出哪些数据作为跨进程传输的关键数据,一般来说,传递的数据越少越好。

3.注意原来功能完整性

我们将逻辑移到临时进程后,需要进行完整的逻辑覆盖自测,毕竟分析阶段只是属于理论,得看真实效果。

3.5 界面布局优化

在Android界面布局的XML中,控件越少和层级嵌套越少,绘制的工作量也就越少,从而应用占用内存也就越少,性能也就越高。界面布局优化的手段一般有下面这些情况:

3.5.1控制布局层级

尽可能地控制布局层级,删除布局中无用的控制和层级

3.5.2 选择性能较好的ViewGroup

有选择地使用性能较好的ViewGroup,比如能使用LinearLayout或FrameLayout不使用RelativeLayout,因为像LinearLayout如果不使用weight属性的话,只measure一次,而RelativeLayout是和它的子View存在彼此依赖关系,所以是需要measure两次的。当然如果要嵌套两个就不如直接使用RelativeLayout。

3.5.3使用<include>和<merge>标签

<include>和<merge>两个标签一般地都是配合使用,它们能使布局降低减少布局的层级,从而也可以使XML代码更加简洁。<include>标签可以将一个指定的布局文件加载到当前的布局文件中,而<merge>标签一般是要跟<include>标签配合使用,从而去掉多余的一层嵌套,它们的示例如下:

<LinearLayout
    android:orientation=”vertical”
    ……>
    <include android:id=“@+id/test_include”
        android:layout_width=”match_parent”
        android:layout_height=”match_parent”
        layout=”@layout/test_include” />
    ……
</LinearLayout>

Test_include.xml:

<merge xmlns:android=”http://schemas.android.com/apk/res/android”>
    <Button
        ……>
    <Button
        ……>
</merge>

说明和注意:

  1. <include>标签只支持android:id除外的android:layout_开头的属性;
  2. 如果在<include>指定了id,同时被包含的布局文件的根元素也指定了id属性,那么以<include>的为准;
  3. <include>标签如果指定了android:layout_*这种属性,那么必须存在layout_width和layout_height属性,否则不起作用;
  4. 由于在当前布局是竖起方向的LinearLayout,这时如果被包含的布局文件也是采用竖直的LinearLayout,那么就多余了,所以通过<merge>标签可去掉多余的一层LinearLayout。

3.5.4使用ViewStub按需加载

ViewStub继承了View,它非常轻量级且宽/高都是0,因此它本身不参与任何的布局和绘制过程。它的意义在于按需加载所需的布局文件。比如网络异常时的界面,这时就没必要在整个界面初始化时将其加载进来,通过ViewStub就可以做到使用的时候再加载,提高了程序初始化时的性能。使用示例如下

<ViewStub
    android:id=”@+id/stub_import”
        android:inflatedId=”@+id/panel_import”
        android:layout=”@layout/layout_network_error”
        android:layout_width=”match_parent”
        android:layout_height=”wrap_content”
        android:layout_gravity=”bottom” />

说明:

1.stub_import是ViewStub的id,而属性inflatedId中panel_import是layout_network_error这个布局的根元素的id;

2.需要时加载,可以在代码中这样实现:

        ((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);

        或

        View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();

3.ViewStub不支持<merge>标签。

3.6 绘制优化

绘制优化是指自定义View中进行绘制的优化,有些时候一个自定义View中加入动画效果后就会占用了大量的内存。如果不做好性能处理的话,若绘制的界面放置时间一长就很简单造成OOM。所以在绘制方面要注意下面事项:

  1. onDraw中尽量不要创建新的局部对象,这是因为onDrarw方法可能会被频繁调用,这样就会在一瞬间产生大量的临时对象,这不仅占用了过多的内存而且还会导致系统更加频繁gc,降低了程序的执行效率。
  2. onDraw方法中不要做耗时的任务,也不能执行成千上万的循环操作,尽管是轻易级,大量的循环十分抢占CPU的时间片,这会造成View的绘制过程不流畅。
  3. onDraw中绘制图形图像尽量避免过度绘制,换句话说就是昼避免叠加绘制。我们在开发调试中,可以打开“开发者模式”->”调示GPU过度绘制”来查看过度绘制区域。
  4. 避免过多调用invalidate()或postInvalidate()方法,特别是避免在属性动画过程中回调来调用postInvalidate()方法,可改成使用postInvalidateOnAnimation()方法。或者直接在属性动画中只更新全局的属性值,然后在onDraw()中读取该属性值进行绘制,在onDraw()方法最后进行一个间隔16毫秒(如果刷新率是60,1秒里60次刷新,就是1000/60=16.67)后再调用invalidate()。
  5. 善用onWindowVisibilityChanged、onAttachedToWindow 和onDetachedFromWindow回调方法监测窗口情况来处理动画的播放和停止。

更多关于自定义View绘制事项,可以参考之前的《Android中的自绘View的那些事儿》系列文章。

3.7 线程优化

大量的线程的创建和销毁也会带来内存的开销。线程池可以重用内部的线程,所以可以避免这种创建和销毁线程的开销,而且还能有效地控制线程池的最大并发数,避免大量的线程互相抢占系统资源从而导致阻塞现象发生。更多线程池的使用和介绍可以参考之前的文章《Android中的线程池》

3.8 使用注解代替枚举

我们在日常开发中会很经常需要使用到枚举,但是其实枚举占用的内存空间要比整型大。因为enum类型是引用类型。通过反编可发现定义的enum类型会生成继承于Java.lang.Enum的类,而类中每个枚举项都会被声明成一个本类的类型的静态变量和产生一个本类类型的静态数组。这就会引起对静态变量的引用,而且Java.lang.Enum类本身也存在变量占用内存。所以可以考虑使用Typedef注解来代替枚举的使用,关于注解的介绍可以参考之前的文章《Android中注解(Support Annotations)的使用》

3.9 善用Android特有的数据结构

使用SparseArray或ArrayMap代替HashMap

SparseArray和ArrayMap比HashMap更省内存,它们对数据采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间,但是同时也牺牲了效率,因为它们使用了二分查找法,并且当删除或者添加数据时,会对空间重新调整。如果key的类型是int、long或者boolean类型,那么使用SparseArray,因为它避免了自动装箱的过程;如果key类型为其它的类型,则使用ArrayMap。两个数据结构都适合数据量不是特别大的情况。使用示例:

SparseArray<String> sparseArray = new SparseArray<String>();
sparseArray.put(1, "zyx");
sparseArray.put(2, "子云心");
//通过int类型的key获取value
sparseArray.get(1);
//获取索引处的key与value
sparseArray.keyAt(1);
sparseArray.valueAt(1);

ArrayMap<String, String> arrayMap = new ArrayMap<>();
arrayMap.put("username", "zyx");
arrayMap.get("username");

善用Pair

Pair是一组元素,是成对存在,使用上跟Map很像。正常Map是有一个关键的key来完成比较和取value等一系列操作,但是Pair不一样,它就几乎只有3个用法:equals()、first、second。在某些情况下,既需要以键值的方式存储数据列表,还需要在输出的时候保持顺序。HashMap满足前者,ArrayList则满足后者,再不打算去多做修改且数据类型相对简单时,可以选择Pair和搭配ArrayList使用。Pair使用示例:

Pair p1 = new Pair(1, "子");
Pair p2 = Pair.create(2, "云");
Pair p3 = Pair.create(3, "心");
boolean result = p1.equals(p2);
int index = (int)p1.first;
String name = (String)p1.second;

 

好了,内存的优化方案暂时就列举到这里,后面遇到新情况或方案会继续补充!!

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值