不須要和用戶交互的提示框。html
更多參見官網:https://developer.android.com/guide/topics/ui/notifiers/toasts.htmljava
Toast.makeText(MainActivity.this.getApplicationContext(),"沉迷學習,日漸消瘦",Toast.LENGTH_SHORT).show()
Toast customToast = new Toast(MainActivity.this.getApplicationContext()); View customView = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_toast,null); ImageView img = (ImageView) customView.findViewById(R.id.img); TextView tv = (TextView) customView.findViewById(R.id.tv); img.setBackgroundResource(R.drawable.daima); tv.setText("沉迷學習,日漸消瘦"); customToast.setView(customView); customToast.setDuration(Toast.LENGTH_SHORT); customToast.setGravity(Gravity.CENTER,0,0); customToast.show();
佈局文件中根元素爲LinearLayout
,垂直放入一個ImageView
和一個TextView
。代碼就不貼了。android
產品狗的需求:點擊一個Button
,網絡請求失敗的狀況下使用Toast
的方式提醒用戶。
程序猿:ok~大筆一揮。網絡
Toast.makeText(MainActivity.this.getApplicationContext(),"沉迷學習,日漸消瘦",Toast.LENGTH_SHORT).show()
測試:你這程序寫的有問題。每次點擊就彈出了氣泡,連續點擊20次,竟然花了一分多鐘才顯示完。改!
程序猿:系統自帶的就這樣。愛要不要。
測試:那我用單元測試模擬點擊50次以後,它就不顯示了,這個怎麼說。
程序猿:…
這個時候,高級自定義Toast
就要出場了~app
activity_main.xml
—->上下兩個按鈕,略。ide
MainActivity.Javaoop
public class MainActivity extends AppCompatActivity implements View.OnClickListener{ public static final String TAG = "MainActivity"; private Button customToastBtn; private Button singleToastBtn; private static int num; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); initClick(); performClick(100); } private void initView() { customToastBtn = (Button) findViewById(R.id.customToastBtn); singleToastBtn = (Button) findViewById(R.id.singleToastBtn); } private void initClick() { customToastBtn.setOnClickListener(this); singleToastBtn.setOnClickListener(this); } /** * 點擊singleToastBtn按鈕 * @param clickFrequency 點擊的次數 */ private void performClick(int clickFrequency) { for (int i = 0; i < clickFrequency; i++){ singleToastBtn.performClick(); } } @Override public void onClick(View view) { switch (view.getId()){ case R.id.customToastBtn: showCustomToast(); break; case R.id.singleToastBtn: showSingleToast(); break; default:break; } } private void showCustomToast() { Toast customToast = new Toast(MainActivity.this.getApplicationContext()); View customView = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_toast,null); ImageView img = (ImageView) customView.findViewById(R.id.img); TextView tv = (TextView) customView.findViewById(R.id.tv); img.setBackgroundResource(R.drawable.daima); tv.setText("沉迷學習,日漸消瘦"); customToast.setView(customView); customToast.setDuration(Toast.LENGTH_SHORT); customToast.setGravity(Gravity.CENTER,0,0); customToast.show(); } private void showSingleToast() { Toast singleToast = SingleToast.getInstance(MainActivity.this.getApplicationContext()); View customView = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_toast,null); ImageView img = (ImageView) customView.findViewById(R.id.img); TextView tv = (TextView) customView.findViewById(R.id.tv); img.setBackgroundResource(R.drawable.daima); tv.setText("沉迷學習,日漸消瘦 第"+num+++"遍 toast="+singleToast); singleToast.setView(customView); singleToast.setDuration(Toast.LENGTH_SHORT); singleToast.setGravity(Gravity.CENTER,0,0); singleToast.show(); } }
SingleToast.java佈局
public class SingleToast { private static Toast mToast; /**雙重鎖定,使用同一個Toast實例*/ public static Toast getInstance(Context context){ if (mToast == null){ synchronized (SingleToast.class){ if (mToast == null){ mToast = new Toast(context); } } } return mToast; } }
那麼有的同窗會問了:你這樣不就是加了個單例嗎,好像也沒有什麼區別。區別大了。僅僅一個單例,既實現了產品狗的需求,又不會有單元測試快速點擊50次的以後不顯示的問題。爲何?Read The Fucking Source Code。post
這裏以Toast.makeText().show
爲例,一步步追尋這個過程當中源碼所作的工做。自定義Toast
至關於本身作了makeText()
方法的工做,道理是同樣同樣的,這裏就再也不分別講述了~單元測試
源碼位置:frameworks/base/core/java/Android/widght/Toast.java
Toast#makeText()
public static Toast makeText(Context context, CharSequence text, @Duration int duration) { // 獲取Toast對象 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); // 設置View和duration屬性 result.mNextView = v; result.mDuration = duration; return result; }
這裏填充的佈局transient_notification.xml
位於frameworks/base/core/res/res/layout/transient_notification.xml。加分項,對於XML佈局文件解析不太瞭解的同窗能夠看下這篇博客。
<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>
能夠發現,裏面只有一個TextView
,平日設置的文本內容就是在這裏展現。接下來只有一個show()
方法,彷佛咱們的源碼解析到這裏就快結束了。不,這只是個開始
public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }
這裏有三個問題。
1. 經過getService()
怎麼就得到一個INotificationManager
對象?
2. TN
類是個什麼鬼?
3. 方法最後只有一個service.enqueueToast()
,顯示和隱藏在哪裏?
Toast
的精華就在這三個問題裏,接下來的內容所有圍繞上述三個問題,尤爲是第三個。已經所有了解的同窗能夠去看別的博客了~
getService()
怎麼就得到一個INotificationManager
對象?static private INotificationManager getService() { if (sService != null) { return sService; } sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); return sService; }
對Binder
機制瞭解的同窗看見XXX.Stub.asInterface
確定會很熟悉,這不就是AIDL
中獲取client
嘛!確實是這樣。
tips: 本着追本溯源的精神,先看下ServiceManager.getService("notification")
。在上上上上篇博客SystemServer啓動流程源碼解析中startOtherServices()
涉及到NotificationManagerService
的啓動,代碼以下,這裏再也不贅述。
mSystemServiceManager.startService(NotificationManagerService.class);
Toast
中AIDL
對應文件的位置。
源碼位置:frameworks/base/core/java/android/app/INotificationManager.aidl
Server
端:NotificationManagerService.java
源碼位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
篇幅有限,這裏不可能將AIDL
文件完整的敘述一遍,不瞭解的同窗能夠理解爲:通過進程間通訊(AIDL
方式),最後調用NotificationManagerService#enqueueToast()
。具體能夠看下這篇博客。
TN
類是個什麼鬼?在Toast#makeText()
中第一行就獲取了一個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); }
源碼位置:frameworks/base/core/java/android/widght/Toast$TN.java
private static class TN extends ITransientNotification.Stub { ... TN() { final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; ... } ... }
源碼中的進程間通訊實在太多了,我不想說這方面的內容啊啊啊~。有時間專門再寫一片博客。這裏提早劇透下TN
類除了設置參數的做用以外,更大的做用是Toast
顯示與隱藏的回調。TN
類在這裏做爲Server
端。NotificationManagerService$NotificationListeners
類做爲client
端。這個暫且按下不提,下文會詳細講述。
show()
方法最後只有一個service.enqueueToast()
,顯示和隱藏在哪裏?源碼位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
private final IBinder mService = new INotificationManager.Stub() { @Override public void enqueueToast(String pkg, ITransientNotification callback, int duration) { if (pkg == null || callback == null) { Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback); return ; } final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg)); ... synchronized (mToastQueue) { int callingPid = Binder.getCallingPid(); long callingId = Binder.clearCallingIdentity(); try { ToastRecord record; int index = indexOfToastLocked(pkg, callback); if (index >= 0) { record = mToastQueue.get(index); record.update(duration); } else { 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; } } } } record = new ToastRecord(callingPid, pkg, callback, duration); mToastQueue.add(record); index = mToastQueue.size() - 1; // 將Toast所在的進程設置爲前臺進程 keepProcessAliveLocked(callingPid); } if (index == 0) { showNextToastLocked(); } } finally { Binder.restoreCallingIdentity(callingId); } } } ... }
在Toast#show()
最終會進入到這個方法。首先經過indexOfToastLocked()
方法獲取應用程序對應的ToastRecord
在mToastQueue
中的位置,Toast
消失後返回-1,不然返回對應的位置。mToastQueue
明明是個ArratList
對象,卻命名Queue
,猜想後面會遵循「後進先出」的原則移除對應的ToastRecord
對象~。這裏先以返回index=-1
查看,也就是進入到else
分支。若是不是系統程序,也就是應用程序。那麼同一個應用程序瞬時在mToastQueue
中存在的消息不能超過50條(Toast
對象不能超過50個)。不然直接return
。這也是上文中爲何快速點擊50次以後沒法繼續顯示的緣由。既然瞬時Toast
不能超過50個,那麼運用單例模式使用同一個Toast
對象不就能夠了嘛?答案是:可行。消息用完了就移除,瞬時存在50個以上的Toast
對象相信在正常的程序中也用不上。並且註釋中也說這樣作是爲了放置DOS攻擊和防止泄露。其實從這裏也能夠看出:爲了防止內存泄露,建立Toast
最好使用getApplicationContext
,不建議使用Activity
、Service
等。
迴歸主題。接下來建立了一個ToastRecord
對象並添加進mToastQueue
。接下來調用showNextToastLocked()
方法顯示一個Toast
。
源碼位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
NotificationManagerService#showNextToastLocked()
void showNextToastLocked() { ToastRecord record = mToastQueue.get(0); while (record != null) { if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback); try { record.callback.show(); scheduleTimeoutLocked(record); return; } catch (RemoteException e) { 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; } } } }
這裏首先調用record.callback.show()
,這裏的record.callback
其實就是TN
類。接下來調用scheduleTimeoutLocked()
方法,咱們知道Toast
顯示一段時間後會本身消失,因此這個方法確定是定時讓Toast
消失。跟進。
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); }
果真如此。重點在於使用mHandler.sendMessageDelayed(m, delay)
延遲發送消息。這裏的delay
只有兩種值,要麼等於LENGTH_LONG
,其他通通的等於SHORT_DELAY
,setDuration
爲其餘值用正常手段是沒有用的(能夠反射,不在重點範圍內)。
handler
收到MESSAGE_TIMEOUT
消息後會調用handleTimeout((ToastRecord)msg.obj)
。跟進。
private void handleTimeout(ToastRecord record) { if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback); synchronized (mToastQueue) { int index = indexOfToastLocked(record.pkg, record.callback); if (index >= 0) { cancelToastLocked(index); } } }
啥也不說了,跟進吧~
void cancelToastLocked(int index) { ToastRecord record = mToastQueue.get(index); try { record.callback.hide(); } catch (RemoteException e) { ... } mToastQueue.remove(index); keepProcessAliveLocked(record.pid); if (mToastQueue.size() > 0) { showNextToastLocked(); } }
延遲調用record.callback.hide()
隱藏Toast
,前文也提到過:record.callback
就是TN
對象。到這,第三個問題已經解決一半了,至少咱們已經直到Toast
的顯示和隱藏在哪裏被調用了,至於怎麼顯示怎麼隱藏的,客觀您接着往下看。
源碼位置:frameworks/base/core/java/android/widght/ToastTN.javaToastTN#show()
final Handler mHandler = new Handler(); @Override public void show() { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.post(mShow); } final Runnable mShow = new Runnable() { @Override public void run() { handleShow(); } };
注意下這裏直接使用new Handler
獲取Handler
對象,這也是爲何在子線程中不用Looper
彈出Toast會出錯的緣由。跟進handleShow()
。
public void handleShow() { if (mView != mNextView) { // remove the old view if necessary handleHide(); mView = mNextView; Context context = mView.getContext().getApplicationContext(); String packageName = mView.getContext().getOpPackageName(); if (context == null) { context = mView.getContext(); } mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); ... mParams.packageName = packageName; if (mView.getParent() != null) { mWM.removeView(mView); } mWM.addView(mView, mParams); trySendAccessibilityEvent(); } }
原來addView
到WindowManager
。這樣就完成了Toast
的顯示。至於隱藏就更簡單了。
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; } }
直接remove
掉。
今天週末,一天時間完成這篇簡單的源碼閱讀加寫做。每次寫完源碼解析老是成就感伴隨着失落感, 成就感來源於我又get到一個原理或者新技能,失落感來自源碼也就是那麼回事,可是回頭想一想我獲得了什麼?其實並很少。但我仍然在樂此不疲的追尋着。或許是我還沒「開竅」,沒有到那種融會貫通的境界。但我清楚的知道,我在進步。我在努力變的更加優秀。