Android性能优化实践

5,683 阅读33分钟

在这里插入图片描述

2019年5月30号:
更新内存泄漏相关内容,新增使用系统服务引发的内存泄漏相关内容。
更新内存泄漏未关闭资源对象内存泄露,新增WebView扩展,介绍WebView的内存分配并提出解决方案。
2019年5月29号:
更新内存优化相关内容,新增内存管理介绍、内存抖动。
2019年5月28号:
用户zhangkai2811指出Fresco拼写错误,现已修改完毕。


绘制优化

绘制原理

View的绘制流程有3个步骤,分别是measure、layout和draw,它们主要运行在系统的应用框架层,而真正将数据渲染到屏幕上的则是系统Native层的SurfaceFlinger服务来完成的。

绘制过程主要由CPU来进行Measure、Layout、Record、Execute的数据计算工作,GPU负责栅格化、渲染。CPU和GPU是通过图形驱动层来进行连接的,图形驱动层维护了一个队列,CPU将display list添加到该队列中,这样GPU就可以从这个队列中取出数据进行绘制。

Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,VSYNC是Vertical Synchronization(垂直同步)的缩写,是一种定时中断,一旦收到VSYNC信号,CPU就开始处理各帧数据。如果某个操作要花费30ms,这样系统在得到VSYNC信号时无法进行正常的渲染,会发生丢帧。

产生卡顿原因有很多,主要有以下几点:

  • 布局Layout过于复杂,无法在16ms内完成渲染。
  • 同一时间动画执行的次数过多,导致CPU和GPU负载过重。
  • View过渡绘制,导致某些像素在同一帧时间内被绘制多次。
  • 在UI线程中做了稍微耗时的操作。
  • GC回收时暂停时间过长或者频繁的GC产生大量的暂停时间。

工具篇

1、Profile GPU Rendering

Profile GPU Rendering是Android 4.1系统提供的开发辅助功能,可以在开发者选项中打开这一功能,如下图:

单击Profile GPU Rendering选项并开启Profile GPU Rendering功能,如下图:

上面的彩色的图的横轴代表时间,纵轴表示某一帧的耗时。绿色的横线为警戒线,超过这条线则意味着时长超过了16m,尽量要保证垂直的彩色柱状图保持在绿线下面。这些垂直的彩色柱状图代表着一帧,不同颜色的彩色柱状图代表不同的含义:

  • 橙色代表处理的时间,是CPU告诉GPU渲染一帧的地方,这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复,如果橙色柱状图很高,则表明GPU很繁忙。
  • 红色代表执行的时间,这部分是Android进行2D渲染 Display List的时间。如果红色柱状图很高,可能是由重新提交了视图而导致的。还有复杂的自定义View也会导致红的柱状图变高。
  • 蓝色代表测量绘制的时间,也就是需要多长时间去创建和更新DisplayList。如果蓝色柱状图很高,可能是需要重新绘制,或者View的onDraw方法处理事情太多。

在Android 6.0中,有更多的颜色被加了进来,如下图所示:

下面来分别介绍它们的含义:

  • Swap Buffers:表示处理的时间,和上面讲到的橙色一样。
  • Command Issue:表示执行的时间,和上面讲到的红色一样。
  • Sync & Upload:表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,我们可以减少屏幕上的图片数量或者是缩小图片的大小。
  • Draw:表示测量和绘制视图列表所需要的时间,和上面讲到的蓝色一样。
  • Measure/Layout:表示布局的onMeasure与onLayout所花费的时间,一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题。
  • Animation:表示计算执行动画所需要花费的时间,包含的动画有ObjectAnimator,ViewPropertyAnimator,Transition等。一旦这里的执行时间过长,就需要检查是不是使用了非官方的动画工具或者是检查动画执行的过程中是不是触发了读写操作等等。
  • Input Handling:表示系统处理输入事件所耗费的时间,粗略等于对事件处理方法所执行的时间。一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作。
  • Misc Time/Vsync Delay:表示在主线程执行了太多的任务,导致UI渲染跟不上VSYNC的信号而出现掉帧的情况。

Profile GPU Rendering可以找到渲染有问题的界面,但是想要修复的话,只依赖Profile GPU Rendering是不够的,可以用另一个工具Hierarchy Viewer来查看布局层次和每个View所花的时间。

2、Systrace

Systrace是Android4.1中新增的性能数据采样和分析工具。它可帮助开发者收集Android关键子系统(SurfaceFlinger、WindowManagerService等Framework部分关键模块、服务,View体系系统等)的运行信息。Systrace的功能包括跟踪系统的I/O操作、内核工作队列、CPU负载以及Android各个子系统的运行状况等。对于UI显示性能,比如动画播放不流畅、渲染卡顿等问题提供了分析数据。在android-sdk/tools/目录的命令行中输入‘monitor’,会打开Android Device Monitor。

3、Traceview

TraceView是Android SDK中自带的数据采集和分析工具。一般来说,通过TraceView我们可以得到以下两种数据:

  • 单次执行耗时的方法。
  • 执行次数多的方法。

在android-sdk/tools/目录的命令行中输入‘monitor’,会打开Android Device Monitor,选择相应的进程,并单击Start Method Profiling按钮,对应用中需要监控的点进行操作,单击Stop Method Profiling按钮,会自动跳到TraceView视图。

也可以代码中添加TraceView监控语句,代码如下所示。

Debug.startMethodTracing();
...
Debug.stopMethodTracing();

在开始监控的地方调用startMethodTracing方法,在需要结束监控的地方调用stopMethodTracing方法。系统会在SD卡中生成trace文件,将trace文件导出并用SDK中的Traceview打开即可。当然不要忘了在manifest中加入

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>权限。

4、GPU过度绘制

Android手机上面的开发者选项提供了工具来检测过度绘制,可以按如下步骤来打开:

开发者选项->调试GPU过度绘制->显示过度绘制区域

如下图所示:

可以看到,界面上出现了一堆红绿蓝的区域,我们来看下这些区域代表什么意思:

需要注意的是,有些过度绘制是无法避免的。因此在优化界面时,应该尽量让大部分的界面显示为真彩色(即无过度绘制)或者为蓝色(仅有 1 次过度绘制)。尽量避免出现粉色或者红色。

优化建议

  1. 使用include标签来进行布局的复用
  2. 使用merge标签去除多余层级
  3. 使用ViewStub提高加载速度(延迟加载)
  4. 移除控件中不需要的背景
  5. 将layout层级扁平化,推荐使用ConstraintLayout
  6. 使用嵌套少的布局,合理运用LinearLayout和RelativeLayout
  7. onDraw()中不要创建新的局部变量以及不要做耗时操作

内存优化

知识扫盲

OOM:

系统分配给app的堆内存是有上限的,不是系统空闲多少内存app就可以用多少,getMemoryClass()可以获取到这个值。 可以在manifest文件中设置largeHeap为true,这样会增大堆内存上限,getLargeMemoryClass()可以获取到这个值。 超出虚拟机堆内存上限会造成OOM。

Low Memory Killer:

android内存管理使用了分页(paging)和内存映射(memory-mapping)技术,但是没有使用swap,而是使用Low Memory Killer策略来提升物理内存的利用率 ,导致除了gc和杀死进程回收物理内存之外没有其他方式来利用已经被占用的内存。 当前台应用切换到后台后,系统并不结束它的进程,而是把它缓存起来,供下次启动。当系统内存不足时,按最近最少使用+优先释放内存使用密集的策略释放缓存进程。

GC:

内存使用的多也会造成GC速度变慢,造成卡顿。 内存占用过高,在创建对象时内存不足,很容易造成触发GC影响APP性能。

图片相关

图片的格式

目前 Android 端支持的图片格式有JPEG、GIF、PNG、BMP、WebP,但是在 Android中能够使用编解码使用的只有其中的三种:JPEG、PNG、WebP。

  • JPEG:是广泛使用的有损压缩图像标准格式,它不支持透明和多帧动画
  • PNG:是一种无损压缩图片格式,它支持完整的透明通道,由于是无损压缩,所以它的占用空间一般比较大。
  • GIF:它支持多帧动画
  • WebP:它支持有损和无损压缩,支持完整的透明通道也支持多帧动画,是一种比较理想的图片格式。
图片压缩工具
  • 无损压缩 ImageOption ImageOption 是一个无损的压缩工具,它通过优化PNG 的压缩参数,移除冗余元数据以及非必须的颜色配置文件等方式,在不牺牲图片质量的前提下,既减小了PNG图片占用的空间,又提高了加载的速度。
  • 有损压缩 ImageAlpha ImageAlpha 是 ImageOptions 作者开发的一个有损的 PNG 压缩工具,相比较而言,图片大小得到极大的降低,当然图片质量同时也会受到一定程度的影响,经过该工具压缩的图片,需要经过设计师的检视才能上线,否则可能会影响到整个 APP 的视觉效果。
  • 使用有损压缩工具 TinyPNG 等。
  • PNG/JPEG 转换为 WebP。
  • 尽量使用 .9格式的PNG 图,因为它体积小,拉伸不变形能够适配 Android 各种机型。
代码压缩

Android中Bitmap所占内存大小计算方式:图片长度 x 图片宽度 x 一个像素点占用的字节数

影响Bitmap占用内存的因素:

  • 图片最终加载的分辨率;
  • 图片的格式(PNG/JPEG/BMP/WebP);
  • 图片所存放的drawable目录;
  • 图片属性设置的色彩模式;
  • 设备的屏幕密度;

1、Bitmap的Compress方法(质量压缩):

public boolean compress(CompressFormat format, int quality, OutputStream stream)

参数format:表示图像的压缩格式,目前有CompressFormat.JPEG、CompressFormat.PNG、CompressFormat.WEBP。

参数quality: 图像压缩率,0-100。 0 压缩100%,100意味着不压缩。

参数stream: 写入压缩数据的输出流。

常用的用法:

public static Bitmap compress(Bitmap bitmap){

    ByteArrayOutputStream baos = new ByteArrayOutputStream();

    bitmap.compress(Bitmap.CompressFormat.JPEG, 90, baos);

    byte[] bytes = baos.toByteArray();

    return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);

}

上面方法中通过bitmap的compress方法对bitmap进行质量压缩,10%压缩,90%不压缩。

图片的大小是没有变的,因为质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,这也是为什么该方法叫质量压缩方法。图片的长,宽,像素都不变,那么bitmap所占内存大小是不会变的。

quality值越小压缩后的baos越小(使用场景:在微信分享时,需要对图片的字节数组大小进行限制,这时可以使用bitmap的compress方法对图片进行质量压缩)。

2、BitmapFactory.Options的inJustDecodeBounds和inSampleSize参数(采样率压缩):

inJustDecodeBounds:当inJustDecodeBounds设置为true的时候,BitmapFactory通过decodeXXXX解码图片时,将会返回空(null)的Bitmap对象,这样可以避免Bitmap的内存分配,但是它可以返回Bitmap的宽度、高度以及MimeType。

inSampleSize: 当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例(1 / inSampleSize)缩小bitmap的宽和高、降低分辨率,大于1时这个值将会被处置为2的倍数。例如,width=100,height=100,inSampleSize=2,那么就会将bitmap处理为,width=50,height=50,宽高降为1 / 2,像素数降为1 / 4。

常用用法:

public static Bitmap inSampleSize(byte[] data,int reqWidth,int reqHeight){

    final BitmapFactory.Options options = new BitmapFactory.Options();

    options.inJustDecodeBounds = true;

    BitmapFactory.decodeByteArray(data, 0, data.length, options);

    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    options.inJustDecodeBounds = false;

    return BitmapFactory.decodeByteArray(data, 0, data.length, options);

}

public static int calculateInSampleSize(BitmapFactory.Options options,

                                        int reqWidth, int reqHeight) {

    final int picheight = options.outHeight;

    final int picwidth = options.outWidth;

    int targetheight = picheight;

    int targetwidth = picwidth;

    int inSampleSize = 1;

    if (targetheight > reqHeight || targetwidth > reqWidth) {

        while (targetheight >= reqHeight

                && targetwidth >= reqWidth) {

            inSampleSize += 1;

            targetheight = picheight / inSampleSize;

            targetwidth = picwidth / inSampleSize;

        }

    }

    return inSampleSize;

}
}

inSampleSize方法中先将inJustDecodeBounds设置为false,在通过BitmapFactory的decodeXXXX方法解码图片,返回空(null)的Bitmap对象,同时获取了bitmap的宽高,再通过calculateInSampleSize方法根据原bitmap的 宽高和目标宽高计算出合适的inSampleSize,最后将inJustDecodeBounds设置为true,通过BitmapFactory的decodeXXXX方法解码图片(使用场景:比如读取本地图片时,防止Bitmap过大导致内存溢出)。

3、通过Matrix压缩图片

Matrix matrix = new Matrix();

matrix.setScale(0.5f, 0.5f);

bm = Bitmap.createBitmap(bit, 0, 0, bit.getWidth(),bit.getHeight(), matrix, true);

}

使用场景:自定义View时,对图片进行缩放、旋转、位移以及倾斜等操作,常见的就是对图片进行缩放处理,以及圆角图片等。

其他知识点

inBitmap: 如果设置,在加载Bitmap的时候会尝试去重用这块内存(内存复用),不能重用的时候会返回null,否则返回bitmap。

复用内存:BitmapFactory.Options 参数inBitmap的使用。inMutable设置为true,并且配合SoftReference软引用使用(内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些软引用对象的内存)。有一点要注意Android4.4以下的平台,需要保证inBitmap和即将要得到decode的Bitmap的尺寸规格一致,Android4.4及其以上的平台,只需要满足inBitmap的尺寸大于要decode得到的Bitmap的尺寸规格即可。

图片加载和缓存

常见的图片加载缓存库有 Picasso、Glide、Fresco。

  • Picasso 是 Square 公司开源的图片加载库,它实现图片的下载和二级缓存缓存功能,库文件 120KB
  • Glide 是 Google 推荐的用于 Android 平台上的图片加载和缓存库,库文件 475KB
  • Fresco 是 Facebook 开源的功能强大的图片加载库,如对图片显示要求很高可选择该库。该库最显著的特点是实现了三级缓存,两级内存缓存一级磁盘缓存。库文件 3.4MB

根据 App 对图片显示和缓存的需求从低到高的选择顺序:Picasso < Glide < Fresco

Bitmap其他方案

1、使用完毕后释放图片资源

Android编程中,往往最容易出现OOM的地方就是在图片处理的时候,我们先上个数据:一个像素的显示需要4字节(R、G、B、A各占一个字节),所以一个1080x720像素的手机一个满屏幕画面就需要近3M内存,而开发一个轻量应用的安装包大小也差不多就3M左右,所以说图片很占内存。在Android中,图片的资源文件叫做Drawable,存储在硬盘上,不耗内存,但我们并无法对其进行处理,最多只能进行展示。而如果想对该图片资源进行处理,我们需要把这个Drawable解析为Bitmap形式装载入内存中。其中Android的不同版本对Bitmap的存储方式还有所不同。下面是Android官方文档中对此描述的一段话

On Android 2.3.3 (API level 10) and lower, the backing pixel data for a bitmap is stored in native memory. It is separate from the bitmap itself, which is stored in the Dalvik heap. The pixel data in native memory is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash. As of Android 3.0 (API level 11), the pixel data is stored on the Dalvik heap along with the associated bitmap.

bitmap分成两个部分,一部分为bitmap对象,用以存储此图片的长、宽、透明度等信息;另一部分为bitmap数据,用以存储bitmap的(A)RGB字节数据。在2.3.3及以前版本中bitmap对象和bitmap数据是存储在不同的内存空间上的,bitmap数据部分存储在native内存中,GC无法涉及。所以之前我们需要调用bitmap的recycle方法来显示的告诉系统此处内存可回收,而在3.0版本开始,bitmap的的这两部分都存储在了Dalvik堆中,可以被GC机制统一处理,也就无需用recycle了。 关于bitmap优化,不同版本方法也不相同,2.3.3版本及以前,就要做到及时调用recycle来回收不在使用的bitmap,而3.0开始可以使用BitmapFactory.Options.inBitmap这个选项,设置一个可复用的bitmap,这样以后新的bitmap且大小相同的就可以直接使用这块内存,而无需重复申请内存。4.4之后解决了对大小的限制,不同大小也可以复用该块空间。

注:若调用了Bitmap.recycle()后,再绘制Bitmap,则会出现Canvas: trying to use a recycled bitmap错误

2、根据分辨率适配 & 缩放图片

若 Bitmap 与 当前设备的分辨率不匹配,则会拉伸Bitmap,而Bitmap分辨率增加后,所占用的内存也会相应增加

因为Bitmap 的内存占用 根据 x、y的大小来增加的

3、按需 选择合适的解码方式

不同的图片解码方式 对应的 内存占用大小 相差很大,具体如下

使用参数:BitmapFactory.inPreferredConfig 设置 默认使用解码方式:ARGB_8888

4、设置 图片缓存

重复加载图片资源耗费太多资源(CPU、内存 & 流量)

内存泄漏

内存泄露,即Memory Leak,指程序中不再使用到的对象因某种原因从而无法被GC正常回收。发生内存泄露,会导致一些不再使用到的对象没有及时释放,这些对象占用了宝贵的内存空间,很容易导致后续需要分配内存的时候,内存空间不足而出现OOM(内存溢出)。

1、静态变量导致的内存泄露

静态变量的生命周期与应用的生命周期一致,该对象会一直被引用直到应用结束。

例子1:

public class MainActivity extends Activity {
    public static Context context;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context=this;
    }
}

上述代码在MainActivity中context为静态变量,并持有Context,当Activity退出后,由于Activity被context一直引用着,导致Activity无法被回收,因此造成了内存泄漏。上述代码比较明显,一般不会犯这种错误。

例子2:

public class MainActivity extends Activity {
    public static Out mOut;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mOut = new Out(this);
    }
}
//外部Out类
public class Out {
    Out(Context context) {

    }
}

上述代码与例子1类似,mOut为静态变量,生命周期与应用一致,传入的MainActivity也被一直引用,导致Activity无法被回收,造成内存泄漏。

解决方案:

1、在不使用静态变量时,置空。

2、可以使用Application的Context。

3、通过弱引用和软引用来引用Activity。

  • 强引用:直接的对象引用。
  • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被GC回收。
  • 弱引用:当一个对象只有弱引用存在时,此对象随时被GC回收。

例子3:

单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄露。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。

public class Singleton {
   private static Singleton singleton = null;
   private Context mContext;

   public Singleton(Context mContext) {
      this.mContext = mContext;
   }

   public static Singleton getSingleton(Context context){
    if (null == singleton){
      singleton = new Singleton(context);
    }
    return singleton;
  }
}

像上面代码中这样的单例,如果我们在调用getInstance(Context context)方法的时候传入的context参数是Activity、Service等上下文,就会导致内存泄露。

当我们退出Activity时,该Activity就没有用了,但是因为singleton作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个Activity的引用,导致这个Activity对象无法被回收释放,这就造成了内存泄露。

2、非静态内部类导致内存泄露

非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。

非静态内部类导致的内存泄露在Android开发中有一种典型的场景就是使用Handler,很多开发者在使用Handler是这样写的:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        start();
    }

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 1) {
                // 做相应逻辑
            }
        }
    };
}

当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露。 通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式。

public class MainActivity extends AppCompatActivity {

    private Handler mHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler = new MyHandler(this);
        start();
    }

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private static class MyHandler extends Handler {

        private WeakReference<MainActivity> activityWeakReference;

        public MyHandler(MainActivity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = activityWeakReference.get();
            if (activity != null) {
                if (msg.what == 1) {
                    // 做相应逻辑
                }
            }
        }
    }
}

mHandler通过弱引用的方式持有Activity,当GC执行垃圾回收时,遇到Activity就会回收并释放所占据的内存单元。这样就不会发生内存泄露了。 上面的做法确实避免了Activity导致的内存泄露,发送的msg不再已经没有持有Activity的引用了,但是msg还是有可能存在消息队列MessageQueue中,所以更好的是在Activity销毁时就将mHandler的回调和发送的消息给移除掉。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
    

非静态内部类造成内存泄露还有一种情况就是使用Thread或者AsyncTask。 比如在Activity中直接new一个子线程Thread:

  public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 模拟相应耗时逻辑
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

或者直接新建AsyncTask异步任务:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                // 模拟相应耗时逻辑
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }.execute();
    }
}

这种方式新建的子线程Thread和AsyncTask都是匿名内部类对象,默认就隐式的持有外部Activity的引用,导致Activity内存泄露。要避免内存泄露的话还是需要像上面Handler一样使用静态内部类+弱应用的方式(代码就不列了,参考上面Hanlder的正确写法)。

3、集合类内存泄露

集合类添加元素后,将会持有元素对象的引用,导致该元素对象不能被垃圾回收,从而发生内存泄漏。

4、未关闭资源对象内存泄露

  • 注销广播:如果广播在Activity销毁后不取消注册,那么这个广播会一直存在系统中,由于广播持有了Activity的引用,因此会导致内存泄露。
  • 关闭输入输出流等:在使用IO、File流等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果不及时关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此我们在不需要使用它们的时候就应该及时关闭,以便缓冲能得到释放,从而避免内存泄露。
  • 回收Bitmap:Bitmap对象比较占内存,当它不再被使用的时候,最好调用Bitmap.recycle()方法主动进行回收。
  • 停止动画:属性动画中有一类无限动画,如果Activity退出时不停止动画的话,动画会一直执行下去。因为动画会持有View的引用,View又持有Activity,最终Activity就不能给回收掉。只要我们在Activity退出把动画停掉即可。
  • 销毁WebView:WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。或是把使用了 WebView 的 Activity (或者 Service) 放在单独的进程里

WebView扩展:

WebView 解析网页时会申请Native堆内存用于保存页面元素,当页面较复杂时会有很大的内存占用。如果页面包含图片,内存占用会更严重。并且打开新页面时,为了能快速回退,之前页面占用的内存也不会释放。有时浏览十几个网页,都会占用几百兆的内存。这样加载网页较多时,会导致系统不堪重负,最终强制关闭应用,也就是出现应用闪退或重启。
由于占用的都是Native 堆内存,所以实际占用的内存大小不会显示在常用的 DDMS Heap 工具中( DMS Heap 工具看到的只是Java虚拟机分配的内存,即使Native堆内存已经占用了几百兆,这里显示的还只是几兆或十几兆)。只有使用 adb shell 中的一些命令比如 dumpsys meminfo 包名,或者在程序中使用 Debug.getNativeHeapSize()才能看到 Native 堆内存信息。

5、使用系统服务引发的内存泄漏

为了方便我们使用一些常见的系统服务,Activity 做了一些封装。比如说,可以通过 getPackageManager在 Activtiy 中获取 PackageManagerService,但是,里面实际上调用了 Activity 对应的 ContextImpl 中的 getPackageManager 方法

ContextWrapper#getPackageManager

@Override
public PackageManager getPackageManager() {
    return mBase.getPackageManager();
}
ContextImpl#getPackageManager

@Override
public PackageManager getPackageManager() {
    if (mPackageManager != null) {
        return mPackageManager;
    }
    IPackageManager pm = ActivityThread.getPackageManager();
    if (pm != null) {
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));//创建 ApplicationPackageManager
    }
    return null;
}

ApplicationPackageManager#ApplicationPackageManager

ApplicationPackageManager(ContextImpl context,
                          IPackageManager pm) {
    mContext = context;//保存 ContextImpl 的强引用
    mPM = pm;
}

private UserManagerService(Context context, PackageManagerService pm,
        Object packagesLock, File dataDir) {
    mContext = context;//持有外部 Context 引用
    mPm = pm;
     //代码省略
}
PackageManagerService#PackageManagerService

public class PackageManagerService extends IPackageManager.Stub {
    static UserManagerService sUserManager;//持有 UMS 静态引用
    public PackageManagerService(Context context, Installer installer,
        boolean factoryTest, boolean onlyCore) {
          sUserManager = new UserManagerService(context, this, mPackages);//初始化 UMS
        }
}

遇到的内存泄漏问题是因为在 Activity 中调用了 getPackageManger 方法获取 PMS ,该方法调用的是 ContextImpl,此时如果ContextImpl 中 PackageManager 为 null,就会创建一个 PackageManger(ContextImpl 会将自己传递进去,而 ContextImpl 的 mOuterContext 为 Activity),创建 PackageManager 实际上会创建 PackageManagerService(简称 PMS),而 PMS 的构造方法中会创建一个 UserManger(UserManger 初始化之后会持有 ContextImpl 的强引用)。 只要 PMS 的 class 未被销毁,那么就会一直引用着 UserManger ,进而导致其关联到的资源无法正常释放。

解决办法:

将getPackageManager()改为 getApplication()#getPackageManager() 。这样引用的就是 Application Context,而非 Activity 了。

内存泄漏工具

1、leakcanary

leakcanary是square开源的一个库,能够自动检测发现内存泄露,其使用也很简单: 在build.gradle中添加依赖:

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'

  //可选项,如果使用了support包中的fragments
  debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.1'
}

根目录下的build.gradle添加mavenCentral()即可,如下:

allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
}

然后在自定义的Application中调用以下代码就可以了。

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return;
        }
        LeakCanary.install(this);

        //正常初始化代码
    }
}

如果检测到有内存泄漏,通知栏会有提示,如下图;如果没有内存泄漏,则没有提示。

2、Memory Profiler

Memory Profiler 是 Android Profiler 中的一个组件,可以帮助你分析应用卡顿,崩溃和内存泄露等等问题。

打开 Memory Profiler后即可看到一个类似下图的视图。

上面的红色数字含义如下:

1.用于强制执行垃圾回收事件的按钮。
2.用于捕获堆转储的按钮。
3.用于记录内存分配情况的按钮。 此按钮仅在连接至运行 Android 7.1 或更低版本的设备时才会显示。
4.用于放大/缩小/还原时间线的按钮。
5.用于跳转至实时内存数据的按钮。
6.Event 时间线,其显示 Activity 状态、用户输入 Event 和屏幕旋转 Event。
7.内存使用量时间线,其包含以下内容:
    一个显示每个内存类别使用多少内存的堆叠图表,如左侧的 y 轴以及顶部的彩色键所示。
    虚线表示分配的对象数,如右侧的 y 轴所示。
    用于表示每个垃圾回收事件的图标。

如何Memory Profiler分析内存泄露,按以下步骤来即可:

1.使用Memory Profiler监听要分析的应用进程
2.旋转几次要分析的Activity。(这是因为旋转Activity后会重新创建)
3.点击捕获堆转储按钮去捕获堆转储
4.在捕获结果中搜索要分析的类。(这里是MainActivity)
5.点击要分析的类,右边会显示这个类创建对象的数量。

如下图:

内存抖动

内存抖动的原因:

内存抖动一般是瞬间创建了大量对象,会在短时间内触发多次GC,产生卡顿。

内存抖动的在分析工具上的表现:

解决方案:

最简单的做法就是把之前的主线程操作放到子线程去,虽然内存抖动依然存在,但是卡顿问题可以大大缓解。

对于内存抖动本身:

尽量避免在循环体内创建对象,应该把对象创建移到循环体外。 需要大量使用Bitmap和其他大型对象时,尽量尝试复用之前创建的对象。

网络优化

客户端请求流程如下:

相关分析工具

分析网络情况的方式可以通过Wireshark, Fiddler, Charlesr等抓包工具,也可以通过Android Studio的Network Profiler

窗口顶部显示的是 Event 时间线以及 1 无线装置功耗状态(低/高)与 WLAN 的对比。 在时间线上,您可以 2点击并拖动选择时间线的一部分来检查网络流量。

下方的3窗口会显示在时间线的选定片段内收发的文件,包括文件名称、大小、类型、状态和时间。 您可以点击任意列标题为此列表排序。

同时,您还可以查看时间线选定片段的明细数据,显示每个文件的发送或接收时间。

点击网络连接的名称即可查看 4 有关所发送或接收的选定文件的详细信息。 点击各个标签可查看响应数据、标题信息或调用堆栈。

注: 必须启用高级分析才能从时间线中选择要检查的片段,查看发送和接收的文件列表,或查看有关所发送或接收的选定文件的详细信息。 要启用高级分析,请参阅启用高级分析。

启用高级分析需要点击Run Configuration:

打开Run/Debug Configurations,左侧选择你的应用,右侧在Profiling中勾选Enable advanced profiling。

通过以上这些工具可以查看某个时间段内网络请求的具体情况,从而进行网络优化的相关工作。

优化建议

1、后端API设计

后端设计API时需要考虑网络请求的频次、资源状态,在某些情况下可以合并多个接口以满足客户端业务需求。

2、Gzip压缩

使用Gzip来压缩request和response, 减少传输数据量, 从而减少流量消耗。同时可以考虑使用Protocol Buffer代替JSON,protobuf会比JSON数据量小很多.

3、图片大小优化

  • 请求图片时告诉服务器需要的图片的宽高。(比如使用七牛时,可以在url后面添加质量、格式、宽高等等来获取合适的图片资源)
  • 列表采用缩略图。
  • 使用Webp格式:安卓系统从Android4.0(API 14)添加了有损耗的WebP support并且在Android4.2(API 17)对无损的,清晰的WebP提供了支持。使用WebP格式;同样的照片,采用WebP格式可大幅节省流量,相对于JPG格式的图片,流量能节省将近 25% 到 35 %;相对于PNG格式的图片,流量可以节省将近80%。最重要的是使用WebP之后图片质量也没有改变。
  • 使用第三方图片加载框架
  • 网络缓存
  • 监听网络状态,非WiFi下可以显示无图页面,WiFi或4G情况下才显示有图页面。
  • IP直连与HttpDns:DNS解析的失败率占联网失败中很大一种,而且首次域名解析一般需要几百毫秒。针对此,我们可以不用域名,才用IP直连省去 DNS 解析过程,节省这部分时间。HttpDNS基于Http协议的域名解析,替代了基于DNS协议向运营商Local DNS发起解析请求的传统方式,可以避免Local DNS造成的域名劫持和跨网访问问题,解决域名解析异常带来的困扰。

电量优化

电量分析工具

1、Batterystats & bugreport

Android 5.0及以上的设备, 允许我们通过adb命令dump出电量使用统计信息.

因为电量统计数据是持续的, 会非常大, 统计我们的待测试App之前先reset下, 连上设备,命令行执行:

$ adb shell dumpsys batterystats --reset
Battery stats reset.

断开测试设备, 操作我们的待测试App,重新连接设备, 使用adb命令导出相关统计数据:

// 此命令持续记录输出, 想要停止记录时按Ctrl+C退出.
$ adb bugreport > bugreport.txt

导出的统计数据存储到bugreport.txt, 此时我们可以借助如下工具来图形化展示电池的消耗情况。

2、Battery Historian

Google提供了一个开源的电池历史数据分析工具

Battery Historian链接

耗电原因

  • 网络请求
  • 使用WakeLock:WakeLock会保持CPU运行,或是防止屏幕变暗/关闭,让手机可以在用户不操作时依然运行。CPU会一直得不到休眠, 而大大增加耗电.
  • GPS
  • 蓝牙传输

建议: 根据具体业务需求,严格限制应用位于后台时是否禁用某些数据传输,尽量能够避免无效的数据传输。 数据传输的频度问题,如网络请求可以压缩合并,如本地数据上传,可以选择恰当的时机上传。

JobScheduler组件

通过不停的唤醒CPU(通过后天常驻的Service)来达到一些功能的使用,这样会造成电量资源的消耗,比如后台日志的上报,定期更新数据等等,在Android 5.0提供了一个JobScheduler组件,通过设置一系列的预置条件,当条件满足时,才执行对应的操作,这样既能省电,有保证了功能的完整性。

JobScheduler的适用场景:

  • 重要不紧急的任务,可以延迟执行,比如定期数据库数据更新和数据上报
  • 耗电量较大的任务,比如充电时才执行的备份数据操作。
  • 不紧急可以不执行的网络任务,比如在Wi-Fi环境下预加载数据。
  • 可以批量执行的任务
  • ......等等

JobScheduler的使用

    private Context mContext;
    private JobScheduler mJobScheduler;

    public JobSchedulerManager(Context context){
        this.mContext=context;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            this.mJobScheduler= (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        }
    }

通过getSystemService()方法获取一个JobSchedule的对象。

    public boolean addTask(int taskId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            JobInfo.Builder builder = new JobInfo.Builder(taskId,
                    new ComponentName("com.apk.administrator.loadapk",
                            JobScheduleService.class.getName()));
            switch (taskId) {
                case 1:
                    //每隔1秒执行一次
                    builder.setPeriodic(1000);
                    break;
                case 2:
                    //设备重启后,不再执行该任务
                    builder.setPersisted(false);
                    break;
                default:
                    break;
            }
            if (null != mJobScheduler) {
                return mJobScheduler.schedule(builder.build()) > 0;
            } else {
                return false;
            }
        } else {
            return true;
        }
    }

创建一个JobInfo对象时传入两个参数,第一个参数是任务ID,可以对不同的任务ID做不同的触发条件,执行任务时根据任务ID执行具体的任务;第二个参数是JobScheduler任务的服务,参数为进程名和服务类名。

JobInfo支持以下几种触发条件:

  • setMinimumLatency(long minLatencyMillis):设置任务的延迟时间(单位是ms),需要注意的是,setMinimumLatency与setPeriodic(long time)方法不兼容,同时调用会引起异常。
  • setOverrideDeadline(long maxExecutionDelayMillis):设置任务最晚的延迟时间。如果到了规定时间,其它条件还未满足,这个任务也会被启动。与setMinimumLatency(long time)一样,setOverriddeDeadline与setPeriodic(long time)同时调用会引起异常。
  • setPersisted(boolean isPersisted):设置重启之后,任务是否还要继续执行。
  • setRequiredNetworkType(int networkType):只有满足指定的网络条件时,才会被执行。有三种网络条件,JobInfo.NETWORK_TYPE_NONE不管是否有网络,这个任务都会被执行(如果未设置,这个参数就是默认参数);JobInfo.NETWORK_TYPE_ANY只有在有网络的情况下,任务才会执行,和网络类型无关;JobInfo.NETWORK_TYPE_UNMETERED非运营商网络(比如在Wi-Fi连接时),任务才会被执行。
  • setRequiresCharging(boolean requiresCharging):只有当设备在充电时,这个任务才会被执行。 setRequiresDeviceIdle(boolean requiresDeviceIdle):只有当用户没有在使用该设备且有一段时间没有使用时,才会启动该任务。
public class JobScheduleService extends JobService {
    @Override
    public boolean onStartJob(JobParameters params) {
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        return false;
    }
}

JobService运行在主线程,如果是耗时任务,使用ThreadHandler或者一个异步任务来运行耗时的任务,防止阻塞主线程。

JobScheduleService继承JobService,实现两个方法onStartJob和onStopJob。 任务开始时,执行onStartJob方法,当任务执行完毕后,需要调用jobFinished方法来通知系统;任务执行完成后,调用jobFinished方法通知JobScheduler;当系统接受到一个取消请求时,调用onStopJob方法取消正在等待执行的任务。如果系统在接受到一个取消请求时,实际任务队列中已经没有正在运行的任务,onStopJob不会被调用。 最后在AndroidManifest中配置下:

<service
   android:name=".JobScheduleService"
   android:permission="android.permission.BIND_JOB_SERVICE" />

APK体积优化

1、从图片入手:.9图、压缩或采用Webp。
2、使用Lint删除无用资源
3、通过Gradle配置,过滤无用资源和.so文件
4、第三方库慎重使用,可以只提取使用到的代码
5、资源混淆:方案有:美团和微信,前者是通过修改AAPT在处理资源文件相关的源码达到资源名的替换,后者通过直接修改resources.arsc文件来达到资源文件名的混淆。
6、插件化

整合网上相关资料,不定期更新此文。