昨天,遇到了一个同时删除多条记录的问题,在android系统中删除操作过慢导致了,导致用户体验不佳的现象。
该问题一直都没有很好的解决,现在将整体的解决方案记录一下。
一。AsyncQueryHander的具体实现
通过代码我们看到这里是通过一个AsyncQueryHander实现的删除操作,该handler的具体实现 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.Entry<Long,Boolean> entry = (Entry<Long,Boolean>) 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得到回调,通过回调显示具体的实现。
二。言归正转,删除的实现
下面是具体的实现,首先通过创建一个线程,然后在线程遍历每条信息,将sms,mms的KEY值(也是数据库中的_id),保存到agrsSmsList中。然后通过判断是否上锁,最后直接调用一个AsyncQueryHandler的删除操作,实现删除。
final boolean deleteLocked = mDeleteLockedMessages;
new Thread(new Runnable() {
public void run() {
Iterator iter = mMsgListAdapter.getItemList().entrySet().iterator();
Uri deleteSmsUri = null;
Uri deleteMmsUri = null;
ArrayList<String> argsSmsList = new ArrayList<String>();
ArrayList<String> argsMmsList = new ArrayList<String>();
int i = 0;
int j = 0;
while (iter.hasNext()) {
@SuppressWarnings("unchecked")
Map.Entry<Long,Boolean> entry = (Entry<Long,Boolean>) 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<String> list=new ArrayList<String>();
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<String> list=new ArrayList<String>();
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();
阅读上面的代码,有两段注释掉的代码,之前就是使用的注释掉的代码来删除,每删除一条调用一次AsyncQueryHander.startDelete()而这个方法涉及到多次回调,在它的回调中也涉及到大量的耗时操作,所以批量的删除信息会比较的慢。
三。数据库中批量删除的实现
content provider 中对于数据库的调用,最后会传递到数据库端,所以上面的调用,会出现不符合SQL的语法。
完整的SQL调用应该
SELECT DISTINCT thread_id FROM sms WHERE _id IN %s //%s 等价与(,,,)
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, <span style="font-family: Arial, Helvetica, sans-serif;">_id in (select _id from sms where </span><span style="font-family: Arial, Helvetica, sans-serif;">_id IN %s )</span>,<span style="font-family: Arial, Helvetica, sans-serif;">selectids</span>);
count += deleteCount;
Log.d(TAG, "deleteMessages, delete " + deleteCount + " sms");
}
通过上面的注释,我们发现里面的 为了避免数据库阻塞,这里对实际删除的操作还进行了一个最大值的判断,这一点在很多地方都没有讲解,但是再实际的应用中,我们都应该注意这一点。