詳解Android中的Toast源碼。本站提示廣大學習愛好者:(詳解Android中的Toast源碼)文章只能為提供參考,不一定能成為您想要的結果。以下是詳解Android中的Toast源碼正文
Toast源碼完成
Toast進口
我們在運用中應用Toast提醒的時刻,普通都是一行簡略的代碼挪用,以下所示:
[java] view plaincopyprint?在CODE上檢查代碼片派生到我的代碼片
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
makeText就是Toast的進口,我們從makeText的源碼來深刻懂得Toast的完成。源碼以下(frameworks/base/core/java/android/widget/Toast.java):
public static Toast makeText(Context context, CharSequence text, int duration) { Toast result = new Toast(context); LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null); TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message); tv.setText(text); result.mNextView = v; result.mDuration = duration; return result; }
從makeText的源碼裡,我們可以看出Toast的結構文件是transient_notification.xml,位於frameworks/base/core/res/res/layout/transient_notification.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="?android:attr/toastFrameBackground"> <TextView android:id="@android:id/message" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:layout_gravity="center_horizontal" android:textAppearance="@style/TextAppearance.Toast" android:textColor="@color/bright_foreground_dark" android:shadowColor="#BB000000" android:shadowRadius="2.75" /> </LinearLayout>
體系Toast的結構文件異常簡略,就是在垂直結構的LinearLayout裡放置了一個TextView。接上去,我們持續跟到show()辦法,研討一下結構構成以後的展現代碼完成:
public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } INotificationManager service = getService(); String pkg = mContext.getPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }
show辦法中有兩點是須要我們留意的。(1)TN是甚麼東東?(2)INotificationManager辦事的感化。帶著這兩個成績,持續我們Toast源碼的摸索。
TN源碼
許多成績都能經由過程浏覽源碼找到謎底,症結在與你能否有與之婚配的耐煩和保持。mTN的完成在Toast的結構函數中,源碼以下:
public Toast(Context context) { mContext = context; mTN = new TN(); mTN.mY = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.toast_y_offset); mTN.mGravity = context.getResources().getInteger( com.android.internal.R.integer.config_toastDefaultGravity); }
接上去,我們就從TN類的源碼動身,探訪TN的感化。TN源碼以下:
private static class TN extends ITransientNotification.Stub { final Runnable mShow = new Runnable() { @Override public void run() { handleShow(); } }; final Runnable mHide = new Runnable() { @Override public void run() { handleHide(); // Don't do this in handleHide() because it is also invoked by handleShow() mNextView = null; } }; private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(); final Handler mHandler = new Handler(); int mGravity; int mX, mY; float mHorizontalMargin; float mVerticalMargin; View mView; View mNextView; WindowManager mWM; TN() { // XXX This should be changed to use a Dialog, with a Theme.Toast // defined that sets up the layout params appropriately. final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = com.android.internal.R.style.Animation_Toast; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; /// M: [ALPS00517576] Support multi-user params.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; } /** * schedule handleShow into the right thread */ @Override public void show() { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.post(mShow); } /** * schedule handleHide into the right thread */ @Override public void hide() { if (localLOGV) Log.v(TAG, "HIDE: " + this); mHandler.post(mHide); } public void handleShow() { if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView + " mNextView=" + mNextView); if (mView != mNextView) { // remove the old view if necessary handleHide(); mView = mNextView; Context context = mView.getContext().getApplicationContext(); if (context == null) { context = mView.getContext(); } mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); // We can resolve the Gravity here by using the Locale for getting // the layout direction final Configuration config = mView.getContext().getResources().getConfiguration(); final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection()); mParams.gravity = gravity; if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { mParams.horizontalWeight = 1.0f; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { mParams.verticalWeight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; if (mView.getParent() != null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeView(mView); } if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this); mWM.addView(mView, mParams); trySendAccessibilityEvent(); } } private void trySendAccessibilityEvent() { AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(mView.getContext()); if (!accessibilityManager.isEnabled()) { return; } // treat toasts as notifications since they are used to // announce a transient piece of information to the user AccessibilityEvent event = AccessibilityEvent.obtain( AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); event.setClassName(getClass().getName()); event.setPackageName(mView.getContext().getPackageName()); mView.dispatchPopulateAccessibilityEvent(event); accessibilityManager.sendAccessibilityEvent(event); } public void handleHide() { if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView); if (mView != null) { // note: checking parent() just to make sure the view has // been added... i have seen cases where we get here when // the view isn't yet added, so let's try not to crash. if (mView.getParent() != null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeView(mView); } mView = null; } } }
經由過程源碼,我們能很顯著的看到繼續關系,TN類繼續自ITransientNotification.Stub,用於過程間通訊。這裡假定讀者都有Android過程間通訊的基本(不太熟的建議進修羅升陽關於Binder過程通訊的一系列博客)。既然TN是用於過程間通訊,那末我們很輕易想到TN類的詳細感化應當是Toast類的回調對象,其他過程經由過程挪用TN類的詳細對象來操作Toast的顯示和消逝。
TN類繼續自ITransientNotification.Stub,ITransientNotification.aidl位於frameworks/base/core/java/android/app/ITransientNotification.aidl,源碼以下:
package android.app; /** @hide */ oneway interface ITransientNotification { void show(); void hide(); }
ITransientNotification界說了兩個辦法show()和hide(),它們的詳細完成就在TN類傍邊。TN類的完成為:
/** * schedule handleShow into the right thread */ @Override public void show() { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.post(mShow); } /** * schedule handleHide into the right thread */ @Override public void hide() { if (localLOGV) Log.v(TAG, "HIDE: " + this); mHandler.post(mHide); }
這裡我們就可以曉得,Toast的show和hide辦法完成是基於Handler機制。而TN類中的Handler完成是:
final Handler mHandler = new Handler();
並且,我們在TN類中沒有發明任何Looper.perpare()和Looper.loop()辦法。解釋,mHandler挪用的是以後地點線程的Looper對象。所以,當我們在主線程(也就是UI線程中)可以隨便挪用Toast.makeText辦法,由於Android體系幫我們完成了主線程的Looper初始化。然則,假如你想在子線程中挪用Toast.makeText辦法,就必需先輩行Looper初始化了,否則就會報出java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() 。Handler機制的進修可以參考我之前寫過的一篇博客:http://blog.csdn.net/wzy_1988/article/details/38346637。
接上去,持續跟一下mShow和mHide的完成,它倆的類型都是Runnable。
final Runnable mShow = new Runnable() { @Override public void run() { handleShow(); } }; final Runnable mHide = new Runnable() { @Override public void run() { handleHide(); // Don't do this in handleHide() because it is also invoked by handleShow() mNextView = null; } };
可以看到,show和hide的真正完成分離是挪用了handleShow()和handleHide()辦法。我們先來看handleShow()的詳細完成:
public void handleShow() { if (mView != mNextView) { // remove the old view if necessary handleHide(); mView = mNextView; Context context = mView.getContext().getApplicationContext(); if (context == null) { context = mView.getContext(); } mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); // We can resolve the Gravity here by using the Locale for getting // the layout direction final Configuration config = mView.getContext().getResources().getConfiguration(); final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection()); mParams.gravity = gravity; if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { mParams.horizontalWeight = 1.0f; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { mParams.verticalWeight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; if (mView.getParent() != null) { mWM.removeView(mView); } mWM.addView(mView, mParams); trySendAccessibilityEvent(); } }
從源碼中,我們曉得Toast是經由過程WindowManager挪用addView加載出去的。是以,hide辦法天然是WindowManager挪用removeView辦法來將Toast視圖移除。
總結一下,經由過程對TN類的源碼剖析,我們曉得了TN類是回調對象,其他過程挪用tn類的show和hide辦法來掌握這個Toast的顯示和消逝。
NotificationManagerService
回到Toast類的show辦法中,我們可以看到,這裡挪用了getService獲得INotificationManager辦事,源碼以下:
private static INotificationManager sService; static private INotificationManager getService() { if (sService != null) { return sService; } sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); return sService; }
獲得INotificationManager辦事後,挪用了enqueueToast辦法將以後的Toast放入到體系的Toast隊列中。傳的參數分離是pkg、tn和mDuration。也就是說,我們經由過程Toast.makeText(context, msg, Toast.LENGTH_SHOW).show()去出現一個Toast,這個Toast其實不是連忙顯示在以後的window上,而是先輩入體系的Toast隊列中,然後體系挪用回調對象tn的show和hide辦法停止Toast的顯示和隱蔽。
這裡INofiticationManager接口的詳細完成類是NotificationManagerService類,位於frameworks/base/services/java/com/android/server/NotificationManagerService.java。
起首,我們來剖析一下Toast入隊的函數完成enqueueToast,源碼以下:
public void enqueueToast(String pkg, ITransientNotification callback, int duration) { // packageName為null或許tn類為null,直接前往,不進隊列 if (pkg == null || callback == null) { return ; } // (1) 斷定能否為體系Toast final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg)); // 斷定以後toast所屬的pkg能否為體系不許可產生Toast的pkg.NotificationManagerService有一個HashSet數據構造,存儲了不許可產生Toast的包名 if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid()) && !areNotificationsEnabledForPackageInt(pkg)) { if (!isSystemToast) { return; } } synchronized (mToastQueue) { int callingPid = Binder.getCallingPid(); long callingId = Binder.clearCallingIdentity(); try { ToastRecord record; // (2) 檢查該Toast能否曾經在隊列傍邊 int index = indexOfToastLocked(pkg, callback); // 假如Toast曾經在隊列中,我們只須要更新顯示時光便可 if (index >= 0) { record = mToastQueue.get(index); record.update(duration); } else { // 非體系Toast,每一個pkg在以後mToastQueue中Toast有總數限制,不克不及跨越MAX_PACKAGE_NOTIFICATIONS if (!isSystemToast) { int count = 0; final int N = mToastQueue.size(); for (int i=0; i<N; i++) { final ToastRecord r = mToastQueue.get(i); if (r.pkg.equals(pkg)) { count++; if (count >= MAX_PACKAGE_NOTIFICATIONS) { Slog.e(TAG, "Package has already posted " + count + " toasts. Not showing more. Package=" + pkg); return; } } } } // 將Toast封裝成ToastRecord對象,放入mToastQueue中 record = new ToastRecord(callingPid, pkg, callback, duration); mToastQueue.add(record); index = mToastQueue.size() - 1; // (3) 將以後Toast地點的過程設置為前台過程 keepProcessAliveLocked(callingPid); } // (4) 假如index為0,解釋以後入隊的Toast在隊頭,須要挪用showNextToastLocked辦法直接顯示 if (index == 0) { showNextToastLocked(); } } finally { Binder.restoreCallingIdentity(callingId); } } }
可以看到,我對上述代碼做了扼要的正文。代碼絕對簡略,然則還有4點標注代碼須要我們來進一步商量。
(1) 斷定能否為體系Toast。假如以後Toast所屬的過程的包名為“android”,則為體系Toast,不然還可以挪用isCallerSystem()辦法來斷定。該辦法的完成源碼為:
boolean isUidSystem(int uid) { final int appid = UserHandle.getAppId(uid); return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0); } boolean isCallerSystem() { return isUidSystem(Binder.getCallingUid()); }
isCallerSystem的源碼也比擬簡略,就是斷定以後Toast所屬過程的uid能否為SYSTEM_UID、0、PHONE_UID中的一個,假如是,則為體系Toast;假如不是,則不為體系Toast。
能否為體系Toast,經由過程上面的源碼浏覽可知,重要有兩點優勢:
體系Toast必定可以進入到體系Toast隊列中,不會被黑名單阻攔。
體系Toast在體系Toast隊列中沒稀有量限制,而通俗pkg所發送的Toast在體系Toast隊列中稀有量限制。
(2) 檢查將要入隊的Toast能否曾經在體系Toast隊列中。這是經由過程比對pkg和callback來完成的,詳細源碼以下所示:
private int indexOfToastLocked(String pkg, ITransientNotification callback) { IBinder cbak = callback.asBinder(); ArrayList<ToastRecord> list = mToastQueue; int len = list.size(); for (int i=0; i<len; i++) { ToastRecord r = list.get(i); if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) { return i; } } return -1; }
經由過程上述代碼,我們可以得出一個結論,只需Toast的pkg稱號和tn對象是分歧的,則體系把這些Toast以為是統一個Toast。
(3) 將以後Toast地點過程設置為前台過程。源碼以下所示:
private void keepProcessAliveLocked(int pid) { int toastCount = 0; // toasts from this pid ArrayList<ToastRecord> list = mToastQueue; int N = list.size(); for (int i=0; i<N; i++) { ToastRecord r = list.get(i); if (r.pid == pid) { toastCount++; } } try { mAm.setProcessForeground(mForegroundToken, pid, toastCount > 0); } catch (RemoteException e) { // Shouldn't happen. } }
這裡的mAm=ActivityManagerNative.getDefault(),挪用了setProcessForeground辦法將以後pid的過程置為前台過程,包管不會體系殺逝世。這也就說明了為何當我們finish以後Activity時,Toast還可以顯示,由於以後過程還在履行。
(4) index為0時,對隊列頭的Toast停止顯示。源碼以下:
private void showNextToastLocked() { // 獲得隊列頭的ToastRecord ToastRecord record = mToastQueue.get(0); while (record != null) { try { // 挪用Toast的回調對象中的show辦法對Toast停止展現 record.callback.show(); scheduleTimeoutLocked(record); return; } catch (RemoteException e) { Slog.w(TAG, "Object died trying to show notification " + record.callback + " in package " + record.pkg); // remove it from the list and let the process die int index = mToastQueue.indexOf(record); if (index >= 0) { mToastQueue.remove(index); } keepProcessAliveLocked(record.pid); if (mToastQueue.size() > 0) { record = mToastQueue.get(0); } else { record = null; } } } }
這裡Toast的回調對象callback就是tn對象。接上去,我們看一下,為何體系Toast的顯示時光只能是2s或許3.5s,症結在於scheduleTimeoutLocked辦法的完成。道理是,挪用tn的show辦法展現完Toast以後,須要挪用scheduleTimeoutLocked辦法來將Toast消逝。(假如年夜家有疑問:不是說tn對象的hide辦法來將Toast消逝,為何要在這裡挪用scheduleTimeoutLocked辦法將Toast消逝呢?是由於tn類的hide辦法一履行,Toast連忙就消逝了,而日常平凡我們所應用的Toast都邑在以後Activity逗留幾秒。若何完成逗留幾秒呢?道理就是scheduleTimeoutLocked發送MESSAGE_TIMEOUT新聞去挪用tn對象的hide辦法,然則這個新聞會有一個delay延遲,這裡也是用了Handler新聞機制)。
private static final int LONG_DELAY = 3500; // 3.5 seconds private static final int SHORT_DELAY = 2000; // 2 seconds private void scheduleTimeoutLocked(ToastRecord r) { mHandler.removeCallbacksAndMessages(r); Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY; mHandler.sendMessageDelayed(m, delay); }
起首,我們看到這裡其實不是直接發送了MESSAGE_TIMEOUT新聞,而是有個delay的延遲。而delay的時光從代碼中“long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;”看出只能為2s或許3.5s,這也就說明了為何體系Toast的出現時光只能是2s或許3.5s。本身在Toast.makeText辦法中隨便傳入一個duration是無感化的。
接上去,我們來看一下WorkerHandler中是若何處置MESSAGE_TIMEOUT新聞的。mHandler對象的類型為WorkerHandler,源碼以下:
private final class WorkerHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_TIMEOUT: handleTimeout((ToastRecord)msg.obj); break; } } }
可以看到,WorkerHandler對MESSAGE_TIMEOUT類型的新聞處置是挪用了handlerTimeout辦法,那我們持續跟蹤handleTimeout源碼:
private void handleTimeout(ToastRecord record) { synchronized (mToastQueue) { int index = indexOfToastLocked(record.pkg, record.callback); if (index >= 0) { cancelToastLocked(index); } } }
handleTimeout代碼中,起首斷定以後須要消逝的Toast所屬ToastRecord對象能否在隊列中,假如在隊列中,則挪用cancelToastLocked(index)辦法。本相就要顯現在我們面前了,持續跟蹤源碼:
private void cancelToastLocked(int index) { ToastRecord record = mToastQueue.get(index); try { record.callback.hide(); } catch (RemoteException e) { // don't worry about this, we're about to remove it from // the list anyway } mToastQueue.remove(index); keepProcessAliveLocked(record.pid); if (mToastQueue.size() > 0) { // Show the next one. If the callback fails, this will remove // it from the list, so don't assume that the list hasn't changed // after this point. showNextToastLocked(); } }
哈哈,看到這裡,我們回調對象的hide辦法也被挪用了,同時也將該ToastRecord對象從mToastQueue中移除。到這裡,一個Toast的完全顯示和消逝就講授停止了。