Android性能优化实践

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


绘制优化

绘制原理

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

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

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

产生卡顿缘由有不少,主要有如下几点:shell

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

工具篇

一、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所花的时间。

二、Systrace

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

三、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"/>权限。
复制代码

四、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目录;
  • 图片属性设置的色彩模式;
  • 设备的屏幕密度;

一、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方法对图片进行质量压缩)。

二、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过大致使内存溢出)。

三、经过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其余方案

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

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错误

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

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

由于Bitmap 的内存占用 根据 x、y的大小来增长的

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

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

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

四、设置 图片缓存

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

内存泄漏

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

一、静态变量致使的内存泄露

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

例子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没法被回收,形成内存泄漏。

解决方案:

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

二、可使用Application的Context。

三、经过弱引用和软引用来引用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对象没法被回收释放,这就形成了内存泄露。

二、非静态内部类致使内存泄露

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

非静态内部类致使的内存泄露在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的正确写法)。

三、集合类内存泄露

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

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

  • 注销广播:若是广播在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 堆内存信息。

五、使用系统服务引起的内存泄漏

为了方便咱们使用一些常见的系统服务,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 了。

内存泄漏工具

一、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);

        //正常初始化代码
    }
}

复制代码

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

二、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。

经过以上这些工具能够查看某个时间段内网络请求的具体状况,从而进行网络优化的相关工做。

优化建议

一、后端API设计

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

二、Gzip压缩

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

三、图片大小优化

  • 请求图片时告诉服务器须要的图片的宽高。(好比使用七牛时,能够在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形成的域名劫持和跨网访问问题,解决域名解析异常带来的困扰。

电量优化

电量分析工具

一、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, 此时咱们能够借助以下工具来图形化展现电池的消耗状况。

二、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体积优化

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

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

转载于:https://juejin.im/post/5cebc989e51d454f72302482