Android 界面滑动卡顿分析与解决方案(入门)

Android 界面滑动卡顿分析与解决方案(入门)

Android 界面滑动卡顿分析与解决方案(入门)

导致Android界面滑动卡顿主要有两个原因:

1.UI线程(main)有耗时操作

2.视图渲染时间过长,导致卡顿

目前只讲第1点,第二点相对比较复杂待以后慢慢研究。

众所周知,界面的流畅度主要依赖FPS这个值,这个值是通过(1s/渲染1帧所花费的时间)计算所得,FPS值越大视频越流畅,所以就需要渲染1帧的时间能尽量缩短。正常流畅度的FPS值在60左右,即渲染一帧的时间不应大于17ms。

先看一个例子:

见Video1,该应用通过一组URL加载网络上的图片并显示在ListView中。从视频中可以看到滑动ListView存在一定的卡顿的现象。

分析工具1:Android systrace

打开Android开发工具中的DDMS,选中应用所在的进程并点击倒数第二个图标出现如下界面:

Android 界面滑动卡顿分析与解决方案(入门)_第1张图片

填上相关信息后点击OK,此时Android systrace已经开始工作,你只需要正常操作手机复现卡顿现象即可。

Android systrace 停止以后就会根据你上图中的设置生成trace.xml文件(注:此文件在火狐浏览器中无法正常浏览,需使用chrome)。

打开trace.xml后观察surfaceflinger存在很多断断续续,分布不够均匀。


放大看

Android 界面滑动卡顿分析与解决方案(入门)_第2张图片

在某间隔出的时间是48ms,这远远的大于17ms,正是这个原因直接导致了界面的卡顿感。

接着再看:

Android 界面滑动卡顿分析与解决方案(入门)_第3张图片

此处也存在不规则分布,可以看到主要的耗时操作在:

Android 界面滑动卡顿分析与解决方案(入门)_第4张图片

obtainView和decodeBitmap上面,看到这两个方法似乎是找到了罪魁祸首。那我们就看看这两个方法是在哪里被调用的。

通过查找源代码知道obtainView这个方法被定义在AbsListView.java这个类中,主要作用就是获得一个和数据绑定过的视图,在这个方法中调用到了

 mAdapter.getView(position, transientView, this);

这个方法大家再熟悉不过了,因为在自定义Adapter的时候都要去重写getView方法,那我们可以推测卡顿的原因很有可能出自getView。毕竟这个方法里有我们自己写的很大一坨代码。

接着再看getView中最主要的方法

 mCacheWrapper.getBitmapFromCache(url, mHandler, position,2);

具体实现是:

  public void getBitmapFromCache(final String urlString, Handler handler,
    final int position, final int scale) {
  final String key = hashKeyForString(urlString);
  final Message msg = handler.obtainMessage();
  Bitmap bitmapCache = mMemoryCache.get(key);
  if (bitmapCache != null) {
    msg.arg1 = position;
    msg.obj = bitmapCache;
    msg.sendToTarget();
    Log.d(TAG, "memory include the key");
    return;
  }
  //InputStream is = getInputStreamFromCache(key);
  //if (is != null) {
  //	Bitmap bitmap = getBitmap(is, key, scale);
  //	if (bitmap != null) {
  //		mMemoryCache.put(key, bitmap);
  //		msg.arg1 = position;
  //		msg.obj = bitmap;
  //		msg.sendToTarget();
  //		Log.d(TAG, "disk include the key");
  //	}
  //	return;
  //}
  mExecutor.execute(new Runnable() {
    @Override
    public void run() {
      if (mCurrentTask.contains(key)) {
        Log.d(TAG, "the key of task is still execute");
        return;
      }
      mCurrentTask.add(key);
      boolean isOK = write2Cache(urlString);
      if (isOK) {
        InputStream is = getInputStreamFromCache(key);
        Log.d(TAG, "the file is write to disk cache");
        is = getInputStreamFromCache(key);
        if (is != null) {
          Bitmap bitmap = getBitmap(is, key, scale);
          if (bitmap != null) {
            mMemoryCache.put(key, bitmap);
            msg.arg1 = position;
            msg.obj = bitmap;
            msg.sendToTarget();
          }
        }
      }
      mCurrentTask.remove(key);
    }
  });
}

看上面这段代码发现加注释代码存在一定的嫌疑,因为它在主线程中做了IO操作和bitmap的decode操作。

我们稍微修改一下上面这段代码:

  public void getBitmapFromCache(final String urlString, Handler handler,
    final int position, final int scale) {
  final String key = hashKeyForString(urlString);
  final Message msg = handler.obtainMessage();
  Bitmap bitmapCache = mMemoryCache.get(key);
  if (bitmapCache != null) {
    msg.arg1 = position;
    msg.obj = bitmapCache;
    msg.sendToTarget();
    Log.d(TAG, "memory include the key");
    return;
  }
  mExecutor.execute(new Runnable() {
    @Override
    public void run() {
      if (mCurrentTask.contains(key)) {
        Log.d(TAG, "the key of task is still execute");
        return;
      }
      //mCurrentTask.add(key);
      //InputStream is = getInputStreamFromCache(key);
      //if (is != null) {
      //	Bitmap bitmap = getBitmap(is, key, scale);
      //	if (bitmap != null) {
      //		mMemoryCache.put(key, bitmap);
      //		msg.arg1 = position;
      //		msg.obj = bitmap;
      //		msg.sendToTarget();
      //		Log.d(TAG, "disk include the key");
      //	}
      //	mCurrentTask.remove(key);
      //	return;
      //}
      boolean isOK = write2Cache(urlString);
      if (isOK) {
        Log.d(TAG, "the file is write to disk cache");
        is = getInputStreamFromCache(key);
        if (is != null) {
          Bitmap bitmap = getBitmap(is, key, scale);
          if (bitmap != null) {
            mMemoryCache.put(key, bitmap);
            msg.arg1 = position;
            msg.obj = bitmap;
            msg.sendToTarget();
          }
        }
      }
      mCurrentTask.remove(key);
    }
  });
}

将置灰出的代码移动到主线程以外,在看看滑动流畅读video2.mp4

可以看到卡顿现象已经没有了,那罪魁祸首就是在主线程中有IO操作和bitmap的decode操作引起的。

上述分析过程跳跃性比较大,这里再推荐一种简单直观的方法:

分析工具2:Method Profiling.

还是打开DDMS,选中你的应用,点击第六个图标,

Android 界面滑动卡顿分析与解决方案(入门)_第5张图片

这边默认OK

点击OK开始抓取,接着滑动Android 界面滑动卡顿分析与解决方案(入门)_第6张图片手机复现卡顿现象。最后再次点击第六个钮即可。


这里只看上图中的main就可以了。

Android 界面滑动卡顿分析与解决方案(入门)_第7张图片 Android 界面滑动卡顿分析与解决方案(入门)_第8张图片

点击main方法后会展开它的父方法(即调用main的方法)和它的子方法(即在main中调用的方法)。这里一般点击后面百分数较大的那个子方法(百分数表示方法执行所占用的cpu时间)。接着要做的就是一步一步往下点,直到找到我们要找的耗时操作。

最后我们还是来到了这边

Android 界面滑动卡顿分析与解决方案(入门)_第9张图片

上图已经定位了decodeStream方法,再往下走也是进到framework没意义了。剩下的就是怎么将decodeStream放到主线程以外的线程的事情了。

总结:

主线程中不要放置耗时的操作,耗时操作可以扔Thread再通过Handler与主线程同步或使用AsyncTask来完成耗时操作。

你可能感兴趣的:(android性能)