普通视图

发现新文章,点击刷新页面。
昨天以前首页

同济樱花

作者 JiaYin
2024年4月1日 21:11

又是一年樱花季,今年樱花看同济。

沪上知名樱花地大都去过了,同济还没有,于是今年提前在公众号上做了预约,去同济赏樱。樱花大道并不长,但因为是在校园,与大多数年轻学生模样的男女擦肩而过,氛围里充满青春气息。我没去过武大,不知道那是种什么模样,但同济这条樱花道我觉得挺喜欢的,明年说不定还会去。而且,同济大学公众号还专门开设了樱花大道直播,那天我穿了一条玫红色的裤子,也被装在楼顶上的直播镜头捕捉到了,也算记录这一时刻。

Android 系统不释放内存吗?

作者 Gracker
2018年9月13日 21:27

除了 CPU,很多用户在选购手机的时候通常也会考虑内存大小,不同版本内存的手机价格也不一样,买多大内存的合适呢?Android 系统是怎么管理内存的呢?普通用户对 Android 手机的内存使用总是一头雾水,这个应用到底占了多少内存?系统到底占了多少内存?内存对我手机的使用体验有什么影响?到底怎么才能用好 Android 手机?换新手机换多大内存的会比较合适呢?

知乎上有一个问题,一个用户问 “Android 系统不释放内存吗?”,用户并不是不知道系统会释放内存,而是想知道其中的细节,好优化使用的体验,下面我就从用户的几个问题入手,来简单说明一下,比较深入的细节我后续的文章会详细介绍。

同时也会对第一段中提到的几个问题提出一些个人的见解,欢迎一起来讨论,留下你的问题,提出你的见解,大家共同进步。

Android系统下关闭程序后,系统内存并不释放?

这个是不准确的,只能说对了一半. 你所描述的”android系统下关闭程序”,指的是怎么个关闭法呢?目前阶段有好几种关闭程序的方法:

点击Back键退出.

这种退出的方法, 进程是否被杀掉,取决于这个应用程序的实现. 举个栗子,如果你创建一个空的应用, 这时候查看系统内存信息(包名为com.exmaple.gaojianwu.myapplication,pid为5708,内存为13910kb):

可以看到,这个应用程序的pid为5708 , 其优先级为Foreground,即前台程序.

这时候我们点击Back键退出,然后再查看系统的内存信息(adb shell dumpsys meminfo)

我们看到,这个程序在 Back 键之后,其进程 5708 依旧是存在的.只是其进程优先级变成了Cache.其占用内存变成了 12337kb,和之前的 13910kb 相比是变小了一些. 但是大部分内存是没有被释放掉的.

在任务管理器中杀掉应用

在任务管理器中杀掉应用,这个结果是不一致的,其取决于这个OS的任务管理器的实现,大部分国内的厂家都会对任务管理器进行定制,以达到更有效的杀掉应用的效果.一般来说厂家定制的任务管理器都会比较暴力,除了少数白名单,其他的应用一概直接将进程杀掉

我们以上面的那个测试程序为例,打开这个程序之后, 其进程优先级为Foreground,这时候我们直接调用任务管理器杀掉改程序(以魅族MX4 Pro为栗子):

可以看到用任务管理器杀掉之后, 整个应用程序的进程都被杀掉了.

通过命令行或者开发者工具杀掉应用

我们可以通过 adb shell am force-stop 包名来杀掉这个程序,其结果也是进程直接被杀掉,IDE(比如Android Studio)选择一个进程后,点击图中的按钮

也是可以干掉这个进程的. 这时候进程是被直接杀掉的。

即使关掉后台进程,内存也增加不多?

这个不对,一个进程被杀死后,其内存会被释放掉的,不管是虚拟机内存还是 Native 内存还是图像所申请的内存,都会被系统回收,放到可用内存中。

我们以知乎 App Android客户端为栗子,打开这个程序之前,系统剩余内存 1.8G:

打开知乎之前,系统剩余内存

打开知乎这个程序之后,系统剩余内存:

打开知乎之后,系统剩余内存

知乎占用的内存:

知乎内存占用

使用任务管理器杀掉知乎(直接杀掉进程),系统剩余内存:

杀掉知乎知乎系统剩余内存

可以看到,杀掉进程之后,系统可用内存是会增加的。

据说即使前台关掉进程,其实该进程在后台还在运行?

这个和第一条一样,取决于你关掉进程的方法:

  1. 如果是按 Back 键,那么该进程还是会在后台。是否在运行,则要取决于这个应用的行为,有的应用 Back 到后台之后,其优先级会降到 Cache,系统内存不足或者触发 Cache 进程的数量限制,都会被系统直接杀掉,回收内存;有的应用 Back 到后台之后,会触发一些后台任务,起个 Service 之类的,还是会继续运行,比如导航软件和听歌软件
  2. 如果是按 Home 键,那么该进程依然会在后台,其大部分资源都没有释放,包括 Activity、Service 等。是否在运行,与 Back 到后台的行为一样,取决于应用的行为。
  3. 如果是被任务管理器或者命令行(Force Stop)杀掉,那么除非自启动(很多国内的应用被杀之后都会走自启动模式,同样很多国内的手机厂商,都会禁止应用被杀后走自启动模式,或者被其他进程唤醒,一般有名单进行控制,名单内的可以自启动,其他的自己想办法),应用是不会在后台运行的。

从上面的三个逻辑来看,用户最佳的使用方法应该遵循下面的建议:

  1. 如果是正在使用的应用,临时切出去干个别的事情,一会还要切回来,那么使用 Home 键把当前应用退到后台最佳
  2. 如果是不想看了,想退出应用了,那么使用 Back 键把当前应用退到后台最佳
  3. 如果不希望这个应用退到后台之后还有可能运行,或者想释放内存,那么用任务管理器把这个应用直接杀掉最好(推荐一键全杀,国内的 Rom 基本都有)

上面也讲到,国内的手机厂商,都会禁止应用被杀后走自启动模式,或者被其他进程唤醒,一般有名单进行控制,这样做是为什么呢?厂商这么做无非是为了限制应用对资源的占用,一个应用退到后台之后,系统肯定是希望其占用最少的资源,能不占资源最好(cpu、gpu、io、memory),但是国内的部分应用却没那么安分守己,大家肯定听说过全家桶和相互唤醒,应用被杀了没问题,我另外一个应用偷偷把你拉起来就可以了,这拉起来的过程就占用了系统的资源,可能会导致前台应用出现卡顿,或者导致整机内存不足,所以厂商对应用的限制是无可厚非的。

百度全家桶

那么应用为什么要抱团取暖,相互唤醒呢?第四个问题就是说这个的。

智能手机无需将程序彻底关掉,可以减少再启动的时间。是这样吗?

这句话没毛病,但是是有前提的,前提就是,如果这个应用在后台可以安分守己,至于现实嘛..不说大家也知道,参考百度全家桶

Android设计的时候,确实是想让大家不去关心内存问题,Android会有一套自己的内存管理机制,在内存不足的时候通过优先级干掉一些应用。每个应用在接收到内存不足的信号,需要根据内存不足的程度,来释放掉一部分内存.以保持自己的进程不被杀死,这样下次启动的时候就不用去fork zygote,这样的话,下一次启动的时间确实会少很多,也就是大家常说的冷启动和热启动的差距。

但是……………..凡是总有个但是,理想是丰满的,现实是很残酷的。严格按照Google想的那一套去做的应用不多,国内开发者对内存的敏感程度很低,导致很多应用程序跑起来分分钟就100-200MB 了,墨迹天气这样的应用,400m 妥妥的(不好意思又黑了墨迹天气) 。所以手机低内存的情况非常常见,所以低内存的情况会很频繁。这时候你再起一个应用,申请内存的时候发现内存不够,就开始杀应用了。

所以经常会出现你在看电子书,突然这时候微信来了个消息,你切过去回了个消息,打开相机拍了个照,然后发给朋友,又发了条微博,再回来看书的时候发现电子书已经挂了,正在重新加载程序….WLGQ…

这时候你就发现限制后台进程的重要性了,把不重要的进程直接干掉,限制应用的自启动和相互唤醒,保证重要的进程不会被系统杀掉,也就保证了用户的基本使用体验。

所以说不重要的程序是需要在使用结束后直接干掉的.一劳永逸,麻麻再也不用担心这货偷跑流量/后台安装程序/占内存/占 CPU 了….

再说后半句: 可以减少启动的时间. 这个是对的, 如果一个应用程序的进程没有被杀死,那么下一次启动这个应用程序的时候,就不需要去创建这个进程了(fork zygote,这个耗时还是蛮多的), 而是直接在这个进程中创建对应的组件即可(Android四大组件),速度比冷启动要快很多。

以汽车发动为例:

  1. 冷启动相当于 上车 -> 拧钥匙 -> 等发动机启动 -> 踩刹车换挡 -> 放手刹 -> Go
  2. 热启动相当于 上车 -> 踩刹车换挡 -> 放手刹 -> Go

关于 Android 内存的其他一些问题

这里来简单解答一下第一段中提到的那些问题

  1. Android 系统是怎么管理内存的呢? – 这个嘛,后续再详细讲
  2. 应用到底占了多少内存? – 这个嘛,后续再详细讲
  3. 系统到底占了多少内存? – 这个嘛,后续再详细讲
  4. 内存对我手机的使用体验有什么影响?– 低内存会影响整机的流畅性和响应速度,也会导致杀应用变得很频繁,用户体验差。
  5. 到底怎么才能用好 Android 手机?– 买 Android 旗舰,多用任务管理器的全杀功能,尽量禁止应用后台运行(Flyme 用户可以在手机关机里面设置)
  6. 换新手机换多大内存的会比较合适呢? – 越大越好,6G 起步,8G 最佳。

内存相关的文章参考

  1. Android代码内存优化建议-Java官方篇
  2. Android代码内存优化建议-Android资源篇
  3. Android代码内存优化建议-Android官方篇
  4. Android代码内存优化建议-OnTrimMemory优化
  5. Android性能优化典范之Understanding Overdraw
  6. Android性能优化之过渡绘制(一)
  7. Android性能优化之过渡绘制(二)
  8. Android内存优化之一:MAT使用入门
  9. Android内存优化之二:MAT使用进阶
  10. Android内存优化之三:打开MAT中的Bitmap原图
  11. 关于 Android 系统流畅性的一些思考

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 代码内存优化建议 - OnTrimMemory 优化

作者 Gracker
2015年7月20日 12:42

Android 内存优化系列文章:

  1. Android代码内存优化建议-Android官方篇
  2. Android代码内存优化建议-Java官方篇
  3. Android代码内存优化建议-Android资源篇
  4. Android代码内存优化建议-OnTrimMemory优化

OnTrimMemory 回调是 Android 4.0 之后提供的一个API,这个 API 是提供给开发者的,它的主要作用是提示开发者在系统内存不足的时候,通过处理部分资源来释放内存,从而避免被 Android 系统杀死。这样应用在下一次启动的时候,速度就会比较快。

本文通过问答的方式,从各个方面来讲解 OnTrimMemory 回调的使用过程和效果。想要开发高性能且用户体验良好的 Android 应用,那么这篇文章你不应该错过。

0. OnTrimMemory回调的作用?

OnTrimMemory是Android在4.0之后加入的一个回调,任何实现了ComponentCallbacks2接口的类都可以重写实现这个回调方法.OnTrimMemory的主要作用就是指导应用程序在不同的情况下进行自身的内存释放,以避免被系统直接杀掉,提高应用程序的用户体验.

Android系统会根据不同等级的内存使用情况,调用这个函数,并传入对应的等级:

  • TRIM_MEMORY_UI_HIDDEN 表示应用程序的所有UI界面被隐藏了,即用户点击了Home键或者Back键导致应用的UI界面不可见.这时候应该释放一些资源.
    TRIM_MEMORY_UI_HIDDEN这个等级比较常用,和下面六个的关系不是很强,所以单独说.

下面三个等级是当我们的应用程序真正运行时的回调:

  • TRIM_MEMORY_RUNNING_MODERATE 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,系统可能会开始根据LRU缓存规则来去杀死进程了。
  • TRIM_MEMORY_RUNNING_LOW 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经非常低了,我们应该去释放掉一些不必要的资源以提升系统的性能,同时这也会直接影响到我们应用程序的性能。
  • TRIM_MEMORY_RUNNING_CRITICAL 表示应用程序仍然正常运行,但是系统已经根据LRU缓存规则杀掉了大部分缓存的进程了。这个时候我们应当尽可能地去释放任何不必要的资源,不然的话系统可能会继续杀掉所有缓存中的进程,并且开始杀掉一些本来应当保持运行的进程,比如说后台运行的服务。

当应用程序是缓存的,则会收到以下几种类型的回调:

  • TRIM_MEMORY_BACKGROUND 表示手机目前内存已经很低了,系统准备开始根据LRU缓存来清理进程。这个时候我们的程序在LRU缓存列表的最近位置,是不太可能被清理掉的,但这时去释放掉一些比较容易恢复的资源能够让手机的内存变得比较充足,从而让我们的程序更长时间地保留在缓存当中,这样当用户返回我们的程序时会感觉非常顺畅,而不是经历了一次重新启动的过程。
  • TRIM_MEMORY_MODERATE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的中间位置,如果手机内存还得不到进一步释放的话,那么我们的程序就有被系统杀掉的风险了。
  • TRIM_MEMORY_COMPLETE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的最边缘位置,系统会最优先考虑杀掉我们的应用程序,在这个时候应当尽可能地把一切可以释放的东西都进行释放。

1. 哪些组件可以实现OnTrimMemory回调?

  • Application.onTrimMemory()
  • Activity.onTrimMemory()
  • Fragment.OnTrimMemory()
  • Service.onTrimMemory()
  • ContentProvider.OnTrimMemory()

2. OnTrimMemory回调中可以释放哪些资源?

通常在架构阶段就要考虑清楚,我们有哪些东西是要常驻内存的,有哪些是伴随界面存在的.一般情况下,有下面几种资源需要进行释放:

  • 缓存 缓存包括一些文件缓存,图片缓存等,在用户正常使用的时候这些缓存很有作用,但当你的应用程序UI不可见的时候,这些缓存就可以被清除以减少内存的使用.比如第三方图片库的缓存.
  • 一些动态生成动态添加的View. 这些动态生成和添加的View且少数情况下才使用到的View,这时候可以被释放,下次使用的时候再进行动态生成即可.比如原生桌面中,会在OnTrimMemory的TRIM_MEMORY_MODERATE等级中,释放所有AppsCustomizePagedView的资源,来保证在低内存的时候,桌面不会轻易被杀掉.

2.1 例子:释放不常用到的View.

代码出处:Launcher

Launcher.java:

1
2
3
4
5
6
7
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
mAppsCustomizeTabHost.onTrimMemory();
}
}

AppsCustomizeTabHost.java:

1
2
3
4
5
6
public void onTrimMemory() {
mContent.setVisibility(GONE);
// Clear the widget pages of all their subviews - this will trigger the widget previews
// to delete their bitmaps
mPagedView.clearAllWidgetPages();
}

AppsCustomizePagedView.java:

1
2
3
4
5
6
7
8
9
10
11
public void clearAllWidgetPages() {
cancelAllTasks();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View v = getPageAt(i);
if (v instanceof PagedViewGridLayout) {
((PagedViewGridLayout) v).removeAllViewsOnPage();
mDirtyPageContent.set(i, true);
}
}
}

PagedViewGridLayout.java

1
2
3
4
5
6
@Override
public void removeAllViewsOnPage() {
removeAllViews();
mOnLayoutListener = null;
setLayerType(LAYER_TYPE_NONE, null);
}

2.2 例子: 清除缓存

代码出处:Contact

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onTrimMemory(int level) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
// Clear the caches. Note all pending requests will be removed too.
clear();
}
}

public void clear() {
mPendingRequests.clear();
mBitmapHolderCache.evictAll();
mBitmapCache.evictAll();
}

3. OnTrimMemory和onStop的关系?

onTrimMemory()方法中的TRIM_MEMORY_UI_HIDDEN回调只有当我们程序中的所有UI组件全部不可见的时候才会触发,这和onStop()方法还是有很大区别的,因为onStop()方法只是当一个Activity完全不可见的时候就会调用,比如说用户打开了我们程序中的另一个Activity。

因此,我们可以在onStop()方法中去释放一些Activity相关的资源,比如说取消网络连接或者注销广播接收器等,但是像UI相关的资源应该一直要等到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)这个回调之后才去释放,这样可以保证如果用户只是从我们程序的一个Activity回到了另外一个Activity,界面相关的资源都不需要重新加载,从而提升响应速度。

需要注意的是,onTrimMemory的TRIM_MEMORY_UI_HIDDEN 等级是在onStop方法之前调用的.

4. OnTrimMemory和OnLowMemory的关系?

在引入OnTrimMemory之前都是使用OnLowMemory回调,需要知道的是,OnLowMemory大概和OnTrimMemory中的TRIM_MEMORY_COMPLETE级别相同,如果你想兼容api<14的机器,那么可以用OnLowMemory来实现,否则你可以忽略OnLowMemory,直接使用OnTrimMemory即可.

5. 为什么要调用OnTrimMemory?

尽管系统在内存不足的时候杀进程的顺序是按照LRU Cache中从低到高来的,但是它同时也会考虑杀掉那些占用内存较高的应用来让系统更快地获得更多的内存。

所以如果你的应用占用内存较小,就可以增加不被杀掉的几率,从而快速地恢复(如果不被杀掉,启动的时候就是热启动,否则就是冷启动,其速度差在2~3倍)。

所以说在几个不同的OnTrimMemory回调中释放自己的UI资源,可以有效地提高用户体验。

6. 有哪些典型的使用场景?

6.1 常驻内存的应用

一些常驻内存的应用,比如Launcher、安全中心、电话等,在用户使用过要退出的时候,需要调用OnTrimMemory来及时释放用户使用的时候所产生的多余的内存资源:比如动态生成的View、图片缓存、Fragment等。

6.2 有后台Service运行的应用

这些应用不是常驻内存的,意味着可以被任务管理器杀掉,但是在某些场景下用户不会去杀。
这类应用包括:音乐、下载等。用户退出UI界面后,音乐还在继续播放,下载程序还在运行。这时候音乐应该释放部分UI资源和Cache。

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 代码内存优化建议 - Android 资源篇

作者 Gracker
2015年7月20日 11:48

Android 内存优化系列文章:

  1. Android代码内存优化建议-Android官方篇
  2. Android代码内存优化建议-Java官方篇
  3. Android代码内存优化建议-Android资源篇
  4. Android代码内存优化建议-OnTrimMemory优化

这篇文章主要介绍在实际Android应用程序的开发中,容易导致内存泄露的一些情况。开发人员如果在进行代码编写之前就有内存泄露方面的基础知识,那么写出来的代码会强壮许多,写这篇文章也是这个初衷。本文从Android开发中的资源使用情况入手,介绍了如何在Bitmap、数据库查询、9-patch、过渡绘制等方面优化内存的使用。

Android资源优化

1. Bitmap优化

Android中的大部分内存问题归根结底都是Bitmap的问题,如果打开MAT(Memory analyzer tool)来看,实际占用内存大的都是一些Bitmap(以byte数组的形式存储)。所以Bitmap的优化应该是我们着重去解决的。Google在其官方有针对Bitmap的使用专门写了一个专题 : Displaying Bitmaps Efficiently, 对应的中文翻译在 :displaying-bitmaps , 在优化Bitmap资源之前,请先看看这个系列的文档,以确保自己正确地使用了Bitmap。

Bitmap如果没有被释放,那么一般只有两个问题:

  • 用户在使用完这个Bitmap之后,没有主动去释放Bitmap资源。
  • 这个Bitmap资源被引用所以无法被释放 。

1.1 主动释放Bitmap资源

当你确定这个Bitmap资源不会再被使用的时候(当然这个Bitmap不释放可能会让程序下一次启动或者resume快一些,但是其占用的内存资源太大,可能导致程序在后台的时候被杀掉,反而得不偿失),我们建议手动调用recycle()方法,释放其Native内存:

1
2
3
4
if(bitmap != null && !bitmap.isRecycled()){  
bitmap.recycle();
bitmap = null;
}

我们也可以看一下Bitmap.java中recycle()方法的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    /**
* Free the native object associated with this bitmap, and clear the
* reference to the pixel data. This will not free the pixel data synchronously;
* it simply allows it to be garbage collected if there are no other references.
* The bitmap is marked as "dead", meaning it will throw an exception if
* getPixels() or setPixels() is called, and will draw nothing. This operation
* cannot be reversed, so it should only be called if you are sure there are no
* further uses for the bitmap. This is an advanced call, and normally need
* not be called, since the normal GC process will free up this memory when
* there are no more references to this bitmap.
*/
public void recycle() {
if (!mRecycled) {
if (nativeRecycle(mNativeBitmap)) {
// return value indicates whether native pixel object was actually recycled.
// false indicates that it is still in use at the native level and these
// objects should not be collected now. They will be collected later when the
// Bitmap itself is collected.
mBuffer = null;
mNinePatchChunk = null;
}
mRecycled = true;
}
}

......
//如果使用过程中抛出异常的判断
if (bitmap.isRecycled()) {
throw new RuntimeException("Canvas: trying to use a recycled bitmap " + bitmap);
}

调用bitmap.recycle之后,这个Bitmap如果没有被引用到,那么就会被垃圾回收器回收。如果不主动调用这个方法,垃圾回收器也会进行回收工作,只不过垃圾回收器的不确定性太大,依赖其自动回收不靠谱(比如垃圾回收器一次性要回收好多Bitmap,那么需要的时间就会很多,导致回收的时候会卡顿)。所以我们需要主动调用recycle。

1.2 主动释放ImageView的图片资源

由于我们在实际开发中,很多情况是在xml布局文件中设置ImageView的src或者在代码中调用ImageView.setImageResource/setImageURI/setImageDrawable等方法设置图像,下面代码可以回收这个ImageView所对应的资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static void recycleImageViewBitMap(ImageView imageView) {
if (imageView != null) {
BitmapDrawable bd = (BitmapDrawable) imageView.getDrawable();
rceycleBitmapDrawable(bd);
}
}

private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) {
if (bitmapDrawable != null) {
Bitmap bitmap = bitmapDrawable.getBitmap();
rceycleBitmap(bitmap);
}
bitmapDrawable = null;
}

private static void rceycleBitmap(Bitmap bitmap) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
}
}

1.3 主动释放ImageView的背景资源

如果你的ImageView是有Background,那么下面的代码可以释放他:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void recycleBackgroundBitMap(ImageView view) {
if (view != null) {
BitmapDrawable bd = (BitmapDrawable) view.getBackground();
rceycleBitmapDrawable(bd);
}
}

public static void recycleImageViewBitMap(ImageView imageView) {
if (imageView != null) {
BitmapDrawable bd = (BitmapDrawable) imageView.getDrawable();
rceycleBitmapDrawable(bd);
}
}

private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) {
if (bitmapDrawable != null) {
Bitmap bitmap = bitmapDrawable.getBitmap();
rceycleBitmap(bitmap);
}
bitmapDrawable = null;
}

1.4 尽量少用Png图,多用NinePatch的图

现在手机的分辨率越来越高,图片资源在被加载后所占用的内存也越来越大,所以要尽量避免使用大的PNG图,在产品设计的时候就要尽量避免用一张大图来进行展示,尽量多用NinePatch资源。

Android中的NinePatch指的是一种拉伸后不会变形的特殊png图,NinePatch的拉伸区域可以自己定义。这种图的优点是体积小,拉伸不变形,可以适配多机型。Android SDK中有自带NinePatch资源制作工具,Android-Studio中在普通png图片点击右键可以将其转换为NinePatch资源,使用起来非常方便。

左边是原图,右边是拉伸后的效果

1.5 使用大图之前,尽量先对其进行压缩

图片有不同的形状与大小。在大多数情况下它们的实际大小都比需要呈现出来的要大很多。例如,系统的Gallery程序会显示那些你使用设备camera拍摄的图片,但是那些图片的分辨率通常都比你的设备屏幕分辨率要高很多。

考虑到程序是在有限的内存下工作,理想情况是你只需要在内存中加载一个低分辨率的版本即可。这个低分辨率的版本应该是与你的UI大小所匹配的,这样才便于显示。一个高分辨率的图片不会提供任何可见的好处,却会占用宝贵的(precious)的内存资源,并且会在快速滑动图片时导致(incurs)附加的效率问题。

Google官网的Training中,有一篇文章专门介绍如何有效地加载大图,里面提到了两个比较重要的技术:

  • 在图片加载前获取其宽高和类型
  • 加载一个按比例缩小的版本到内存中

原文地址:Loading Large Bitmaps Efficiently,中文翻译地址:有效地加载大尺寸位图,强烈建议每一位Android开发者都去看一下,并在自己的实际项目中使用到。

更多关于Bitmap的使用和优化,可以参考Android官方Training专题的displaying-bitmaps

2 查询数据库没有关闭游标

程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
示例代码:

1
2
3
4
Cursor cursor = getContentResolver().query(uri ...);
if (cursor.moveToNext()) {
... ...
}

修正示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Cursor cursor = null;
try {
cursor = getContentResolver().query(uri ...);
if (cursor != null && cursor.moveToNext()) {
... ...
}
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
//ignore this
}
}
}

`

3 构造Adapter时,没有使用缓存的convertView

以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法:

1
public View getView(int position, View convertView, ViewGroup parent)

来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参 View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。
由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。ListView回收list item的view对象的过程可以查看:android.widget.AbsListView.java –> void addScrapView(View scrap) 方法。

ListView的getView

示例代码:

1
2
3
4
5
public View getView(int position, View convertView, ViewGroup parent) {
View view = new Xxx(...);
... ...
return view;
}

`
示例修正代码:

1
2
3
4
5
6
7
8
9
10
11
12
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
if (convertView != null) {
view = convertView;
populate(view, getItem(position));
...
} else {
view = new Xxx(...);
...
}
return view;
}

关于ListView的使用和优化,可以参考这两篇文章:

4 释放对象的引用

前面有说过,一个对象的内存没有被释放是因为他被其他的对象所引用,系统不回去释放这些有GC Root的对象。

示例A:
假设有如下操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DemoActivity extends Activity {
... ...
private Handler mHandler = ...
private Object obj;
public void operation() {
obj = initObj();
...
[Mark]
mHandler.post(new Runnable() {
public void run() {
useObj(obj);
}
});
}
}

我们有一个成员变量 obj,在operation()中我们希望能够将处理obj实例的操作post到某个线程的MessageQueue中。在以上的代码中,即便是mHandler所在的线程使用完了obj所引用的对象,但这个对象仍然不会被垃圾回收掉,因为DemoActivity.obj还保有这个对象的引用。所以如果在DemoActivity中不再使用这个对象了,可以在[Mark]的位置释放对象的引用,而代码可以修改为:

1
2
3
4
5
6
7
8
9
10
11
public void operation() {
obj = initObj();
...
final Object o = obj;
obj = null;
mHandler.post(new Runnable() {
public void run() {
useObj(o);
}
}
}

示例B:
假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。

但是如果在释放LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_ui进程挂掉。

总之当一个生命周期较短的对象A,被一个生命周期较长的对象B保有其引用的情况下,在A的生命周期结束时,要在B中清除掉对A的引用。

使用MAT可以很方便地查看对象之间的引用,

5 在Activity的生命周期中释放资源

Android应用程序中最典型的需要注意释放资源的情况是在Activity的生命周期中,在onPause()、onStop()、onDestroy()方法中需要适当的释放资源的情况。由于此情况很基础,在此不详细说明,具体可以查看官方文档对Activity生命周期的介绍,以明确何时应该释放哪些资源。

6 消除过渡绘制

过渡绘制指的是在屏幕一个像素上绘制多次(超过一次),比如一个TextView后有背景,那么显示文本的像素至少绘了两次,一次是背景,一次是文本。GPU过度绘制或多或少对性能有些影响,设备的内存带宽是有限的,当过度绘制导致应用需要更多的带宽(超过了可用带宽)的时候性能就会降低。带宽的限制每个设备都可能是不一样的。

过渡绘制的原因:

  1. 同一层级的View叠加
  2. 复杂的层级叠加

减少过渡绘制能去掉一些无用的View,能有效减少GPU的负载,也可以减轻一部分内存压力。关于过渡绘制我专门写了一篇文章来介绍:过渡绘制及其优化

7 使用Android系统自带的资源

在Android应用开发过程中,屏幕上控件的布局代码和程序的逻辑代码通常是分开的。界面的布局代码是放在一个独立的xml文件中的,这个文件里面是树型组织的,控制着页面的布局。通常,在这个页面中会用到很多控件,控件会用到很多的资源。Android系统本身有很多的资源,包括各种各样的字符串、图片、动画、样式和布局等等,这些都可以在应用程序中直接使用。这样做的好处很多,既可以减少内存的使用,又可以减少部分工作量,也可以缩减程序安装包的大小。

比如下面的代码就是使用系统的ListView:

1
2
3
4
<ListView 
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>

8 使用内存相关工具检测

在开发中,不可能保证一次就开发出一个内存管理非常棒的应用,所以在开发的每一个阶段,都要有意识地去针对内存进行专门的检查。目前Android提供了许多布局、内存相关的工具,比如Lint、MAT等。学会这些工具的使用是一个Android开发者必不可少的技能。

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 代码内存优化建议 - Android 官方篇

作者 Gracker
2015年7月20日 11:29

Android 内存优化系列文章:

  1. Android代码内存优化建议-Android官方篇
  2. Android代码内存优化建议-Java官方篇
  3. Android代码内存优化建议-Android资源篇
  4. Android代码内存优化建议-OnTrimMemory优化

为了使垃圾回收器可以正常释放程序所占用的内存,在编写代码的时候就一定要注意尽量避免出现内存泄漏的情况(通常都是由于全局成员变量持有对象引用所导致的),并且在适当的时候去释放对象引用。对于大多数的应用程序而言,后面其它的事情就可以都交给垃圾回收器去完成了,如果一个对象的引用不再被其它对象所持有,那么系统就会将这个对象所分配的内存进行回收。

我们在开发软件的时候应当自始至终都把内存的问题充分考虑进去,这样的话才能开发出更加高性能的软件。而内存问题也并不是无规律可行的,Android系统给我们提出了很多内存优化的建议技巧,只要按照这些技巧来编写程序,就可以让我们的程序在内存性能发面表现得相当不错。

正文

本文原文来自Android开发者官网Managing Your App’s Memory章节中的
How Your App Should Manage Memory部分。是Android官方帮助应用开发者更好地管理应用的内存而写的。作为一个应用程序开发者,你需要在你开发应用程序的时时刻刻都考虑内存问题。

1. 节制地使用Service

如果应用程序当中需要使用Service来执行后台任务的话,请一定要注意只有当任务正在执行的时候才应该让Service运行起来。另外,当任务执行完之后去停止Service的时候,要小心Service停止失败导致内存泄漏的情况。

当我们启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,这样就会导致这个进程变得非常消耗内存。并且,系统可以在LRU cache当中缓存的进程数量也会减少,导致切换应用程序的时候耗费更多性能。严重的话,甚至有可能会导致崩溃,因为系统在内存非常吃紧的时候可能已无法维护所有正在运行的Service所依赖的进程了。

为了能够控制Service的生命周期,Android官方推荐的最佳解决方案就是使用IntentService,这种Service的最大特点就是当后台任务执行结束后会自动停止,从而极大程度上避免了Service内存泄漏的可能性。

让一个Service在后台一直保持运行,即使它并不执行任何工作,这是编写Android程序时最糟糕的做法之一。所以Android官方极度建议开发人员们不要过于贪婪,让Service在后台一直运行,这不仅可能会导致手机和程序的性能非常低下,而且被用户发现了之后也有可能直接导致我们的软件被卸载

2. 当界面不可见时释放内存

当用户打开了另外一个程序,我们的程序界面已经不再可见的时候,我们应当将所有和界面相关的资源进行释放。在这种场景下释放资源可以让系统缓存后台进程的能力显著增加,因此也会让用户体验变得更好。
那么我们如何才能知道程序界面是不是已经不可见了呢?其实很简单,只需要在Activity中重写onTrimMemory()方法,然后在这个方法中监听TRIM_MEMORY_UI_HIDDEN这个级别,一旦触发了之后就说明用户已经离开了我们的程序,那么此时就可以进行资源释放操作了,如下所示:

1
2
3
4
5
6
7
8
9
@Override  
public void onTrimMemory(int level) {  
    super.onTrimMemory(level);  
    switch (level) {  
    case TRIM_MEMORY_UI_HIDDEN:  
        // 进行资源释放操作  
        break;  
    }  
}  

注意onTrimMemory()方法中的TRIM_MEMORY_UI_HIDDEN回调只有当我们程序中的所有UI组件全部不可见的时候才会触发,这和onStop()方法还是有很大区别的,因为onStop()方法只是当一个Activity完全不可见的时候就会调用,比如说用户打开了我们程序中的另一个Activity。因此,我们可以在onStop()方法中去释放一些Activity相关的资源,比如说取消网络连接或者注销广播接收器等,但是像UI相关的资源应该一直要等到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)这个回调之后才去释放,这样可以保证如果用户只是从我们程序的一个Activity回到了另外一个Activity,界面相关的资源都不需要重新加载,从而提升响应速度。

3.当内存紧张时释放内存

除了刚才讲的TRIM_MEMORY_UI_HIDDEN这个回调,onTrimMemory()方法还有很多种其它类型的回调,可以在手机内存降低的时候及时通知我们。我们应该根据回调中传入的级别来去决定如何释放应用程序的资源:

3.1 应用程序正在运行时

TRIM_MEMORY_RUNNING_MODERATE 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,系统可能会开始根据LRU缓存规则来去杀死进程了。
TRIM_MEMORY_RUNNING_LOW 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经非常低了,我们应该去释放掉一些不必要的资源以提升系统的性能,同时这也会直接影响到我们应用程序的性能。
TRIM_MEMORY_RUNNING_CRITICAL 表示应用程序仍然正常运行,但是系统已经根据LRU缓存规则杀掉了大部分缓存的进程了。这个时候我们应当尽可能地去释放任何不必要的资源,不然的话系统可能会继续杀掉所有缓存中的进程,并且开始杀掉一些本来应当保持运行的进程,比如说后台运行的服务。

3.2 应用程序被缓存

TRIM_MEMORY_BACKGROUND 表示手机目前内存已经很低了,系统准备开始根据LRU缓存来清理进程。这个时候我们的程序在LRU缓存列表的最近位置,是不太可能被清理掉的,但这时去释放掉一些比较容易恢复的资源能够让手机的内存变得比较充足,从而让我们的程序更长时间地保留在缓存当中,这样当用户返回我们的程序时会感觉非常顺畅,而不是经历了一次重新启动的过程。
TRIM_MEMORY_MODERATE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的中间位置,如果手机内存还得不到进一步释放的话,那么我们的程序就有被系统杀掉的风险了。
TRIM_MEMORY_COMPLETE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的最边缘位置,系统会最优先考虑杀掉我们的应用程序,在这个时候应当尽可能地把一切可以释放的东西都进行释放。

因为onTrimMemory()是在API14才加进来的,所以如果要支持API14之前的话,则可以考虑 onLowMemory()这个方法,它粗略的相等于onTrimMemory()回调的TRIM_MEMORY_COMPLETE事件。

注意:当系统安装LRU cache杀进程的时候,尽管大部分时间是从下往上按顺序杀,有时候系统也会将占用内存比较大的进程纳入被杀范围,以尽快得到足够的内存。所以你的应用在LRU list中占用的内存越少,你就越能避免被杀掉,当你恢复的时候也会更快。

4. 检查你应该使用多少的内存

正如前面提到的,每一个Android设备都会有不同的RAM总大小与可用空间,因此不同设备为app提供了不同大小的heap限制。你可以通过调用getMemoryClass()来获取你的app的可用heap大小。如果你的app尝试申请更多的内存,会出现OutOfMemory的错误。

在一些特殊的情景下,你可以通过在manifest的application标签下添加largeHeap=true的属性来声明一个更大的heap空间。如果你这样做,你可以通过getLargeMemoryClass()来获取到一个更大的heap size。

然而,能够获取更大heap的设计本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用)。不要轻易的因为你需要使用大量的内存而去请求一个大的heap size。只有当你清楚的知道哪里会使用大量的内存并且为什么这些内存必须被保留时才去使用large heap. 因此请尽量少使用large heap。使用额外的内存会影响系统整体的用户体验,并且会使得GC的每次运行时间更长。在任务切换时,系统的性能会变得大打折扣。

另外, large heap并不一定能够获取到更大的heap。在某些有严格限制的机器上,large heap的大小和通常的heap size是一样的。因此即使你申请了large heap,你还是应该通过执行getMemoryClass()来检查实际获取到的heap大小。

5. 避免在Bitmap上浪费内存

当我们读取一个Bitmap图片的时候,有一点一定要注意,就是千万不要去加载不需要的分辨率。在一个很小的ImageView上显示一张高分辨率的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存。需要仅记的一点是,将一张图片解析成一个Bitmap对象时所占用的内存并不是这个图片在硬盘中的大小,可能一张图片只有100k你觉得它并不大,但是读取到内存当中是按照像素点来算的,比如这张图片是15001000像素,使用的ARGB_8888颜色类型,那么每个像素点就会占用4个字节,总内存就是15001000*4字节,也就是5.7M,这个数据看起来就比较恐怖了。

6. 使用优化过的数据集合

利用Android Framework里面优化过的容器类,例如SparseArray, SparseBooleanArray, 与 LongSparseArray。 通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效在于他们避免了对key与value的autobox自动装箱,并且避免了装箱后的解箱。

7. 知晓内存的开支情况

我们还应当清楚我们所使用语言的内存开支和消耗情况,并且在整个软件的设计和开发当中都应该将这些信息考虑在内。可能有一些看起来无关痛痒的写法,结果却会导致很大一部分的内存开支,例如:

  • 使用枚举通常会比使用静态常量要消耗两倍以上的内存,在Android开发当中我们应当尽可能地不使用枚举。
  • 任何一个Java类,包括内部类、匿名类,都要占用大概500字节的内存空间。
  • 任何一个类的实例要消耗12-16字节的内存开支,因此频繁创建实例也是会一定程序上影响内存的。
  • 在使用HashMap时,即使你只设置了一个基本数据类型的键,比如说int,但是也会按照对象的大小来分配内存,大概是32字节,而不是4字节。因此最好的办法就是像上面所说的一样,使用优化过的数据集合。

8. 谨慎使用抽象编程

许多程序员都喜欢各种使用抽象来编程,认为这是一种很好的编程习惯。当然,这一点不可否认,因为的抽象的编程方法更加面向对象,而且在代码的维护和可扩展性方面都会有所提高。但是,在Android上使用抽象会带来额外的内存开支,因为抽象的编程方法需要编写额外的代码,虽然这些代码根本执行不到,但是却也要映射到内存当中,不仅占用了更多的内存,在执行效率方面也会有所降低。当然这里我并不是提倡大家完全不使用抽象编程,而是谨慎使用抽象编程,不要认为这是一种很酷的编程方式而去肆意使用它,只在你认为有必要的情况下才去使用。

9. 为序列化的数据使用nano protobufs

Protocol buffers是由Google为序列化结构数据而设计的,一种语言无关,平台无关,具有良好扩展性的协议。类似XML,却比XML更加轻量,快速,简单。如果你需要为你的数据实现协议化,你应该在客户端的代码中总是使用nano protobufs。通常的协议化操作会生成大量繁琐的代码,这容易给你的app带来许多问题:增加RAM的使用量,显著增加APK的大小,更慢的执行速度,更容易达到DEX的字符限制。

关于更多细节,请参考protobuf readme的”Nano version”章节。

10. 尽量避免使用依赖注入框架

现在有很多人都喜欢在Android工程当中使用依赖注入框架,比如说像Guice或者RoboGuice等,因为它们可以简化一些复杂的编码操作,比如可以将下面的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class AndroidWay extends Activity {   
    TextView name;   
    ImageView thumbnail;   
    LocationManager loc;   
    Drawable icon;   
    String myName;   
  
    public void onCreate(Bundle savedInstanceState) {   
        super.onCreate(savedInstanceState);   
        setContentView(R.layout.main);  
        name      = (TextView) findViewById(R.id.name);   
        thumbnail = (ImageView) findViewById(R.id.thumbnail);   
        loc       = (LocationManager) getSystemService(Activity.LOCATION_SERVICE);   
        icon      = getResources().getDrawable(R.drawable.icon);   
        myName    = getString(R.string.app_name);   
        name.setText( "Hello, " + myName );   
    }   
}   

简化成这样的一种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ContentView(R.layout.main)  
class RoboWay extends RoboActivity {   
    @InjectView(R.id.name)             TextView name;   
    @InjectView(R.id.thumbnail)        ImageView thumbnail;   
    @InjectResource(R.drawable.icon)   Drawable icon;   
    @InjectResource(R.string.app_name) String myName;   
    @Inject                            LocationManager loc;   
  
    public void onCreate(Bundle savedInstanceState) {   
        super.onCreate(savedInstanceState);   
        name.setText( "Hello, " + myName );   
    }   
}  

看上去确实十分诱人,我们甚至可以将findViewById()这一类的繁琐操作全部省去了。但是这些框架为了要搜寻代码中的注解,通常都需要经历较长的初始化过程,并且还可能将一些你用不到的对象也一并加载到内存当中。这些用不到的对象会一直占用着内存空间,可能要过很久之后才会得到释放,相较之下,也许多敲几行看似繁琐的代码才是更好的选择。

11. 谨慎使用external libraries

很多External library的代码都不是为移动网络环境而编写的,在移动客户端则显示的效率不高。至少,当你决定使用一个external library的时候,你应该针对移动网络做繁琐的porting与maintenance的工作。

即使是针对Android而设计的library,也可能是很危险的,因为每一个library所做的事情都是不一样的。例如,其中一个lib使用的是nano protobufs, 而另外一个使用的是micro protobufs。那么这样,在你的app里面就有2种protobuf的实现方式。这样的冲突同样可能发生在输出日志,加载图片,缓存等等模块里面。

同样不要陷入为了1个或者2个功能而导入整个library的陷阱。如果没有一个合适的库与你的需求相吻合,你应该考虑自己去实现,而不是导入一个大而全的解决方案。

12. 优化整体性能

官方有列出许多优化整个app性能的文章:Best Practices for Performance. 这篇文章就是其中之一。有些文章是讲解如何优化app的CPU使用效率,有些是如何优化app的内存使用效率。

你还应该阅读optimizing your UI来为layout进行优化。同样还应该关注lint工具所提出的建议,进行优化。

13. 使用ProGuard来剔除不需要的代码

ProGuard能够通过移除不需要的代码,重命名类,域与方法等方对代码进行压缩,优化与混淆。使用ProGuard可以是的你的代码更加紧凑,这样能够使用更少mapped代码所需要的RAM。

14. 对最终的APK使用zipalign

在编写完所有代码,并通过编译系统生成APK之后,你需要使用zipalign对APK进行重新校准。如果你不做这个步骤,会导致你的APK需要更多的RAM,因为一些类似图片资源的东西不能被mapped。

**Notes::**Google Play不接受没有经过zipalign的APK。

15. 分析你的RAM使用情况

一旦你获取到一个相对稳定的版本后,需要分析你的app整个生命周期内使用的内存情况,并进行优化,更多细节请参考Investigating Your RAM Usage.

16. 使用多进程

如果合适的话,有一个更高级的技术可以帮助你的app管理内存使用:通过把你的app组件切分成多个组件,运行在不同的进程中。这个技术必须谨慎使用,大多数app都不应该运行在多个进程中。因为如果使用不当,它会显著增加内存的使用,而不是减少。当你的app需要在后台运行与前台一样的大量的任务的时候,可以考虑使用这个技术。

一个典型的例子是创建一个可以长时间后台播放的Music Player。如果整个app运行在一个进程中,当后台播放的时候,前台的那些UI资源也没有办法得到释放。类似这样的app可以切分成2个进程:一个用来操作UI,另外一个用来后台的Service.

你可以通过在manifest文件中声明’android:process’属性来实现某个组件运行在另外一个进程的操作。

1
2
<service android:name=".PlaybackService"
android:process=":background" />

更多关于使用这个技术的细节,请参考原文,链接如下。
http://developer.android.com/training/articles/memory.html

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 代码内存优化建议 - Java 官方篇

作者 Gracker
2015年7月20日 11:26

Android 内存优化系列文章:

  1. Android代码内存优化建议-Android官方篇
  2. Android代码内存优化建议-Java官方篇
  3. Android代码内存优化建议-Android资源篇
  4. Android代码内存优化建议-OnTrimMemory优化

这篇文章主要是介绍了一些小细节的优化技巧,当这些小技巧综合使用起来的时候,对于整个App的性能提升还是有作用的,只是不能较大幅度的提升性能而已。选择合适的算法与数据结构才应该是你首要考虑的因素,在这篇文章中不会涉及这方面。你应该使用这篇文章中的小技巧作为平时写代码的习惯,这样能够提升代码的效率。

本文的原文为Google官方Training的性能优化部分,这一章节主要讲解的是高性能Android代码优化建议,建议所有Android应用开发者都仔细阅读这份文档,并将所提到的编码思想运用到实际的Android开发中。

原文地址:http://developer.android.com/training/articles/perf-tips.html

正文

通常来说,高效的代码需要满足下面两个规则:

  • 不要做冗余的动作
  • 如果能避免,尽量不要分配内存

你会面临最棘手的一个问题是当你优化一个肯定会在多种类型的硬件上运行的应用程序。不同版本的VM在不同的处理器上运行速度不同。它甚至不是你可以简单地说“设备X因为F原因比设备Y快/慢”那么简单,而且也不能简单地从一个设备拓展到另一个设备。特别提醒的是模拟器在性能方面和其他的设备没有可比性。通常有JIT优化和没有JIT优化的设备之间存在巨大差异:经过JIT代码优化的设备并不一定比没有经过JIT代码优化的设备好。

代码的执行效果会受到设备CPU,设备内存,系统版本等诸多因素的影响。为了确保代码能够在不同设备上都运行良好,需要最大化代码的效率。

1)避免创建不必要的对象

虽然GC可以回收不用的对象,可是为这些对象分配内存,并回收它们同样是需要耗费资源的。
因此请尽量避免创建不必要的对象,有下面一些例子来说明这个问题:

  • 如果你需要返回一个String对象,并且你知道它最终会需要连接到一个StringBuffer,请修改你的实现方式,避免直接进行连接操作,应该采用创建一个临时对象来做这个操作.
  • 当从输入的数据集中抽取出Strings的时候,尝试返回原数据的substring对象,而不是创建一个重复的对象。

一个稍微激进点的做法是把所有多维的数据分解成1维的数组:

  • 一组int数据要比一组Integer对象要好很多。可以得知,两组1维数组要比一个2维数组更加的有效率。同样的,这个道理可以推广至其他原始数据类型。
  • 如果你需要实现一个数组用来存放(Foo,Bar)的对象,尝试分解为Foo[]与Bar[]要比(Foo,Bar)好很多。(当然,为了某些好的API的设计,可以适当做一些妥协。但是在自己的代码内部,你应该多多使用分解后的容易。

通常来说,需要避免创建更多的对象。更少的对象意味者更少的GC动作,GC会对用户体验有比较直接的影响。

2)选择Static而不是Virtual

如果你不需要访问一个对象的值域,请保证这个方法是static类型的,这样方法调用将快15%-20%。这是一个好的习惯,因为你可以从方法声明中得知调用无法改变这个对象的状态。

3)常量声明为Static Final

先看下面这种声明的方式

1
2
static int intVal = 42;
static String strVal = "Hello, world!";

编译器会在类首次被使用到的时候,使用初始化<clinit>方法来初始化上面的值,之后访问的时候会需要先到它那里查找,然后才返回数据。我们可以使用static final来提升性能:

1
2
static final int intVal = 42;
static final String strVal = "Hello, world!";

这时再也不需要上面的那个方法来做多余的查找动作了。
** 所以,请尽可能的为常量声明为static final类型的。**

4)避免内部的Getters/Setters

像C++等native language,通常使用getters(i = getCount())而不是直接访问变量(i = mCount).这是编写C++的一种优秀习惯,而且通常也被其他面向对象的语言所采用,例如C#与Java,因为编译器通常会做inline访问,而且你需要限制或者调试变量,你可以在任何时候在getter/setter里面添加代码。
然而,在Android上,这是一个糟糕的写法。Virtual method的调用比起直接访问变量要耗费更多。那么合理的做法是:在面向对象的设计当中应该使用getter/setter,但是在类的内部你应该直接访问变量。
没有JIT(Just In Time Compiler)时,直接访问变量的速度是调用getter的3倍。有JIT时,直接访问变量的速度是通过getter访问的7倍。
请注意,如果你使用ProGuard, 你可以获得同样的效果,因为ProGuard可以为你inline accessors.

5)使用增强的For循环写法

请比较下面三种循环的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static class Foo {
int mSplat;
}

Foo[] mArray = ...

public void zero() {
int sum = 0;
for (int i = 0; i < mArray.length; ++i) {
sum += mArray[i].mSplat;
}
}

public void one() {
int sum = 0;
Foo[] localArray = mArray;
int len = localArray.length;

for (int i = 0; i < len; ++i) {
sum += localArray[i].mSplat;
}
}

public void two() {
int sum = 0;
for (Foo a : mArray) {
sum += a.mSplat;
}
}
  • zero()是最慢的,因为JIT没有办法对它进行优化。
  • one()稍微快些。
  • two() 在没有做JIT时是最快的,可是如果经过JIT之后,与方法one()是差不多一样快的。它使用了增强的循环方法for-each。

所以请尽量使用for-each的方法,但是对于ArrayList,请使用方法one()。

6)使用包级访问而不是内部类的私有访问

参考下面一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Foo {
private class Inner {
void stuff() {
Foo.this.doStuff(Foo.this.mValue);
}
}

private int mValue;

public void run() {
Inner in = new Inner();
mValue = 27;
in.stuff();
}

private void doStuff(int value) {
System.out.println("Value is " + value);
}
}

这里重要的是,我们定义了一个私有的内部类(Foo$Inner),它直接访问了外部类中的私有方法以及私有成员对象。这是合法的,这段代码也会如同预期一样打印出”Value is 27”。

问题是,VM因为Foo和Foo$Inner是不同的类,会认为在Foo$Inner中直接访问Foo类的私有成员是不合法的。即使Java语言允许内部类访问外部类的私有成员。为了去除这种差异,编译器会产生一些仿造函数:

1
2
3
4
5
6
/*package*/ static int Foo.access$100(Foo foo) {
return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
foo.doStuff(value);
}

每当内部类需要访问外部类中的mValue成员或需要调用doStuff()函数时,它都会调用这些静态方法。这意味着,上面的代码可以归结为,通过accessor函数来访问成员变量。早些时候我们说过,通过accessor会比直接访问域要慢。所以,这是一个特定语言用法造成性能降低的例子。

如果你正在性能热区(hotspot:高频率、重复执行的代码段)使用像这样的代码,你可以把内部类需要访问的域和方法声明为包级访问,而不是私有访问权限。不幸的是,这意味着在相同包中的其他类也可以直接访问这些域,所以在公开的API中你不能这样做。

7)避免使用float类型

Android系统中float类型的数据存取速度是int类型的一半,尽量优先采用int类型。

8)使用库函数

尽量使用System.arraycopy()等一些封装好的库函数,它的效率是手动编写copy实现的9倍多。

** Tip: Also see Josh Bloch’s Effective Java, item 47. **

9)谨慎使用native函数

当你需要把已经存在的native code迁移到Android,请谨慎使用JNI。如果你要使用JNI,请学习JNI Tips

10)关于性能的误区

在没有做JIT之前,使用一种确切的数据类型确实要比抽象的数据类型速度要更有效率。(例如,使用HashMap要比Map效率更高。) 有误传效率要高一倍,实际上只是6%左右。而且,在JIT之后,他们直接并没有大多差异。

11)关于测量

上面文档中出现的数据是Android的实际运行效果。我们可以用Traceview 来测量,但是测量的数据是没有经过JIT优化的,所以实际的效果应该是要比测量的数据稍微好些。

关于如何测量与调试,还可以参考下面两篇文章:

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 性能优化典范 - Profile GPU Rendering

作者 Gracker
2015年4月19日 15:53

系列文章目录:

  1. Android性能优化典范综述
  2. Android性能优化典范之Render Performance
  3. Android性能优化典范之Understanding Overdraw
  4. Android性能优化典范之Understanding VSYNC
  5. Android性能优化典范之Profile GPU Rendering

“If you can measure it, you can optimize it” is a common term in the computing world, and for Android’s rendering system, the same thing holds true. In order to optimize your pipeline to be more efficient for rendering, you need a tool to give you feedback on where the current perf problems lie.

And in this video, +Colt McAnlis walks you through an on-device tool that’s built for this exact reason. “Profile GPU Rendering” will help you understand the stages of the rendering pipeline, and also get a chance to see what portions of it might be taking too long, and what you can do about it for your application.

GPU Profile工具

渲染性能问题往往是偷取你宝贵帧数的罪魁祸首,这种问题很容易产生,很容易出现,而且在一个非常方便的工具的帮助下,也非常容易去追踪. 使用Peofile GPU Rendering tool,你可以在手机上就可以看到究竟是什么导致你的应用程序出现卡顿,变慢的情况.

这个工具在设置-开发者选项-Profile GPU rendering选项,打开后选择on screen as bars:

Profile GPU rendering

然后手机屏幕上就会出现三个颜色组成的小柱状图,以及一条绿线:

gpu工具

这个工具会在屏幕上显示经过分析后的图形数据,最底部的图显示的是Navigation的相关信息,最上面显示的是Notification的相关信息,中间的图显示的是当前应用程序的图.

使用GPU Profile工具

当你的应用程序在运行时,你会看到一排柱状图在屏幕上,从左到右动态地显示,每一个垂直的柱状图代表一帧的渲染,越长的垂直柱状图表示这一帧需要渲染的时间越长.随着需要渲染的帧数越来越多,他们会堆积在一起,这样你就可以观察到这段时间帧率的变化.

绿线

下图中的绿线代表16ms,要确保一秒内打到60fps,你需要确保这些帧的每一条线都在绿色的16ms标记线之下.任何时候你看到一个竖线超过了绿色的标记现,你就会看到你的动画有卡顿现象产生.

绿线

柱状图

每一条柱状图都由三种颜色组成: 蓝-红-黄. 这些线直接和Android的渲染流水线和他实际运行帧数的时间关联:

柱状图

  • 蓝色代表测量绘制的时间,或者说它代表需要多长时间去创建和更新你的DisplayList.在Android中,一个视图在可以实际的进行渲染之前,它必须被转换成GPU所熟悉的格式,简单来说就是几条绘图命令,复杂点的可能是你的自定义的View嵌入了自定义的Path. 一旦完成,结果会作为一个DisplayList对象被系统送入缓存,蓝色就是记录了需要花费多长时间在屏幕上更新视图(说白了就是执行每一个View的onDraw方法,创建或者更新每一个View的Display List对象).

    Draw Phase

    当你看到蓝色的线很高的时候,有可能是因为你的一堆视图突然变得无效了(即需要重新绘制),或者你的几个自定义视图的onDraw函数过于复杂.

    自定义视图

  • 红色代表执行的时间,这部分是Android进行2D渲染 Display List的时间,为了绘制到屏幕上,Android需要使用OpenGl ES的API接口来绘制Display List.这些API有效地将数据发送到GPU,最总在屏幕上显示出来.

    红色

    记住绘制下图这样自定义的比较复杂的视图时,需要用到的OpenGl的绘制命令也会更复杂

    自定义的复杂View

    当你看到红色的线非常高的时候,这些复杂的自定义View就是罪魁祸首:

    Paste_Image.png

    值得一提的是,上面图中红色线较高的一种可能性是因为重新提交了视图而导致的.这些视图并不是失效的视图,但是有些时候发生了某些事,例如视图旋转,我们需要重新清理这个区域的视图,这样可能会影响这个视图下面的视图,因为这些视图都需要进行重新的绘制操作.

  • 橙色部分表示的是处理时间,或者说是CPU告诉GPU渲染一帧的地方,这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复,如果柱状图很高,那就意味着你给GPU太多的工作,太多的负责视图需要OpenGL命令去绘制和处理.

保持动画流畅的关键就在于让这些垂直的柱状条尽可能地保持在绿线下面,任何时候超过绿线,你就有可能丢失一帧的内容.

总结

GPU Profile工具能够很好地帮助你找到渲染相关的问题,但是要修复这些问题就不是那么简单了. 你需要结合代码来具体分析,找到性能的瓶颈,并进行优化.

有时候你可以以这个为工具,让负责设计这个产品的人修改他的设计,以获得良好的用户体验.

Perf Matters

keep calm, profile your code, and always remember, Perf Matters

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 性能优化典范 - Understanding VSYNC

作者 Gracker
2015年4月19日 15:47

系列文章目录:

  1. Android性能优化典范综述
  2. Android性能优化典范之Render Performance
  3. Android性能优化典范之Understanding Overdraw
  4. Android性能优化典范之Understanding VSYNC
  5. Android性能优化典范之Profile GPU Rendering

Unbeknown to most developers, there’s a simple hardware design that defines everything about how fast your application can draw things to the screen.

You may have heard the term VSYNC - VSYNC stands for vertical synchronization and it’s an event that happens every time your screen starts to refresh the content it wants to show you.

Effectively, VSYNC is the product of two components Refresh Rate (how fast the hardware can refresh the screen), and Frames Per Second (how fast the GPU can draw images), and in this video +Colt McAnlis walks through each of these topics, and discusses where VSYNC (and the 16ms rendering barrier) comes from, and why it’s critical to understand if you want a silky smooth application.

基本概念

想要开发一个高性能的应用程序,首先你得了解他的硬件工作原理,那么最好的办法就是去使用它,应用程序运行速度的快慢,很容易被人误解为硬件进程的控制问题,然而这最主要的根源在于渲染性能.如果你想要提高你应用程序的渲染性能,你就必须知道什么是VSYNC.

在了解VSYNC之前,我们需要了解两个概念:

刷新率

刷新率代表屏幕在一秒内刷新屏幕的次数,这个值用赫兹来表示,取决于硬件的固定参数. 这个值一般是60Hz,即每16.66ms刷新一次屏幕.

刷新率

帧速率

帧速率代表了GPU在一秒内绘制操作的帧数,比如30fps/60fps.在这种情况下,高点的帧速率总是好的.

帧速率

工作原理

刷新率和帧速率需要协同工作,才能让你的应用程序的内容显示到屏幕上,GPU会获取图像数据进行绘制,然后硬件负责把内容呈现到屏幕上,这将在你的应用程序的生命周期中周而复始地发生.下面的图中每一个竖行代表了一帧的绘制和呈现工作.

协同工作

不幸的是,刷新率和帧速率并不是总能够保持相同的节奏:

  • 如果帧速率实际上比刷新率快,那么就会出现一些视觉上的问题,下面的图中可以看到,当帧速率在100fps而刷新率只有75Hz的时候,GPU所渲染的图像并非全都被显示出来.

    帧速率比刷新率快的情况

    举个例子, 你拍了一张照片,然后旋转5度再拍一张照片, 将两种图片的中间剪开并拼接在一起:

    拍两张照片

    剪贴在一起
    这两张图有相似之处,但是上面和下面部分有明显的区别,这就叫Tearing(撕裂),是刷新率和帧速率不一致的结果.

    上面的原因是因为,当你的显卡正在使用,一个内存区正在写入帧数据(用来显示一帧的一个Buffer),从顶部开始, 新的一帧覆盖前一帧,并立刻输出一行内容. 现在,当屏幕开始刷新时,实际上并不知道缓冲区是什么状态(即不知道缓冲区中的一帧是否绘制完毕,即存在只绘制了一半的情况,另一半还是之前的那帧),因此它从GPU中抓住的帧肯可能并不是完全完整的.

    图像撕裂

    目前Android的双缓冲(或者三缓冲/四缓冲), 这是非常有效的,当GPU将一帧写入一个被成为后缓冲的存储器, 而存储器中的次级区域被称为帧缓冲,当写入下一帧时,它会开始填充后缓冲,而帧缓冲保持不变,现在我们刷新屏幕,它将使用帧缓冲(事先已经绘制好),而不是正在处于绘制状态的后缓冲, 这就是VSYNC的作用.如果在屏幕刷新中,VSYNC,即垂直同步,将会在让从后缓冲到帧缓冲的拷贝过程保持同样的复制操作:

    Vsync

    GPU的频率比屏幕刷新率高是正常的,因为你的GPU刷新会比屏幕刷新快,在这种情况下,当屏幕刷新成功,你的GPU将会等待VSYNC信号,直到下一个VSYNC信号到来时(即屏幕刷新时),这时你的帧速率就可以达到设备的刷新率上限. 当然这只是理想情况, 当fps达到60的时候,GPU需要在16.66ms内准备好一帧,这对应用程序的要求是非常高的.更不用说100fps了…

  • 屏幕刷新率比帧速率快的情况
    如果屏幕刷新率比帧速率快,屏幕会在两帧中显示同一个画面,当这种断断续续的情况发生时,你就遇到麻烦了.比如你的帧速率比屏幕刷新率高的时候,用户看到的是非常流畅的画面, 但是帧速率降下来的时候(GPU绘制太多东西的时候),用户将会很明显地察觉到动画卡住了或者掉帧,然后又恢复了流畅.这通常会被描述为闪屏, 跳帧,延迟.

    屏幕刷新率比帧速率快

    你的应用程序应该避免这些帧率突降的情况.以确保GPU迅速获取数据,并在屏幕再次刷新之前写录内容.

Perf Matters

keep calm, profile your code, and always remember, Perf Matters

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 性能优化典范之 Understanding Overdraw

作者 Gracker
2015年4月19日 15:38

系列文章目录:

  1. Android性能优化典范综述
  2. Android性能优化典范之Render Performance
  3. Android性能优化典范之Understanding Overdraw
  4. Android性能优化典范之Understanding VSYNC
  5. Android性能优化典范之Profile GPU Rendering

One of the most problematic performance problems on Android is the easiest to create; thankfully, it’s also easy to fix.

OVERDRAW is a term used to describe how many times a pixel has been re-drawn in a single frame of rendering. It’s a troublesome issue, because in most cases, pixels that are overdrawn do not end up contributing to the final rendered image. As such, it amounts to wasted work for your GPU and CPU.

Fixing overdraw has everything to do with using the available on-device tools, like Show GPU Overdraw, and then adjusting your view hierarchy in order to reduce areas where it may be occurring.

OverDraw概念

视频开头作者举了一个例子,说如果你是一个粉刷匠,你应该会知道,给墙壁粉刷是一件工作量非常大的工作,而且如果你需要重新粉刷一遍的话(比如对颜色不满意),那么第一次的粉刷就白干了. 同样的道理,如果你的应用程序中出现了过度绘制问题,那么你之前所做的事情也就白费了.如果你想兼顾高性能和完美的设计,那么你的程序可能会出现一个性能问题:OverDraw!

OverDraw是一个术语, 它表示某些组件在屏幕上的一个像素点的绘制超过1次.如下面的图所示,我们有一堆重叠的卡片,被用户激活的卡片在最上面,而那些没有激活的卡片在下面,这意味着我们画大力气绘制的那些卡片,基本都是不可见的.问题就在于次,我们像素渲染的并不全是用户最后能看打的部分, 这是在浪费GPU的时间!

OverDraw

目前流行的一些布局是一把双刃剑,带给我们漂亮的画面的同时,也带来了很大的麻烦.为了最大限度地提高应用程序的性能,你得减少OverDraw!

性能和界面

追踪OverDraw

Android手机中提供了查看OverDraw情况的工具,在设置-开发者选项中,找到打开”Show GPU OverDraw”按钮即可:

Show GPU OverDraw

Android会使用不同深浅的颜色来表示OverDraw的程序,没有OverDraw的时候, 你看到的是它本来的颜色,其他颜色表示不同的过度绘制程序:

  • 蓝色: 1倍过度绘制,即一个像素点绘制了2次
  • 绿色:2倍过度绘制,即一个像素点绘制了3次
  • 浅红色:3倍过度绘制,即一个像素点绘制了4次
  • 深红色:4倍过度绘制及以上,即一个像素点绘制了5次及以上

OverDraw

你的应用程序的目标应该是尽可能地减少过度绘制,即更多的蓝色色块而不是红色色块:

Good and Bad

OverDraw的根源

虽然OverDraw很大程序上来自于你的视图互相重叠的问题,但是各位开发者更需要注意的是不必要的背景重叠.

Bad

比如在一个应用程序中,你的所有的View都有背景的话,就会看起来像第一张图中那样,而在去除这些不必要的背景之后(指的是Window的默认背景,Layout的背景,文字以及图片的可能存在的背景),效果就像第二张图那样,基本没有过度绘制的情况.

去掉不必要的背景

比如去除Window的默认背景:

1
this.getWindow().setBackgroundDrawableResource(android.R.color.transparent);

Perf Matters

keep calm, profile your code, and always remember, Perf Matters

附录

关于过度绘制及其优化,我博客有两篇文章专门介绍:

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 性能优化典范 - Render Performance

作者 Gracker
2015年4月19日 15:26

系列文章目录:

  1. Android性能优化典范综述
  2. Android性能优化典范之Render Performance
  3. Android性能优化典范之Understanding Overdraw
  4. Android性能优化典范之Understanding VSYNC
  5. Android性能优化典范之Profile GPU Rendering

Rendering performance is all about how fast you can draw your activity, and get it updated on the screen. Success here means your users feeling like your application is smooth and responsive, which means that you’ve got to get all your logic completed, and all your rendering done in 16ms or less, each and every frame. But that might be a bit more difficult than you think.

In this video, +Colt McAnlis takes a look at what “rendering performance” means to developers, alongside some of the most common pitfalls that are ran into; and let’s not forget the important stuff: the tools that help you track down, and fix these issues before they become large problems.

Android渲染知识

当你觉得自己开发了一个改变世界的应用的时候,你的用户可能并不会这么认为,他们认为你的应用又慢又卡,达不到他们所期望的那种顺滑,更谈不上改变这该死的世界了,回收站走你!等等!明明我这个应用在我的Nexus5上非常顺滑啊?你咋能说又慢又卡呢?如果你对Android的碎片化有一定了解的话,你就应该知道,很多低配置的手机并不像Nexus5那样有强大的处理器和GPU,以及没有被怎么污染的原生系统。

如果有大量的用户投诉说你的应用又卡又慢的时候,不要总是抱怨用户的低端手机,有时候问题就出在你的应用本身,也就意味着你的Android存在比较严重的渲染性能问题。只有真正了解问题发生的根源,才能有效的解决问题。所以了解Android渲染相关的知识,是一个Android开发者必不可少的知识。

设计与性能

渲染问题是你建立一个应用程序是最经常碰到的问题,一方面,设计师希望展现给用户一个超自然的体验,另一方面,这些华丽的动画和视图并不能在所有的Android手机上都流畅地运行。所以这就是问题所在。

Design vs Performance

绘制原理

Android系统每16ms都会重新绘制一次你的Activity,也就是说,你的逻辑控制画面更新要保证最多16ms一帧才能每秒达到60帧(至于为什么是60帧,这个后面会有一个专题来讲解这个)。如下图,每一帧都在16ms内绘制完成,此时的世界是顺滑的。

Draw Good

但是如果你的应用程序没有在16ms内完成这一帧的绘制,假设你花费了24ms来绘制这一帧,那么就会出现我们称之为掉帧的情况,世界变得有延迟了。如下图:

Draw Bad

系统准备将新的一帧绘制到屏幕上,但是这一帧并没有准备好,所有就不会有绘制操作,画面也就不会刷新。反馈到用户身上,就是用户盯着同一张图看了32ms而不是16ms,也就是说掉帧发生了。

掉帧

掉帧是用户体验中一个非常核心的问题,用户将很容易察觉到由于掉帧而产生的卡顿感,如果此时用户正在与系统进行交互,比如滑动列表,或者正在打字,那么卡顿感是非常明显的。用户会马上对你的应用进行吐槽,下一步工作肯定是回收站走你!所以弄清楚掉帧的原因是非常重要的。

不过蛋疼的是,引起掉帧发生的原因非常多,比如:

  • 你花了太多的时间重新绘制你视图中的大部分东西,这样非常浪费CPU周期
    Too Much View
  • 你有太多的对象堆叠到了一起,你在绘制用户看不到的对象上花费了太多的时间
    Draw Hidden View
  • 你有一大堆的动画重复了一遍又一遍,导致CPU和GPU组件的大量浪费
    Too Much Animations

检测和解决

检测和解决这些问题很大程度上依赖于你的应用程序架构,但是幸运的是,我们有很多开发者工具来协助我们发现和解决这些问题,有些工具甚至能追踪到具体出错的代码行数或者UI控件,这些工具包括但不限于:

  • Hierarchy View

Hierarchy View
你可以使用Hierarchy View 来查看你的View是否过于复杂,如果是,那么说明你有很多时间没有利用。并且浪费了许多时间进行重绘。
Hierarchy View 位于Android Device Monitor 中,Android Device Monitor在Eclipse和Android Studio中都有有对应的入口,依次选则Window-Open Perspective-Hierarchy View即可打开Hierarchy View视图。 Hierarchy View视图虽然比较简单,但是非常有效。花费一点了解这个工具每一个细节,对于以后排查问题来说都是事半功倍。关于Hierarchy View视图的用法,会有更详细的单独的教程来讲解。

  • On-Device Tools – Profile GPU Rendering 、Show GPU Overdraw、GPU View Updates

On-Device Tools

这三个选项在设置-辅助功能- 开发者选项中,默认都是关闭的。Profile GPU Rendering 和 GPU Overdraw比较重要,所以系列视频后面会有专门的专题会讲解,这里简单介绍一下GPU View Updates。GPU View Updates的作用是使用GPU进行绘图时闪烁显示窗口中的视图。随着android版本的更新,越来越多的绘制操作能使用GPU来完成,详见http://developer.android.com/guide/topics/graphics/hardware-accel.html,而这个工具打开之后,使用GPU绘制的区域会用红色来标注,而没有红色标注的区域,则是使用CPU绘制的。这个选项也可以用来查看redraw的区域大小。

  • TraceView

TraceView

TraceView是一个很棒的检查是否掉帧的工具,视频中没有对此工具进行介绍,但是这个工具非常的重要,他可以找到你代码中花费时间的地方,精确到每一个函数,不论这个函数是你应用程序中的还是系统函数。另外在Android Studio中,TraceView得到了改进,其视图能非常直观的显示出每一帧所消耗的时间,函数像倒金字塔一般展现在面前,我们可以很容易地看出掉帧的地方以及那一帧里面所有的函数调用情况。鉴于此工具非常实用,所有会有更详细的单独的教程来讲解。

Perf Matters

keep calm, profile your code, and always remember, Perf Matters

总结

这是这个系列视频的第一个视频,从内容上来看,是从一个大的角度来看Render Performance,简单地讲述了一下Render Performance基本的概念,出现的原因以及排查的工具。在发现问题–定位问题–解决问题的流程上属于发现问题–定位问题,解决问题则基本没有提到。这也基本符合这一系列视频的基调:即着重于发现问题(使用工具发现问题、挖掘问题出现的原理和原因)和定位问题(使用工具定位),如何解决问题则需要自己通过实战去进行锻炼,毕竟这种问题并没有一个通用的解决方法,每个应用都有每个应用自己的问题。

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 性能优化典范综述

作者 Gracker
2015年4月19日 15:20

系列文章目录:

  1. Android性能优化典范综述
  2. Android性能优化典范之Render Performance
  3. Android性能优化典范之Understanding Overdraw
  4. Android性能优化典范之Understanding VSYNC
  5. Android性能优化典范之Profile GPU Rendering

2015年1月6日,Google官方发布了一系列关于Android性能优化的小视频,将其命名为Android Performance Patterns,这一些列视频放在YouTube上,观看的话需要科学地上网。

Android性能优化典范

官方简介:

Android Performance Patterns is a collection of videos focused entirely on helping developers write faster, more performant Android Applications. On one side, it’s about peeling back the layers of the Android System, and exposing how things are working under the hood. On the other side, it’s about teaching you how the tools work, and what to look for in order to extract the right perf out of your app.
But at the end of the day, Android Performance Patterns is all giving you the right resources, at the right time to help make the fastest, smoothest, most awesome experience for your users. And that’s the whole point, right?

总之就是一系列讲解Android性能相关的视频。这些小视频的时间非常短,在3-5分钟之内,主讲人的英文语速也非常快,初期这些视频没有翻译的时候,着实考验了一把听力。好消息是现在这些视频已经都有中英文字幕了。

这些视频的时间虽然很短,但是信息量却非常大,有些他一句话带过的内容,我们却需要花费很多的时间去研究他的原理,或者研究一个调试工具如何使用。也就是说,这一系列视频并没有真正教你如何去优化你的应用,而是告诉你关于Android性能优化你需要知道的知识,这样你去优化你的Android应用的时候,知道该用什么工具,该采取什么样的步骤,需要达到什么样的目标。

由于我最近也在研究Android性能优化相关的课题,这个视频第一时间出来的时候我就看了好几遍,所以一早就有将这一系列视频翻译成中文的想法。后来看了几遍之后,我发现仅仅翻译成中文其实意义不大,他所讲述的每一个知识点,都是可以写成一篇博文甚至好几篇博文的,所以就有了这一系列文章的出现。

每一篇文章中,我都会先将视频中涉及到的知识点列出来,然后一一进行讲解。有些调试工具由于篇幅原因,可能会写到单独的博文中。

另外催生我写这一系列文章的是胡凯,他的博客 http://hukai.me/android-performance-patterns 第一时间就将这一些列视频的内容翻译成了中文,优美的排版加上过硬的翻译,让这篇博文被广泛传播,备受好评。同时他也是github上 android-training-course-in-chinese 项目的发起人,他的Github主页:https://github.com/kesenhoo。他对于分享的热情我非常敬佩。如果你并非是Android应用开发者或者对技术细节不感兴趣的话,直接看他的那篇Android性能优化典范即可,看完之后你会对这一些列视频有一个大概的认识。

下面是关于Android性能优化典范这一些列视频的一些资源信息:

OK,Let us start!!!

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 内存优化之三 - 打开 MAT 中的 Bitmap 原图

作者 Gracker
2015年4月11日 18:41

本文是 MAT 工具使用系列的第二篇,这个系列共三篇,详细介绍了如何使用 MAT 来分析内存问题,既可以是 Java 应用的内存问题,也可以是 Android 应用的内存问题:

  1. Android 内存优化之一:MAT 使用入门
  2. Android内存优化之二:MAT使用进阶
  3. Android内存优化之三:打开MAT中的Bitmap原图

在使用MAT查看应用程序内存使用情况的时候,我们经常会碰到Bitmap对象以及BitmapDrawable$BitmapState对象,而且在内存使用上,Bitmap所占用的内存占大多数.在这样的情况下, Bitmap所造成的内存泄露尤其严重, 需要及时发现并且及时处理.在这样的需求下, 当我们在MAT中发现和图片相关的内存泄露的时候, 如果能知道是那一张图片,对分析问题会有很大的帮助.

本文就介绍如何将MAT中的Bitmap数组对象还原成一张图片。

Bitmap对象如图:

Bitmap对象

导出Bitmap原始数据

在MAT中打开Dominator Tree视图 , 选择一个Bitmap对象 , 查看此时右边的Inspector窗口,内容如下图:

image

这个视图中,可以看到这个Bitmap的一些基本的信息: mBuffer, mHeight, mWidth , mNativeBitmap等, 宽和高的值我们一会需要用的到 .

mBuffer的定义在Bitmap.java中:

1
2
3
4
5
6
7
8
9
/**
* Backing buffer for the Bitmap.
* Made public for quick access from drawing methods -- do NOT modify
* from outside this class
*
* @hide
*/
@SuppressWarnings("UnusedDeclaration") // native code only
public byte[] mBuffer;

其值是保存在byte数组中的, 我们需要的就是这个byte数组中的内容. 在Inspector窗口的mBuffer这一栏或者Dominator Tree视图的Bitmap这一栏点开下一级,都可以看到这个byte数组的内容. 鼠标右键选择Copy –>Save Value To File. 弹出如下对话框:

image

选择存储路径和文件名,这里需要注意的是,文件名一定要以 .data为后缀,否则无法正常使用,切记.

打开原始资源数据

Linux

这时需要借助Linux上强大的图片应用:GIMP,没安装的可以去安装一下. 安装好之后, 打开GIMP,选择文件-打开.选择我们上一步导出的.data文件(比如image.data),然后会出现如下图的属性框:

image

图像类型这一栏选择RGB Alpha, 宽度和高度必填, 其值可以在MAT中查看到,第一步的时候有说到这个值的位置, 其他的选择默认即可,然后点击打开. GIMP就会把这个文件打开.

Mac && Windows

Mac和Windows可以选择使用PhotoShop作为打开的工具, 和Linux唯一不同的地方在于. 保存的文件的格式需要以.raw结尾 (比如image.raw),选择深度为32位. 其余的和Linux相同.

另外GIMP也有Mac、Windows版本,建议大家在各个平台都使用GIMP,这样学习成本比较低,而且GIMP为免费软件,使用起来功能也非常多。

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 内存优化之二 - MAT使用进阶

作者 Gracker
2015年4月11日 18:26

本文是 MAT 工具使用系列的第二篇,这个系列共三篇,详细介绍了如何使用 MAT 来分析内存问题,既可以是 Java 应用的内存问题,也可以是 Android 应用的内存问题:

  1. Android 内存优化之一:MAT 使用入门
  2. Android内存优化之二:MAT使用进阶
  3. Android内存优化之三:打开MAT中的Bitmap原图

Java的内存泄露的特点

  • Java中的内存泄露主要特征:可达,无用
  • 无用指的是创建了但是不再使用之后没有释放
  • 能重用但是却创建了新的对象进行处理

MAT使用技巧进阶

使用Android Studio Dump内存文件

Android Studio的最新版本可以直接获取hprof文件:

Android-Studio

然后选择文件,点击右键转换成标准的hprof文件,就可以在MAT中打开了。

在使用使用Eclipse或者AndroidStudio抓内存之前,一定要手动点击 Initiate GC按钮手动触发GC,这样抓到的内存使用情况就是不包括Unreachable对象的。

手动触发GC

Unreachable对象

Unreachable指的是可以被垃圾回收器回收的对象,但是由于没有GC发生,所以没有释放,这时抓的内存使用中的Unreachable就是这些对象。

Unreachable Objects

Unreachable Objects Histogram

点击Calculate Retained Size之后,会出现Retained Size这一列

Calculate Retained Size

Unreachable Objects Histogram

可以看到Unreachable Object的对象其Retained Heap值都为0.这也是正常的。

Histogram

MAT中Histogram的主要作用是查看一个instance的数量,一般用来查看自己创建的类的实例的个数。

  • 可以很容易的找出占用内存最多的几个对象,根据Percentage(百分比)来排序。
  • 可以分不同维度来查看对象的Dominator Tree视图,Group by class、Group by class loader、Group by package
    和Histogram类似,时间久了,通过多次对比也可以把溢出对象找出来。
  • Dominator Tree和Histogram的区别是站的角度不一样,Histogram是站在类的角度上去看,Dominator Tree是站的对象实例的角度上看,Dominator Tree可以更方便的看出其引用关系。

Histogram group by package

通过查看Object的个数,结合代码就可以找出存在内存泄露的类(即可达但是无用的对象,或者是可以重用但是重新创建的对象

Histogram中还可以对对象进行Group,更方便查看自己Package中的对象信息。

Paste_Image.png

Thread信息

MAT中可以查看当前的Thread信息:

Thread

从图中可以得到的信息:

  1. 可以看到可能有内存问题的Thread:

内存异常

  1. 可以看到数量可能有问题的Thread

数量异常

帮助信息

MAT中的各个视图中,在每一个Item中点击右键会出现很多选项,很多时候我们需要依赖这些选项来进行分析:

右键选项

这些选项的具体含义则可以通过右键中的Search Queries这个选项(上图中的倒数第四个选项)进行搜索和查看,非常的有用。

帮助信息

可以看到,所有的命令其实就是配置不同的SQL查询语句。

比如我们最常用的:

  • List objects -> with incoming references:查看这个对象持有的外部对象引用
  • List objects -> with outcoming references:查看这个对象被哪些外部对象引用
  • Path To GC Roots -> exclude all phantim/weak/soft etc. references:查看这个对象的GC Root,不包含虚、弱引用、软引用,剩下的就是强引用。从GC上说,除了强引用外,其他的引用在JVM需要的情况下是都可以 被GC掉的,如果一个对象始终无法被GC,就是因为强引用的存在,从而导致在GC的过程中一直得不到回收,因此就内存溢出了。
  • Path To GC Roots -> exclude weak/soft references:查看这个对象的GC Root,不含弱引用和软引用所有的引用.
  • **Merge Shortest path to GC root **:找到从GC根节点到一个对象或一组对象的共同路径

Debug Bitmap

如果经常使用MAT分析内存,就会发现Bitmap所占用的内存是非常大的,这个和其实际显示面积是有关系的。在2K屏幕上,一张Bitmap能达到20MB的大小。

所以要是MAT提供了一种方法,可以将存储Bitmap的byte数组导出来,使用第三方工具打开。这个大大提高了我们分析内存泄露的效率。

关于这个方法的操作流程,可以参考这篇文章还原MAT中的Bitmap图像.
I

Debug ArrayList

ArrayList是使用非常常用的一个数据结构,在MAT中,如果想知道ArrayList中有哪些数据,需要右键-> List Objects -> With outgoing references,然后可以看到下面的图:

Outgoing

从上图可以看到,这个ArrayList的内容在一个array数组中,即暴漏了ArrayList的内部结构,查看的时候有点不方便,所以MAT提供了另外一种查看ArrayList内数据的方式:

Extrace List Values

其结果非常直观:

Extrace List Values Result

Big Drops In Dominator Tree

Big Drops In Dominator Tree选项在右键->

Displays memory accumulation points in the dominator tree. Displayed are objects with a big difference between the retained size of the parent and the children and the first “interesting” dominator of the accumulation point. These are places where the memory of many small objects is accumulated under one object.

Big Drops In Dominator Tree

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 内存优化(1) - MAT 使用入门

作者 Gracker
2015年4月11日 18:10

本文是 MAT 工具使用系列的第一篇,这个系列共三篇,详细介绍了如何使用 MAT 来分析内存问题,既可以是 Java 应用的内存问题,也可以是 Android 应用的内存问题:

  1. Android 内存优化(1) - MAT 使用入门
  2. Android 内存优化(2) - MAT使用进阶
  3. Android 内存优化(3) - 打开MAT中的Bitmap原图

MAT简介

MAT介绍

MAT(Memory Analyzer Tool),一个基于 Eclipse 的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。

image

当然MAT也有独立的不依赖Eclipse的版本,只不过这个版本在调试Android内存的时候,需要将DDMS生成的文件进行转换,才可以在独立版本的MAT上打开。不过Android SDK中已经提供了这个Tools,所以使用起来也是很方便的。

MAT工具的下载安装

这里是MAT的下载地址:[https://eclipse.org/mat/downloads.php](https://eclipse.org/mat/ downloads.php),下载时会提供三种选择的方式:

image

  • Update Site 这种方式后面会有一个网址:比如http://download.eclipse.org/mat/1.4/update-site/ ,安装过Eclipse插件的同学应该知道,只要把这段网址复制到对应的Eclipse的Install New Software那里,就可以进行在线下载了。

image

  • Archived Update Site 这种方式安装的位置和上一种差不多,只不过第一种是在线下载,这一种是使用离线包进行更新,这种方式劣势是当这个插件更新后,需要重新下载离线包,而第一种方式则可以在线下载更新。
  • Stand-alone Eclipse RCP Applications 这种方式就是把MAT当成一个独立的工具使用,不再依附于Eclipse,适合不使用Eclipse而使用Android Studio的同学。这种方式有个麻烦的地方就是DDMS导出的文件,需要进行转换才可以在MAT中打开。

下载安装好之后,就可以使用MAT进行实际的操作了。

Android(Java)中常见的容易引起内存泄露的不良代码

Android内存

使用MAT工具之前,要对Android的内存分配方式有基本的了解,对容易引起内存泄露的代码也要保持敏感,在代码级别对内存泄露的排查,有助于内存的使用。

Android主要应用在嵌入式设备当中,而嵌入式设备由于一些众所周知的条件限制,通常都不会有很高的配置,特别是内存是比较有限的。如果我们编写的代码当中有太多的对内存使用不当的地方,难免会使得我们的设备运行缓慢,甚至是死机。为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。一方面,如果程序在运行过程中出现了内存泄漏的问题,仅仅会使得自己的进程被kill掉,而不会影响其他进程(如果是system_process等系统进程出问题的话,则会引起系统重启)。另一方面Android为不同类型的进程分配了不同的内存使用上限,如果应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉。

常见的内存使用不当的情况

查询数据库没有关闭游标

描述:
程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
示例代码:

1
2
3
4
Cursor cursor = getContentResolver().query(uri ...);
if (cursor.moveToNext()) {
... ...
}

修正示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Cursor cursor = null;
try {
cursor = getContentResolver().query(uri ...);
if (cursor != null && cursor.moveToNext()) {
... ...
}
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
//ignore this
}
}
}

`

构造Adapter时,没有使用缓存的 convertView

描述:以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法:

1
public View getView(int position, View convertView, ViewGroup parent)

来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参 View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。
由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。ListView回收list item的view对象的过程可以查看:android.widget.AbsListView.java –> void addScrapView(View scrap) 方法。

示例代码:

1
2
3
4
5
public View getView(int position, View convertView, ViewGroup parent) {
View view = new Xxx(...);
... ...
return view;
}

`
示例修正代码:

1
2
3
4
5
6
7
8
9
10
11
12
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
if (convertView != null) {
view = convertView;
populate(view, getItem(position));
...
} else {
view = new Xxx(...);
...
}
return view;
}

关于ListView的使用和优化,可以参考这两篇文章:

Bitmap对象不在使用时调用recycle()释放内存

描述:有时我们会手工的操作Bitmap对象,如果一个Bitmap对象比较占内存,当它不在被使用的时候,可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存。
另外在最新版本的Android开发时,使用下面的方法也可以释放此Bitmap所占用的内存

1
2
3
4
5
Bitmap bitmap ;
...
bitmap初始化以及使用
...
bitmap = null;

释放对象的引用

描述:这种情况描述起来比较麻烦,举两个例子进行说明。

示例A:
假设有如下操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DemoActivity extends Activity {
... ...
private Handler mHandler = ...
private Object obj;
public void operation() {
obj = initObj();
...
[Mark]
mHandler.post(new Runnable() {
public void run() {
useObj(obj);
}
});
}
}

我们有一个成员变量 obj,在operation()中我们希望能够将处理obj实例的操作post到某个线程的MessageQueue中。在以上的代码中,即便是mHandler所在的线程使用完了obj所引用的对象,但这个对象仍然不会被垃圾回收掉,因为DemoActivity.obj还保有这个对象的引用。所以如果在DemoActivity中不再使用这个对象了,可以在[Mark]的位置释放对象的引用,而代码可以修改为:

1
2
3
4
5
6
7
8
9
10
11
public void operation() {
obj = initObj();
...
final Object o = obj;
obj = null;
mHandler.post(new Runnable() {
public void run() {
useObj(o);
}
}
}

示例B:
假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。

但是如果在释放LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_process进程挂掉。

总之当一个生命周期较短的对象A,被一个生命周期较长的对象B保有其引用的情况下,在A的生命周期结束时,要在B中清除掉对A的引用。

其他

Android应用程序中最典型的需要注意释放资源的情况是在Activity的生命周期中,在onPause()、onStop()、onDestroy()方法中需要适当的释放资源的情况。由于此情况很基础,在此不详细说明,具体可以查看官方文档对Activity生命周期的介绍,以明确何时应该释放哪些资源。

另外一些其他的例子,将会在补充版本加入。

使用MAT进行内存调试

获取HPROF文件

HPROF文件是MAT能识别的文件,HPROF文件存储的是特定时间点,java进程的内存快照。有不同的格式来存储这些数据,总的来说包含了快照被触发时java对象和类在heap中的情况。由于快照只是一瞬间的事情,所以heap dump中无法包含一个对象在何时、何地(哪个方法中)被分配这样的信息。
这个文件可以使用DDMS导出:

  1. DDMS中在Devices上面有一排按钮,选择一个进程后(即在Devices下面列出的列表中选择你要调试的应用程序的包名),点击Dump HPROF file 按钮:

image

选择存储路径保存后就可以得到对应进程的HPROF文件。eclipse插件可以把上面的工作一键完成。只需要点击Dump HPROF file图标,然后MAT插件就会自动转换格式,并且在eclipse中打开分析结果。eclipse中还专门有个Memory Analysis视图

  1. 得到对应的文件后,如果安装了Eclipse插件,那么切换到Memory Analyzer视图。使用独立安装的,要使用Android SDK自带的的工具(hprof-conv 位置在sdk/platform-tools/hprof-conv)进行转换
1
hprof-conv xxx.xxx.xxx.hprof xxx.xxx.xxx.hprof

转换过后的.hprof文件即可使用MAT工具打开了。

MAT主界面介绍

这里介绍的不是MAT这个工具的主界面,而是导入一个文件之后,显示OverView的界面。

  • 打开经过转换的hprof文件:
    image

如果选择了第一个,则会生成一个报告。这个无大碍。

image

  • 选择OverView界面:

    Image

我们需要关注的是下面的Actions区域

  • Histogram:列出内存中的对象,对象的个数以及大小

    image

  • Dominator Tree:列出最大的对象以及其依赖存活的Object (大小是以Retained Heap为标准排序的)

    image

  • Top Consumers : 通过图形列出最大的object

    image

  • Duplicate Class:通过MAT自动分析泄漏的原因

一般Histogram和 Dominator Tree是最常用的。

MAT中一些概念介绍

要看懂MAT的列表信息,Shallow heap、Retained Heap、GC Root这几个概念一定要弄懂。

3.3.1 Shallow heap

Shallow size就是对象本身占用内存的大小,不包含其引用的对象。

  • 常规对象(非数组)的Shallow size有其成员变量的数量和类型决定。
  • 数组的shallow size有数组元素的类型(对象类型、基本类型)和数组长度决定

因为不像c++的对象本身可以存放大量内存,java的对象成员都是些引用。真正的内存都在堆上,看起来是一堆原生的byte[], char[], int[],所以我们如果只看对象本身的内存,那么数量都很小。所以我们看到Histogram图是以Shallow size进行排序的,排在第一位第二位的是byte,char 。

3.3.2 Retained Heap

Retained Heap的概念,它表示如果一个对象被释放掉,那会因为该对象的释放而减少引用进而被释放的所有的对象(包括被递归释放的)所占用的heap大小。于是,如果一个对象的某个成员new了一大块int数组,那这个int数组也可以计算到这个对象中。相对于shallow heap,Retained heap可以更精确的反映一个对象实际占用的大小(因为如果该对象释放,retained heap都可以被释放)。

这里要说一下的是,Retained Heap并不总是那么有效。例如我在A里new了一块内存,赋值给A的一个成员变量。此时我让B也指向这块内存。此时,因为A和B都引用到这块内存,所以A释放时,该内存不会被释放。所以这块内存不会被计算到A或者B的Retained Heap中。为了纠正这点,MAT中的Leading Object(例如A或者B)不一定只是一个对象,也可以是多个对象。此时,(A, B)这个组合的Retained Set就包含那块大内存了。对应到MAT的UI中,在Histogram中,可以选择Group By class, superclass or package来选择这个组。

为了计算Retained Memory,MAT引入了Dominator Tree。加入对象A引用B和C,B和C又都引用到D(一个菱形)。此时要计算Retained Memory,A的包括A本身和B,C,D。B和C因为共同引用D,所以他俩的Retained Memory都只是他们本身。D当然也只是自己。我觉得是为了加快计算的速度,MAT改变了对象引用图,而转换成一个对象引用树。在这里例子中,树根是A,而B,C,D是他的三个儿子。B,C,D不再有相互关系。把引用图变成引用树,计算Retained Heap就会非常方便,显示也非常方便。对应到MAT UI上,在dominator tree这个view中,显示了每个对象的shallow heap和retained heap。然后可以以该节点位树根,一步步的细化看看retained heap到底是用在什么地方了。要说一下的是,这种从图到树的转换确实方便了内存分析,但有时候会让人有些疑惑。本来对象B是对象A的一个成员,但因为B还被C引用,所以B在树中并不在A下面,而很可能是平级。

为了纠正这点,MAT中点击右键,可以List objects中选择with outgoing references和with incoming references。这是个真正的引用图的概念,

  • outgoing references :表示该对象的出节点(被该对象引用的对象)。
  • incoming references :表示该对象的入节点(引用到该对象的对象)。

为了更好地理解Retained Heap,下面引用一个例子来说明:

把内存中的对象看成下图中的节点,并且对象和对象之间互相引用。这里有一个特殊的节点GC Roots,这就是reference chain(引用链)的起点:
image image

从obj1入手,上图中蓝色节点代表仅仅只有通过obj1才能直接或间接访问的对象。因为可以通过GC Roots访问,所以左图的obj3不是蓝色节点;而在右图却是蓝色,因为它已经被包含在retained集合内。
所以对于左图,obj1的retained size是obj1、obj2、obj4的shallow size总和;
右图的retained size是obj1、obj2、obj3、obj4的shallow size总和。
obj2的retained size可以通过相同的方式计算。

GC Root

GC发现通过任何reference chain(引用链)无法访问某个对象的时候,该对象即被回收。名词GC Roots正是分析这一过程的起点,例如JVM自己确保了对象的可到达性(那么JVM就是GC Roots),所以GC Roots就是这样在内存中保持对象可到达性的,一旦不可到达,即被回收。通常GC Roots是一个在current thread(当前线程)的call stack(调用栈)上的对象(例如方法参数和局部变量),或者是线程自身或者是system class loader(系统类加载器)加载的类以及native code(本地代码)保留的活动对象。所以GC Roots是分析对象为何还存活于内存中的利器。

MAT中的一些有用的视图

Thread OvewView

Thread OvewView可以查看这个应用的Thread信息:
image

Group

在Histogram和Domiantor Tree界面,可以选择将结果用另一种Group的方式显示(默认是Group by Object),切换到Group by package,可以更好地查看具体是哪个包里的类占用内存大,也很容易定位到自己的应用程序。
image

Path to GC Root

在Histogram或者Domiantor Tree的某一个条目上,右键可以查看其GC Root Path:
image

这里也要说明一下Java的引用规则:
从最强到最弱,不同的引用(可到达性)级别反映了对象的生命周期。

  • Strong Ref(强引用):通常我们编写的代码都是Strong Ref,于此对应的是强可达性,只有去掉强可达,对象才被回收。
  • Soft Ref(软引用):对应软可达性,只要有足够的内存,就一直保持对象,直到发现内存吃紧且没有Strong Ref时才回收对象。一般可用来实现缓存,通过java.lang.ref.SoftReference类实现。
  • Weak Ref(弱引用):比Soft Ref更弱,当发现不存在Strong Ref时,立刻回收对象而不必等到内存吃紧的时候。通过java.lang.ref.WeakReference和java.util.WeakHashMap类实现。
  • Phantom Ref(虚引用):根本不会在内存中保持任何对象,你只能使用Phantom Ref本身。一般用于在进入finalize()方法后进行特殊的清理过程,通过 java.lang.ref.PhantomReference实现。

点击Path To GC Roots –> with all references
image

参考文档

  1. Shallow and retained sizes
  2. MAT的wiki:http://wiki.eclipse.org/index.php/MemoryAnalyzer

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android性能优化后续

作者 Gracker
2015年3月31日 09:29

前言

本文是一篇译文,原文Android Performance Case Study Follow-up的作者是大名鼎鼎的Romain Guy。本文讲述了Android性能优化的一些技巧、方法和工具。

译文正文

两年前,我发表了一篇名为Android Performance Case Study 的文章,来帮助Android开发者了解需要使用什么工具和技术手段来确定、追踪和优化性能问题。

那篇文章以一个Twitter客户端 Falcon Pro为典范,其开发人员为 Joaquim Vergès. Joaquim人不错,他允许我在我的文章中使用它的程序作为例子,并且快速处理了我发现的所有问题。一切都OK,直到Joaquim 从头开始开发Falcon Pro 3,前不久在他准备发布它的新应用的时候,他联系了我,因为他有一个和滚动相关的性能问题需要我来帮助他,这一次我依然没有源代码可以参考。

Joaquim使用了所有的工具来找出问题所在,他发现Overdraw不是问题的原因,他觉得是 ViewPager 的用法导致了这个问题。他给我发来了下面的截图:

Falcon Pro

Joaquim使用了系统内置的GPU profiling工具来发现掉帧现象, 左边的截图是在没有ViewPager 的情况下滑动时间线,右边的截图是有ViewPager的情况下滑动(他使用的是2014年的Moto x来截的图),问题看起来很明显。

我最先想到的是查看ViewPager是不是由于滥用硬件加速导致,这个性能问题看起来像是在滑动的过程中每一帧都使用了硬件加速。系统的 hardware layers updates debugging tool没有显示什么有用的信息。我反复使用HierarchyViewer 查看布局情况,令我满意的是ViewPager的表现很正确(相反,不太可能会出问题)

之后我打开了另一个强大的工具却很少用到的工具:Tracer for OpenGL 。我之前的那篇文章解释了如何使用工具获得更多细节。你首先需要知道的是这个工具收集了所有UI界面发给GPU的绘制命令。

Android 4.3 and upTracer has unfortunately become a little more difficult to use since Android 4.3 when we introducedreordering and merging of drawing commands. It’s an amazingly useful optimization but it prevents Tracer from grouping drawing commands by view. You can restore the old behavior by disabling display lists optimization using the following command (before you start your application)(意思是说Android4.3之后,这个工具不太好用了,因为有reordering and merging 机制的引进)

Reading OpenGL traces: Commands shown in blue are GL operations that draw pixels on screen. All other commands are used to transfer data or set state and can easily be ignored. Every time you click on one of the blue commands, Tracer will update the Details tab and show you the content of the current render target right after the command you clicked is executed. You can thus reconstruct a frame by clicking on each blue command one after another. It’s pretty much how I analyze performance issues with Tracer. Seeing how a frame is rendered gives a lot of insight on what the application is doing.(意思是说只蓝色的行是真正进行绘制的命令,点击可以看到绘制的这一帧的图像,其他的命令都是一些数据的转换)

滑动一段时间Falcon Pro应用后,我仔细查看Gl Trace收集到的数据,我很惊奇地发现很多SaveLayer/ComposeLayer阻塞命令。

Paste_Image.png

这些命令表明应用在生成一个临时的Hardware Layer。这些临时的Layer被不同的 [Canvas.saveLayer()](http://developer.android.com/reference/android/graphics/Canvas.html#saveLayer(float, float, float, float, android.graphics.Paint, int))所创建,这些UI控件在下面的情况下使用Canvas.saveLayer()方法去绘制 alpha < 1 (seeView.setAlpha()的View(即半透明View):

我和Chet 在很多演示中解释过为什么你应该 use alpha with care,每次UI控件使用一个临时的Layer,绘制命令会发送不同的渲染目标,对GPU来说,切换渲染目标是很昂贵的操作,这对于使用tiling/deferred架构的GPU(ImaginationTech’s SGX, Qualcomm’s Adreno, etc)等是硬伤,直接渲染架构的GPU,比如 Nvidia,则会好一点。因为我和Joaquim 使用的是搭载高通处理器的Moto X 2014版本,所以使用多个临时硬件层是最有可能的性能问题的根源。

那么问题来了,是什么创建了这些临时的Layer呢?Tracer告诉我们了答案,如果你看了刚刚上面那张,你可以看到只有SaveLayer这个组中OpenGl命令绘制了一个小圆圈(图被工具放大了),我们来看一下应用截图:

Falcon Pro 3

你看到最上面的小圆圈了么?那是ViewPager的指示器,来显示当前的位置。Joaquim 使用了一个第三方库来绘制这些指示器,有趣的是这些库如何绘制指示器的:当前的Page用一个白色的圈指示,其他的页用类似灰色的圆圈来指示。我说类似灰色因为这个圆圈其实是半透明的白色圆圈。这个库使用 setAlpha()方法来给每个圆圈设置颜色。

有下面几种方法来解决这个问题:

  • Use a customizable “inactive” color instead of setting an opacity on the View( 使用动态的“inactive”颜色(即根据状态来设置View的颜色)而不是设置透明度。)

  • Return false from hasOverlappingRendering() and the framework will set the proper alpha on the Paint
     for you(使hasOverlappingRendering()返回false,这样系统会设置适当的alpha,关于这个的用法,这篇文章中有提到:同时Android提供了hasOverlappingRendering()接口,通过重写该接口可以告知系统当前View是否存在内容重叠的情况,帮助系统优化绘制流程,原理是这样的:对于有重叠内容的View,系统简单粗暴的使用 offscreen buffer来协助处理。当告知系统该View无重叠内容时,系统会分别使用合适的alpha值绘制每一层。)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
     /**
    * Returns whether this View has content which overlaps. This function, intended to be
    * overridden by specific View types, is an optimization when alpha is set on a view. If
    * rendering overlaps in a view with alpha < 1, that view is drawn to an offscreen buffer
    * and then composited it into place, which can be expensive. If the view has no overlapping
    * rendering, the view can draw each primitive with the appropriate alpha value directly.
    * An example of overlapping rendering is a TextView with a background image, such as a
    * Button. An example of non-overlapping rendering is a TextView with no background, or
    * an ImageView with only the foreground image. The default implementation returns true;
    * subclasses should override if they have cases which can be optimized.
    *
    * @return true if the content in this view might overlap, false otherwise.
    */
    public boolean hasOverlappingRendering() {
    return true;
    }

  • Return true from onSetAlpha() and set an alpha on the Paint used to draw the “gray” circles(使onSetAlpha() 返回True并对Paint设置alpha来绘制“gray”圆圈)

    1
    2
    paint.setAlpha((int) alpha * 255);
    canvas.draw*(..., paint);

最简单的方法是使用第二种,但是他只能在API16以上使用,如果你要支持旧版本的Android,使用其他两个方法,我相信Joaquim 已经丢弃那个第三方库并使用自己的指示器了。

我希望这篇文章能让大家清楚如何从看似无辜的和无害的操作中寻找可能会出现性能问题。所以请记住:不要仅仅做出假设,要实际去验证、测量。

附录

更多关于Alpha的使用,可以参考这篇文章:
Android Tips: Best Practices for Using Alpha

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

我的二零二三

作者 JiaYin
2023年12月30日 17:43

今年有点奇怪,已经是最后两天了,却一点也没有写总结的兴致,不像往年,提前一周就开始在心里打腹稿了,今年完全没有想法,但怎么说也是一个十年博客了,还是得写点什么,以示记录。今天下午外出回家路上突然想到了些什么,所以还是写下这段吧,不同于往年好多照片,好多文字,今年此时此刻想写的也就这几句,算是真实感受了。

今年时不时地会思考很多,思考家里的事,思考公司的事,思考自己的事情,思前想后其实都无解,所以,收起回忆、放下期待、过好现在。

这就是我二零二三唯一想写的,读了一遍,自问难道以前没有“过好现在”吗?也不是,以前也过得很好,只是很好的同时还有很多期待。举个今年很简单的例子吧,今年其实我和往年一样,准备拍一组同一位置的春夏秋冬,但拍到初夏就戛然而止了,因为这次选的地方是我定期健身的一兆韦德门口的位置,可惜一兆韦德今年实际控制人消失门店纷纷关闭易主,所以也不去那家店了,于是第三季的春夏秋冬组照就没有拍成,原本的一个小小的期待就这样落空了,这只是一个最简单的生活例子,也是如今这个大环境下的一个小case,还有其他许多就不提了。所以感觉年纪越大,越发没有期待了,因为往往大部分期待就像一张空头支票,兑现不了。

明年会怎样?我不知道,所以过好现在吧。

P.S. 这篇文章是昨天写的,而现在是北京时间12月31日19:38分,我又带着儿子来儿童医院看病了,今天下午他有点发烧,不放心还是来看一下,前面排队的还有100个人,而且赶上年度最后一天,上海的医保结算系统关闭,只能先自费支付,好像还没有在年度最后一天晚上在医院度过,今天算是解锁成就吧,2023,就到这里~

❌
❌