方法耗时
在我之前的博客如何优雅的检测主线程中的耗时方法中分析了:利用Android系统的消息机制原理去检测主线程中的耗时方法,其实对于执行方法引起的性能开销主要分两类:
- 执行时间长的方法。
- 执行次数多的方法。
对于这两类方法,可以使用工具Traceview(在Android Studio 3.0 版本和以下版本中使用)和CPU Profiler(Android Studio 3.1版本以上使用)进行分析。
由于Traceview已经被抛弃,所以这里不再做介绍。
CPU Profiler
关于CPU Profiler,官方提供了中文文档,清查看:使用 CPU Profiler 检查 CPU Activity 和函数跟踪。
例子:
模拟在Activity中执行时间长的方法和次数多的方法:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.activity_main_test1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
method1();
}
});
findViewById(R.id.activity_main_test2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for (int i = 0; i < 20; i++) {
method2();
}
}
});
}
//执行次数多的方法
private void method2() {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行时间长的方法
private void method1() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
上面代码主要是测试点击按钮让主线程睡一秒的两种方法,使用CPU Profiler分析如下:
Call Chart 标签:
图1.Call Chart 标签
Call Chart 标签提供函数跟踪的图形表示形式,其中,水平轴表示函数调用(或调用方)的时间段和时间,并沿垂直轴显示其被调用者。 对系统 API 的函数调用显示为橙色,对应用自有函数的调用显示为绿色,对第三方 API(包括 Java 语言 API)的函数调用显示为蓝色。
通过上面图1中的Call Chart 标签,可以看到函数从上到下的栈调用轨迹:
com.android.internal.os.ZygoteLnit.main ->......-> android.app.ActivityThread,main -> ...... android.os.Looper.loop -> ...... -> MainActivity.onClick -> ...... -> MainActivity.method1 -> ......
Flame Chart 标签:
图2.Flame Chart 标签
上面图2中显示了Flame Chart 标签,Flame Chart 标签提供一个倒置的调用图表,其汇总相同的调用堆栈。 即,收集共享相同调用方顺序的完全相同的函数,并在火焰图中用一个较长的横条表示它们(而不是将它们显示为多个较短的横条,如调用图表中所示)。 这样更方便您查看哪些函数消耗最多时间。 不过,这也意味着水平轴不再代表时间线,相反,它表示每个函数相对的执行时间。
调用Thread类方法的相同调用堆栈:
android.os.Handler.dispatchMessage -> ...... -> android.view.View.performClick -> MainActivity.onClick -> ...... -> MainActivity.method1 -> Thread.sleep -> ......
android.os.Handler.dispatchMessage -> ...... -> android.view.View.performClick -> MainActivity.onClick -> ...... -> MainActivity.method2 -> Thread.sleep -> ......
调用MainActivity类方法的相同调用堆栈:
android.os.Handler.dispatchMessage -> ...... -> android.view.View.performClick -> MainActivity.onClick -> ...... -> MainActivity.method1
android.os.Handler.dispatchMessage -> ...... -> android.view.View.performClick -> MainActivity.onClick -> ...... -> MainActivity.method2
Top Down 标签:
图3.Top Down 标签
上面图3显示Top Down 标签,Top Down 标签显示一个函数调用列表,在该列表中展开函数节点会显示函数的被调用方。
从上面可以看到method2()方法执行的时间是:1006692us。
另外:Top Down 标签还提供了在每个函数调用上所花费的 CPU 时间(时间也可以用线程总时间占所选时间范围的持续时间的百分比表示):
- Self: 表示函数调用在执行自己的代码(而非被调用方的代码)上所花的时间,如下图中的函数 D 所示。
- Children: 表示函数调用在执行自己的被调用方(而非自己的代码)上所花的时间,如下图中的函数 D 所示。
- 总和: 函数的 Self 和 Children 时间的总和。 这表示应用在执行函数调用上所花的总时间,如下图 中函数 D 所示
Bottom Up标签:
图4. Bottom Up标签
上面图4显示了Bottom Up标签, Bottom Up 标签显示一个函数调用列表,在该列表中展开函数节点将显示函数的调用方。
从上面可以看到函数method1的调用方。另外,Bottom Up 标签还用于按照消耗最多(最少)CPU 时间排序函数,上面函数列表从上到下,函数执行时间以此递减。
Top Down 标签和Bottom Up标签的区别:
图5. 一个“Top Down”树。
图6. 图 5 中函数 C 的“Bottom Up”树。
如图 5 所示,在“Top Down”标签中展开函数 A 的节点可显示它的被调用方,即函数 B 和 D。 然后,展开函数 D 的节点可显示它的被调用方,即函数 B 和 C 等等。 与 Flame chart 标签相似,“Top Down”树汇总共享相同调用堆栈的相同函数的跟踪信息。 也就是说,Flame chart 标签可提供Top down 标签的图形化表示形式。
图 6 为函数 C 提供了一个“Bottom Up”树。 在“Bottom Up”树中打开函数 C 的节点可显示它独有的调用方,即函数 B 和 D。 请注意,尽管 B 调用 C 两次,但在“Bottom Up”树中展开函数 C 的节点时,B 仅显示一次。 然后,展开 B 的节点显示其调用方,即函数 A 和 D。
使用辅助类来帮助执行时间长的方法
执行时间长的方法,可以放在子线程中去执行。在Android中提供了IntentService,HandlerThread,AsyncTask类,在Java中也提供了ThreadPoolExecutor类来帮助执行时间长的方法。
HandlerThread
用于启动具有looper的新线程的方便类。 然后可以使用looper来创建处理程序类。 请注意,仍然必须调用start()。
HandlerThread继承自Thread类,在 run方法中创建了一个当前线程的Looper并调用了消息循环loop()方法:
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
HandlerThread可以做定时,延时或者无限循环的任务。
IntentService
IntentService是服务的基类,可根据需要处理异步请求(表示为Intents)。 客户端通过Context.startService(Intent)调用发送请求; 服务根据需要启动,使用工作线程依次处理每个Intent,并在工作失败时自行停止。
这种“工作队列处理器”模式通常用于从应用程序的主线程卸载任务。 存在IntentService类以简化此模式并处理机制。 要使用它,请扩展IntentService并实现onHandleIntent(Intent)。 IntentService将接收Intents,启动工作线程,并根据需要停止服务。
所有请求都在一个工作线程上处理 - 它们可能需要多长时间(并且不会阻止应用程序的主循环),但一次只能处理一个请求。
由于IntentService是一个Service,所以需要在AndroidManifest.xml配置。
IntentService内部使用了HandlerThread:
private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}
@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.
super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
@Override
public void onStart(@Nullable Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}
在HandlerThread类中的子线程中执行任务,任务来自Intent(必须是一个Inteng),执行任务后会停止自己。
IntentService适合执行时间长的耗时任务,比如下载,上传,复杂计算等。
AsyncTask
AsyncTask可以正确,方便地使用UI线程。 此类允许您执行后台操作并在UI线程上发布结果,而无需操作线程和/或处理程序。
AsyncTask被设计为围绕Thread和Handler的助手类,并不构成通用的线程框架。 理想情况下,AsyncTasks应该用于短操作(最多几秒钟)。如果需要保持线程长时间运行,强烈建议您使用java.util.concurrent包提供的各种API,例如 Executor,ThreadPoolExecutor和FutureTask。
异步任务由在后台线程上运行的计算定义,其结果在UI线程上发布。 异步任务由3种泛型类型定义,称为Params,Progress和Result,以及4个步骤,称为onPreExecute,doInBackground,onProgressUpdate和onPostExecute。
AsyncTask内部实现是THREAD_POOL_EXECUTOR+SERIAL_EXECUTOR+Handler:
- THREAD_POOL_EXECUTOR:可用于并行执行任务的Executor。
- SERIAL_EXECUTOR:用于任务排队的Executor。
- Handler:将线程切换到主线程。
执行任务的THREAD_POOL_EXECUTOR:
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));//线程池大小:最少2个线程,最多4个线程。
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;//最大线程数
private static final int KEEP_ALIVE_SECONDS = 30;//线程闲置时,30秒后被回收
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);//大小为128的有界队列
public static final Executor THREAD_POOL_EXECUTOR;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
threadPoolExecutor.allowCoreThreadTimeOut(true);//核心线程闲置时,超时也会被回收
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
用于任务排队的SERIAL_EXECUTOR:
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();//用于任务排队的队列
Runnable mActive;
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();//执行当前任务
} finally {
scheduleNext();//去执行下一个任务
}
}
});
if (mActive == null) {
scheduleNext();//去执行下一个任务
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
线程池
Executors提供的工厂方法:
- newFixedThreadPool:线程数量固定的线程池,当线程处于空闲时,它们并不会回收,除非线程池被关闭了。当所有的线程处于活动状态时,新任务都会处于等待状态,直到线程空闲出来。只有核心线程,没有非核心线程,最大线程量为核心线程数量,线程闲置时没有超时时长,使用的队列是LinkedBlockingQueue。
- newCachedThreadPool:线程数量不定的线程,最大线程数量为Integer.MAX_VALUE。当线程池中的线程都处于活动状态时,线程池会创建新的线程来处理新任务,否则就会利用空闲的线程来处理新任务。线程池中的线程都有超时机制,这个超时时长为60秒,超过线程就会被回收。 没有核心线程,只有非核心线程,最大线程量为Integer.MAX_VALUE,线程闲置时超时时长为60秒,使用的队列是SynchronoseQueue。
- newScheduledThreadPool:核心线程数量固定,非核心线程数量没有限制,并且非核心线程闲置时就会被回收。有核心线程,也有非核心线程,最大线程量为Integer.MAX_VALUE,线程闲置时超时时长为0秒,使用的队列是DelayWorkQueue。
- newSingleThreadExecutor:只有一个核心线程,确保所有的任务都在同一个线程中按顺序执行。有核心线程,没有非核心线程,最大线程量为1,线程闲置时没有超时时长,使用的队列是LinkedBlockingQueue。
创建线程池ThreadPoolExecutor:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- corePoolSize:核心线程。
- maximumPoolSize:最大线程数。
- keepAliveTime:线程闲置时的存活时间。
- unit:存活时间的时间单位。
- workQueue:存储任务的队列。
- threadFactory:线程工厂。
- handler:饱和策略。AbortPolicy (中止策略,默认方式,该策略会抛出未检查异常 RejectedExecutionException), CallerRunsPolicy(调用着运行策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用着,从而降低新任务的流量) , DiscardPolicy(当新提价的任务无法保存到队列中等待执行时,抛弃策略会悄悄抛弃该任务) , DiscardOldestPolicy(抛弃最旧的策略则会抛弃下一个将被执行的任务,然后尝试重新提交新的任务)。
在执行次数多的方法中应避免的事项
1.避免创建大量的对象
在自定义View时,通常会重写onMeasure,onLayout,onDraw方法,这些方法在View的生命周期过程中,通常会被调用多次,所以应该避免在方法中创建大量的对像,能够定义为全局对象的就定义为全局对象。
2.及时移除回调监听接口
ViewTreeObserver.OnGlobalLayoutListener监听器:
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//do somthing
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}else{
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
}
});
RecyclerView.OnScrollListener监听器:
recyclerView.clearOnScrollListeners();
ViewPager.OnPageChangeListener监听器:
viewPager.clearOnPageChangeListeners();
3.不要做复杂的计算
在频繁的被回调的方法中不要做复杂的计算。
RecyclerView滑动的时候,RecyclerView.OnScrollListener监听器的onScrolled方法会被不断的回调:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
输入文本时,TextWatcher监听器的方法会不断的被回调:
editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
}
});