Android 应用开发性能优化彻底分析

1 背景

其实有点不想写这篇文章的,可是又想写,有些矛盾。不想写的缘由是随便上网一搜一堆关于性能的建议,感受你们你一总结、我一总结的都说到了不少优化注意事项,可是看过这些文章后大多数存在一个问题就是只给出啥啥啥不能用,啥啥啥该咋用等,却不多有较为系统的进行真正性能案例分析的,大多数都是嘴上喊喊或者死记住规则而已(固然了,这话我本身听着都有些刺耳,实在很差意思,其实关于性能优化的优质博文网上也仍是有不少的,譬如Google官方都已经推出了优化专题,我这里只是总结下自的感悟而已,如有得罪欢迎拍砖,我愿挨打,由于我工做的一半时间都是负责性能优化)。html

固然了,本文不会就此编辑这么一次,由于技术在发展,工具在强大(写着写着Android Studio 1.4版本都推送了),本身的经验也在增长,因此本文天然不会覆盖全部性能优化及分析;解决的办法就是该文章会长期维护更新,同时在评论区欢迎你关于性能优化点子的探讨。java

Android应用的性能问题其实能够划分为几个大的模块的,并且都具备相对不错的优化调试技巧,下面咱们就会依据一个项目常规开发的大类型来进行一些分析讲解。python

PS:以前呆过一家初创医疗互联网公司,别提性能优化了,老板立完新项目后一个月就要求见到上线成品,这种压迫下谈何性能优化,纯属扯蛋,因此不到三个月时间我主动选择撤了,这种现象后来我一打听发如今不少初创公司都很严重,都想速成却忽略了体验。android

PPPS:本文只是达到抛砖引玉的做用,不少东西细究下去都是值得深刻研究的,再加上性能优化原本就是一个须要综合考量的任务,不是说会了本文哪一点就能作性能分析了,须要面面俱到才可高效定位问题缘由。git

2 应用UI性能问题分析

UI可谓是一个应用的脸,因此每一款应用在开发阶段咱们的交互、视觉、动画工程师都拼命的想让它变得天然大方美丽,但是现实老是不尽人意,动画和交互总会以为开发作出来的应用用上去感受不天然,没有达到他们心目中的天然流畅细节;这种状况之下就更别提发布给终端用户使用了,用户要是可以感受出来,少则影响心情,多则卸载应用;因此一个应用的UI显示性能问题就不得不被开发人员重视。github

2-1 应用UI卡顿原理

人类大脑与眼睛对一个画面的连贯性感知实际上是有一个界限的,譬如咱们看电影会以为画面很天然连贯(帧率为24fps),用手机固然也须要感知屏幕操做的连贯性(尤为是动画过分),因此Android索性就把达到这种流畅的帧率规定为60fps。shell

有了上面的背景,咱们开发App的帧率性能目标就是保持在60fps,也就是说咱们在进行App性能优化时心中要有以下准则:数据库

从上面能够看出来,所谓的卡顿实际上是能够量化的,每次是否可以成功渲染是很是重要的问题,16ms可否完整的作完一次操做直接决定了卡顿性能问题。canvas

固然了,针对Android系统的设计咱们还须要知道另外一个常识;虚拟机在执行GC垃圾回收操做时全部线程(包括UI线程)都须要暂停,当GC垃圾回收完成以后全部线程才可以继续执行(这个细节下面小节会有详细介绍)。也就是说当在16ms内进行渲染等操做时若是恰好赶上大量GC操做则会致使渲染时间明显不足,也就从而致使了丢帧卡顿问题。设计模式

有了上面这两个简单的理论基础以后咱们下面就会探讨一些UI卡顿的缘由分析及解决方案。

2-2 应用UI卡顿常见缘由

咱们在使用App时会发现有些界面启动卡顿、动画不流畅、列表等滑动时也会卡顿,究其缘由,不少都是丢帧致使的;经过上面卡顿原理的简单说明咱们从应用开发的角度往回推理能够得出常见卡顿缘由,以下:

  1. 人为在UI线程中作轻微耗时操做,致使UI线程卡顿;
  2. 布局Layout过于复杂,没法在16ms内完成渲染;
  3. 同一时间动画执行的次数过多,致使CPU或GPU负载太重;
  4. View过分绘制,致使某些像素在同一帧时间内被绘制屡次,从而使CPU或GPU负载太重;
  5. View频繁的触发measure、layout,致使measure、layout累计耗时过多及整个View频繁的从新渲染;
  6. 内存频繁触发GC过多(同一帧中频繁建立内存),致使暂时阻塞渲染操做;
  7. 冗余资源及逻辑等致使加载和执行缓慢;
  8. 臭名昭著的ANR;

能够看见,上面这些致使卡顿的缘由都是咱们平时开发中很是常见的。有些人可能会以为本身的应用用着还蛮OK的,其实那是由于你没进行一些瞬时测试和压力测试,一旦在这种环境下运行你的App你就会发现不少性能问题。

2-3 应用UI卡顿分析解决方法

分析UI卡顿咱们通常都借助工具,经过工具通常均可以直观的分析出问题缘由,从而反推寻求优化方案,具体以下细说各类强大的工具。

2-3-1 使用HierarchyViewer分析UI性能

咱们能够经过SDK提供的工具HierarchyViewer来进行UI布局复杂程度及冗余等分析,以下:

选中一个Window界面item,而后点击右上方Hierarchy window或者Pixel Perfect window(这里不介绍,主要用来检查像素属性的)便可操做。

先看下Hierarchy window,以下:

10-11-7

一个Activity的View树,经过这个树能够分析出View嵌套的冗余层级,左下角能够输入View的id直接自动跳转到中间显示;Save as PNG用来把左侧树保存为一张图片;Capture Layers用来保存psd的PhotoShop分层素材;右侧剧中显示选中View的当前属性状态;右下角显示当前View在Activity中的位置等;左下角三个进行切换;Load View Hierarchy用来手动刷新变化(不会自动刷新的)。当咱们选择一个View后会以下图所示:

10-11-8

相似上图能够很方便的查看到当前View的许多信息;上图最底那三个彩色原点表明了当前View的性能指标,从左到右依次表明测量、布局、绘制的渲染时间,红色和黄色的点表明速度渲染较慢的View(固然了,有些时候较慢不表明有问题,譬如ViewGroup子节点越多、结构越复杂,性能就越差)。

固然了,在自定义View的性能调试时,HierarchyViewer上面的invalidate Layout和requestLayout按钮的功能更增强大,它能够帮助咱们debug自定义View执行invalidate()和requestLayout()过程,咱们只须要在代码的相关地方打上断点就好了,接下来经过它观察绘制便可。

能够发现,有了HierarchyViewer调试工具,咱们的UI性能分析变得十分容易,这个工具也是咱们开发中调试UI的利器,在平时写代码时会时常伴随咱们左右。

2-3-2 使用GPU过分绘制分析UI性能

咱们对于UI性能的优化还能够经过开发者选项中的GPU过分绘制工具来进行分析。在设置->开发者选项->调试GPU过分绘制(不一样设备可能位置或者叫法不一样)中打开调试后能够看见以下图(对settings当前界面过分绘制进行分析):

10-11-9

能够发现,开启后在咱们想要调试的应用界面中能够看到各类颜色的区域,具体含义以下:

颜色 含义
无色 WebView等的渲染区域
蓝色 1x过分绘制
绿色 2x过分绘制
淡红色 3x过分绘制
红色 4x(+)过分绘制

因为过分绘制指在屏幕的一个像素上绘制屡次(譬如一个设置了背景色的TextView就会被绘制两次,一次背景一次文本;这里须要强调的是Activity设置的Theme主题的背景不被算在过分绘制层级中),因此最理想的就是绘制一次,也就是蓝色(固然这在不少绚丽的界面是不现实的,因此你们有个度便可,咱们的开发性能优化标准要求最极端界面下红色区域不能长期持续超过屏幕三分之一,可见仍是比较宽松的规定),所以咱们须要依据此颜色分布进行代码优化,譬如优化布局层级、减小不必的背景、暂时不显示的View设置为GONE而不是INVISIBLE、自定义View的onDraw方法设置canvas.clipRect()指定绘制区域或经过canvas.quickreject()减小绘制区域等。

2-3-3 使用GPU呈现模式图及FPS考核UI性能

Android界面流畅度除过视觉感知之外是能够考核的(测试妹子专用),常见的方法就是经过GPU呈现模式图或者实时FPS显示进行考核,这里咱们主要针对GPU呈现模式图进行下说明,由于FPS考核测试方法有不少(譬如本身写代码实现、第三方App测试、固件支持等),因此不作统一说明。

经过开发者选项中GPU呈现模式图工具来进行流畅度考量的流程是(注意:若是是在开启应用后才开启此功能,记得先把应用结束后从新启动)在设置->开发者选项->GPU呈现模式(不一样设备可能位置或者叫法不一样)中打开调试后能够看见以下图(对settings当前界面上下滑动列表后的图表):

10-11-10

固然,也能够在执行完UI滑动操做后在命令行输入以下命令查看命令行打印的GPU渲染数据(分析依据:Draw + Process + Execute = 完整的显示一帧时间 < 16ms):

打开上图可视化工具后,咱们能够在手机画面上看到丰富的GPU绘制图形信息,分别展现了StatusBar、NavgationBar、Activity区域等的GPU渲染时间信息,随着界面的刷新,界面上会以实时柱状图来显示每帧的渲染时间,柱状图越高表示渲染时间越长,每一个柱状图偏上都有一根表明16ms基准的绿色横线,每一条竖着的柱状线都包含三部分(蓝色表明测量绘制Display List的时间,红色表明OpenGL渲染Display List所须要的时间,黄色表明CPU等待GPU处理的时间),只要咱们每一帧的总时间低于基准线就不会发生UI卡顿问题(个别超出基准线其实也不算啥问题的)。

能够发现,这个工具是有局限性的,他虽然可以看出来有帧耗时超过基准线致使了丢帧卡顿,但却分析不到形成丢帧的具体缘由。因此说为了配合解决分析UI丢帧卡顿问题咱们还须要借助traceview和systrace来进行缘由追踪,下面咱们会介绍这两种工具的。

2-3-4 使用Lint进行资源及冗余UI布局等优化

上面说了,冗余资源及逻辑等也可能会致使加载和执行缓慢,因此咱们就来看看Lint这个工具是如何发现优化这些问题的(固然了,Lint实际的功能是很是强大的,咱们开发中也是常用它来发现一些问题的,这里主要有点针对UI性能的说明了,其余的雷同)。

在Android Studio 1.4版本中使用Lint最简单的办法就是将鼠标放在代码区点击右键->Analyze->Inspect Code–>界面选择你要检测的模块->点击确认开始检测,等待一下后会发现以下结果:

10-11-11

能够看见,Lint检测完后给了咱们不少建议的,咱们重点看一个关于UI性能的检测结果;上图中高亮的那一行明确说明了存在冗余的UI层级嵌套,因此咱们是能够点击跳进去进行优化处理掉的。

固然了,Lint还有不少功能,你们能够自行探索发挥,这里只是达到抛砖引玉的做用。

2-3-5 使用Memory监测及GC打印与Allocation Tracker进行UI卡顿分析

关于Android的内存管理机制下面的一节会详细介绍,这里咱们主要针对GC致使的UI卡顿问题进行详细说明。

Android系统会依据内存中不一样的内存数据类型分别执行不一样的GC操做,常见应用开发中致使GC频繁执行的缘由主要多是由于短期内有大量频繁的对象建立与释放操做,也就是俗称的内存抖动现象,或者短期内已经存在大量内存暂用介于阈值边缘,接着每当有新对象建立时都会致使超越阈值触发GC操做。

以下是我工做中一个项目的一次经历(我将代码回退特地抓取的),出现这个问题的场景是一次压力测试致使整个系统卡顿,瞬间杀掉应用就OK了,究其缘由最终查到是一个API的调运位置写错了方式,致使一直被狂调,当普通使用时不会有问题,压力测试必现卡顿。具体内存参考图以下:

10-11-12
与此抖动图对应的LogCat抓取以下:

咱们知道,相似上面logcat打印同样,触发垃圾回收的主要缘由有如下几种:

  • GC_MALLOC——内存分配失败时触发;
  • GC_CONCURRENT——当分配的对象大小超过384K时触发;
  • GC_EXPLICIT——对垃圾收集的显式调用(System.gc()) ;
  • GC_EXTERNAL_ALLOC——外部内存分配失败时触发;

能够看见,这种不停的大面积打印GC致使全部线程暂停的操做一定会致使UI视觉的卡顿,因此咱们要避免此类问题的出现,具体的常见优化方式以下:

  • 检查代码,尽可能避免有些频繁触发的逻辑方法中存在大量对象分配;
  • 尽可能避免在屡次for循环中频繁分配对象;
  • 避免在自定义View的onDraw()方法中执行复杂的操做及建立对象(譬如Paint的实例化操做不要写在onDraw()方法中等);
  • 对于并发下载等相似逻辑的实现尽可能避免屡次建立线程对象,而是交给线程池处理。

固然了,有了上面说明GC致使的性能后咱们就该定位分析问题了,能够经过运行DDMS->Allocation Tracker标签打开一个新窗口,而后点击Start Tracing按钮,接着运行你想分析的代码,运行完毕后点击Get Allocations按钮就可以看见一个已分配对象的列表,以下:

10-11-13

点击上面第一个表格中的任何一项就可以在第二个表格中看见致使该内存分配的栈信息,经过这个工具咱们能够很方便的知道代码分配了哪类对象、在哪一个线程、哪一个类、哪一个文件的哪一行。譬如咱们能够经过Allocation Tracker分别作一次Paint对象实例化在onDraw与构造方法的一个自定义View的内存跟踪,而后你就明白这个工具的强大了。

PS一句,Android Studio新版本除过DDMS之外在Memory视图的左侧已经集成了Allocation Tracker功能,只是用起来仍是没有DDMS的方便实用,以下图:

10-11-14

2-3-6 使用Traceview和dmtracedump进行分析优化

关于UI卡顿问题咱们还能够经过运行Traceview工具进行分析,他是一个分析器,记录了应用程序中每一个函数的执行时间;咱们能够打开DDMS而后选择一个进程,接着点击上面的“Start Method Profiling”按钮(红色小点变为黑色即开始运行),而后操做咱们的卡顿UI(小范围测试,因此操做最好不要超过5s),完事再点一下刚才按的那个按钮,稍等片刻便可出现下图,以下:

10-11-15

花花绿绿的一幅图咱们怎么分析呢?下面咱们解释下如何经过该工具定位问题:

整个界面包括上下两部分,上面是你测试的进程中每一个线程运行的时间线,下面是每一个方法(包含parent及child)执行的各个指标的值。经过上图的时间面板能够直观发现,整个trace时间段main线程作的事情特别多,其余的作的相对较少。当咱们选择上面的一个线程后能够发现下面的性能面板很复杂,其实这才是TraceView的核心图表,它主要展现了线程中各个方法的调用信息(CPU使用时间、调用次数等),这些信息就是咱们分析UI性能卡顿的核心关注点,因此咱们先看几个重要的属性说明,以下:

属性名 含义
name 线程中调运的方法名;
Incl CPU Time 当前方法(包含内部调运的子方法)执行占用的CPU时间;
Excl CPU Time 当前方法(不包含内部调运的子方法)执行占用的CPU时间;
Incl Real Time 当前方法(包含内部调运的子方法)执行的真实时间,ms单位;
Excl Real Time 当前方法(不包含内部调运的子方法)执行的真实时间,ms单位;
Calls+Recur Calls/Total 当前方法被调运的次数及递归调运占总调运次数百分比;
CPU Time/Call 当前方法调运CPU时间与调运次数比,即当前方法平均执行CPU耗时时间;
Real Time/Call 当前方法调运真实时间与调运次数比,即当前方法平均执行真实耗时时间;(重点关注)

有了对上面Traceview图表的一个认识以后咱们就来看看具体致使UI性能后该如何切入分析,通常Traceview能够定位两类性能问题:

  • 方法调运一次须要耗费很长时间致使卡顿;
  • 方法调运一次耗时不长,但被频繁调运致使累计时长卡顿。

譬如咱们来举个实例,有时候咱们写完App在使用时不以为有啥大的影响,可是当咱们启动完App后静止在那却十分费电或者致使设备发热,这种状况咱们就能够打开Traceview而后按照Cpu Time/Call或者Real Time/Call进行降序排列,而后打开可疑的方法及其child进行分析查看,而后再回到代码定位检查逻辑优化便可;固然了,咱们也能够经过该工具来trace咱们自定义View的一些方法来权衡性能问题,这里再也不一一列举喽。

能够看见,Traceview可以帮助咱们分析程序性能,已经很方便了,然而Traceview家族还有一个更加直观强大的小工具,那就是能够经过dmtracedump生成方法调用图。具体作法以下:

经过这个生成的方法调运图咱们能够更加直观的发现一些方法的调运异常现象。不过本人优化到如今还没怎么用到它,每次用到Traceview分析就已经搞定问题了,因此说dmtracedump本身酌情使用吧。

PS一句,Android Studio新版本除过DDMS之外在CPU视图的左侧已经集成了Traceview(start Method Tracing)功能,只是用起来仍是没有DDMS的方便实用(这里有一篇AS MT我的以为不错的分析文章(引用自网络,连接属于原做者功劳)),以下图:

10-11-16

2-3-7 使用Systrace进行分析优化

Systrace其实有些相似Traceview,它是对整个系统进行分析(同一时间轴包含应用及SurfaceFlinger、WindowManagerService等模块、服务运行信息),不过这个工具须要你的设备内核支持trace(命令行检查/sys/kernel/debug/tracing)且设备是eng或userdebug版本才能够,因此使用前麻烦本身确认一下。

咱们在分析UI性能时通常只关注图形性能(因此必须选择Graphics和View,其余随意),同时通常对于卡顿的抓取都是5s,最多10s。启动Systrace进行数据抓取能够经过两种方式,命令行方式以下:

图形模式:
打开DDMS->Capture system wide trace using Android systrace->设置时间与选项点击OK就开始了抓取,接着操做APP,完事生成一个trace.html文件,用Chrome打开便可以下图:

10-11-17 在Chrome中浏览分析该文件咱们能够经过键盘的W-A-S-D键来搞定,因为上面咱们在进行trace时选择了一些选项,因此上图生成了左上方相关的CPU频率、负载、状态等信息,其中的CPU N表明了CPU核数,每一个CPU行的柱状图表表明了当前时间段当前核上的运行信息;下面咱们再来看看SurfaceFlinger的解释,以下:

10-11-18

能够看见上面左边栏的SurfaceFlinger其实就是负责绘制Android程序UI的服务,因此SurfaceFlinger能反应出总体绘制状况,能够关注上图VSYNC-app一行能够发现前5s多基本都可以达到16ms刷新间隔,5s多开始到7s多大于了15ms,说明此时存在绘制丢帧卡顿;同时能够发现surfaceflinger一行明显存在相似不规律间隔,这是由于有的地方是不须要从新渲染UI,因此有大范围不规律,有的是由于阻塞致使不规律,明显能够发现0到4s间大可能是不须要渲染,而5s之后大可能是阻塞致使;对应这个时间点咱们放大能够看到每一个部分所使用的时间和正在执行的任务,具体以下:

10-11-19

能够发现具体的执行明显存在超时性能卡顿(原点不是绿色的基本都表明存在必定问题,下面和右侧都会提示你选择的帧相关详细信息或者alert信息),可是遗憾的是经过Systrace只能大致上发现是否存在性能问题,具体问题还须要经过Traceview或者代码中嵌入Trace工具类等去继续详细分析,总之很蛋疼。

PS:若是你想使用Systrace很轻松的分析定位全部问题,看明白全部的行含义,你还须要具有很是扎实的Android系统框架的原理才能够将该工具使用的驾轻就熟。

2-3-8 使用traces.txt文件进行ANR分析优化

ANR(Application Not Responding)是Android中AMS与WMS监测应用响应超时的表现;之因此把臭名昭著的ANR单独做为UI性能卡顿的分析来讲明是由于ANR是直接卡死UI不动且必需要解掉的Bug,咱们必须尽可能在开发时避免他的出现,固然了,万一出现了那就用下面介绍的方法来分析吧。

咱们应用开发中常见的ANR主要有以下几类:

  • 按键触摸事件派发超时ANR,通常阈值为5s(设置中开启ANR弹窗,默认有事件派发才会触发弹框ANR);
  • 广播阻塞ANR,通常阈值为10s(设置中开启ANR弹窗,默认不弹框,只有log提示);
  • 服务超时ANR,通常阈值为20s(设置中开启ANR弹窗,默认不弹框,只有log提示);

当ANR发生时除过logcat能够看见的log之外咱们还能够在系统指定目录下找到traces文件或dropbox文件进行分析,发生ANR后咱们能够经过以下命令获得ANR trace文件:

而后咱们用txt编辑器打开能够发现以下结构分析:

至此常见的应用开发中ANR分析定位就能够解决了。

2-4 应用UI性能分析解决总结

能够看见,关于Android UI卡顿的性能分析仍是有不少工具的,上面只是介绍了应用开发中咱们常用的一些而已,还有一些其余的,譬如Oprofile等工具不怎么经常使用,这里就再也不详细介绍。

经过上面UI性能的原理、缘由、工具分析总结能够发现,咱们在开发应用时必定要时刻重视性能问题,如若真的没留意出现了性能问题,不妨使用上面的一些案例方式进行分析。可是那终归是补救措施,在咱们知道上面UI卡顿原理以后咱们应该尽可能从项目代码架构搭建及编写时就避免一些UI性能问题,具体项目中常见的注意事项以下:

  • 布局优化;尽可能使用include、merge、ViewStub标签,尽可能不存在冗余嵌套及过于复杂布局(譬如10层就会直接异常),尽可能使用GONE替换INVISIBLE,使用weight后尽可能将width和heigh设置为0dp减小运算,Item存在很是复杂的嵌套时考虑使用自定义Item View来取代,减小measure与layout次数等。
  • 列表及Adapter优化;尽可能复用getView方法中的相关View,不重复获取实例致使卡顿,列表尽可能在滑动过程当中不进行UI元素刷新等。
  • 背景和图片等内存分配优化;尽可能减小没必要要的背景设置,图片尽可能压缩处理显示,尽可能避免频繁内存抖动等问题出现。
  • 自定义View等绘图与布局优化;尽可能避免在draw、measure、layout中作过于耗时及耗内存操做,尤为是draw方法中,尽可能减小draw、measure、layout等执行次数。
  • 避免ANR,不要在UI线程中作耗时操做,遵照ANR规避守则,譬如屡次数据库操做等。

固然了,上面只是列出了咱们项目中常见的一些UI性能注意事项而已,相信还有不少其余的状况这里没有说到,欢迎补充。还有一点就是咱们上面所谓的UI性能优化分析总结等都是建议性的,由于性能这个问题是一个涉及面很广很泛的问题,有些优化不是必需的,有些优化是必需的,有些优化掉之后又是得不偿失的,因此咱们通常着手解决那些必须的就能够了。

3 应用开发Memory内存性能分析优化

说完了应用开发中的UI性能问题后咱们就该来关注应用开发中的另外一个重要、严重、很是重要的性能问题了,那就是内存性能优化分析。Android其实就是嵌入式设备,嵌入式设备核心关注点之一就是内存资源;有人说如今的设备都在堆硬件配置(譬如国产某米的某兔跑分手机、盒子等),因此内存不会再像之前那么紧张了,其实这句话听着没错,但为啥再牛逼配置的Android设备上有些应用仍是越用系统越卡呢?这里面的缘由有不少,不过相信有了这一章下面的内容分析,做为一个移动开发者的你就有能力打理好本身应用的那一亩三分地内存了,能作到这样就足以了。关于Android内存优化,这里有一篇Google的官方指导文档,可是本文为本身项目摸索,会有不少不同的地方。

3-1 Android内存管理原理

系统级内存管理:

Android系统内核是基于Linux,因此说Android的内存管理其实也是Linux的升级版而已。Linux在进程中止后就结束该进程,而Android把这些中止的进程都保留在内存中,直到系统须要更多内存时才选择性的释放一些,保留在内存中的进程默认(不包含后台service与Thread等单独UI线程的进程)不会影响总体系统的性能(速度与电量等)且当再次启动这些保留在内存的进程时能够明显提升启动速度,不须要再去加载。

再直白点就是说Android系统级内存管理机制其实相似于Java的垃圾回收机制,这下明白了吧;在Android系统中框架会定义以下几类进程、在系统内存达到规定的不一样level阈值时触发清空不一样level的进程类型。

10-11-20

能够看见,所谓的咱们的Service在后台跑着跑着挂了,或者盒子上有些大型游戏启动起来就挂(以前我在上家公司作盒子时碰见过),有一个直接的缘由就是这个阈值定义的太大,致使系统一直认为已经达到阈值,因此进行优先清除了符合类型的进程。因此说,该阈值的设定是有一些讲究的,额,扯多了,咱们主要是针对应用层内存分析的,系统级内存回收了解这些就基本够解释咱们应用在设备上的一些表现特征了。

应用级内存管理:

在说应用级别内存管理原理时你们先想一个问题,假设有一个内存为1G的Android设备,上面运行了一个很是很是吃内存的应用,若是没有任何机制的状况下是否是用着用着整个设备会由于咱们这个应用把1G内存吃光而后整个系统运行瘫痪呢?

哈哈,其实Google的工程师才不会这么傻的把系统设计这么差劲。为了使系统不存在咱们上面假想状况且能安全快速的运行,Android的框架使得每一个应用程序都运行在单独的进程中(这些应用进程都是由Zygote进程孵化出来的,每一个应用进程都对应本身惟一的虚拟机实例);若是应用在运行时再存在上面假想的状况,那么瘫痪的只会是本身的进程,不会直接影响系统运行及其余进程运行。

既然每一个Android应用程序都执行在本身的虚拟机中,那了解Java的必定明白,每一个虚拟机一定会有堆内存阈值限制(值得一提的是这个阈值通常都由厂商依据硬件配置及设备特性本身设定,没有统一标准,能够为64M,也能够为128M等;它的配置是在Android的属性系统的/system/build.prop中配置dalvik.vm.heapsize=128m便可,若存在dalvik.vm.heapstartsize则表示初始申请大小),也即一个应用进程同时存在的对象必须小于阈值规定的内存大小才能够正常运行。

接着咱们运行的App在本身的虚拟机中内存管理基本就是遵循Java的内存管理机制了,系统在特定的状况下主动进行垃圾回收。可是要注意的一点就是在Android系统中执行垃圾回收(GC)操做时全部线程(包含UI线程)都必须暂停,等垃圾回收操做完成以后其余线程才能继续运行。这些GC垃圾回收通常都会有明显的log打印出回收类型,常见的以下:

  • GC_MALLOC——内存分配失败时触发;
  • GC_CONCURRENT——当分配的对象大小超过384K时触发;
  • GC_EXPLICIT——对垃圾收集的显式调用(System.gc()) ;
  • GC_EXTERNAL_ALLOC——外部内存分配失败时触发;

经过上面这几点的分析能够发现,应用的内存管理其实就是一个萝卜一个坑,坑都通常大,你在开发应用时要保证的是内存使用同一时刻不能超过坑的大小,不然就装不下了。

3-2 Android内存泄露性能分析

有了关于Android的一些内存认识,接着咱们来看看关于Android应用开发中常出现的一种内存问题—-内存泄露。

3-2-1 Android应用内存泄露概念

众所周知,在Java中有些对象的生命周期是有限的,当它们完成了特定的逻辑后将会被垃圾回收;可是,若是在对象的生命周期原本该被垃圾回收时这个对象还被别的对象所持有引用,那就会致使内存泄漏;这样的后果就是随着咱们的应用被长时间使用,他所占用的内存愈来愈大。以下就是一个最多见简单的泄露例子(其它的泄露再也不一一列举了):

能够看见,上面例子中咱们让一个单例模式的对象持有了当前Activity的强引用,那在当前Acvitivy执行完onDestroy()后,这个Activity就没法获得垃圾回收,也就形成了内存泄露。

内存泄露能够引起不少的问题,常见的内存泄露致使问题以下:

  • 应用卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC);
  • 应用被从后台进程干为空进程(上面系统内存原理有介绍,也就是超过了阈值);
  • 应用莫名的崩溃(上面应用内存原理有介绍,也就是超过了阈值OOM);

形成内存泄露泄露的最核心原理就是一个对象持有了超过本身生命周期之外的对象强引用致使该对象没法被正常垃圾回收;能够发现,应用内存泄露是个至关棘手重要的问题,咱们必须重视。

3-2-2 Android应用内存泄露察觉手段

知道了内存泄露的概念以后确定就是想办法来确认本身的项目是否存在内存泄露了,那该如何察觉本身项目是否存在内存泄露呢?以下提供了几种经常使用的方式:

察觉方式 场景
AS的Memory窗口 平时用来直观了解本身应用的全局内存状况,大的泄露才能有感知。
DDMS-Heap内存监测工具 同上,大的泄露才能有感知。
dumpsys meminfo命令 经常使用方式,能够很直观的察觉一些泄露,但不全面且常规足够用。
leakcanary神器 比较强大,能够感知泄露且定位泄露;实质是MAT原理,只是更加自动化了,当现有代码量已经庞大成型,且没法很快察觉掌控全局代码时极力推荐;或者是偶现泄露的状况下极力推荐。

AS的Memory窗口以下,详细的说明这里就不解释了,很简单很直观(使用频率高):

10-11-21

DDMS-Heap内存监测工具窗口以下,详细的说明这里就不解释了,很简单(使用频率不高):

10-11-22

dumpsys meminfo命令以下(使用频率很是高,很是高效,个人最爱之一,平时通常关注几个重要的Object个数便可判断通常的泄露;固然了,adb shell dumpsys meminfo不跟参数直接展现系统全部内存状态):

10-11-23

leakcanary神器使用这里先不说,下文会专题介绍,你会震撼的一B。有了这些工具的定位咱们就能很方便的察觉咱们App的内存泄露问题,察觉到之后该怎么定位分析呢,继续往下看。

3-2-3 Android应用内存泄露leakcanary工具定位分析

leakcanary是一个开源项目,一个内存泄露自动检测工具,是著名的GitHub开源组织Square贡献的,它的主要优点就在于自动化过早的发觉内存泄露、配置简单、抓取贴心,缺点在于还存在一些bug,不过正常使用百分之九十状况是OK的,其核心原理与MAT工具相似。

关于leakcanary工具的配置使用方式这里再也不详细介绍,由于真的很简单,详情点我参考官方教程学习使用便可

PS:以前在优化性能时发现咱们有一个应用有两个界面退出后Activity没有被回收(dumpsys meminfo发现一直在加),因此就怀疑可能存在内存泄露。可是问题来了,这两个Activity的逻辑十分复杂,代码也不是我写的,相关联的代码量也十分庞大,更加郁闷的是很难判断是哪一个版本修改致使的,这时候只知道有泄露,却没法定位具体缘由,使用MAT分析解决掉了一个可疑泄露后发现泄露又变成了几率性的。能够发现,对于这种几率性的泄露用MAT去主动抓取确定是很耗时耗力的,因此决定直接引入leakcanary神器来检测项目,后来很快就完全解决了项目中全部必现的、偶现的内存泄露。

总之一点,工具再强大也只是帮咱们定位可能的泄露点,而最核心的GC ROOT泄露信息推导出泄露问题及如何解决仍是须要你把住代码逻辑及泄露核心概念去推理解决。

3-2-4 Android应用内存泄露MAT工具定位分析

Eclipse Memory Analysis Tools(点我下载)是一个专门分析Java堆数据内存引用的工具,咱们能够使用它方便的定位内存泄露缘由,核心任务就是找到GC ROOT位置便可,哎呀,关于这个工具的使用我是真的不想说了,本身搜索吧,实在简单、传统的不行了。

PS:这是开发中使用频率很是高的一个工具之一,麻烦务必掌握其核心使用技巧,虽然Android Studio已经实现了部分功能,可是真的很难用,遇到问题目前仍是使用Eclipse Memory Analysis Tools吧。

原谅我该小节的放荡不羁!!!!(其实我是困了,呜呜!)

3-2-5 Android应用开发规避内存泄露建议

有了上面的原理及案例处理其实还不够,由于上面这些处理办法是补救的措施,咱们正确的作法应该是在开发过程当中就养成良好的习惯和敏锐的嗅觉才对,因此下面给出一些应用开发中常见的规避内存泄露建议:

  • Context使用不当形成内存泄露;不要对一个Activity Context保持长生命周期的引用(譬如上面概念部分给出的示例)。尽可能在一切能够使用应用ApplicationContext代替Context的地方进行替换(原理我前面有一篇关于Context的文章有解释)。
  • 非静态内部类的静态实例容易形成内存泄漏;即一个类中若是你不可以控制它其中内部类的生命周期(譬如Activity中的一些特殊Handler等),则尽可能使用静态类和弱引用来处理(譬如ViewRoot的实现)。
  • 警戒线程未终止形成的内存泄露;譬如在Activity中关联了一个生命周期超过Activity的Thread,在退出Activity时切记结束线程。一个典型的例子就是HandlerThread的run方法是一个死循环,它不会本身结束,线程的生命周期超过了Activity生命周期,咱们必须手动在Activity的销毁方法中中调运thread.getLooper().quit();才不会泄露。
  • 对象的注册与反注册没有成对出现形成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。
  • 建立与关闭没有成对出现形成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等。
  • 不要在执行频率很高的方法或者循环中建立对象,能够使用HashTable等建立一组对象容器从容器中取那些对象,而不用每次new与释放。
  • 避免代码设计模式的错误形成内存泄露;譬如循环引用,A持有B,B持有C,C持有A,这样的设计谁都得不到释放。

关于规避内存泄露上面我只是列出了我在项目中常常碰见的一些状况而已,确定不全面,欢迎拍砖!固然了,只有咱们作到好的规避加上强有力的判断嗅觉泄露才能让咱们的应用驾驭好本身的一亩三分地。

3-3 Android内存溢出OOM性能分析

上面谈论了Android应用开发的内存泄露,下面谈谈内存溢出(OOM);其实能够认为内存溢出与内存泄露是交集关系,具体以下图:

10-11-24

下面咱们就来看看内存溢出(OOM)相关的东东吧。

3-3-1 Android应用内存溢出OOM概念

上面咱们探讨了Android内存管理和应用开发中的内存泄露问题,能够知道内存泄露通常影响就是致使应用卡顿,可是极端的影响是使应用挂掉。前面也提到过应用的内存分配是有一个阈值的,超过阈值就会出问题,这里咱们就来看看这个问题—–内存溢出(OOM–OutOfMemoryError)。

内存溢出的主要致使缘由有以下几类:

  • 应用代码存在内存泄露,长时间积累没法释放致使OOM;
  • 应用的某些逻辑操做疯狂的消耗掉大量内存(譬如加载一张不通过处理的超大超高清图片等)致使超过阈值OOM;

能够发现,不管哪一种类型,致使内存溢出(OutOfMemoryError)的核心缘由就是应用的内存超过阈值了。

3-3-2 Android应用内存溢出OOM性能分析

经过上面的OOM概念和那幅交集图能够发现,要想分析OOM缘由和避免OOM须要分两种状况考虑,泄露致使的OOM,申请过大致使的OOM。

内存泄露致使的OOM分析:

这种OOM一旦发生后会在logcat中打印相关OutOfMemoryError的异常栈信息,不过你别高兴太早,这种状况下致使的OOM打印异常信息是没有太大做用,由于这种OOM的致使通常都以下图状况(图示为了说明问题数据和场景有夸张,请忽略):

10-11-25

从图片能够看见,这种OOM咱们有时也遇到,第一反应是去分析OOM异常打印栈,但是后来发现打印栈打印的地方没有啥问题,没有可优化的余地了,因而就郁闷了。其实这时候你留心观察几个现象便可,以下:

  • 留意你执行触发OOM操做前的界面是否有卡顿或者比较密集的GC打印;
  • 使用命令查看下当前应用占用内存状况;

确认了以上这些现象你基本能够判定该OOM的log真的没用,真正致使问题的缘由是内存泄露,因此咱们应该按照上节介绍的方式去着手排查内存泄露问题,解决掉内存泄露后红色空间都能获得释放,再去显示一张0.8M的优化图片就不会再报OOM异常了。

不珍惜内存致使的OOM分析:

上面说了内存泄露致使的OOM异常,下面咱们再来看一幅图(数据和场景描述有夸张,请忽略),以下:

10-11-26

可见,这种类型的OOM就很好定位缘由了,通常均可以从OOM后的log中得出分析定位。

以下例子,咱们在Activity中的ImageView放置一张未优化的特大的(30多M)高清图片,运行直接崩溃以下:

经过上面的log能够很方便的看出来问题缘由所在地,那接下来的作法就是优化呗,下降图片的相关规格便可(譬如使用BitmapFactory的Option类操做等)。

PS:提醒一句的是记得应用所属的内存是区分Java堆和native堆的!

3-3-3 Android应用规避内存溢出OOM建议

仍是那句话,等待OOM发生是为时已晚的事,咱们应该将其扼杀于萌芽之中,至于如何在开发中规避OOM,以下给出一些咱们应用开发中的经常使用的策略建议:

  • 时刻记得不要加载过大的Bitmap对象;譬如对于相似图片加载咱们要经过BitmapFactory.Options设置图片的一些采样比率和复用等,具体作法点我参考官方文档,不过过咱们通常都用fresco或Glide开源库进行加载。
  • 优化界面交互过程当中频繁的内存使用;譬如在列表等操做中只加载可见区域的Bitmap、滑动时不加载、中止滑动后再开始加载。
  • 有些地方避免使用强引用,替换为弱引用等操做。
  • 避免各类内存泄露的存在致使OOM。
  • 对批量加载等操做进行缓存设计,譬如列表图片显示,Adapter的convertView缓存等。
  • 尽量的复用资源;譬如系统自己有不少字符串、颜色、图片、动画、样式以及简单布局等资源可供咱们直接使用,咱们本身也要尽可能复用style等资源达到节约内存。
  • 对于有缓存等存在的应用尽可能实现onLowMemory()和onTrimMemory()方法。
  • 尽可能使用线程池替代多线程操做,这样能够节约内存及CPU占用率。
  • 尽可能管理好本身的Service、Thread等后台的生命周期,不要浪费内存占用。
  • 尽量的不要使用依赖注入,中看不中用。
  • 尽可能在作一些大内存分配等可疑内存操做时进行try catch操做,避免没必要要的应用闪退。
  • 尽可能的优化本身的代码,减小冗余,进行编译打包等优化对齐处理,避免类加载时浪费内存。

能够发现,上面只是列出了咱们开发中常见的致使OOM异常的一些规避原则,还有不少相信尚未列出来,你们能够自行追加参考便可。

3-4 Android内存性能优化总结

不管是什么电子设备的开发,内存问题永远都是一个很深奥、无底洞的话题,上面的这些内存分析建议也单单只是Android应用开发中一些常见的场景而已,真正的达到合理的优化仍是须要不少知识和功底的。

合理的应用架构设计、设计风格选择、开源Lib选择、代码逻辑规范等都会决定到应用的内存性能,咱们必须时刻头脑清醒的意识到这些问题潜在的风险与优劣,由于内存优化必需要有一个度,不能一味的优化,亦不能置之不理。

4 Android应用API使用及代码逻辑性能分析

在咱们开发中除过常规的那些经典UI、内存性能问题外其实还存在不少潜在的性能优化、这种优化不是十分明显,可是在某些场景下倒是很是有必要的,因此咱们简单列举一些常见的其余潜在性能优化技巧,具体以下探讨。

4-1 Android应用String/StringBuilder/StringBuffer优化建议

字符串操做在Android应用开发中是十分常见的操做,也就是这个最简单的字符串操做却也暗藏不少潜在的性能问题,下面咱们实例来讲说。

先看下面这个关于String和StringBuffer的对比例子:

经过这个例子能够看出来,String对象(记得是对象,不是常量)和StringBuffer对象的主要性能区别在于String对象是不可变的,因此每次对String对象作改变操做(譬如“+”操做)时其实都生成了新的String对象实例,因此会致使内存消耗性能问题;而StringBuffer对象作改变操做每次都会对本身进行操做,因此不须要消耗额外的内存空间。

咱们再看一个关于String和StringBuffer的对比例子:

在这种状况下你会发现StringBuffer的性能反而没有String的好,缘由是在JVM解释时认为
String Str = "Name:" + "GJRS";就是String Str = "Name:GJRS";,因此天然比StringBuffer快了。

能够发现,若是咱们拼接的是字符串常量则String效率比StringBuffer高,若是拼接的是字符串对象,则StringBuffer比String效率高,咱们在开发中要酌情选择。固然,除过注意StringBuffer和String的效率问题,咱们还应该注意另外一个问题,那就是StringBuffer和StringBuilder的区别,其实StringBuffer和StringBuilder都继承自同一个父类,只是StringBuffer是线程安全的,也就是说在不考虑多线程状况下StringBuilder的性能又比StringBuffer高。

PS:若是想追究清楚他们之间具体细节差别,麻烦本身查看实现源码便可。

4-2 Android应用OnTrimMemory()实现性能建议

OnTrimMemory是Android 4.0以后加入的一个回调方法,做用是通知应用在不一样的状况下进行自身的内存释放,以免被系统直接杀掉,提升应用程序的用户体验(冷启动速度是热启动的2~3倍)。系统会根据当前不一样等级的内存使用状况调用这个方法,而且传入当前内存等级,这个等级有不少种,咱们能够依据状况实现不一样的等级,这里不详细介绍,可是要说的是咱们应用应该至少实现以下等级:

  • TRIM_MEMORY_BACKGROUND
    内存已经很低了,系统准备开始根据LRU缓存来清理进程。这时候若是咱们手动释放一些不重要的缓存资源,则当用户返回咱们应用时会感受到很顺畅,而不是从新启动应用。

能够实现OnTrimMemory方法的系统组件有Application、Activity、Fragement、
Service、ContentProvider;关于OnTrimMemory释放哪些内存其实在架构阶段就要考虑清楚哪些对象是要常驻内存的,哪些是伴随组件周期存在的,通常须要释放的都是缓存。
以下给出一个咱们项目中经常使用的例子:

一般在咱们代码实现了onTrimMemory后很难复显这种内存消耗场景,可是你又怕引入新Bug,想一想办法测试。好在咱们有一个快捷的方式来模拟触发该水平内存释放,以下命令:

packagename为包名或者进程id,value为ComponentCallbacks2.java里面定义的值,能够为80、60、40、20、5等,咱们模拟触发其中的等级便可。

4-3 Android应用HashMap与ArrayMap及SparseArray优化建议

在Android开发中涉及到数据逻辑部分大部分用的都是Java的API(譬如HashMap),可是对于Android设备来讲有些Java的API并不适合,可能会致使系统性能降低,好在Google团队已经意识到这些问题,因此他们针对Android设备对Java的一些API进行了优化,优化最多就是使用了ArrayMap及SparseArray替代HashMap来得到性能提高。

HashMap:

HashMap内部使用一个默认容量为16的数组来存储数据,数组中每个元素存放一个链表的头结点,其实整个HashMap内部结构就是一个哈希表的拉链结构。HashMap默认实现的扩容是以2倍增长,且获取一个节点采用了遍历法,因此相对来讲不管从内存消耗仍是节点查找上都是十分昂贵的。

SparseArray:

SparseArray比HashMap省内存是由于它避免了对Key进行自动装箱(int转Integer),它内部是用两个数组来进行数据存储的(一个存Key,一个存Value),它内部对数据采用了压缩方式来表示稀疏数组数据,从而节约内存空间,并且其查找节点的实现采用了二分法,很明显能够看见性能的提高。

ArrayMap:

ArrayMap内部使用两个数组进行数据存储,一个记录Key的Hash值,一个记录Value值,它和SparseArray相似,也会在查找时对Key采用二分法。

有了上面的基本了解咱们能够得出结论供开发时参考,当数据量不大(千位级内)且Key为int类型时使用SparseArray替换HashMap效率高;当数据量不大(千位级内)且数据类型为Map类型时使用ArrayMap替换HashMap效率高;其余状况下HashMap效率相对高于两者。

4-4 Android应用ContentProviderOperation优化建议

ContentProvider是Android应用开发的核心组件之一,有时候在开发中须要使用ContentProvider对多行数据进行操做,咱们的作法通常是屡次调运相关操做方法,却不知这种实现方式是很是低性能的,取而代之的作法应该是使用批量操做,具体为了使批量更新、插入、删除数据操做更加方便官方提供了ContentProviderOperation工具类。因此在咱们开发中遇到相似情景时请务必使用批量操做,具体的优点以下:

  • 全部的操做都在一个事务中执行,能够保证数据的完整性。
  • 批量操做在一个事务中执行,因此只用打开、关闭一个事务。
  • 减轻应用程序与ContentProvider间的屡次频繁交互,提高性能。

能够看见,这对于数据库操做来讲是一个很是有用的优化措施,烦请务必重视(咱们项目优化过,的确有很大提高)。

4-5 Android应用其余逻辑优化建议

关于API及逻辑性能优化其实有多知识点的,这里没法一一列出,只能给出一些重要的知识点,下面再给出一些常见的优化建议:

  • 避免在Android中使用Java的枚举类型,由于编译后不但占空间,加载也费时,彻底没有static final的变量好用、高效。
  • Handler发送消息时尽可能使用obtain去获取已经存在的Message对象进行复用,而不是新new Message对象,这样能够减轻内存压力。
  • 在使用后台Service时尽可能将可以替换为IntentService的地方替换为此,这样能够减轻系统压力、省电、省内存、省CPU占用率。
  • 在当前类内部尽可能不要经过本身的getXXX、setXXX对本身内部成员进行操做,而是直接使用,这样能够提升代码执行效率。
  • 不要一味的为了设计模式而过度的抽象代码,由于代码抽象系数与代码加载执行时间成正比。
  • 尽可能减小锁个数、减少锁范围,避免形成性能问题。
  • 合理的选择使用for循环与加强型for循环,譬如不要在ArrayList上使用加强型for循环等。

哎呀,相似的小优化技巧有不少,这里不一一列举了,自行发挥留意便可。

5 Android应用移动设备电池耗电性能分析

有了UI性能优化、内存性能优化、代码编写优化以后咱们在来讲说应用开发中很重要的一个优化模块—–电量优化。

5-1 Android应用耗电量概念

在盒子等开发时可能电量优化不是特别重视(视盒子待机真假待机模式而定),可是在移动设备开发中耗电量是一个很是重要的指标,若是用户一旦发现咱们的应用很是耗电,很差意思,他们大多会选择卸载来解决此类问题,因此耗电量是一个十分重要的问题。

关于咱们应用的耗电量状况咱们能够进行定长时间测试,至于具体的耗电量统计等请参考此文,同时咱们还能够直接经过Battery Historian Tool来查看详细的应用电量消耗状况。最简单经常使用办法是经过命令直接查看,以下:

其实咱们一款应用耗电量最大的部分不是UI绘制显示等,常见耗电量最大缘由基本都是由于网络数据交互、GPS定位、大量内存性能问题、冗余的后台线程和Service等形成。

5-2 Android应用耗电量优化建议

优化电量使用状况咱们不只能够使用系统提供的一些API去处理,还能够在平时编写代码时就养成好的习惯。具体的一些建议以下:

  • 在须要网络的应用中,执行某些操做前尽可能先进行网络状态判断。
  • 在网络应用传输中使用高效率的数据格式和解析方法,譬如JSON等。
  • 在传输用户反馈或者下载OTA升级包等不是十分紧急的操做时尽可能采用压缩数据进行传输且延迟到设备充电和WIFI状态时进行。
  • 在有必要的状况下尽可能经过PowerManager.WakeLock和JobScheduler来控制一些逻辑操做达到省电优化。
  • 对定位要求不过高的场景尽可能使用网络定位,而不是GPS定位。
  • 对于定时任务尽可能使用AlarmManager,而不是sleep或者Timer进行管理。
  • 尽量的减小网络请求次数和减少网络请求时间间隔。
  • 后台任务要尽量少的唤醒CPU,譬如IM通讯的长链接心跳时间间隔、一些应用的后台定时唤醒时间间隔等要设计合理。
  • 特殊耗电业务状况能够进行弹窗等友好的交互设计提醒用户该操做会耗用过多电量。

能够看见,上面只是一些常见的电量消耗优化建议。总之,做为应用开发者的咱们要意识到电量损耗对于用户来讲是很是敏感的,只有咱们作到合理的电量优化才能赢得用户的芳心。

6 Android应用开发性能优化总结

性能优化是一个很大的话题,上面咱们谈到的只是应用开发中常见的性能问题,也是应用开发中性能问题的冰山一角,更多的性能优化技巧和能力不是靠看出来,而是靠经验和实战结果总结出来的,因此说性能优化是一个涉及面很是广的话题,若是你想对你的应用进行性能你必须对你应用的整个框架有一个很是清晰的认识。

固然了,若是在咱们开发中只是一味的追求各类极致的优化也是不对的。由于优化原本就是存在风险的,甚至有些过分的优化会直接致使项目的臃肿,因此不要由于极致的性能优化而破坏掉了你项目的合理架构。

总之一句话,性能优化适可而止,请酌情优化。

PS:附上Google关于Android开发的一些专题建议视频连接,不过在天朝须要自备梯

相关文章
相关标签/搜索