編輯:關於Android編程
昨天,遇到了一個同時刪除多條記錄的問題,在android系統中刪除操作過慢導致了,導致用戶體驗不佳的現象。
該問題一直都沒有很好的解決,現在將整體的解決方案記錄一下。
public static class ConversationQueryHandler extends AsyncQueryHandler { private int mDeleteToken; public ConversationQueryHandler(ContentResolver cr) { super(cr); } public void setDeleteToken(int token) { mDeleteToken = token; } /** * Always call this super method from your overridden onDeleteComplete function. */ @Override protected void onDeleteComplete(int token, Object cookie, int result) { if (token == mDeleteToken) { // Test code // try { // Thread.sleep(10000); // } catch (InterruptedException e) { // } // release lock synchronized (sDeletingThreadsLock) { sDeletingThreads = false; sDeletingThreadsLock.notifyAll(); } } } }
因為每個具體的ComposeMessageActivity中都有關於這個刪除操作的回調,這裡主要完成的有兩個這樣
protected void onQueryComplete(int token, Object cookie, Cursor cursor)
protected void onDeleteComplete(int token, Object cookie, int result)這裡的三個參數也是非常的實用,token 令牌,cookie (實體標識),cusor游標
下面是具體的實現。
private final class BackgroundQueryHandler extends ConversationQueryHandler { public BackgroundQueryHandler(ContentResolver contentResolver) { super(contentResolver); } @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { /// M: @{ MmsLog.d(TAG, "onQueryComplete, token=" + token + "activity=" + ComposeMessageActivity.this); /// @} switch(token) { case MESSAGE_LIST_QUERY_TOKEN: /// @} if (cursor == null) { MmsLog.w(TAG, "onQueryComplete, cursor is null."); return; } /// M: If adapter or listener has been cleared, just close this cursor@{ if (mMsgListAdapter == null) { MmsLog.w(TAG, "onQueryComplete, mMsgListAdapter is null."); cursor.close(); return; } if (mMsgListAdapter.getOnDataSetChangedListener() == null) { MmsLog.d(TAG, "OnDataSetChangedListener is cleared"); cursor.close(); return; } /// @} if (isRecipientsEditorVisible()) { MmsLog.d(TAG, "RecipientEditor visible, it means no messagelistItem!"); return; } // check consistency between the query result and 'mConversation' long tid = (Long) cookie; if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { log("##### onQueryComplete: msg history result for threadId " + tid); } if (tid != mConversation.getThreadId()) { log("onQueryComplete: msg history query result is for threadId " + tid + ", but mConversation has threadId " + mConversation.getThreadId() + " starting a new query"); if (cursor != null) { cursor.close(); } startMsgListQuery(); return; } // check consistency b/t mConversation & mWorkingMessage.mConversation ComposeMessageActivity.this.sanityCheckConversation(); int newSelectionPos = -1; long targetMsgId = getIntent().getLongExtra("select_id", -1); if (targetMsgId != -1) { if (cursor != null) { cursor.moveToPosition(-1); while (cursor.moveToNext()) { long msgId = cursor.getLong(COLUMN_ID); if (msgId == targetMsgId) { newSelectionPos = cursor.getPosition(); break; } } } } else if (mSavedScrollPosition != -1) { // mSavedScrollPosition is set when this activity pauses. If equals maxint, // it means the message list was scrolled to the end. Meanwhile, messages // could have been received. When the activity resumes and we were // previously scrolled to the end, jump the list so any new messages are // visible. if (mSavedScrollPosition == Integer.MAX_VALUE) { int cnt = mMsgListAdapter.getCount(); if (cnt > 0) { // Have to wait until the adapter is loaded before jumping to // the end. newSelectionPos = cnt - 1; mSavedScrollPosition = -1; } } else { // remember the saved scroll position before the activity is paused. // reset it after the message list query is done newSelectionPos = mSavedScrollPosition; mSavedScrollPosition = -1; } } /// M: Code analyze 047, Extra uri from message body and get number from uri. /// Then use this number to update contact cache. @{ if (mNeedUpdateContactForMessageContent) { updateContactCache(cursor); mNeedUpdateContactForMessageContent = false; } /// @} mMsgListAdapter.changeCursor(cursor); if (newSelectionPos != -1) { /// M: remove bug ALPS00404266 patch, keep item top @{ mMsgListView.setSelection(newSelectionPos); // jump the list to the pos //View child = mMsgListView.getChildAt(newSelectionPos); //int top = 0; //if (child != null) { // top = child.getTop(); //} // mMsgListView.setSelectionFromTop(newSelectionPos, top); /// @} } else { /// M: google jb.mr1 patch, Conversation should scroll to the bottom /// when incoming received @{ int count = mMsgListAdapter.getCount(); long lastMsgId = 0; if (cursor != null && count > 0) { cursor.moveToLast(); lastMsgId = cursor.getLong(COLUMN_ID); } // mScrollOnSend is set when we send a message. We always want to scroll // the message list to the end when we send a message, but have to wait // until the DB has changed. We also want to scroll the list when a // new message has arrived. //added by hongtao.fu for defect 2360303 at 2016.7.12 begin //smoothScrollToEnd(mScrollOnSend || lastMsgId != mLastMessageId, 0); if(lastMsgId != mLastMessageId){ mMsgListView.setSelection(mMsgListView.getCount()); mLastMessageId=lastMsgId; } //added by hongtao.fu for defect 2360303 at 2016.7.12 end mLastMessageId = lastMsgId; /// @} mScrollOnSend = false; } // Adjust the conversation's message count to match reality. The // conversation's message count is eventually used in // WorkingMessage.clearConversation to determine whether to delete // the conversation or not. if (mMsgListAdapter.getCount() == 0 && mWaitingForSendMessage) { mConversation.setMessageCount(1); } else { mConversation.setMessageCount(mMsgListAdapter.getCount()); } updateThreadIdIfRunning(); cursor.moveToPosition(-1); while (cursor.moveToNext()) { int read = cursor.getInt(MessageListAdapter.COLUMN_MMS_READ); read += cursor.getInt(MessageListAdapter.COLUMN_SMS_READ); if (read == 0) { mConversation.setHasUnreadMessages(true); break; } } MmsLog.d(TAG, "onQueryComplete(): Conversation.ThreadId=" + mConversation.getThreadId() + ", MessageCount=" + mConversation.getMessageCount()); // Once we have completed the query for the message history, if // there is nothing in the cursor and we are not composing a new // message, we must be editing a draft in a new conversation (unless // mSentMessage is true). // Show the recipients editor to give the user a chance to add // more people before the conversation begins. if (cursor != null && cursor.getCount() == 0 && !isRecipientsEditorVisible() && !mSentMessage) { /// M: fix bug ALPS01098902, avoding checkObsoleteThreadId in this case if (mSubSelectDialog != null && mSubSelectDialog.isShowing() && mOldThreadID > 0 && mCutRecipients != null) { mIsSameConv = false; } initRecipientsEditor(null); } // FIXME: freshing layout changes the focused view to an unexpected // one, set it back to TextEditor forcely. if (mSubjectTextEditor == null || (mSubjectTextEditor != null && !mSubjectTextEditor.isFocused())) { mTextEditor.requestFocus(); } invalidateOptionsMenu(); // some menu items depend on the adapter's count if (!mIsActivityStoped) { mConversation.blockMarkAsRead(false); mConversation.markAsRead(); } return; case MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN: // check consistency between the query result and 'mConversation' tid = (Long) cookie; if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { log("##### onQueryComplete (after delete): msg history result for threadId " + tid); } if (cursor == null) { return; } if (tid > 0 && cursor.getCount() == 0) { // We just deleted the last message and the thread will get deleted // by a trigger in the database. Clear the threadId so next time we // need the threadId a new thread will get created. log("##### MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN clearing thread id: " + tid); Conversation conv = Conversation.get(getApplicationContext(), tid, false); if (conv != null) { conv.clearThreadId(); conv.setDraftState(false); } finish(); } cursor.close(); break; case MESSAGE_GROUP_DETAILS_QUERY_TOKEN: if (null == mConversation) { Log.i(TAG, "group_details: mConversation is null!"); return; } ContactList contactList = mConversation.getRecipients(); if (null == contactList) return; int failCount = (null != cursor) ? cursor.getCount() : 0; if (failCount >= 1) { List newContactList = new ArrayList(); Object[] contacts = contactList.toArray(); for (Object contact : contacts) { Contact contactN = (Contact) contact; String number = contactN.getNumber(); if (null != number && !"".equals(number)) { newContactList.add(number); } } StringBuffer failedSend = new StringBuffer(); StringBuffer successSend = new StringBuffer(); long date = 0l; cursor.moveToPosition(-1); int i = 0; while (cursor.moveToNext()) { String address = cursor.getString(0); date = cursor.getLong(1); if (i >= 1) failedSend.append(","); failedSend.append(address); newContactList.remove(address); i ++; } for (int pos = 0; pos < newContactList.size(); pos ++) { String address = (String) newContactList.get(pos); if (null == address) continue; if (pos >= 1) failedSend.append(","); successSend.append(address); } String messageDetails = MessageUtils.getTextGroupFailMessageDetails( ComposeMessageActivity.this, successSend.toString(), failedSend.toString(), date); mDetailDialog = new AlertDialog.Builder(ComposeMessageActivity.this) .setTitle(R.string.message_details_title) .setMessage(messageDetails) .setPositiveButton(android.R.string.ok, null) .setCancelable(true) .show(); } break; default: MmsLog.d(TAG, "unknown token."); break; } } @Override protected void onDeleteComplete(int token, Object cookie, int result) { super.onDeleteComplete(token, cookie, result); /// M: fix bug ALPS00351620; for requery searchactivity. SearchActivity.setNeedRequery(); switch(token) { case ConversationList.DELETE_CONVERSATION_TOKEN: /// M: @{ /* mConversation.setMessageCount(0); // fall through */ try { if (TelephonyManagerEx.getDefault().isTestIccCard(0)) { MmsLog.d(TAG, "All threads has been deleted, send notification.."); SmsManager .getSmsManagerForSubscriptionId( SmsReceiverService.sLastIncomingSmsSubId).getDefault().setSmsMemoryStatus(true); } } catch (Exception ex) { MmsLog.e(TAG, " " + ex.getMessage()); } // Update the notification for new messages since they // may be deleted. MessagingNotification.nonBlockingUpdateNewMessageIndicator( ComposeMessageActivity.this, MessagingNotification.THREAD_NONE, false); // Update the notification for failed messages since they // may be deleted. updateSendFailedNotification(); MessagingNotification.updateDownloadFailedNotification(ComposeMessageActivity.this); break; /// @} case DELETE_MESSAGE_TOKEN: /// M: google jb.mr1 patch, Conversation should scroll to the bottom /// when incoming received @{ if (cookie instanceof Boolean && ((Boolean)cookie).booleanValue()) { // If we just deleted the last message, reset the saved id. mLastMessageId = 0; } /// @} /// M: Code analyze 027,Add for deleting one message.@{ MmsLog.d(TAG, "onDeleteComplete(): before update mConversation, ThreadId = " + mConversation.getThreadId()); ContactList recipients = getRecipients(); mConversation = Conversation.upDateThread(ComposeMessageActivity.this, mConversation.getThreadId(), false); mThreadCountManager.isFull(mThreadId, ComposeMessageActivity.this, ThreadCountManager.OP_FLAG_DECREASE); /// @} // Update the notification for new messages since they // may be deleted. MessagingNotification.nonBlockingUpdateNewMessageIndicator( ComposeMessageActivity.this, MessagingNotification.THREAD_NONE, false); // Update the notification for failed messages since they // may be deleted. updateSendFailedNotification(); /// M: Code analyze 027,Add for deleting one message.@{ MessagingNotification.updateDownloadFailedNotification(ComposeMessageActivity.this); MmsLog.d(TAG, "onDeleteComplete(): MessageCount = " + mConversation.getMessageCount() + ", ThreadId = " + mConversation.getThreadId()); if (mIpCompose.onDeleteComplete(token)) { break; } if (mConversation.getMessageCount() <= 0 || mConversation.getThreadId() <= 0L) { mMsgListAdapter.changeCursor(null); if (needSaveDraft() && (recipients != null)) { if (!isRecipientsEditorVisible()) { makeDraftEditable(recipients); } } else { /// M: fix bug for ConversationList select all performance ,update selected threads array.@{ ConversationListAdapter.removeSelectedState(mSelectedThreadId); /// @ finish(); } } /// @} break; } // If we're deleting the whole conversation, throw away // our current working message and bail. if (token == ConversationList.DELETE_CONVERSATION_TOKEN) { ContactList recipients = mConversation.getRecipients(); Iterator iter = mMsgListAdapter.getItemList().entrySet().iterator(); Boolean isMsgLocked = false; while (iter.hasNext()) { @SuppressWarnings("unchecked") Map.Entryentry = (Entry ) iter.next(); if (entry.getValue()) { if (isMsgLocked(entry)) { Log.d(TAG," isMsgLocked(entry) locked "); isMsgLocked = true; break; } } } if( (! isMsgLocked) || (isMsgLocked && mIsDeleteLockMsg) ){ mWorkingMessage.discard(); } // Remove any recipients referenced by this single thread from the // contacts cache. It's possible for two or more threads to reference // the same contact. That's ok if we remove it. We'll recreate that contact // when we init all Conversations below. if (recipients != null) { for (Contact contact : recipients) { contact.removeFromCache(); } } // Make sure the conversation cache reflects the threads in the DB. Conversation.init(getApplicationContext()); //finish(); startMsgListQuery(MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN, 0); //modify by hongtao.fu for defect 1552661 at 2016.2.17 end } else if (token == DELETE_MESSAGE_TOKEN) { /// M: Code analyze 027,Add for deleting one message.@{ // Check to see if we just deleted the last message startMsgListQuery(MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN, 0); /// @} } if (mActionMode != null) { mHandler.postDelayed(new Runnable() { public void run() { if (mActionMode != null && !isFinishing()) { mActionMode.finish(); } } }, 300); } } }
上面是刪除操作的具體實現AsyncQueryHandler類的具體實現,下面才是正式的調用,
例如,查詢一條SMS的detail
final String mGroupsSmsFlag = msgItem.mGroupsSmsFlag; mBackgroundQueryHandler .cancelOperation(MESSAGE_GROUP_DETAILS_QUERY_TOKEN); try { mBackgroundQueryHandler.postDelayed(new Runnable() { public void run() { mBackgroundQueryHandler.startQuery( MESSAGE_GROUP_DETAILS_QUERY_TOKEN, null, ContentUris.withAppendedId(Uri .withAppendedPath( Telephony.MmsSms.CONTENT_URI, "group_details"), mConversation.getThreadId()), new String[] { Sms.ADDRESS }, "group_sms_flag=?", new String[] { mGroupsSmsFlag }, null); } }, 50); } catch (SQLiteException e) { SqliteWrapper.checkSQLiteException(ComposeMessageActivity.this, e); }調用完成之後注意查看上面的第二段代碼,裡在裡面可以通過token和cookie得到回調,通過回調顯示具體的實現。
final boolean deleteLocked = mDeleteLockedMessages; new Thread(new Runnable() { public void run() { Iterator iter = mMsgListAdapter.getItemList().entrySet().iterator(); Uri deleteSmsUri = null; Uri deleteMmsUri = null; ArrayList閱讀上面的代碼,有兩段注釋掉的代碼,之前就是使用的注釋掉的代碼來刪除,每刪除一條調用一次AsyncQueryHander.startDelete()而這個方法涉及到多次回調,在它的回調中也涉及到大量的耗時操作,所以批量的刪除信息會比較的慢。argsSmsList = new ArrayList (); ArrayList argsMmsList = new ArrayList (); int i = 0; int j = 0; while (iter.hasNext()) { @SuppressWarnings("unchecked") Map.Entry entry = (Entry ) iter.next(); if (entry.getValue()) { if (!mDeleteLockedMessages && isMsgLocked(entry)) { continue; } if (entry.getKey() > 0) { MmsLog.i(TAG, "sms"); argsSmsList.add(Long.toString(entry.getKey())); //argsSms[i] = Long.toString(entry.getKey()); MmsLog.i(TAG, "argsSms[i]" + argsSmsList.get(i)); deleteSmsUri = Sms.CONTENT_URI; i++; } else { MmsLog.i(TAG, "mms"); argsMmsList.add(Long.toString(-entry.getKey())); // argsMms[j] = Long.toString(-entry.getKey()); MmsLog.i(TAG, "argsMms[j]" + argsMmsList.get(j)); deleteMmsUri = Mms.CONTENT_URI; j++; } } } String[] argsSms = new String[argsSmsList.size()]; String[] argsMms = new String[argsMmsList.size()]; argsMmsList.toArray(argsMms); argsSmsList.toArray(argsSms); if (deleteSmsUri != null) { List list=new ArrayList (); for (int count = 0; count < argsSms.length; count++) { MessageItem mMessageItem = getMessageItem("sms",Long.parseLong(argsSms[count]),true); Boolean deletingLastItem = false; Cursor cursor = mMsgListAdapter != null ? mMsgListAdapter.getCursor() : null; if (cursor != null) { cursor.moveToLast(); long msgId = cursor.getLong(COLUMN_ID); deletingLastItem = msgId == mMessageItem.mMsgId; } boolean isLocked = mMessageItem.mLocked; MmsLog.d(TAG,"SMS the isLocked is :"+isLocked + " and the deleteLocked is :"+deleteLocked); MmsLog.d(TAG, "SMS the mMessageItem.mMessageUri is : "+mMessageItem.mMessageUri); if (isLocked) { if (deleteLocked) { // mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN, // deletingLastItem, mMessageItem.mMessageUri, // isLocked ? null : "locked=0", null); list.add(argsSms[count]); } } else { // mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN, // deletingLastItem, mMessageItem.mMessageUri, // isLocked ? null : "locked=0", null); list.add(argsSms[count]); } } if(list.size()>=1){ String s[]=new String[list.size()]; list.toArray(s); mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN, null, deleteSmsUri, FOR_MULTIDELETE, s); } } if (deleteMmsUri != null) { List list=new ArrayList (); for (int count = 0; count < argsMms.length; count++) { MessageItem mMessageItem = getMessageItem("mms",Long.parseLong(argsMms[count]),true); Boolean deletingLastItem = false; Cursor cursor = mMsgListAdapter != null ? mMsgListAdapter.getCursor() : null; if (cursor != null) { cursor.moveToLast(); long msgId = cursor.getLong(COLUMN_ID); deletingLastItem = msgId == mMessageItem.mMsgId; } boolean isLocked = mMessageItem.mLocked; MmsLog.d(TAG,"MMS the isLocked is :"+isLocked + " and the deleteLocked is :"+deleteLocked); MmsLog.d(TAG, "MMS the mMessageItem.mMessageUri is : "+mMessageItem.mMessageUri); if (isLocked) { if (deleteLocked) { // mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN, // deletingLastItem, mMessageItem.mMessageUri, // isLocked ? null : "locked=0", null); list.add(argsMms[count]); } } else { // mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN, // deletingLastItem, mMessageItem.mMessageUri, // isLocked ? null : "locked=0", null); list.add(argsMms[count]); } } if(list.size()>=1){ String s[]=new String[list.size()]; list.toArray(s); mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN, null, deleteMmsUri, FOR_MULTIDELETE, s); } } } }).start();
public int delete(Uri url, String where, String[] whereArgs) { 。。。。 if (where != null && where.equals(FOR_MULTIDELETE)) { Log.d(TAG, "delete FOR_MULTIDELETE"); String selectids = getSmsIdsFromArgs(whereArgs); String threadQuery = String.format("SELECT DISTINCT thread_id FROM sms " + "WHERE _id IN %s", selectids); Cursor cursor = db.rawQuery(threadQuery, null); /// M: fix ALPS01263429, consider cursor as view, we should read cursor /// before delete related records. long[] deletedThreads = null; try { deletedThreads = new long[cursor.getCount()]; int i = 0; while (cursor.moveToNext()) { deletedThreads[i++] = cursor.getLong(0); } } finally { cursor.close(); } String finalSelection = String.format(" _id IN %s", selectids); count = deleteMessages(db, finalSelection, null); if (count != 0) { MmsSmsDatabaseHelper.updateMultiThreads(db, deletedThreads); } Log.d(TAG, "delete FOR_MULTIDELETE count = " + count);
實際的刪除操作:
/// M: because of triggers on sms and pdu, delete a large number of sms/pdu through an /// atomic operation will cost too much time. To avoid blocking other database operation, /// remove trigger sms_update_thread_on_delete, and set a limit to each delete operation. @{ private static final int DELETE_LIMIT = 100; static int deleteMessages(SQLiteDatabase db, String selection, String[] selectionArgs) { Log.d(TAG, "deleteMessages, start"); int deleteCount = DELETE_LIMIT; if (TextUtils.isEmpty(selection)) { selection = "_id in (select _id from sms limit " + DELETE_LIMIT + ")"; } else { selection = "_id in (select _id from sms where " + selection + " limit " + DELETE_LIMIT + ")"; } int count = 0; while (deleteCount > 0) { deleteCount = db.delete(TABLE_SMS, selection, selectionArgs); count += deleteCount; Log.d(TAG, "deleteMessages, delete " + deleteCount + " sms"); } Log.d(TAG, "deleteMessages, delete sms end"); return count; }
對上面方法調用進行整理,得到實際的應用語句應該是:
while (deleteCount > 0) { deleteCount = db.delete(TABLE_SMS, _id in (select _id from sms where _id IN %s ),selectids); count += deleteCount; Log.d(TAG, "deleteMessages, delete " + deleteCount + " sms"); }
通過上面的注釋,我們發現裡面的 為了避免數據庫阻塞,這裡對實際刪除的操作還進行了一個最大值的判斷,這一點在很多地方都沒有講解,但是再實際的應用中,我們都應該注意這一點。
先看一下效果圖支持本地圖片以及網絡圖片or本地網絡混合。使用方式:<com.jalen.autobanner.BannerView android:id=@+id/
一、JNI概述 JNI 是Java Native Interface的縮寫,中文翻譯為“Java本地調用”,JNI 是本地編程接口。它使得在 Java 虛擬機 (VM)
一、實現思路1、在build.gradle中添加依賴,例如:compile com.android.support:support-v4:23.4.0compile co
開發App過程中,免不了要進行網絡請求操作進行數據交換,比如下載圖片,如果自己寫一個下載圖片的類進行操作的話,要考慮太多太多內容,必須線程池,內存溢出,圖片磁盤緩存操作,