这是一个一年前的bug,因为我要写一篇专利,想到了他,就整理出来!
bug描述
客户提出了一个问题: (android 5.0 高通平台)
【压力测试】:
通话记录中有500个时滑动不流畅 通话记录中有500个通话记录,上下滑动时,不流畅
一看,是一个性能优化问题,这种问题有点不是怎么好解决的。
bug重现
我想先看一下这个现象是什么情况,问题来了,如何在通话记录中添加500条记录信息啊,显然手动添加是不太科学的。那么我就应该先把这个工作做完。
本来想找测试要一个apk,直接写入500条通话日志就可以,坑人的是他们要说什么签名,把apk在代码中编译,还要找对应的项目组……,好吧,大公司,果然是非常规范,一切都是那么的井井有条,算了,对于通话Log,我有一点印象,记得这个db数据库非常简单,那就自己写了。
通话日志的db数据库是在:
/data/data/com.android.providers.contacts/databases/contacts2.db
表calls
uri为 CallLog.Calls.CONTENT_URI
够了,就要这么多,我们就可以完成了一个插入500条通话日志的操作了。
核心代码:
public void onAsyncInsertDataLister(CallLogInfo myCallLogInfo) {
// TODO Auto-generated method stub
Log.i(TAG, "Controller--onAsyncInsertDataLister");
ContentValues values = new ContentValues();
values.put(CALL_LOG_PROJECTION[1], myCallLogInfo.getNumber());
values.put(CALL_LOG_PROJECTION[2], myCallLogInfo.getName());
values.put(CALL_LOG_PROJECTION[3], myCallLogInfo.getDuration());
values.put(CALL_LOG_PROJECTION[4], myCallLogInfo.getType());
asyncQuery.startInsert(0, null, CallLog.Calls.CONTENT_URI, values);
}
好了,现在,我们可以插入500条通话日志了,我们进入通话日志界面,发现确实是如果通话日志数量一多,listview滑动确实是卡:
bug初步分析
我看了一下代码,在CallLogAdapter.java文件中找到了view显示的bindView代码:
private void bindView(View view, Cursor c, int count) {
view.setAccessibilityDelegate(mAccessibilityDelegate);
final CallLogListItemView callLogItemView = (CallLogListItemView) view;
final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
// Default case: an item in the call log.
views.primaryActionView.setVisibility(View.VISIBLE);
final String number = c.getString(CallLogQuery.NUMBER);
final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION);
final long date = c.getLong(CallLogQuery.DATE);
final long duration = c.getLong(CallLogQuery.DURATION);
final int callType = c.getInt(CallLogQuery.CALL_TYPE);
final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount(
c.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME),
c.getString(CallLogQuery.ACCOUNT_ID));
final Drawable accountIcon = PhoneAccountUtils.getAccountIcon(mContext,
accountHandle);
final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
final long rowId = c.getLong(CallLogQuery.ID);
views.rowId = rowId;
String accId = c.getString(CallLogQuery.ACCOUNT_ID);
long subId = SubscriptionManager.DEFAULT_SUB_ID;
if (accId!= null && !accId.equals("E") && !accId.toLowerCase().contains("sip")) {
subId = Long.parseLong(accId);
}
// For entries in the call log, check if the day group has changed and display a header
// if necessary.
if (mIsCallLog) {
int currentGroup = getDayGroupForCall(rowId);
int previousGroup = getPreviousDayGroup(c);
if (currentGroup != previousGroup) {
views.dayGroupHeader.setVisibility(View.VISIBLE);
views.dayGroupHeader.setText(getGroupDescription(currentGroup));
} else {
views.dayGroupHeader.setVisibility(View.GONE);
}
} else {
views.dayGroupHeader.setVisibility(View.GONE);
}
// Store some values used when the actions ViewStub is inflated on expansion of the actions
// section.
views.number = number;
views.numberPresentation = numberPresentation;
views.callType = callType;
// NOTE: This is currently not being used, but can be used in future versions.
views.accountHandle = accountHandle;
views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
// Stash away the Ids of the calls so that we can support deleting a row in the call log.
views.callIds = getCallIds(c, count);
final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c);
final boolean isVoicemailNumber =
PhoneNumberUtilsWrapper.INSTANCE.isVoicemailNumber(subId, number);
// Where binding and not in the call log, use default behaviour of invoking a call when
// tapping the primary view.
if (!mIsCallLog) {
views.primaryActionView.setOnClickListener(this.mActionListener);
// Set return call intent, otherwise null.
if (PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) {
// Sets the primary action to call the number.
views.primaryActionView.setTag(IntentProvider.getReturnCallIntentProvider(number));
} else {
// Number is not callable, so hide button.
views.primaryActionView.setTag(null);
}
} else {
// In the call log, expand/collapse an actions section for the call log entry when
// the primary view is tapped.
views.primaryActionView.setOnClickListener(this.mExpandCollapseListener);
// Note: Binding of the action buttons is done as required in configureActionViews
// when the user expands the actions ViewStub.
}
// Lookup contacts with this number
final ContactInfo info = mAdapterHelper.lookupContact(
number, numberPresentation, countryIso, cachedContactInfo);
final Uri lookupUri = info.lookupUri;
final String name = info.name;
final int ntype = info.type;
final String label = info.label;
final long photoId = info.photoId;
final Uri photoUri = info.photoUri;
CharSequence formattedNumber = info.formattedNumber;
final int[] callTypes = getCallTypes(c, count);
final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
final int sourceType = info.sourceType;
final int features = getCallFeatures(c, count);
final String transcription = c.getString(CallLogQuery.TRANSCRIPTION);
final String operator = c.getString(CallLogQuery.OPERATOR);
Long dataUsage = null;
if (!c.isNull(CallLogQuery.DATA_USAGE)) {
dataUsage = c.getLong(CallLogQuery.DATA_USAGE);
}
final PhoneCallDetails details;
final String accountName = info.accountName;
final String accountType = info.accountType;
Account contactAccount;
views.reported = info.isBadData;
// The entry can only be reported as invalid if it has a valid ID and the source of the
// entry supports marking entries as invalid.
views.canBeReportedAsInvalid = mContactInfoHelper.canReportAsInvalid(info.sourceType,
info.objectId);
// Restore expansion state of the row on rebind. Inflate the actions ViewStub if required,
// and set its visibility state accordingly.
expandOrCollapseActions(callLogItemView, isExpanded(rowId));
if (TextUtils.isEmpty(name)) {
if (mContext.getResources().getBoolean(R.bool.mark_emergency_call_in_call_log) &&
PhoneNumberUtils.isLocalEmergencyNumber(mContext, number)) {
String emergencyName = mContext.getString(
com.android.internal.R.string.emergency_call_dialog_number_for_display);
details = new PhoneCallDetails(number, numberPresentation,
formattedNumber, countryIso, geocode, callTypes, date, duration,
emergencyName, 0, "", null, null, 0, null, accountIcon, features,
dataUsage, transcription, Calls.DURATION_TYPE_ACTIVE, subId, operator);
} else {
details = new PhoneCallDetails(number, numberPresentation,
formattedNumber, countryIso, geocode, callTypes, date, duration,
null, accountIcon, features, dataUsage, transcription, subId, operator);
}
} else {
details = new PhoneCallDetails(number, numberPresentation,
formattedNumber, countryIso, geocode, callTypes, date,
duration, name, ntype, label, lookupUri, photoUri, sourceType,
null, accountIcon, features, dataUsage, transcription,
Calls.DURATION_TYPE_ACTIVE, subId, operator);
}
int contactType = ContactPhotoManager.TYPE_DEFAULT;
if (isVoicemailNumber) {
contactType = ContactPhotoManager.TYPE_VOICEMAIL;
} else if (mContactInfoHelper.isBusiness(info.sourceType)) {
contactType = ContactPhotoManager.TYPE_BUSINESS;
}
String lookupKey = lookupUri == null ? null
: ContactInfoHelper.getLookupKeyFromUri(lookupUri);
String nameForDefaultImage = null;
if (TextUtils.isEmpty(name)) {
nameForDefaultImage = mPhoneNumberHelper.getDisplayNumber(details.accountId,
details.number, details.numberPresentation,
details.formattedNumber).toString();
} else {
nameForDefaultImage = name;
}
if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
contactAccount = new Account(accountName, accountType);
} else {
contactAccount = null;
}
if (photoId == 0 && photoUri != null) {
setPhoto(views, photoUri, lookupUri, nameForDefaultImage, lookupKey, contactType, contactAccount);
} else {
setPhoto(views, photoId, lookupUri, nameForDefaultImage, lookupKey, contactType, contactAccount);
}
// Listen for the first draw
mAdapterHelper.registerOnPreDrawListener(view);
bindBadge(view, info, details, callType);
}
好了,到现在,我们看到了没有,如此多的与db数据库操作,这速度能快吗。
那问题如何解决了,我一开始看了一下小米机器,发现他的通话log界面,通话人的信息是没有联系人的头像信息,我就简单的把联系人的头像隐藏,但是没有发现性能有明显的提升,想想,其实也是,我们没有把耗时的从数据库中读取头像的操作去掉,效果当然是不明显的。
bug解决
我同时给高通提了一个case,第二天高通给了一个patch,我把patch打上,乖乖,速度明显的得到了提升。
那么,我们看看此patch:
CallLogAdapter.java
public class CallLogAdapter extends GroupingListAdapter
- implements CallLogAdapterHelper.Callback, CallLogGroupBuilder.GroupCreator {
+ implements CallLogAdapterHelper.Callback, CallLogGroupBuilder.GroupCreator,
+ OnScrollListener {
+ if (!mAdapterHelper.isBusy()) {
+ // Only update views when ListView's scroll state is not SCROLL_STATE_FLING.
+ mCallLogViewsHelper.setPhoneCallDetails(mContext, views, details);
+ }
+ if (!mAdapterHelper.isBusy()) {
+ // Only update views when ListView's scroll state is not SCROLL_STATE_FLING.
+ mCallLogViewsHelper.setPhoneCallDetails(mContext, views, details);
+ }
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ switch (scrollState) {
+ case OnScrollListener.SCROLL_STATE_IDLE:
+ mAdapterHelper.setBusy(false);
+ dataSetChanged();
+ break;
+ case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
+ mAdapterHelper.setBusy(false);
+ break;
+ case OnScrollListener.SCROLL_STATE_FLING:
+ // Do not update views when scroll state is SCROLL_STATE_FLING
+ mAdapterHelper.setBusy(true);
+ break;
+ }
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+ // no-op
+ }
CallLogAdapterHelper.java
+ private boolean mBusy;
+ public void setBusy(boolean isBusy) {
+ mBusy = isBusy;
+ }
+
+ public boolean isBusy(){
+ return mBusy;
+ }
private class QueryThread extends Thread {
private volatile boolean mDone = false;
public QueryThread() {
super("CallLogAdapter.QueryThread");
}
public void stopProcessing() {
mDone = true;
}
@Override
public void run() {
boolean needRedraw = false;
while (true) {
// Check if thread is finished, and if so return immediately.
if (mDone) return;
//BEGIN< DATE20150804> <patch for Dialer: FPS is low when scrolling call logs> hexiaoming
// only update contact info when scroll state is not fling.
if (mBusy) continue;
//END< DATE20150804> <patch for Dialer: FPS is low when scrolling call logs> hexiaoming
// Obtain next request, if any is available.
// Keep synchronized section small.
ContactInfoRequest req = null;
synchronized (mRequests) {
if (!mRequests.isEmpty()) {
req = mRequests.removeFirst();
}
}
if (req != null) {
// Process the request. If the lookup succeeds, schedule a
// redraw.
needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo);
} else {
// Throttle redraw rate by only sending them when there are
// more requests.
if (needRedraw) {
needRedraw = false;
mHandler.sendEmptyMessage(REDRAW);
}
// Wait until another request is available, or until this
// thread is no longer needed (as indicated by being
// interrupted).
try {
synchronized (mRequests) {
mRequests.wait(1000);
}
} catch (InterruptedException ie) {
// Ignore, and attempt to continue processing requests.
}
}
}
}
}
CallLogFragment.java
+ getListView().setOnScrollListener(mAdapter);
看到代码,明白了没,他的优化原理,其实是非常的简单,核心是加了一个标志位:mBusy
先开始给listview加了一个滑动监听器OnScrollListener,当我们手机在滑动列表时,列表的状态为SCROLL_STATE_FLING,就会将标志位mBusy设为true,不让列表刷新界面,同时也让读取数据库的操作不进行,就解决了滑动列表时滑动不流畅的问题。