Android性能优化之电量篇
最后更新于:2022-04-01 02:41:34
> 原文出处:http://hukai.me/android-performance-battery/
> 作者: 胡凯
Google近期在Udacity上发布了[Android性能优化的在线课程](https://www.udacity.com/course/ud825),分别从渲染,运算与内存,电量几个方面介绍了如何去优化性能,这些课程是Google之前在Youtube上发布的[Android性能优化典范](http://hukai.me/android-performance-patterns/)专题课程的细化与补充。
下面是电量篇章的学习笔记,部分内容与前面的性能优化典范有重合,欢迎大家一起学习交流!
## 1)Understanding Battery Drain
手机各个硬件模块的耗电量是不一样的,有些模块非常耗电,而有些模块则相对显得耗电量小很多。
![android_perf_battery_drain](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c69ae8048.png)
电量消耗的计算与统计是一件麻烦而且矛盾的事情,记录电量消耗本身也是一个费电量的事情。唯一可行的方案是使用第三方监测电量的设备,这样才能够获取到真实的电量消耗。
当设备处于待机状态时消耗的电量是极少的,以N5为例,打开飞行模式,可以待机接近1个月。可是点亮屏幕,硬件各个模块就需要开始工作,这会需要消耗很多电量。
使用WakeLock或者JobScheduler唤醒设备处理定时的任务之后,一定要及时让设备回到初始状态。每次唤醒蜂窝信号进行数据传递,都会消耗很多电量,它比WiFi等操作更加的耗电。
![battery_drain_radio](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c6a50c29b.png)
## 2)Battery Historian
[Battery Historian](https://developer.android.com/about/versions/android-5.0.html#Power)是Android 5.0开始引入的新API。通过下面的指令,可以得到设备上的电量消耗信息:
~~~
$ adb shell dumpsys batterystats > xxx.txt //得到整个设备的电量消耗信息
$ adb shell dumpsys batterystats > com.package.name > xxx.txt //得到指定app相关的电量消耗信息
~~~
得到了原始的电量消耗数据之后,我们需要通过Google编写的一个[python脚本](https://github.com/google/battery-historian)把数据信息转换成可读性更好的html文件:
~~~
$ python historian.py xxx.txt > xxx.html
~~~
打开这个转换过后的html文件,可以看到类似TraceView生成的列表数据,这里的数据信息量很大,这里就不展开了。
![android_perf_battery_historian](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c6bb864fa.png)
## 3)Track Battery Status & Battery Manager
我们可以通过下面的代码来获取手机的当前充电状态:
~~~
// It is very easy to subscribe to changes to the battery state, but you can get the current
// state by simply passing null in as your receiver. Nifty, isn't that?
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = this.registerReceiver(null, filter);
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
boolean acCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_AC);
if (acCharge) {
Log.v(LOG_TAG,“The phone is charging!”);
}
~~~
在上面的例子演示了如何立即获取到手机的充电状态,得到充电状态信息之后,我们可以有针对性的对部分代码做优化。比如我们可以判断只有当前手机为AC充电状态时 才去执行一些非常耗电的操作。
~~~
/**
* This method checks for power by comparing the current battery state against all possible
* plugged in states. In this case, a device may be considered plugged in either by USB, AC, or
* wireless charge. (Wireless charge was introduced in API Level 17.)
*/
private boolean checkForPower() {
// It is very easy to subscribe to changes to the battery state, but you can get the current
// state by simply passing null in as your receiver. Nifty, isn't that?
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = this.registerReceiver(null, filter);
// There are currently three ways a device can be plugged in. We should check them all.
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
boolean usbCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_USB);
boolean acCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_AC);
boolean wirelessCharge = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
wirelessCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS);
}
return (usbCharge || acCharge || wirelessCharge);
}
~~~
## 4)Wakelock and Battery Drain
高效的保留更多的电量与不断促使用户使用你的App会消耗电量,这是矛盾的选择题。不过我们可以使用一些更好的办法来平衡两者。
假设你的手机里面装了大量的社交类应用,即使手机处于待机状态,也会经常被这些应用唤醒用来检查同步新的数据信息。Android会不断关闭各种硬件来延长手机的待机时间,首先屏幕会逐渐变暗直至关闭,然后CPU进入睡眠,这一切操作都是为了节约宝贵的电量资源。但是即使在这种睡眠状态下,大多数应用还是会尝试进行工作,他们将不断的唤醒手机。一个最简单的唤醒手机的方法是使用PowerManager.WakeLock的API来保持CPU工作并防止屏幕变暗关闭。这使得手机可以被唤醒,执行工作,然后回到睡眠状态。知道如何获取WakeLock是简单的,可是及时释放WakeLock也是非常重要的,不恰当的使用WakeLock会导致严重错误。例如网络请求的数据返回时间不确定,导致本来只需要10s的事情一直等待了1个小时,这样会使得电量白白浪费了。这也是为何使用带超时参数的wakelock.acquice()方法是很关键的。
但是仅仅设置超时并不足够解决问题,例如设置多长的超时比较合适?什么时候进行重试等等?解决上面的问题,正确的方式可能是使用非精准定时器。通常情况下,我们会设定一个时间进行某个操作,但是动态修改这个时间也许会更好。例如,如果有另外一个程序需要比你设定的时间晚5分钟唤醒,最好能够等到那个时候,两个任务捆绑一起同时进行,这就是非精确定时器的核心工作原理。我们可以定制计划的任务,可是系统如果检测到一个更好的时间,它可以推迟你的任务,以节省电量消耗。
![alarmmanager_inexact_wakelock](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c6c7ba220.png)
这正是JobScheduler API所做的事情。它会根据当前的情况与任务,组合出理想的唤醒时间,例如等到正在充电或者连接到WiFi的时候,或者集中任务一起执行。我们可以通过这个API实现很多免费的调度算法。
## 5)Network and Battery Drain
下面内容来自官方Training文档中[高效下载](http://hukai.me/android-training-course-in-chinese/connectivity/efficient-downloads/efficient-network-access.html)章节关于手机(Radio)蜂窝信号对电量消耗的介绍。
通常情况下,使用3G移动网络传输数据,电量的消耗有三种状态:
* **Full power**: 能量最高的状态,移动网络连接被激活,允许设备以最大的传输速率进行操作。
* **Low power**: 一种中间状态,对电量的消耗差不多是Full power状态下的50%。
* **Standby**: 最低的状态,没有数据连接需要传输,电量消耗最少。
下图是一个典型的3G Radio State Machine的图示(来自AT&T,详情请点击[这里](http://www.research.att.com/articles/featured_stories/2011_03/201102_Energy_efficient?fbid=SYuI20FzBum)):
![mobile_radio_state_machine.png](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c6cc9c1cd.png "Figure 1\. Typical 3G wireless radio state machine.")
**总之,为了减少电量的消耗,在蜂窝移动网络下,最好做到批量执行网络请求,尽量避免频繁的间隔网络请求。**
通过前面学习到的Battery Historian我们可以得到设备的电量消耗数据,如果数据中的移动蜂窝网络(Mobile Radio)电量消耗呈现下面的情况,间隔很小,又频繁断断续续的出现,说明电量消耗性能很不好:
![android_perf_battery_bad.png](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c6ceb6d86.png)
经过优化之后,如果呈现下面的图示,说明电量消耗的性能是良好的:
![android_perf_battery_good](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c6d1c8297.png)
另外WiFi连接下,网络传输的电量消耗要比移动网络少很多,应该尽量减少移动网络下的数据传输,多在WiFi环境下传输数据。
![android_perf_battery_wifi](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c6dda75ec.png)
那么如何才能够把任务缓存起来,做到批量化执行呢?下面就轮到Job Scheduler出场了。
## 6)Using Job Scheduler
使用[Job Scheduler](https://developer.android.com/reference/android/app/job/JobScheduler.html),应用需要做的事情就是判断哪些任务是不紧急的,可以交给Job Scheduler来处理,Job Scheduler集中处理收到的任务,选择合适的时间,合适的网络,再一起进行执行。
下面是使用Job Scheduler的一段简要示例,需要先有一个JobService:
~~~
public class MyJobService extends JobService {
private static final String LOG_TAG = "MyJobService";
@Override
public void onCreate() {
super.onCreate();
Log.i(LOG_TAG, "MyJobService created");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(LOG_TAG, "MyJobService destroyed");
}
@Override
public boolean onStartJob(JobParameters params) {
// This is where you would implement all of the logic for your job. Note that this runs
// on the main thread, so you will want to use a separate thread for asynchronous work
// (as we demonstrate below to establish a network connection).
// If you use a separate thread, return true to indicate that you need a "reschedule" to
// return to the job at some point in the future to finish processing the work. Otherwise,
// return false when finished.
Log.i(LOG_TAG, "Totally and completely working on job " + params.getJobId());
// First, check the network, and then attempt to connect.
if (isNetworkConnected()) {
new SimpleDownloadTask() .execute(params);
return true;
} else {
Log.i(LOG_TAG, "No connection on job " + params.getJobId() + "; sad face");
}
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
// Called if the job must be stopped before jobFinished() has been called. This may
// happen if the requirements are no longer being met, such as the user no longer
// connecting to WiFi, or the device no longer being idle. Use this callback to resolve
// anything that may cause your application to misbehave from the job being halted.
// Return true if the job should be rescheduled based on the retry criteria specified
// when the job was created or return false to drop the job. Regardless of the value
// returned, your job must stop executing.
Log.i(LOG_TAG, "Whelp, something changed, so I'm calling it on job " + params.getJobId());
return false;
}
/**
* Determines if the device is currently online.
*/
private boolean isNetworkConnected() {
ConnectivityManager connectivityManager =
(ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return (networkInfo != null && networkInfo.isConnected());
}
/**
* Uses AsyncTask to create a task away from the main UI thread. This task creates a
* HTTPUrlConnection, and then downloads the contents of the webpage as an InputStream.
* The InputStream is then converted to a String, which is logged by the
* onPostExecute() method.
*/
private class SimpleDownloadTask extends AsyncTask<JobParameters, Void, String> {
protected JobParameters mJobParam;
@Override
protected String doInBackground(JobParameters... params) {
// cache system provided job requirements
mJobParam = params[0];
try {
InputStream is = null;
// Only display the first 50 characters of the retrieved web page content.
int len = 50;
URL url = new URL("https://www.google.com");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(10000); //10sec
conn.setConnectTimeout(15000); //15sec
conn.setRequestMethod("GET");
//Starts the query
conn.connect();
int response = conn.getResponseCode();
Log.d(LOG_TAG, "The response is: " + response);
is = conn.getInputStream();
// Convert the input stream to a string
Reader reader = null;
reader = new InputStreamReader(is, "UTF-8");
char[] buffer = new char[len];
reader.read(buffer);
return new String(buffer);
} catch (IOException e) {
return "Unable to retrieve web page.";
}
}
@Override
protected void onPostExecute(String result) {
jobFinished(mJobParam, false);
Log.i(LOG_TAG, result);
}
}
}
~~~
然后模拟通过点击Button触发N个任务,交给JobService来处理
~~~
public class FreeTheWakelockActivity extends ActionBarActivity {
public static final String LOG_TAG = "FreeTheWakelockActivity";
TextView mWakeLockMsg;
ComponentName mServiceComponent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_wakelock);
mWakeLockMsg = (TextView) findViewById(R.id.wakelock_txt);
mServiceComponent = new ComponentName(this, MyJobService.class);
Intent startServiceIntent = new Intent(this, MyJobService.class);
startService(startServiceIntent);
Button theButtonThatWakelocks = (Button) findViewById(R.id.wakelock_poll);
theButtonThatWakelocks.setText(R.string.poll_server_button);
theButtonThatWakelocks.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pollServer();
}
});
}
/**
* This method polls the server via the JobScheduler API. By scheduling the job with this API,
* your app can be confident it will execute, but without the need for a wake lock. Rather, the
* API will take your network jobs and execute them in batch to best take advantage of the
* initial network connection cost.
*
* The JobScheduler API works through a background service. In this sample, we have
* a simple service in MyJobService to get you started. The job is scheduled here in
* the activity, but the job itself is executed in MyJobService in the startJob() method. For
* example, to poll your server, you would create the network connection, send your GET
* request, and then process the response all in MyJobService. This allows the JobScheduler API
* to invoke your logic without needed to restart your activity.
*
* For brevity in the sample, we are scheduling the same job several times in quick succession,
* but again, try to consider similar tasks occurring over time in your application that can
* afford to wait and may benefit from batching.
*/
public void pollServer() {
JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
for (int i=0; i<10; i++) {
JobInfo jobInfo = new JobInfo.Builder(i, mServiceComponent)
.setMinimumLatency(5000) // 5 seconds
.setOverrideDeadline(60000) // 60 seconds (for brevity in the sample)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // WiFi or data connections
.build();
mWakeLockMsg.append("Scheduling job " + i + "!\n");
scheduler.schedule(jobInfo);
}
}
}
~~~
**Notes:**关于更多电量优化,还有一篇文章,请点击[这里](http://developer.android.com/training/efficient-downloads/index.html)
Android性能优化之内存篇
最后更新于:2022-04-01 02:41:32
> 原文出处:http://hukai.me/android-performance-memory/
> 作者: 胡凯
Google近期在Udacity上发布了[Android性能优化的在线课程](https://www.udacity.com/course/ud825),分别从渲染,运算与内存,电量几个方面介绍了如何去优化性能,这些课程是Google之前在Youtube上发布的[Android性能优化典范](http://hukai.me/android-performance-patterns/)专题课程的细化与补充。
下面是内存篇章的学习笔记,部分内容与前面的性能优化典范有重合,欢迎大家一起学习交流!
## 1)Memory, GC, and Performance
众所周知,与C/C++需要通过手动编码来申请以及释放内存有所不同,Java拥有GC的机制。Android系统里面有一个**Generational Heap Memory**的模型,系统会根据内存中不同的内存数据类型分别执行不同的GC操作。例如,最近刚分配的对象会放在Young Generation区域,这个区域的对象通常都是会快速被创建并且很快被销毁回收的,同时这个区域的GC操作速度也是比Old Generation区域的GC操作速度更快的。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c57183b4b.png)
除了速度差异之外,执行GC操作的时候,所有线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c58009fb7.png)
通常来说,单个的GC并不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了。
## 2)Memory Monitor Walkthrough
Android Studio中的Memory Monitor可以很好的帮助我们查看程序的内存使用情况。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c58718124.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c58a90b72.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c58e596c2.png)
## 3)Memory Leaks
内存泄漏表示的是不再用到的对象因为被错误引用而无法进行回收。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c59177938.png)
发生内存泄漏会导致Memory Generation中的剩余可用Heap Size越来越小,这样会导致频繁触发GC,更进一步引起性能问题。
举例内存泄漏,下面`init()`方法来自某个自定义View:
~~~
private void init() {
ListenerCollector collector = new ListenerCollector();
collector.setListener(this, mListener);
}
~~~
上面的例子容易存在内存泄漏,如果activity因为设备翻转而重新创建,自定义的View会自动重新把新创建出来的mListener给绑定到ListenerCollector中,但是当activity被销毁的时候,mListener却无法被回收了。
## 4)Heap Viewer Walkthrough
下图演示了Android Tools里面的Heap Viewer的功能,我们可以看到当前进程中的Heap Size的情况,分别有哪些类型的数据,占比是多少。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c597e65aa.png)
## 5)Understanding Memory Churn
**Memory Churn内存抖动**,内存抖动是因为在短时间内大量的对象被创建又马上被释放。瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,会触发GC从而导致刚产生的对象又很快被回收。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c59f099cf.png)
解决上面的问题有简洁直观方法,如果你在**Memory Monitor**里面查看到短时间发生了多次内存的涨跌,这意味着很有可能发生了内存抖动。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c5ab31234.png)
同时我们还可以通过**Allocation Tracker**来查看在短时间内,同一个栈中不断进出的相同对象。这是内存抖动的典型信号之一。
当你大致定位问题之后,接下去的问题修复也就显得相对直接简单了。例如,你需要避免在for循环里面分配对象占用内存,需要尝试把对象的创建移到循环体之外,自定义View中的onDraw方法也需要引起注意,每次屏幕发生绘制以及动画执行过程中,onDraw方法都会被调用到,避免在onDraw方法里面执行复杂的操作,避免创建对象。对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,但是这里需要注意结束使用之后,需要手动释放对象池中的对象。
## 6)Allocation Tracker
关于Allocation Tracker工具的使用,不展开了,参考下面的链接:
* [http://developer.android.com/tools/debugging/ddms.html#alloc](http://developer.android.com/tools/debugging/ddms.html#alloc)
* [http://android-developers.blogspot.com/2009/02/track-memory-allocations.html](http://android-developers.blogspot.com/2009/02/track-memory-allocations.html)
## 7)Improve Your Code To Reduce Churn
下面演示一个例子,如何通过修改代码来避免内存抖动。优化之前的内存检测图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c5aec4af0.png)
定位代码之后,修复了String拼接的问题:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c5b04d1ea.png)
优化之后的内存监测图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c5b4e2f4c.png)
## 8)Recap
上面提到了三种测量内存的工具,下面再简要概括一下他们各自的特点:
* **Memory Monitor:**跟踪整个app的内存变化情况。
* **Heap Viewer:**查看当前内存快照,便于对比分析哪些对象有可能发生了泄漏。
* **Allocation Tracker:**追踪内存对象的来源。
**Notes:**关于更多内存优化,这里还有一篇文章,请点击[这里](http://hukai.me/android-training-managing_your_app_memory/)
Android性能优化之运算篇
最后更新于:2022-04-01 02:41:30
> 原文出处:http://hukai.me/android-performance-compute/
> 作者:胡凯
Google近期在Udacity上发布了[Android性能优化的在线课程](https://www.udacity.com/course/ud825),分别从渲染,运算与内存,电量几个方面介绍了如何去优化性能,这些课程是Google之前在Youtube上发布的[Android性能优化典范](http://hukai.me/android-performance-patterns/)专题课程的细化与补充。
下面是运算篇章的学习笔记,部分内容与前面的性能优化典范有重合,欢迎大家一起学习交流!
## 1)Intro to Compute and Memory Problems
Android中的Java代码会需要经过编译优化再执行的过程。代码的不同写法会影响到Java编译器的优化效率。例如for循环的不同写法就会对编译器优化这段代码产生不同的效率,当程序中包含大量这种可优化的代码的时候,运算性能就会出现问题。想要知道如何优化代码的运算性能就需要知道代码在硬件层的执行差异。
## 2)Slow Function Performance
如果你写了一段代码,它的执行效率比想象中的要差很多。我们需要知道有哪些因素有可能影响到这段代码的执行效率。例如:比较两个float数值大小的执行时间是int数值的4倍左右。这是因为CPU的运算架构导致的,如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c37c1c1c8.png)
虽然现代的CPU架构得到了很大的提升,也许并不存在上面所示的那么大的差异,但是这个例子说明了代码写法上的差异会对运算性能产生很大的影响。
通常来说有两类运行效率差的情况:第1种是相对执行时间长的方法,我们可以很轻松的找到这些方法并做一定的优化。第2种是执行时间短,但是执行频次很高的方法,因为执行次数多,累积效应下就会对性能产生很大的影响。
修复这些细节效率问题,需要使用Android SDK提供的工具,进行仔细的测量,然后再进行微调修复。
## 3)Traceview Walkthrough
通过Android Studio打开里面的Android Device Monitor,切换到DDMS窗口,点击左边栏上面想要跟踪的进程,再点击上面的Start Method Tracing的按钮,如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c3879209a.png)
启动跟踪之后,再操控app,做一些你想要跟踪的事件,例如滑动listview,点击某些视图进入另外一个页面等等。操作完之后,回到Android Device Monitor,再次点击Method Tracing的按钮停止跟踪。此时工具会为刚才的操作生成TraceView的详细视图。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c38a64550.png)
关于TraceView中详细数据如何查看,这里不展开了,有很多文章介绍过。
## 4)Batching and Caching
为了提升运算性能,这里介绍2个非常重要的技术,Batching与Caching。
**Batching**是在真正执行运算操作之前对数据进行批量预处理,例如你需要有这样一个方法,它的作用是查找某个值是否存在与于一堆数据中。假设一个前提,我们会先对数据做排序,然后使用二分查找法来判断值是否存在。我们先看第一种情况,下图中存在着多次重复的排序操作。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c392b57c6.png)
在上面的那种写法下,如果数据的量级并不大的话,应该还可以接受,可是如果数据集非常大,就会有严重的效率问题。那么我们看下改进的写法,把排序的操作打包绑定只执行一次:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c397c6560.png)
上面就是Batching的一种示例:把重复的操作拎出来,打包只执行一次。
**Caching**的理念很容易理解,在很多方面都有体现,下面举一个for循环的例子:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c3a391462.png)
上面这2种基础技巧非常实用,积极恰当的使用能够显著提升运算性能。
## 5)Blocking the UI Thread
提升代码的运算效率是改善性能的一方面,让代码执行在哪个线程也同样很重要。我们都知道Android的Main Thread也是UI Thread,它需要承担用户的触摸事件的反馈,界面视图的渲染等操作。这就意味着,我们不能在Main Thread里面做任何非轻量级的操作,类似I/O操作会花费大量时间,这很有可能会导致界面渲染发生丢帧的现象,甚至有可能导致ANR。防止这些问题的解决办法就是把那些可能有性能问题的代码移到非UI线程进行操作。
## 6)Container Performance
另外一个我们需要注意的运算性能问题是基础算法的合理选择,例如冒泡排序与快速排序的性能差异:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c3a71ae44.png)
避免我们重复造轮子,Java提供了很多现成的容器,例如Vector,ArrayList,LinkedList,HashMap等等,在Android里面还有新增加的SparseArray等,我们需要了解这些基础容器的性能差异以及适用场景。这样才能够选择合适的容器,达到最佳的性能。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c3a8bc99c.png)
**Notes:**关于更多代码优化的小技巧,请点击[这里](http://hukai.me/android-training-performance-tips/)
Android性能优化之渲染篇
最后更新于:2022-04-01 02:41:28
> 原文出处:http://hukai.me/android-performance-render/
> 作者: 胡凯
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c0be76081.jpg)
Google近期在Udacity上发布了[Android性能优化的在线课程](https://www.udacity.com/course/ud825),分别从渲染,运算与内存,电量几个方面介绍了如何去优化性能,这些课程是Google之前在Youtube上发布的[Android性能优化典范](http://hukai.me/android-performance-patterns/)专题课程的细化与补充。
下面是渲染篇章的学习笔记,部分内容和前面的性能优化典范有重合,欢迎大家一起学习交流!
## 1)Why Rendering Performance Matters
现在有不少App为了达到很华丽的视觉效果,会需要在界面上层叠很多的视图组件,但是这会很容易引起性能问题。如何平衡Design与Performance就很需要智慧了。
## 2)Defining ‘Jank’
大多数手机的屏幕刷新频率是60hz,如果在1000/60=16.67ms内没有办法把这一帧的任务执行完毕,就会发生丢帧的现象。丢帧越多,用户感受到的卡顿情况就越严重。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c0d35cdfe.png)
## 3)Rendering Pipeline: Common Problems
渲染操作通常依赖于两个核心组件:CPU与GPU。CPU负责包括Measure,Layout,Record,Execute的计算操作,GPU负责Rasterization(栅格化)操作。CPU通常存在的问题的原因是存在非必需的视图组件,它不仅仅会带来重复的计算操作,而且还会占用额外的GPU资源。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c0dc0b60c.jpg)
## 4)Android UI and the GPU
了解Android是如何利用GPU进行画面渲染有助于我们更好的理解性能问题。一个很直接的问题是:activity的画面是如何绘制到屏幕上的?那些复杂的XML布局文件又是如何能够被识别并绘制出来的?
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c0dea2d48.png)
**Resterization栅格化**是绘制那些Button,Shape,Path,String,Bitmap等组件最基础的操作。它把那些组件拆分到不同的像素上进行显示。这是一个很费时的操作,GPU的引入就是为了加快栅格化的操作。
CPU负责把UI组件计算成Polygons,Texture纹理,然后交给GPU进行栅格化渲染。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c0e2c9bae.png)
然而每次从CPU转移到GPU是一件很麻烦的事情,所幸的是OpenGL ES可以把那些需要渲染的纹理Hold在GPU Memory里面,在下次需要渲染的时候直接进行操作。所以如果你更新了GPU所hold住的纹理内容,那么之前保存的状态就丢失了。
在Android里面那些由主题所提供的资源,例如Bitmaps,Drawables都是一起打包到统一的Texture纹理当中,然后再传递到GPU里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。当然随着UI组件的越来越丰富,有了更多演变的形态。例如显示图片的时候,需要先经过CPU的计算加载到内存中,然后传递给GPU进行渲染。文字的显示比较复杂,需要先经过CPU换算成纹理,然后交给GPU进行渲染,返回到CPU绘制单个字符的时候,再重新引用经过GPU渲染的内容。动画则存在一个更加复杂的操作流程。
为了能够使得App流畅,我们需要在每帧16ms以内处理完所有的CPU与GPU的计算,绘制,渲染等等操作。
## 5)GPU Problem: Overdraw
Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的UI结构里面,如果不可见的UI也在做绘制的操作,会导致某些像素区域被绘制了多次。这样就会浪费大量的CPU以及GPU资源。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c0e727ebf.png)
当设计上追求更华丽的视觉效果的时候,我们就容易陷入采用复杂的多层次重叠视图来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳的性能,我们必须尽量减少Overdraw的情况发生。
幸运的是,我们可以通过手机设置里面的开发者选项,打开Show GPU Overdraw的选项,观察UI上的Overdraw情况。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c10c3eb2a.png)
蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。
## 6)Visualize and Fix Overdraw - Quiz & Solution
这里举了一个例子,通过XML文件可以看到有好几处非必需的background。通过把XML中非必需的background移除之后,可以显著减少布局的过度绘制。其中一个比较有意思的地方是:针对ListView中的Avatar ImageView的设置,在getView的代码里面,判断是否获取到对应的Bitmap,在获取到Avatar的图像之后,把ImageView的Background设置为Transparent,只有当图像没有获取到的时候才设置对应的Background占位图片,这样可以避免因为给Avatar设置背景图而导致的过度渲染。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c110a3230.png)
总结一下,优化步骤如下:
* 移除Window默认的Background
* 移除XML布局文件中非必需的Background
* 按需显示占位背景图片
## 7)ClipRect & QuickReject
前面有提到过,对不可见的UI组件进行绘制更新会导致Overdraw。例如Nav Drawer从前置可见的Activity滑出之后,如果还继续绘制那些在Nav Drawer里面不可见的UI组件,这就导致了Overdraw。为了解决这个问题,Android系统会通过避免绘制那些完全不可见的组件来尽量减少Overdraw。那些Nav Drawer里面不可见的View就不会被执行浪费资源。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c11476105.png)
但是不幸的是,对于那些过于复杂的自定义的View(通常重写了onDraw方法),Android系统无法检测在onDraw里面具体会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过[canvas.clipRect()](http://developer.android.com/reference/android/graphics/Canvas.html)来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c11c64834.png)
除了clipRect方法之外,我们还可以使用[canvas.quickreject()](http://developer.android.com/reference/android/graphics/Canvas.html)来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。
## 8)Apply clipRect and quickReject - Quiz & Solution
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c12376b4a.png)
上面的示例图中显示了一个自定义的View,主要效果是呈现多张重叠的卡片。这个View的onDraw方法如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c1457357f.png)
打开开发者选项中的显示过度渲染,可以看到我们这个自定义的View部分区域存在着过度绘制。那么是什么原因导致过度绘制的呢?
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c148228c5.png)
## 9)Fixing Overdraw with Canvas API
下面的代码显示了如何通过clipRect来解决自定义View的过度绘制,提高自定义View的绘制性能:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c15021847.png)
下面是优化过后的效果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c156861fa.png)
## 10)Layouts, Invalidations and Perf
Android需要把XML布局文件转换成GPU能够识别并绘制的对象。这个操作是在**DisplayList**的帮助下完成的。DisplayList持有所有将要交给GPU绘制到屏幕上的数据信息。
在某个View第一次需要被渲染时,Display List会因此被创建,当这个View要显示到屏幕上时,我们会执行GPU的绘制指令来进行渲染。
如果View的Property属性发生了改变(例如移动位置),我们就仅仅需要Execute Display List就够了。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c165ab504.png)
然而如果你修改了View中的某些可见组件的内容,那么之前的DisplayList就无法继续使用了,我们需要重新创建一个DisplayList并重新执行渲染指令更新到屏幕上。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c1680b8e2.png)
请注意:任何时候View中的绘制内容发生变化时,都会需要重新创建DisplayList,渲染DisplayList,更新到屏幕上等一系列操作。这个流程的表现性能取决于你的View的复杂程度,View的状态变化以及渲染管道的执行性能。举个例子,假设某个Button的大小需要增大到目前的两倍,在增大Button大小之前,需要通过父View重新计算并摆放其他子View的位置。修改View的大小会触发整个HierarcyView的重新计算大小的操作。如果是修改View的位置则会触发HierarchView重新计算其他View的位置。如果布局很复杂,这就会很容易导致严重的性能问题。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c16a5fb99.png)
## 11)Hierarchy Viewer: Walkthrough
Hierarchy Viewer可以很直接的呈现布局的层次关系,视图组件的各种属性。 我们可以通过红,黄,绿三种不同的颜色来区分布局的Measure,Layout,Executive的相对性能表现如何。
## 12)Nested Hierarchies and Performance
提升布局性能的关键点是尽量保持布局层级的扁平化,避免出现重复的嵌套布局。例如下面的例子,有2行显示相同内容的视图,分别用两种不同的写法来实现,他们有着不同的层级。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c16de36ad.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c178a0f43.png)
下图显示了使用2种不同的写法,在Hierarchy Viewer上呈现出来的性能测试差异:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c183eeda7.png)
## 13)Optimizing Your Layout
下图举例演示了如何优化ListItem的布局,通过RelativeLayout替代旧方案中的嵌套LinearLayout来优化布局。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c189eca2c.png)
第三季
最后更新于:2022-04-01 02:41:25
> 原文出处: http://hukai.me/android-performance-patterns-season-3/
![document/2015-08-21/55d6cb715eea8](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/document_2015-08-21_55d6cb715eea8.png)
[Android性能优化典范](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE)的课程最近更新到第三季了,这次一共12个短视频课程,包括的内容大致有:更高效的ArrayMap容器,使用Android系统提供的特殊容器来避免自动装箱,避免使用枚举类型,注意onLowMemory与onTrimMemory的回调,避免内存泄漏,高效的位置更新操作,重复layout操作的性能影响,以及使用Batching,Prefetching优化网络请求,压缩传输数据等等使用技巧。下面是对这些课程的总结摘要,认知有限,理解偏差的地方请多多交流指正!
## 1)Fun with ArrayMaps
程序内存的管理是否合理高效对应用的性能有着很大的影响,有的时候对容器的使用不当也会导致内存管理效率低下。Android为移动操作系统特意编写了一些更加高效的容器,例如SparseArray,今天要介绍的是一个新的容器,叫做**[ArrayMap](https://android.googlesource.com/platform/frameworks/base.git/+/master/core/java/android/util/ArrayMap.java)**。
我们经常会使用到HashMap这个容器,它非常好用,但是却很占用内存。下图演示了HashMap的简要工作原理:
![android_perf_3_arraymap_key_value](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9f92d095.png)
为了解决HashMap更占内存的弊端,Android提供了内存效率更高的**ArrayMap**。它内部使用两个数组进行工作,其中一个数组记录key hash过后的顺序列表,另外一个数组按key的顺序记录Key-Value值,如下图所示:
![android_perf_3_arraymap_two_array](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9fe57785.png)
当你想获取某个value的时候,ArrayMap会计算输入key转换过后的hash值,然后对hash数组使用二分查找法寻找到对应的index,然后我们可以通过这个index在另外一个数组中直接访问到需要的键值对。如果在第二个数组键值对中的key和前面输入的查询key不一致,那么就认为是发生了碰撞冲突。为了解决这个问题,我们会以该key为中心点,分别上下展开,逐个去对比查找,直到找到匹配的值。如下图所示:
![android_perf_3_arraymap_binary_search](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9ffcfb40.png)
随着数组中的对象越来越多,查找访问单个对象的花费也会跟着增长,这是在内存占用与访问时间之间做权衡交换。
既然ArrayMap中的内存占用是连续不间断的,那么它是如何处理插入与删除操作的呢?请看下图所示,演示了Array的特性:
![android_perf_3_arraymap_del](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca06b37d1.png)
![android_perf_3_arraymap_add](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca08dd97f.png)
很明显,ArrayMap的插入与删除的效率是不够高的,但是如果数组的列表只是在一百这个数量级上,则完全不用担心这些插入与删除的效率问题。HashMap与ArrayMap之间的内存占用效率对比图如下:
![android_perf_3_arraymap_memory_compare](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca0a518b0.png)
与HashMap相比,ArrayMap在循环遍历的时候也更加简单高效,如下图所示:
![android_perf_3_arraymap_list](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca0b57029.png)
前面演示了很多ArrayMap的优点,但并不是所有情况下都适合使用ArrayMap,我们应该在满足下面2个条件的时候才考虑使用ArrayMap:
* 对象个数的数量级最好是千以内
* 数据组织形式包含Map结构
我们需要学会在特定情形下选择相对更加高效的实现方式。
## 2)Beware Autoboxing
有时候性能问题也可能是因为那些不起眼的小细节引起的,例如在代码中不经意的“自动装箱”。我们知道基础数据类型的大小:boolean(8 bits), int(32 bits), float(32 bits),long(64 bits),为了能够让这些基础数据类型在大多数Java容器中运作,会需要做一个autoboxing的操作,转换成Boolean,Integer,Float等对象,如下演示了循环操作的时候是否发生autoboxing行为的差异:
![android_perf_3_autoboxing_for](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca0f442e7.png)
![android_perf_3_autoboxing_perf](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca120c0c2.png)
Autoboxing的行为还经常发生在类似HashMap这样的容器里面,对HashMap的增删改查操作都会发生了大量的autoboxing的行为。
![android_perf_3_autoboxing_hashmap](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca1b774f4.png)
为了避免这些autoboxing带来的效率问题,Android特地提供了一些如下的Map容器用来替代HashMap,不仅避免了autoboxing,还减少了内存占用:
![android_perf_3_autoboxing_sparse](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca1d61d29.png)
## 3)SparseArray Family Ties
为了避免HashMap的autoboxing行为,Android系统提供了SparseBoolMap,SparseIntMap,SparseLongMap,LongSparseMap等容器。关于这些容器的基本原理请参考前面的ArrayMap的介绍,另外这些容器的使用场景也和ArrayMap一致,需要满足数量级在千以内,数据组织形式需要包含Map结构。
## 4)The price of ENUMs
在StackOverFlow等问答社区常常出现关于在Android系统里面使用枚举类型的性能讨论,关于这一点,Android官方的Training课程里面有下面这样一句话:
> Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
![android_perf_3_enum](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca20a7b91.png)
关于enum的效率,请看下面的讨论。假设我们有这样一份代码,编译之后的dex大小是2556 bytes,在此基础之上,添加一些如下代码,这些代码使用普通static常量相关作为判断值:
![android_perf_3_enum_static](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca2e54003.png)
增加上面那段代码之后,编译成dex的大小是2680 bytes,相比起之前的2556 bytes只增加124 bytes。假如换做使用enum,情况如下:
![android_perf_3_enum_enum](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca31d262f.png)
使用enum之后的dex大小是4188 bytes,相比起2556增加了1632 bytes,增长量是使用static int的13倍。不仅仅如此,使用enum,运行时还会产生额外的内存占用,如下图所示:
![android_perf_3_enum_memory](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca3485183.png)
Android官方强烈建议不要在Android程序里面使用到enum。
## 5)Trimming and Sharing Memory
Android系统的一大特色是多任务,用户可以随意在不同的app之间进行快速切换。为了确保你的应用在这种复杂的多任务环境中正常运行,我们需要了解下面的知识。
为了让background的应用能够迅速的切换到forground,每一个background的应用都会占用一定的内存。Android系统会根据当前的系统内存使用情况,决定回收部分background的应用内存。如果background的应用从暂停状态直接被恢复到forground,能够获得较快的恢复体验,如果background应用是从Kill的状态进行恢复,就会显得稍微有点慢。
![android_perf_3_memory_bg_2_for](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca35e3722.png)
Android系统提供了一些回调来通知应用的内存使用情况,通常来说,当所有的background应用都被kill掉的时候,forground应用会收到**onLowMemory()**的回调。在这种情况下,需要尽快释放当前应用的非必须内存资源,从而确保系统能够稳定继续运行。Android系统还提供了onTrimMemory()的回调,当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调,同时在这个回调里面会传递以下的参数,代表不同的内存使用情况,下图介绍了各种不同的回调参数:
![android_perf_3_memory_ontrimmemory](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca3ceb3d8.png)
关于每个参数的更多介绍,请参考这里 [http://hukai.me/android-training-managing_your_app_memory/](http://hukai.me/android-training-managing_your_app_memory/),另外onTrimMemory()的回调可以发生在Application,Activity,Fragment,Service,Content Provider。
从Android 4.4开始,ActivityManager提供了**isLowRamDevice()**的API,通常指的是Heap Size低于512M或者屏幕大小<=800*480的设备。
## 6)DO NOT LEAK VIEWS
内存泄漏的概念,下面一张图演示下:
![android_perf_3_leak](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca460e82d.png)
通常来说,View会保持Activity的引用,Activity同时还和其他内部对象也有可能保持引用关系。当屏幕发生旋转的时候,activity很容易发生泄漏,这样的话,里面的view也会发生泄漏。Activity以及view的泄漏是非常严重的,为了避免出现泄漏,请特别留意以下的规则:
### 6.1)避免使用异步回调
异步回调被执行的时间不确定,很有可能发生在activity已经被销毁之后,这不仅仅很容易引起crash,还很容易发生内存泄露。
![android_perf_3_leak_asyncback](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca47b5a7c.png)
### 6.2)避免使用Static对象
因为static的生命周期过长,使用不当很可能导致leak,在Android中应该尽量避免使用static对象。
![android_perf_3_leak_static](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca4b8d5cc.png)
### 6.3)避免把View添加到没有清除机制的容器里面
假如把view添加到[WeakHashMap](http://stackoverflow.com/questions/5511279/what-is-a-weakhashmap-and-when-to-use-it),如果没有执行清除操作,很可能会导致泄漏。
![android_perf_3_leak_map](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca4f8f106.png)
## 7)Location & Battery Drain
开启定位功能是一个相对来说比较耗电的操作,通常来说,我们会使用类似下面这样的代码来发出定位请求:
![android_perf_3_location_request](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca511cb23.png)
上面演示中有一个方法是**setInterval()**指的意思是每隔多长的时间获取一次位置更新,时间相隔越短,自然花费的电量就越多,但是时间相隔太长,又无法及时获取到更新的位置信息。其中存在的一个优化点是,我们可以通过判断返回的位置信息是否相同,从而决定设置下次的更新间隔是否增加一倍,通过这种方式可以减少电量的消耗,如下图所示:
![android_perf_3_location_reduce](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca5366c67.png)
在位置请求的演示代码中还有一个方法是**setFastestInterval()**,因为整个系统中很可能存在其他的应用也在请求位置更新,那些应用很有可能设置的更新间隔时间很短,这种情况下,我们就可以通过setFestestInterval的方法来过滤那些过于频繁的更新。
通过GPS定位服务相比起使用网络进行定位更加的耗电,但是也相对更加精准一些,他们的图示关系如下:
![android_perf_3_location_provider](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca57ab766.png)
为了提供不同精度的定位需求,同时屏蔽实现位置请求的细节,Android提供了下面4种不同精度与耗电量的参数给应用进行设置调用,应用只需要决定在适当的场景下使用对应的参数就好了,通过LocationRequest.setPriority()方法传递下面的参数就好了。
![android_perf_3_location_accuracy](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca59ac043.png)
## 8)Double Layout Taxation
布局中的任何一个View一旦发生一些属性变化,都可能引起很大的连锁反应。例如某个button的大小突然增加一倍,有可能会导致兄弟视图的位置变化,也有可能导致父视图的大小发生改变。当大量的layout()操作被频繁调用执行的时候,就很可能引起丢帧的现象。
![android_perf_3_layout_double](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca61085cb.png)
例如,在RelativeLayout中,我们通常会定义一些类似alignTop,alignBelow等等属性,如图所示:
![android_perf_3_layout_relative](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca64234d5.png)
为了获得视图的准确位置,需要经过下面几个阶段。首先子视图会触发计算自身位置的操作,然后RelativeLayout使用前面计算出来的位置信息做边界的调整的操作,如下面两张图所示:
![android_perf_3_layout_first_cal](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca664d99a.png)
![android_perf_3_layout_first_adjust](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca6b25d16.png)
经历过上面2个步骤,relativeLayout会立即触发第二次layout()的操作来确定所有子视图的最终位置与大小信息。
除了RelativeLayout会发生两次layout操作之外,LinearLayout也有可能触发两次layout操作,通常情况下LinearLayout只会发生一次layout操作,可是一旦调用了measureWithLargetChild()方法就会导致触发两次layout的操作。另外,通常来说,GridLayout会自动预处理子视图的关系来避免两次layout,可是如果GridLayout里面的某些子视图使用了weight等复杂的属性,还是会导致重复的layout操作。
如果只是少量的重复layout本身并不会引起严重的性能问题,但是如果它们发生在布局的根节点,或者是ListView里面的某个ListItem,这样就会引起比较严重的性能问题。如下图所示:
![android_perf_3_layout_hierachy](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca6e1c608.png)
我们可以使用Systrace来跟踪特定的某段操作,如果发现了疑似丢帧的现象,可能就是因为重复layout引起的。通常我们无法避免重复layout,在这种情况下,我们应该尽量保持View Hierarchy的层级比较浅,这样即使发生重复layout,也不会因为布局的层级比较深而增大了重复layout的倍数。另外还有一点需要特别注意,在任何时候都请避免调用**requestLayout()**的方法,因为一旦调用了requestLayout,会导致该layout的所有父节点都发生重新layout的操作。
![android_perf_3_layout_request](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca7076165.png)
## 9)Network Performance 101
在性能优化第一季与第二季的课程里面都介绍过,网络请求的操作是非常耗电的,其中在移动蜂窝网络情况下执行网络数据的请求则尤其比较耗电。关于如何减少移动网络下的网络请求的耗电量,有两个重要的原则需要遵守:第一个是减少移动网络被激活的时间与次数,第二个是压缩传输数据。
### 9.1)减少移动网络被激活的时间与次数
通常来说,发生网络行为可以划分为如下图所示的三种类型,一个是用户主动触发的请求,另外被动接收服务器的返回数据,最后一个是数据上报,行为上报,位置更新等等自定义的后台操作。
![android_perf_3_network_three_type](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca7424810.png)
我们绝对坚决肯定不应该使用Polling(轮询)的方式去执行网络请求,这样不仅仅会造成严重的电量消耗,还会浪费许多网络流量,例如:
![android_perf_3_network_polling](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca772d96a.png)
Android官方推荐使用[Google Cloud Messaging](https://developers.google.com/cloud-messaging/)(在大陆,然并卵),这个框架会帮助把更新的数据推送给手机客户端,效率极高!我们应该遵循下面的规则来处理数据同步的问题:
首先,我们应该使用回退机制来避免固定频繁的同步请求,例如,在发现返回数据相同的情况下,推迟下次的请求时间,如下图所示:
![android_perf_3_network_backoff](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca7ad032a.png)
其次,我们还可以使用**Batching**(批处理)的方式来集中发出请求,避免频繁的间隔请求,如下图所示:
![android_perf_3_network_batching](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca7dd5b82.png)
最后,我们还可以使用**Prefetching**(预取)的技术提前把一些数据拿到,避免后面频繁再次发起网络请求,如下图所示:
![android_perf_3_network_prefetching](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca7faaf97.png)
Google Play Service中提供了一个叫做[GCMNetworkManager](https://developers.google.com/cloud-messaging/network-manager)的类来帮助我们实现上面的那些功能,我们只需要调用对应的API,设置一些简单的参数,其余的工作就都交给Google来帮我们实现了。
![android_perf_3_network_gcm_network_manager](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca81c61cb.png)
### 9.2)压缩传输数据
关于压缩传输数据,我们可以学习以下的一些课程(真的够喝好几壶了):
* [CompressorHead](https://www.youtube.com/playlist?list=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H):这系列的课程会介绍压缩的基本概念以及一些常见的压缩算法知识。
* [Image Compression](http://www.html5rocks.com/en/tutorials/speed/img-compression/):介绍关于图片的压缩知识。
* [Texture Wranglin](http://www.gdcvault.com/play/1020682/Texture-Wranglin-Getting-your-Android):介绍了游戏开发相关的知识。
* [Grabby](https://www.youtube.com/watch?v=P7riQin9Bfo&feature=iv&src_vid=l5mE3Tpjejs&annotation_id=annotation_3146342489):介绍了游戏开发相关的知识。
* [Gzip is not enough](https://www.youtube.com/watch?v=whGwm0Lky2s&feature=iv&src_vid=l5mE3Tpjejs&annotation_id=annotation_1270272007)
* [Text Compression](http://www.html5rocks.com/en/tutorials/speed/txt-compression/)
* [FlatBuffers](https://www.youtube.com/watch?v=iQTxMkSJ1dQ&feature=iv&src_vid=l5mE3Tpjejs&annotation_id=annotation_632816183)
## 10)Effective Network Batching
在性能优化课程的第一季与第二季里面,我们都有提到过下面这样一个网络请求与电量消耗的示意图:
![android_perf_3_batching_networking](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca83eab00.png)
发起网络请求与接收返回数据都是比较耗电的,在网络硬件模块被激活之后,会继续保持几十秒的电量消耗,直到没有新的网络操作行为之后,才会进入休眠状态。前面一个段落介绍了使用Batching的技术来捆绑网络请求,从而达到减少网络请求的频率。那么如何实现Batching技术呢?通常来说,我们可以会把那些发出的网络请求,先暂存到一个PendingQueue里面,等到条件合适的时候再触发Queue里面的网络请求。
![android_perf_3_batching_queue](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca8620c7b.png)
可是什么时候才算是条件合适了呢?最简单粗暴的,例如我们可以在Queue大小到10的时候触发任务,也可以是当手机开始充电,或者是手机连接到WiFi等情况下才触发队列中的任务。手动编写代码去实现这些功能会比较复杂繁琐,Google为了解决这个问题,为我们提供了GCMNetworkManager来帮助实现那些功能,仅仅只需要调用API,设置触发条件,然后就OK了。
## 11)Optimizing Network Request Frequencies
前面的段落已经提到了应该减少网络请求的频率,这是为了减少电量的消耗。我们可以使用Batching,Prefetching的技术来避免频繁的网络请求。Google提供了GCMNetworkManager来帮助开发者实现那些功能,通过提供的API,我们可以选择在接入WiFi,开始充电,等待移动网络被激活等条件下再次激活网络请求。
## 12)Effective Prefetching
假设我们有这样的一个场景,最开始网络请求了一张图片,隔了10秒需要请求另外一张图片,再隔6秒会请求第三张图片,如下图所示:
![android_perf_3_prefetching](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca8773690.png)
类似上面的情况会频繁触发网络请求,但是如果我们能够预先请求后续可能会使用到网络资源,避免频繁的触发网络请求,这样就能够显著的减少电量的消耗。可是预先获取多少数据量是很值得考量的,因为如果预取数据量偏少,就起不到减少频繁请求的作用,可是如果预取数据过多,就会造成资源的浪费。
![android_perf_3_prefetching_over](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ca8f14238.png)
我们可以参考在WiFi,4G,3G等不同的网络下设计不同大小的预取数据量,也可以是按照图片数量或者操作时间来作为阀值。这需要我们需要根据特定的场景,不同的网络情况设计合适的方案。
第二季
最后更新于:2022-04-01 02:41:23
> 原文出处:http://hukai.me/android-performance-patterns-season-2/
![document/2015-08-21/55d6cb600dd32](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/document_2015-08-21_55d6cb600dd32.png)
Google前几天刚发布了[Android性能优化典范第2季](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE)的课程,一共20个短视频,包括的内容大致有:电量优化,网络优化,Wear上如何做优化,使用对象池来提高效率,LRU Cache,Bitmap的缩放,缓存,重用,PNG压缩,自定义View的性能,提升设置alpha之后View的渲染性能,以及Lint,StictMode等等工具的使用技巧。 下面是对这些课程的总结摘要,认知有限,理解偏差的地方请多多指教!
## 1)Battery Drain and Networking
对于手机程序,网络操作相对来说是比较耗电的行为。优化网络操作能够显著节约电量的消耗。在性能优化第1季里面有提到过,手机硬件的各个模块的耗电量是不一样的,其中移动蜂窝模块对电量消耗是比较大的,另外蜂窝模块在不同工作强度下,对电量的消耗也是有差异的。当程序想要执行某个网络请求之前,需要先唤醒设备,然后发送数据请求,之后等待返回数据,最后才慢慢进入休眠状态。这个流程如下图所示:
![android_perf_2_network_request_mode](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9192d59a.png)
在上面那个流程中,蜂窝模块的电量消耗差异如下图所示:
![android_perf_2_battery_drain_mode](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c91fd5125.png)
从图示中可以看到,激活瞬间,发送数据的瞬间,接收数据的瞬间都有很大的电量消耗,所以,我们应该从如何传递网络数据以及何时发起网络请求这两个方面来着手优化。
#### 1.1)何时发起网络请求
首先我们需要区分哪些网络请求是需要及时返回结果的,哪些是可以延迟执行的。例如,用户主动下拉刷新列表,这种行为需要立即触发网络请求,并等待数据返回。但是对于上传用户操作的数据,同步程序设置等等行为则属于可以延迟的行为。我们可以通过Battery Historian这个工具来查看关于移动蜂窝模块的电量消耗(关于这部分的细节,请点击[Android性能优化之电量篇](http://hukai.me/android-performance-battery/))。在Mobile Radio那一行会显示蜂窝模块的电量消耗情况,红色的部分代表模块正在工作,中间的间隔部分代表模块正在休眠状态,如果看到有一段区间,红色与间隔频繁的出现,那就说明这里有可以优化的行为。如下图所示:
![android_perf_2_battery_mobile_radio](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c92215fd2.png)
对于上面可以优化的部分,我们可以有针对性的把请求行为捆绑起来,延迟到某个时刻统一发起请求。如下图所示:
![android_perf_2_battery_batch_delay](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c92417450.png)
经过上面的优化之后,我们再回头使用Battery Historian导出电量消耗图,可以看到唤醒状态与休眠状态是连续大块间隔的,这样的话,总体电量的消耗就会变得更少。
![android_perf_2_battery_mobile_radio_2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c92ad6255.png)
当然,我们甚至可以把请求的任务延迟到手机网络切换到WiFi,手机处于充电状态下再执行。在前面的描述过程中,我们会遇到的一个难题是如何把网络请求延迟,并批量进行执行。还好,Android提供了[JobScheduler](http://developer.android.com/intl/zh-cn/reference/android/app/job/JobScheduler.html)来帮助我们达成这个目标。
#### 1.2)如何传递网络数据
关于这部分主要会涉及到Prefetch(预取)与Compressed(压缩)这两个技术。对于Prefetch的使用,我们需要预先判断用户在此次操作之后,后续零散的请求是否很有可能会马上被触发,可以把后面5分钟有可能会使用到的零散请求都一次集中执行完毕。对于Compressed的使用,在上传与下载数据之前,使用CPU对数据进行压缩与解压,可以很大程度上减少网络传输的时间。
想要知道我们的应用程序中网络请求发生的时间,每次请求的数据量等等信息,可以通过Android Studio中的[Networking Traffic Tool](http://developer.android.com/intl/zh-cn/tools/debugging/ddms.html#network)来查看详细的数据,如下图所示:
![android_perf_2_battery_network_tracking](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c92c1910a.png)
## 2)Wear & Sensors
在Android Wear上会大量的使用Sensors来实现某些特殊功能,如何在尽量节约电量的前提下利用好Sensor会是我们需要特别注意的问题。下面会介绍一些在Android Wear上的最佳实践典范。
尽量减少刷新请求,例如我们可以在不需要某些数据的时候尽快注销监听,减小刷新频率,对Sensor的数据做批量处理等等。那么如何做到这些优化呢?
* 首先我们需要尽量使用Android平台提供的既有运动数据,而不是自己去实现监听采集数据,因为大多数Android Watch自身记录Sensor数据的行为是有经过做电量优化的。
* 其次在Activity不需要监听某些Sensor数据的时候需要尽快释放监听注册。
* 还有我们需要尽量控制更新的频率,仅仅在需要刷新显示数据的时候才触发获取最新数据的操作。
* 另外我们可以针对Sensor的数据做批量处理,待数据累积一定次数或者某个程度的时候才更新到UI上。
* 最后当Watch与Phone连接起来的时候,可以把某些复杂操作的事情交给Phone来执行,Watch只需要等待返回的结果。
更对关于Sensors的知识,可以点击[这里](https://www.youtube.com/watch?v=82M8DmFz4P8&index=2&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE)
## 3)Smooth Android Wear Animation
Android Material Design风格的应用采用了大量的动画来进行UI切换,优化动画的性能不仅能够提升用户体验还可以减少电量的消耗,下面会介绍一些简单易行的方法。
在Android里面一个相对操作比较繁重的事情是对Bitmap进行旋转,缩放,裁剪等等。例如在一个圆形的钟表图上,我们把时钟的指针抠出来当做单独的图片进行旋转会比旋转一张完整的圆形图的所形成的帧率要高56%。
![android_perf_2_waer_animation](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9406893a.png)
另外尽量减少每次重绘的元素可以极大的提升性能,假如某个钟表界面上有很多需要显示的复杂组件,我们可以把这些组件做拆分处理,例如把背景图片单独拎出来设置为一个独立的View,通过setLayerType()方法使得这个View强制用Hardware来进行渲染。至于界面上哪些元素需要做拆分,他们各自的更新频率是多少,需要有针对性的单独讨论。
如何使用Systrace等工具来查看某些View的渲染性能,在前面的章节里面有提到过,感兴趣的可以点击[这里](http://hukai.me/android-performance-render/)
对于大多数应用中的动画,我们会使用PropertyAnimation或者ViewAnimation来操作实现,Android系统会自动对这些Animation做一定的优化处理,在Android上面学习到的大多数性能优化的知识同样也适用于Android Wear。
想要获取更多关于Android Wear中动画效果的优化,请点击[WatchFace](http://developer.android.com/samples/WatchFace/index.html)这个范例。
## 4)Android Wear Data Batching
在Android Training里面有关于Wear上面如何利用Wearable API与Phone进行沟通协作的课程(详情请点击[这里](http://developer.android.com/training/wearables/data-layer/index.html))。因为Phone的CPU与电量都比Wear要强大,另外Phone还可以直接接入网络,而Wear要接入网络则相对更加困难,所以我们在开发Wear应用的时候需要尽量做到把复杂的操作交给Phone来执行。例如我们可以让Phone来获取天气信息,然后把数据返回Wear进行显示。更进一步,在之前的性能优化课程里面我们有学习过如何使用JobScheduler来延迟批量处理任务,假设Phone收到来自Wear的其中一个任务是每隔5分钟检查一次天气情况,那么Phone使用JobScheduler执行检查天气任务之后,先判断这次返回的结果和之前是否有差异,仅仅当天气发生变化的时候,才有必要把结果通知到Wear,或者仅仅把变化的某一项数据通知给Wear,这样可以更大程度上减少Wear的电量消耗。
下面我们总结一下如何优化Wear的性能与电量:
* 仅仅在真正需要刷新界面的时候才发出请求
* 尽量把计算复杂操作的任务交给Phone来处理
* Phone仅仅在数据发生变化的时候才通知到Wear
* 把零碎的数据请求捆绑一起再进行操作
## 5)Object Pools
在程序里面经常会遇到的一个问题是短时间内创建大量的对象,导致内存紧张,从而触发GC导致性能问题。对于这个问题,我们可以使用对象池技术来解决它。通常对象池中的对象可能是bitmaps,views,paints等等。关于对象池的操作原理,不展开述说了,请看下面的图示:
![android_perf_2_object_pool](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c941e6570.png)
使用对象池技术有很多好处,它可以避免内存抖动,提升性能,但是在使用的时候有一些内容是需要特别注意的。通常情况下,初始化的对象池里面都是空白的,当使用某个对象的时候先去对象池查询是否存在,如果不存在则创建这个对象然后加入对象池,但是我们也可以在程序刚启动的时候就事先为对象池填充一些即将要使用到的数据,这样可以在需要使用到这些对象的时候提供更快的首次加载速度,这种行为就叫做**预分配**。使用对象池也有不好的一面,程序员需要手动管理这些对象的分配与释放,所以我们需要慎重地使用这项技术,避免发生对象的内存泄漏。为了确保所有的对象能够正确被释放,我们需要保证加入对象池的对象和其他外部对象没有互相引用的关系。
## 6)To Index or Iterate?
遍历容器是编程里面一个经常遇到的场景。在Java语言中,使用Iterate是一个比较常见的方法。可是在Android开发团队中,大家却尽量避免使用Iterator来执行遍历操作。下面我们看下在Android上可能用到的三种不同的遍历方法:
![android_perf_2_iterate_1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c944d1f88.png)
![android_perf_2_iterate_for_loop](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9479663f.png)
![android_perf_2_iterate_simple_loop](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c94e1abad.png)
使用上面三种方式在同一台手机上,使用相同的数据集做测试,他们的表现性能如下所示:
![android_perf_2_iterate_result](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c94f00792.png)
从上面可以看到for index的方式有更好的效率,但是因为不同平台编译器优化各有差异,我们最好还是针对实际的方法做一下简单的测量比较好,拿到数据之后,再选择效率最高的那个方式。
## 7)The Magic of LRU Cache
这小节我们要讨论的是缓存算法,在Android上面最常用的一个缓存算法是LRU(Least Recently Use),关于LRU算法,不展开述说,用下面一张图演示下含义:
![android_perf_2_lru_mode](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c954029af.png)
LRU Cache的基础构建用法如下:
![android_perf_2_lru_key_value](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c957dd0c8.png)
为了给LRU Cache设置一个比较合理的缓存大小值,我们通常是用下面的方法来做界定的:
![android_perf_2_lru_size](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c96bc9b81.png)
使用LRU Cache时为了能够让Cache知道每个加入的Item的具体大小,我们需要Override下面的方法:
![android_perf_2_lru_sizeof](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c96cd9b8d.png)
使用LRU Cache能够显著提升应用的性能,可是也需要注意LRU Cache中被淘汰对象的回收,否者会引起严重的内存泄露。
## 8)Using LINT for Performance Tips
Lint是Android提供的一个静态扫描应用源码并找出其中的潜在问题的一个强大的工具。
![android_perf_2_lint_overview](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c96e67e15.png)
例如,如果我们在onDraw方法里面执行了new对象的操作,Lint就会提示我们这里有性能问题,并提出对应的建议方案。Lint已经集成到Android Studio中了,我们可以手动去触发这个工具,点击工具栏的Analysis -> Inspect Code,触发之后,Lint会开始工作,并把结果输出到底部的工具栏,我们可以逐个查看原因并根据指示做相应的优化修改。
Lint的功能非常强大,他能够扫描各种问题。当然我们可以通过Android Studio设置找到Lint,对Lint做一些定制化扫描的设置,可以选择忽略掉那些不想Lint去扫描的选项,我们还可以针对部分扫描内容修改它的提示优先级。
建议把与内存有关的选项中的严重程度标记为红色的Error,对于Layout的性能问题标记为黄色Warning。
## 9)Hidden Cost of Transparency
这小节会介绍如何减少透明区域对性能的影响。通常来说,对于不透明的View,显示它只需要渲染一次即可,可是如果这个View设置了alpha值,会至少需要渲染两次。原因是包含alpha的view需要事先知道混合View的下一层元素是什么,然后再结合上层的View进行Blend混色处理。
在某些情况下,一个包含alpha的View有可能会触发改View在HierarchyView上的父View都被额外重绘一次。下面我们看一个例子,下图演示的ListView中的图片与二级标题都有设置透明度。
![android_perf_2_trans_listview](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c97063340.png)
大多数情况下,屏幕上的元素都是由后向前进行渲染的。在上面的图示中,会先渲染背景图(蓝,绿,红),然后渲染人物头像图。如果后渲染的元素有设置alpha值,那么这个元素就会和屏幕上已经渲染好的元素做blend处理。很多时候,我们会给整个View设置alpha的来达到fading的动画效果,如果我们图示中的ListView做alpha逐渐减小的处理,我们可以看到ListView上的TextView等等组件会逐渐融合到背景色上。但是在这个过程中,我们无法观察到它其实已经触发了额外的绘制任务,我们的目标是让整个View逐渐透明,可是期间ListView在不停的做Blending的操作,这样会导致不少性能问题。
如何渲染才能够得到我们想要的效果呢?我们可以先按照通常的方式把View上的元素按照从后到前的方式绘制出来,但是不直接显示到屏幕上,而是使用GPU预处理之后,再又GPU渲染到屏幕上,GPU可以对界面上的原始数据直接做旋转,设置透明度等等操作。使用GPU进行渲染,虽然第一次操作相比起直接绘制到屏幕上更加耗时,可是一旦原始纹理数据生成之后,接下去的操作就比较省时省力。
![android_perf_2_trans_hw_layer](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c973498d7.png)
如何才能够让GPU来渲染某个View呢?我们可以通过setLayerType的方法来指定View应该如何进行渲染,从SDK 16开始,我们还可以使用ViewPropertyAnimator.alpha().withLayer()来指定。如下图所示:
![android_perf_2_trans_setlayertype](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c97896420.png)
另外一个例子是包含阴影区域的View,这种类型的View并不会出现我们前面提到的问题,因为他们并不存在层叠的关系。
![android_perf_2_trans_overlap](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c97da785e.png)
为了能够让渲染器知道这种情况,避免为这种View占用额外的GPU内存空间,我们可以做下面的设置。
![android_perf_2_trans_override_lap](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c985c98e2.png)
通过上面的设置以后,性能可以得到显著的提升,如下图所示:
![android_perf_2_trans_overlap_compare](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c987d4b01.png)
## 10)Avoiding Allocations in onDraw()
我们都知道应该避免在onDraw()方法里面执行导致内存分配的操作,下面讲解下为何需要这样做。
首先onDraw()方法是执行在UI线程的,在UI线程尽量避免做任何可能影响到性能的操作。虽然分配内存的操作并不需要花费太多系统资源,但是这并不意味着是免费无代价的。设备有一定的刷新频率,导致View的onDraw方法会被频繁的调用,如果onDraw方法效率低下,在频繁刷新累积的效应下,效率低的问题会被扩大,然后会对性能有严重的影响。
![android_perf_2_ondraw_gc](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c98948cf5.png)
如果在onDraw里面执行内存分配的操作,会容易导致内存抖动,GC频繁被触发,虽然GC后来被改进为执行在另外一个后台线程(GC操作在2.3以前是同步的,之后是并发),可是频繁的GC的操作还是会影响到CPU,影响到电量的消耗。
那么简单解决频繁分配内存的方法就是把分配操作移动到onDraw()方法外面,通常情况下,我们会把onDraw()里面new Paint的操作移动到外面,如下面所示:
![android_perf_2_ondraw_paint](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c98bbc0c9.png)
## 11)Tool: Strict Mode
UI线程被阻塞超过5秒,就会出现ANR,这太糟糕了。防止程序出现ANR是很重要的事情,那么如何找出程序里面潜在的坑,预防ANR呢?很多大部分情况下执行很快的方法,但是他们有可能存在巨大的隐患,这些隐患的爆发就很容易导致ANR。
Android提供了一个叫做Strict Mode的工具,我们可以通过手机设置里面的开发者选项,打开Strict Mode选项,如果程序存在潜在的隐患,屏幕就会闪现红色。我们也可以通过[StrictMode](http://developer.android.com/reference/android/os/StrictMode.html) API在代码层面做细化的跟踪,可以设置StrictMode监听那些潜在问题,出现问题时如何提醒开发者,可以对屏幕闪红色,也可以输出错误日志。下面是官方的代码示例:
|
~~~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~~~
|
~~~
public void onCreate() {
if (DEVELOPER_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build());
}
super.onCreate();
}
~~~
|
## 12)Custom Views and Performance
Android系统有提供超过70多种标准的View,例如TextView,ImageView,Button等等。在某些时候,这些标准的View无法满足我们的需要,那么就需要我们自己来实现一个View,这节会介绍如何优化自定义View的性能。
通常来说,针对自定义View,我们可能犯下面三个错误:
* **Useless calls to onDraw():**我们知道调用View.invalidate()会触发View的重绘,有两个原则需要遵守,第1个是仅仅在View的内容发生改变的时候才去触发invalidate方法,第2个是尽量使用ClipRect等方法来提高绘制的性能。
* **Useless pixels:**减少绘制时不必要的绘制元素,对于那些不可见的元素,我们需要尽量避免重绘。
* **Wasted CPU cycles:**对于不在屏幕上的元素,可以使用Canvas.quickReject把他们给剔除,避免浪费CPU资源。另外尽量使用GPU来进行UI的渲染,这样能够极大的提高程序的整体表现性能。
最后请时刻牢记,尽量提高View的绘制性能,这样才能保证界面的刷新帧率尽量的高。更多关于这部分的内容,可以看[这里](http://hukai.me/android-performance-patterns/)
## 13)Batching Background Work Until Later
优化性能时大多数时候讨论的都是如何减少不必要的操作,但是选择何时去执行某些操作同样也很重要。在[第1季](http://hukai.me/android-performance-patterns/)以及上一期的[性能优化之电量篇](http://hukai.me/android-performance-battery/)里面,我们有提到过移动蜂窝模块的电量消耗模型。为了避免我们的应用程序过多的频繁消耗电量,我们需要学习如何把后台任务打包批量,并选择一个合适的时机进行触发执行。下图是每个应用程序各自执行后台任务导致的电量消耗示意图:
![android_perf_2_batching_bg_1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c98e40193.png)
因为像上面那样做会导致浪费很多电量,我们需要做的是把部分应用的任务延迟处理,等到一定时机,这些任务一并进行处理。结果如下面的示意图:
![android_perf_2_batching_bg_2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c991093ce.png)
执行延迟任务,通常有下面三种方式:
#### 1)AlarmManager
使用AlarmManager设置定时任务,可以选择精确的间隔时间,也可以选择非精确时间作为参数。除非程序有很强烈的需要使用精确的定时唤醒,否者一定要避免使用他,我们应该尽量使用非精确的方式。
#### 2)SyncAdapter
我们可以使用SyncAdapter为应用添加设置账户,这样在手机设置的账户列表里面可以找到我们的应用。这种方式功能更多,但是实现起来比较复杂。我们可以从这里看到官方的培训课程:[http://developer.android.com/training/sync-adapters/index.html](http://developer.android.com/training/sync-adapters/index.html)
#### 3)JobSchedulor
这是最简单高效的方法,我们可以设置任务延迟的间隔,执行条件,还可以增加重试机制。
## 14)Smaller Pixel Formats
常见的png,jpeg,webp等格式的图片在设置到UI上之前需要经过解码的过程,而解压时可以选择不同的解码率,不同的解码率对内存的占用是有很大差别的。在不影响到画质的前提下尽量减少内存的占用,这能够显著提升应用程序的性能。
Android的Heap空间是不会自动做兼容压缩的,意思就是如果Heap空间中的图片被收回之后,这块区域并不会和其他已经回收过的区域做重新排序合并处理,那么当一个更大的图片需要放到heap之前,很可能找不到那么大的连续空闲区域,那么就会触发GC,使得heap腾出一块足以放下这张图片的空闲区域,如果无法腾出,就会发生OOM。如下图所示:
![android_perf_2_pixel_heap_free](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c99d30d58.png)
所以为了避免加载一张超大的图片,需要尽量减少这张图片所占用的内存大小,Android为图片提供了4种解码格式,他们分别占用的内存大小如下图所示:
![android_perf_2_pixel_format](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c99edc7a3.png)
随着解码占用内存大小的降低,清晰度也会有损失。我们需要针对不同的应用场景做不同的处理,大图和小图可以采用不同的解码率。在Android里面可以通过下面的代码来设置解码率:
![android_perf_2_pixel_decode](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9a1b9898.png)
## 15)Smaller PNG Files
尽量减少PNG图片的大小是Android里面很重要的一条规范。相比起JPEG,PNG能够提供更加清晰无损的图片,但是PNG格式的图片会更大,占用更多的磁盘空间。到底是使用PNG还是JPEG,需要设计师仔细衡量,对于那些使用JPEG就可以达到视觉效果的,可以考虑采用JPEG即可。我们可以通过Google搜索到很多关于PNG压缩的工具,如下图所示:
![android_perf_2_png_tools](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9a42e135.png)
这里要介绍一种新的图片格式:Webp,它是由Google推出的一种既保留png格式的优点,又能够减少图片大小的一种新型图片格式。关于Webp的更多细节,请点击[这里](https://developers.google.com/speed/webp/?csw=1)
## 16)Pre-scaling Bitmaps
对bitmap做缩放,这也是Android里面最遇到的问题。对bitmap做缩放的意义很明显,提示显示性能,避免分配不必要的内存。Android提供了现成的bitmap缩放的API,叫做createScaledBitmap(),使用这个方法可以获取到一张经过缩放的图片。
![android_perf_2_sacle_bitmap_created](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9a6248c0.png)
上面的方法能够快速的得到一张经过缩放的图片,可是这个方法能够执行的前提是,原图片需要事先加载到内存中,如果原图片过大,很可能导致OOM。下面介绍其他几种缩放图片的方式。
inSampleSize能够等比的缩放显示图片,同时还避免了需要先把原图加载进内存的缺点。我们会使用类似像下面一样的方法来缩放bitmap:
![android_perf_2_sacle_bitmap_code](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9a797465.png)
![android_perf_2_sacle_bitmap_insamplesize](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9a97e42c.png)
另外,我们还可以使用inScaled,inDensity,inTargetDensity的属性来对解码图片做处理,源码如下图所示:
![android_perf_2_sacle_bitmap_inscale](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9aaa81d9.png)
还有一个经常使用到的技巧是inJustDecodeBounds,使用这个属性去尝试解码图片,可以事先获取到图片的大小而不至于占用什么内存。如下图所示:
![android_perf_2_sacle_bitmap_injust](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9aed2795.png)
## 17)Re-using Bitmaps
我们知道bitmap会占用大量的内存空间,这节会讲解什么是inBitmap属性,如何利用这个属性来提升bitmap的循环效率。前面我们介绍过使用对象池的技术来解决对象频繁创建再回收的效率问题,使用这种方法,bitmap占用的内存空间会差不多是恒定的数值,每次新创建出来的bitmap都会需要占用一块单独的内存区域,如下图所示:
![android_perf_2_inbitmap_old](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9afd325e.png)
为了解决上图所示的效率问题,Android在解码图片的时候引进了**inBitmap**属性,使用这个属性可以得到下图所示的效果:
![android_perf_2_inbitmap_new](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9b18b047.png)
使用inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的bitmap会尝试去使用之前那张bitmap在heap中所占据的pixel data内存区域,而不是去问内存重新申请一块区域来存放bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小。下面是如何使用inBitmap的代码示例:
![android_perf_2_inbitmap_code](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9b8552dc.png)
使用inBitmap需要注意几个限制条件:
* 在SDK 11 -> 18之间,重用的bitmap大小必须是一致的,例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为100-100才能够被重用。从SDK 19开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小。
* 新申请的bitmap与旧的bitmap必须有相同的解码格式,例如大家都是8888的,如果前面的bitmap是8888,那么就不能支持4444与565格式的bitmap了。
我们可以创建一个包含多种典型可重用bitmap的对象池,这样后续的bitmap创建都能够找到合适的“模板”去进行重用。如下图所示:
![android_perf_2_inbitmap_pool](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c9ba409fb.png)
Google介绍了一个开源的加载bitmap的库:[Glide](https://github.com/bumptech/glide),这里面包含了各种对bitmap的优化技巧。
## 18)The Performance Lifecycle
大多数开发者在没有发现严重性能问题之前是不会特别花精力去关注性能优化的,通常大家关注的都是功能是否实现。当性能问题真的出现的时候,请不要慌乱。我们通常采用下面三个步骤来解决性能问题。
#### Gather:收集数据
我们可以通过Android SDK里面提供的诸多工具来收集CPU,GPU,内存,电量等等性能数据,
#### Insight:分析数据
通过上面的步骤,我们获取到了大量的数据,下一步就是分析这些数据。工具帮我们生成了很多可读性强的表格,我们需要事先了解如何查看表格的数据,每一项代表的含义,这样才能够快速定位问题。如果分析数据之后还是没有找到问题,那么就只能不停的重新收集数据,再进行分析,如此循环。
#### Action:解决问题
定位到问题之后,我们需要采取行动来解决问题。解决问题之前一定要先有个计划,评估这个解决方案是否可行,是否能够及时的解决问题。
## 19)Tools not Rules
虽然前面介绍了很多调试的方法,处理技巧,规范建议等等,可是这并不意味着所有的情况都适用,我们还是需要根据当时的情景做特定灵活的处理。
## 20)Memory Profiling 101
围绕Android生态系统,不仅仅有Phone,还有Wear,TV,Auto等等。对这些不同形态的程序进行性能优化,都离不开内存调试这个步骤。这节中介绍的内容大部分和[Android性能优化典范](http://hukai.me/android-performance-patterns/)与[Android性能优化之内存篇](http://hukai.me/android-performance-memory/)重合,不展开了。
第一季
最后更新于:2022-04-01 02:41:21
> 原文出处: http://hukai.me/android-performance-patterns/
![document/2015-08-21/55d6cb4ea9659](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/document_2015-08-21_55d6cb4ea9659.png)
2015新年伊始,Google发布了关于[Android性能优化典范的专题](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE),一共16个短视频,每个3-5分钟,帮助开发者创建更快更优秀的Android App。课程专题不仅仅介绍了Android系统中有关性能问题的底层工作原理,同时也介绍了如何通过工具来找出性能问题以及提升性能的建议。主要从三个方面展开,Android的渲染机制,内存与GC,电量优化。下面是对这些问题和建议的总结梳理。
## 0)Render Performance
大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。从设计师的角度,他们希望App能够有更多的动画,图片等时尚元素来实现流畅的用户体验。但是Android系统很有可能无法及时完成那些复杂的界面渲染操作。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c812a9341.png)
如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的会是同一帧画面。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c82646579.png)
用户容易在UI执行动画或者滑动ListView的时候感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。有很多原因可以导致丢帧,也许是因为你的layout太过复杂,无法在16ms内完成渲染,有可能是因为你的UI上有层叠太多的绘制单元,还有可能是因为动画执行的次数过多。这些都会导致CPU或者GPU负载过重。
我们可以通过一些工具来定位问题,比如可以使用HierarchyViewer来查找Activity中的布局是否过于复杂,也可以使用手机设置里面的开发者选项,打开Show GPU Overdraw等选项进行观察。你还可以使用TraceView来观察CPU的执行情况,更加快捷的找到性能瓶颈。
## 1)Understanding Overdraw
Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c82884789.png)
当设计上追求更华丽的视觉效果的时候,我们就容易陷入采用越来越多的层叠组件来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳的性能,我们必须尽量减少Overdraw的情况发生。
幸运的是,我们可以通过手机设置里面的开发者选项,打开Show GPU Overdraw的选项,可以观察UI上的Overdraw情况。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c82eeddb1.png)
蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。
Overdraw有时候是因为你的UI布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景。例如某个Activity有一个背景,然后里面的Layout又有自己的背景,同时子View又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就能够减少大量的红色Overdraw区域,增加蓝色区域的占比。这一措施能够显著提升程序性能。
## 2)Understanding VSYNC
为了理解App是如何进行渲染的,我们必须了解手机硬件是如何工作,那么就必须理解什么是*VSYNC*。
在讲解VSYNC之前,我们需要了解两个相关的概念:
* Refresh Rate:代表了屏幕在一秒内刷新屏幕的次数,这取决于硬件的固定参数,例如60Hz。
* Frame Rate:代表了GPU在一秒内绘制操作的帧数,例如30fps,60fps。
GPU会获取图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,他们两者不停的进行协作。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8319ad5d.png)
不幸的是,刷新频率和帧率并不是总能够保持相同的节奏。如果发生帧率与刷新频率不一致的情况,就会容易出现**Tearing**的现象(画面上下两部分显示内容发生断裂,来自不同的两帧数据发生重叠)。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8368a3d3.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c83d08730.png)
理解图像渲染里面的双重与三重缓存机制,这个概念比较复杂,请移步查看这里:[http://source.android.com/devices/graphics/index.html](http://source.android.com/devices/graphics/index.html),还有这里[http://article.yeeyan.org/view/37503/304664](http://article.yeeyan.org/view/37503/304664)。
通常来说,帧率超过刷新频率只是一种理想的状况,在超过60fps的情况下,GPU所产生的帧数据会因为等待VSYNC的刷新信息而被Hold住,这样能够保持每次刷新都有实际的新的数据可以显示。但是我们遇到更多的情况是帧率小于刷新频率。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8437756c.png)
在这种情况下,某些帧显示的画面内容就会与上一帧的画面相同。糟糕的事情是,帧率从超过60fps突然掉到60fps以下,这样就会发生**LAG**,**JANK**,**HITCHING**等卡顿掉帧的不顺滑的情况。这也是用户感受不好的原因所在。
## 3)Tool:Profile GPU Rendering
性能问题如此的麻烦,幸好我们可以有工具来进行调试。打开手机里面的开发者选项,选择Profile GPU Rendering,选中On screen as bars的选项。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8490c5a2.png)
选择了这样以后,我们可以在手机画面上看到丰富的GPU绘制图形信息,分别关于StatusBar,NavBar,激活的程序Activity区域的GPU Rending信息。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c84fde3eb.png)
随着界面的刷新,界面上会滚动显示垂直的柱状图来表示每帧画面所需要渲染的时间,柱状图越高表示花费的渲染时间越长。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c852ca9b0.png)
中间有一根绿色的横线,代表16ms,我们需要确保每一帧花费的总时间都低于这条横线,这样才能够避免出现卡顿的问题。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c85c13e18.png)
每一条柱状线都包含三部分,蓝色代表测量绘制Display List的时间,红色代表OpenGL渲染Display List所需要的时间,黄色代表CPU等待GPU处理的时间。
## 4)Why 60fps?
我们通常都会提到60fps与16ms,可是知道为何会是以程序是否达到60fps来作为App性能的衡量标准吗?这是因为人眼与大脑之间的协作无法感知超过60fps的画面更新。
12fps大概类似手动快速翻动书籍的帧率,这明显是可以感知到不够顺滑的。24fps使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的效果。24fps是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。但是低于30fps是无法顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果,当然超过60fps是没有必要的。
开发app的性能目标就是保持60fps,这意味着每一帧你只有16ms=1000/60的时间来处理所有的任务。
## 5)Android, UI and the GPU
了解Android是如何利用GPU进行画面渲染有助于我们更好的理解性能问题。那么一个最实际的问题是:activity的画面是如何绘制到屏幕上的?那些复杂的XML布局文件又是如何能够被识别并绘制出来的?
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8672bbe3.png)
**Resterization栅格化**是绘制那些Button,Shape,Path,String,Bitmap等组件最基础的操作。它把那些组件拆分到不同的像素上进行显示。这是一个很费时的操作,GPU的引入就是为了加快栅格化的操作。
CPU负责把UI组件计算成Polygons,Texture纹理,然后交给GPU进行栅格化渲染。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c86ccecc0.png)
然而每次从CPU转移到GPU是一件很麻烦的事情,所幸的是OpenGL ES可以把那些需要渲染的纹理Hold在GPU Memory里面,在下次需要渲染的时候直接进行操作。所以如果你更新了GPU所hold住的纹理内容,那么之前保存的状态就丢失了。
在Android里面那些由主题所提供的资源,例如Bitmaps,Drawables都是一起打包到统一的Texture纹理当中,然后再传递到GPU里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。当然随着UI组件的越来越丰富,有了更多演变的形态。例如显示图片的时候,需要先经过CPU的计算加载到内存中,然后传递给GPU进行渲染。文字的显示更加复杂,需要先经过CPU换算成纹理,然后再交给GPU进行渲染,回到CPU绘制单个字符的时候,再重新引用经过GPU渲染的内容。动画则是一个更加复杂的操作流程。
为了能够使得App流畅,我们需要在每一帧16ms以内处理完所有的CPU与GPU计算,绘制,渲染等等操作。
## 6)Invalidations, Layouts, and Performance
顺滑精妙的动画是app设计里面最重要的元素之一,这些动画能够显著提升用户体验。下面会讲解Android系统是如何处理UI组件的更新操作的。
通常来说,Android需要把XML布局文件转换成GPU能够识别并绘制的对象。这个操作是在**DisplayList**的帮助下完成的。DisplayList持有所有将要交给GPU绘制到屏幕上的数据信息。
在某个View第一次需要被渲染时,DisplayList会因此而被创建,当这个View要显示到屏幕上时,我们会执行GPU的绘制指令来进行渲染。如果你在后续有执行类似移动这个View的位置等操作而需要再次渲染这个View时,我们就仅仅需要额外操作一次渲染指令就够了。然而如果你修改了View中的某些可见组件,那么之前的DisplayList就无法继续使用了,我们需要回头重新创建一个DisplayList并且重新执行渲染指令并更新到屏幕上。
需要注意的是:任何时候View中的绘制内容发生变化时,都会重新执行创建DisplayList,渲染DisplayList,更新到屏幕上等一系列操作。这个流程的表现性能取决于你的View的复杂程度,View的状态变化以及渲染管道的执行性能。举个例子,假设某个Button的大小需要增大到目前的两倍,在增大Button大小之前,需要通过父View重新计算并摆放其他子View的位置。修改View的大小会触发整个HierarcyView的重新计算大小的操作。如果是修改View的位置则会触发HierarchView重新计算其他View的位置。如果布局很复杂,这就会很容易导致严重的性能问题。我们需要尽量减少Overdraw。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c86f77b0b.png)
我们可以通过前面介绍的Monitor GPU Rendering来查看渲染的表现性能如何,另外也可以通过开发者选项里面的Show GPU view updates来查看视图更新的操作,最后我们还可以通过HierarchyViewer这个工具来查看布局,使得布局尽量扁平化,移除非必需的UI组件,这些操作能够减少Measure,Layout的计算时间。
## 7)Overdraw, Cliprect, QuickReject
引起性能问题的一个很重要的方面是因为过多复杂的绘制操作。我们可以通过工具来检测并修复标准UI组件的Overdraw问题,但是针对高度自定义的UI组件则显得有些力不从心。
有一个窍门是我们可以通过执行几个APIs方法来显著提升绘制操作的性能。前面有提到过,非可见的UI组件进行绘制更新会导致Overdraw。例如Nav Drawer从前置可见的Activity滑出之后,如果还继续绘制那些在Nav Drawer里面不可见的UI组件,这就导致了Overdraw。为了解决这个问题,Android系统会通过避免绘制那些完全不可见的组件来尽量减少Overdraw。那些Nav Drawer里面不可见的View就不会被执行浪费资源。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c87197111.png)
但是不幸的是,对于那些过于复杂的自定义的View(重写了onDraw方法),Android系统无法检测具体在onDraw里面会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过[canvas.clipRect()](http://developer.android.com/reference/android/graphics/Canvas.html)来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c881a1e25.png)
除了clipRect方法之外,我们还可以使用[canvas.quickreject()](http://developer.android.com/reference/android/graphics/Canvas.html)来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。做了那些优化之后,我们可以通过上面介绍的Show GPU Overdraw来查看效果。
## 8)Memory Churn and performance
虽然Android有自动管理内存的机制,但是对内存的不恰当使用仍然容易引起严重的性能问题。在同一帧里面创建过多的对象是件需要特别引起注意的事情。
Android系统里面有一个**Generational Heap Memory**的模型,系统会根据内存中不同的内存数据类型分别执行不同的GC操作。例如,最近刚分配的对象会放在Young Generation区域,这个区域的对象通常都是会快速被创建并且很快被销毁回收的,同时这个区域的GC操作速度也是比Old Generation区域的GC操作速度更快的。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c88845eec.png)
除了速度差异之外,执行GC操作的时候,所有线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c88a1c9e7.png)
通常来说,单个的GC并不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了。
导致GC频繁执行有两个原因:
* **Memory Churn内存抖动**,内存抖动是因为大量的对象被创建又在短时间内马上被释放。
* 瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c88c08a71.png)
解决上面的问题有简洁直观方法,如果你在**Memory Monitor**里面查看到短时间发生了多次内存的涨跌,这意味着很有可能发生了内存抖动。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c88f389b7.png)
同时我们还可以通过**Allocation Tracker**来查看在短时间内,同一个栈中不断进出的相同对象。这是内存抖动的典型信号之一。
当你大致定位问题之后,接下去的问题修复也就显得相对直接简单了。例如,你需要避免在for循环里面分配对象占用内存,需要尝试把对象的创建移到循环体之外,自定义View中的onDraw方法也需要引起注意,每次屏幕发生绘制以及动画执行过程中,onDraw方法都会被调用到,避免在onDraw方法里面执行复杂的操作,避免创建对象。对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,但是这里需要注意结束使用之后,需要手动释放对象池中的对象。
## 9)Garbage Collection in Android
JVM的回收机制给开发人员带来很大的好处,不用时刻处理对象的分配与回收,可以更加专注于更加高级的代码实现。相比起Java,C与C++等语言具备更高的执行效率,他们需要开发人员自己关注对象的分配与回收,但是在一个庞大的系统当中,还是免不了经常发生部分对象忘记回收的情况,这就是内存泄漏。
原始JVM中的GC机制在Android中得到了很大程度上的优化。Android里面是一个三级Generation的内存模型,最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后到Permanent Generation区域。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c894cc04c.png)
每一个级别的内存区域都有固定的大小,此后不断有新的对象被分配到此区域,当这些对象总的大小快达到这一级别内存区域的阀值时,会触发GC的操作,以便腾出空间来存放其他新的对象。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8966ef1a.png)
前面提到过每次GC发生的时候,所有的线程都是暂停状态的。GC所占用的时间和它是哪一个Generation也有关系,Young Generation的每次GC操作时间是最短的,Old Generation其次,Permanent Generation最长。执行时间的长短也和当前Generation中的对象数量有关,遍历查找20000个对象比起遍历50个对象自然是要慢很多的。
虽然Google的工程师在尽量缩短每次GC所花费的时间,但是特别注意GC引起的性能问题还是很有必要。如果不小心在最小的for循环单元里面执行了创建对象的操作,这将很容易引起GC并导致性能问题。通过Memory Monitor我们可以查看到内存的占用情况,每一次瞬间的内存降低都是因为此时发生了GC操作,如果在短时间内发生大量的内存上涨与降低的事件,这说明很有可能这里有性能问题。我们还可以通过**Heap and Allocation Tracker**工具来查看此时内存中分配的到底有哪些对象。
## 10)Performance Cost of Memory Leaks
虽然Java有自动回收的机制,可是这不意味着Java中不存在内存泄漏的问题,而内存泄漏会很容易导致严重的性能问题。
内存泄漏指的是那些程序不再使用的对象无法被GC识别,这样就导致这个对象一直留在内存当中,占用了宝贵的内存空间。显然,这还使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,从而引起性能问题。
寻找内存泄漏并修复这个漏洞是件很棘手的事情,你需要对执行的代码很熟悉,清楚的知道在特定环境下是如何运行的,然后仔细排查。例如,你想知道程序中的某个activity退出的时候,它之前所占用的内存是否有完整的释放干净了?首先你需要在activity处于前台的时候使用Heap Tool获取一份当前状态的内存快照,然后你需要创建一个几乎不这么占用内存的空白activity用来给前一个Activity进行跳转,其次在跳转到这个空白的activity的时候主动调用System.gc()方法来确保触发一个GC操作。最后,如果前面这个activity的内存都有全部正确释放,那么在空白activity被启动之后的内存快照中应该不会有前面那个activity中的任何对象了。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8997a691.png)
如果你发现在空白activity的内存快照中有一些可疑的没有被释放的对象存在,那么接下去就应该使用**Alocation Track Tool**来仔细查找具体的可疑对象。我们可以从空白activity开始监听,启动到观察activity,然后再回到空白activity结束监听。这样操作以后,我们可以仔细观察那些对象,找出内存泄漏的真凶。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8a887c30.png)
## 11)Memory Performance
通常来说,Android对GC做了大量的优化操作,虽然执行GC操作的时候会暂停其他任务,可是大多数情况下,GC操作还是相对很安静并且高效的。但是如果我们对内存的使用不恰当,导致GC频繁执行,这样就会引起不小的性能问题。
为了寻找内存的性能问题,Android Studio提供了工具来帮助开发者。
* **Memory Monitor:**查看整个app所占用的内存,以及发生GC的时刻,短时间内发生大量的GC操作是一个危险的信号。
* **Allocation Tracker:**使用此工具来追踪内存的分配,前面有提到过。
* **Heap Tool:**查看当前内存快照,便于对比分析哪些对象有可能是泄漏了的,请参考前面的Case。
## 12)Tool - Memory Monitor
Android Studio中的Memory Monitor可以很好的帮助我们查看程序的内存使用情况。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8b800a63.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8ba7622f.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8beb0981.png)
## 13)Battery Performance
电量其实是目前手持设备最宝贵的资源之一,大多数设备都需要不断的充电来维持继续使用。不幸的是,对于开发者来说,电量优化是他们最后才会考虑的的事情。但是可以确定的是,千万不能让你的应用成为消耗电量的大户。
Purdue University研究了最受欢迎的一些应用的电量消耗,平均只有30%左右的电量是被程序最核心的方法例如绘制图片,摆放布局等等所使用掉的,剩下的70%左右的电量是被上报数据,检查位置信息,定时检索后台广告信息所使用掉的。如何平衡这两者的电量消耗,就显得非常重要了。
有下面一些措施能够显著减少电量的消耗:
* 我们应该尽量减少唤醒屏幕的次数与持续的时间,使用WakeLock来处理唤醒的问题,能够正确执行唤醒操作并根据设定及时关闭操作进入睡眠状态。
* 某些非必须马上执行的操作,例如上传歌曲,图片处理等,可以等到设备处于充电状态或者电量充足的时候才进行。
* 触发网络请求的操作,每次都会保持无线信号持续一段时间,我们可以把零散的网络请求打包进行一次操作,避免过多的无线信号引起的电量消耗。关于网络请求引起无线信号的电量消耗,还可以参考这里[http://hukai.me/android-training-course-in-chinese/connectivity/efficient-downloads/efficient-network-access.html](http://hukai.me/android-training-course-in-chinese/connectivity/efficient-downloads/efficient-network-access.html)
我们可以通过手机设置选项找到对应App的电量消耗统计数据。我们还可以通过**Battery Historian Tool**来查看详细的电量消耗。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8c2476f4.png)
如果发现我们的App有电量消耗过多的问题,我们可以使用JobScheduler API来对一些任务进行定时处理,例如我们可以把那些任务重的操作等到手机处于充电状态,或者是连接到WiFi的时候来处理。 关于JobScheduler的更多知识可以参考[http://hukai.me/android-training-course-in-chinese/background-jobs/scheduling/index.html](http://hukai.me/android-training-course-in-chinese/background-jobs/scheduling/index.html)
## 14)Understanding Battery Drain on Android
电量消耗的计算与统计是一件麻烦而且矛盾的事情,记录电量消耗本身也是一个费电量的事情。唯一可行的方案是使用第三方监测电量的设备,这样才能够获取到真实的电量消耗。
当设备处于待机状态时消耗的电量是极少的,以N5为例,打开飞行模式,可以待机接近1个月。可是点亮屏幕,硬件各个模块就需要开始工作,这会需要消耗很多电量。
使用WakeLock或者JobScheduler唤醒设备处理定时的任务之后,一定要及时让设备回到初始状态。每次唤醒无线信号进行数据传递,都会消耗很多电量,它比WiFi等操作更加的耗电,详情请关注[http://hukai.me/android-training-course-in-chinese/connectivity/efficient-downloads/efficient-network-access.html](http://hukai.me/android-training-course-in-chinese/connectivity/efficient-downloads/efficient-network-access.html)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8c5c5dc7.png)
修复电量的消耗是另外一个很大的课题,这里就不展开继续了。
## 15)Battery Drain and WakeLocks
高效的保留更多的电量与不断促使用户使用你的App会消耗电量,这是矛盾的选择题。不过我们可以使用一些更好的办法来平衡两者。
假设你的手机里面装了大量的社交类应用,即使手机处于待机状态,也会经常被这些应用唤醒用来检查同步新的数据信息。Android会不断关闭各种硬件来延长手机的待机时间,首先屏幕会逐渐变暗直至关闭,然后CPU进入睡眠,这一切操作都是为了节约宝贵的电量资源。但是即使在这种睡眠状态下,大多数应用还是会尝试进行工作,他们将不断的唤醒手机。一个最简单的唤醒手机的方法是使用PowerManager.WakeLock的API来保持CPU工作并防止屏幕变暗关闭。这使得手机可以被唤醒,执行工作,然后回到睡眠状态。知道如何获取WakeLock是简单的,可是及时释放WakeLock也是非常重要的,不恰当的使用WakeLock会导致严重错误。例如网络请求的数据返回时间不确定,导致本来只需要10s的事情一直等待了1个小时,这样会使得电量白白浪费了。这也是为何使用带超时参数的wakelock.acquice()方法是很关键的。但是仅仅设置超时并不足够解决问题,例如设置多长的超时比较合适?什么时候进行重试等等?
解决上面的问题,正确的方式可能是使用非精准定时器。通常情况下,我们会设定一个时间进行某个操作,但是动态修改这个时间也许会更好。例如,如果有另外一个程序需要比你设定的时间晚5分钟唤醒,最好能够等到那个时候,两个任务捆绑一起同时进行,这就是非精确定时器的核心工作原理。我们可以定制计划的任务,可是系统如果检测到一个更好的时间,它可以推迟你的任务,以节省电量消耗。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6c8cc5c222.png)
这正是JobScheduler API所做的事情。它会根据当前的情况与任务,组合出理想的唤醒时间,例如等到正在充电或者连接到WiFi的时候,或者集中任务一起执行。我们可以通过这个API实现很多免费的调度算法。
从Android 5.0开始发布了Battery History Tool,它可以查看程序被唤醒的频率,又谁唤醒的,持续了多长的时间,这些信息都可以获取到。
请关注程序的电量消耗,用户可以通过手机的设置选项观察到那些耗电量大户,并可能决定卸载他们。所以尽量减少程序的电量消耗是非常有必要的。
性能优化典范
最后更新于:2022-04-01 02:41:18