自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Toast與Snackbar的那點(diǎn)事

企業(yè)動(dòng)態(tài)
Toast是Android平臺上的常用技術(shù)。從用戶角度來看,Toast是用戶與App交互最基本的提示控件;從開發(fā)者角度來看,Toast是開發(fā)過程中常用的調(diào)試手段之一。此外,Toast語法也非常簡單,僅需一行代碼。基于簡單易用的優(yōu)點(diǎn),Toast在Android開發(fā)過程中被廣泛使用。

背景

Toast是Android平臺上的常用技術(shù)。從用戶角度來看,Toast是用戶與App交互最基本的提示控件;從開發(fā)者角度來看,Toast是開發(fā)過程中常用的調(diào)試手段之一。此外,Toast語法也非常簡單,僅需一行代碼?;诤唵我子玫膬?yōu)點(diǎn),Toast在Android開發(fā)過程中被廣泛使用。

[[224670]]

但是,Toast是系統(tǒng)層面提供的,不依賴于前臺頁面,存在濫用的風(fēng)險(xiǎn)。為了規(guī)避這些風(fēng)險(xiǎn),Google在Android系統(tǒng)版本的迭代過程中,不斷進(jìn)行了優(yōu)化和限制。這些限制不可避免的影響到了正常的業(yè)務(wù)邏輯,在迭代過程中,我們遇到過以下幾個(gè)問題:

  1. 設(shè)置中關(guān)閉某個(gè)App的【顯示通知】開關(guān),Toast不再彈出,極大的影響了用戶體驗(yàn)。
  2. Toast在Android 7.1.2(API25)以下會發(fā)生BadTokenException異常,導(dǎo)致App崩潰。
  3. 自定義TYPE_TOAST類型的Window,在Android 7.1.1、7.1.2發(fā)生token null is not valid異常,導(dǎo)致App崩潰。

與Toast斗爭

在美團(tuán)平臺的業(yè)務(wù)中,Toast被用作主流程交互的提示控件,比如在完成下單、評價(jià)、分享后進(jìn)行各種提示。Toast被限制之后會給用戶帶來誤解。為了解決正常的業(yè)務(wù)Toast被系統(tǒng)限制誤傷的問題,我們與Toast展開了一系列的斗爭。

斗爭一:Toast不彈出

舉個(gè)案例:某個(gè)用戶投訴美團(tuán)App在分享朋友圈后沒有任何提示,不知道是否分享成功。具體原因是用戶在設(shè)置里關(guān)閉了美團(tuán)App的【顯示通知】開關(guān),導(dǎo)致通知權(quán)限無法獲取,這極大的影響了用戶體驗(yàn)。然而,在Android 4.4(API19)以下系統(tǒng)中,這個(gè)開關(guān)的打開狀態(tài),也就是通知權(quán)限是否開啟的狀態(tài)我們是無法判斷的,因此我們也無法感知Toast彈出與否,為了解決這個(gè)問題,需要從Toast的源碼入手,***源碼總結(jié)步驟如下:

  1. 在Toast#show()源碼中,Toast的展示并非自己控制,而是通過AIDL使用INotificationManager獲取到NotificationManagerService(NMS)這個(gè)遠(yuǎn)程服務(wù)。
  2. 調(diào)用service.enqueueToast(pkg, tn, mDuration)將當(dāng)前Toast的顯示加入到通知隊(duì)列,并傳遞了一個(gè)tn對象,這個(gè)對象就是NMS用作回傳Toast的顯示狀態(tài)。
  3. 在tn的回調(diào)方法中,使用WindowManager將構(gòu)造的Toast添加到當(dāng)前的window中,需要注意的是這個(gè)window的type類型是TYPE_TOAST。

Toast不彈出原因分析

那么為什么禁掉通知權(quán)限會導(dǎo)致Toast不再彈出呢?

通過以上分析,Toast的展示是由NMS服務(wù)控制的,NMS服務(wù)會做一些權(quán)限、token等的校驗(yàn),當(dāng)通知權(quán)限一旦關(guān)閉,Toast將不再彈出。

可行性方案調(diào)研

如果能夠繞過NMS服務(wù)的校驗(yàn)?zāi)敲淳涂梢赃_(dá)到我們的訴求,繞過的方法是按照Toast的源碼,實(shí)現(xiàn)我們自己的MToast,并將NMS替換成自己的ToastManager,如下圖:

方案定了后,需要做的事情就是代碼替換。作為平臺型App,美團(tuán)App大量使用了Toast,人工替換肯定會出現(xiàn)遺漏的地方,為了能用更少的人力來解決這個(gè)問題,我們采用了如下方案。

解決方案

美團(tuán)App在早期就因業(yè)務(wù)需要接入了AspectJ,AspectJ是Java中做AOP編程的利器,基本原理就是在代碼編譯期對切面的代碼進(jìn)行修改,插入我們預(yù)先寫好的邏輯或者直接替換當(dāng)前方法的實(shí)現(xiàn)。美團(tuán)App的做法就是借用AspectJ,從源頭攔截并替換Toast的調(diào)用實(shí)現(xiàn)。

關(guān)鍵代碼如下:

  1. @Aspect 
  2. public class ToastAspect { 
  3.   @Pointcut("call(* android.widget.Toast+.show(..))"
  4.   public void toastShow() { 
  5.   } 
  6.  
  7.   @Around("toastShow()"
  8.   public void toastShow(ProceedingJoinPoint point) { 
  9.      Toast toast = (Toast) point.getTarget(); 
  10.      Context context = (Context) ReflectUtils.getValue(toast, "mContext"); 
  11.      if (Build.VERSION.SDK_INT >= 19 && NotificationManagerCompat.from(context).areNotificationsEnabled()) { 
  12.          point.proceed(point.getArgs()); 
  13.      } else { 
  14.          floatToastShow(toast, context); 
  15.      } 
  16.   } 
  17.  
  18.   private static void floatToastShow(Toast toast, Context context) { 
  19.     ... 
  20.  
  21.     new MToast(context) 
  22.            .setDuration(mDuration) 
  23.            .setView(mNextView) 
  24.            .setGravity(mGravity, mX, mY) 
  25.            .setMargin(mHorizontalMargin, mVerticalMargin) 
  26.            .show(); 
  27.   } 

其中MToast是TYPE_TOAST類型的的Window,這樣即使禁掉通知權(quán)限,業(yè)務(wù)代碼也可以不作任何修改,繼續(xù)彈出Toast。而底層已經(jīng)被無感知的替換成自己的MToast了,以最小的成本達(dá)到了目標(biāo)。

斗爭二:BadTokenException

美團(tuán)App在線上經(jīng)常會上報(bào)BadTokenExceptionCrash,而且集中在Android 5.0 - Android 7.1.2的機(jī)型上。具體Crash堆棧如下:

  1. android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@6caa743 is not valid; is your activity running? 
  2.     at android.view.ViewRootImpl.setView(ViewRootImpl.java:607) 
  3.     at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:341) 
  4.     at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:106) 
  5.     at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3242)`BadTokenException` 
  6.     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2544) 
  7.     at android.app.ActivityThread.access$900(ActivityThread.java:168) 
  8.     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1378) 
  9.     at android.os.Handler.dispatchMessage(Handler.java:102) 
  10.     at android.os.Looper.loop(Looper.java:150) 
  11.     at android.app.ActivityThread.main(ActivityThread.java:5665) 
  12.     at java.lang.reflect.Method.invoke(Native Method) 
  13.     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:822) 
  14.     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:712) 

BadTokenException原因分析

我們知道在Android上,任何視圖的顯示都要依賴于一個(gè)視圖窗口Window,同樣Toast的顯示也需要一個(gè)窗口,前文已經(jīng)分析了這個(gè)窗口的類型就是TYPE_TOAST,是一個(gè)系統(tǒng)窗口,這個(gè)窗口最終會被WindowManagerService(WMS)標(biāo)記管理。但是我們的普通應(yīng)用程序怎么能擁有添加系統(tǒng)窗口的權(quán)限呢?查看源碼后發(fā)現(xiàn)需要以下幾個(gè)步驟:

  1. 當(dāng)顯示一個(gè)Toast時(shí),NMS會生成一個(gè)token,而NMS本身就是一個(gè)系統(tǒng)級的服務(wù),所以由它生成的token必然擁有權(quán)限添加系統(tǒng)窗口。
  2. NMS通過ITransientNotification也就是tn對象,將生成的token回傳到我們自己的應(yīng)用程序進(jìn)程中。
  3. 應(yīng)用程序調(diào)用handleShow方法,去向WindowManager添加窗口。
  4. WindowManager檢查當(dāng)前窗口的token是否有效,如果有效,則添加窗口展示Toast;如果無效,則拋出上述異常,Crash發(fā)生。

詳細(xì)的原理圖如下:

在Android 7.1.1的NMS源碼中,關(guān)鍵代碼如下:

  1. void showNextToastLocked() { 
  2.    ToastRecord record = mToastQueue.get(0); 
  3.    while (record != null) { 
  4.        try { 
  5.            // 調(diào)用tn對象的show方法展示toast,并回傳token 
  6.            record.callback.show(record.token); 
  7.            // 超時(shí)處理 
  8.            scheduleTimeoutLocked(record); 
  9.            return
  10.        } catch (RemoteException e) { 
  11.            ... 
  12.        } 
  13.    } 
  14.  
  15. private void scheduleTimeoutLocked(ToastRecord r) 
  16.    mHandler.removeCallbacksAndMessages(r); 
  17.    Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); 
  18.    long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY; 
  19.    // 根據(jù)toast顯示的時(shí)長,延遲觸發(fā)消息,最終調(diào)用下面的方法 
  20.    mHandler.sendMessageDelayed(m, delay); 
  21.  
  22. private void handleTimeout(ToastRecord record) 
  23.    synchronized (mToastQueue) { 
  24.        int index = indexOfToastLocked(record.pkg, record.callback); 
  25.        if (index >= 0) { 
  26.            cancelToastLocked(index); 
  27.        } 
  28.    } 
  29.  
  30. void cancelToastLocked(int index) { 
  31.    ToastRecord record = mToastQueue.get(index); 
  32.    try { 
  33.        // 調(diào)用tn對象的hide方法隱藏toast 
  34.        record.callback.hide(); 
  35.    } catch (RemoteException e) { 
  36.       ... 
  37.    } 
  38.  
  39.    ToastRecord lastToast = mToastQueue.remove(index); 
  40.    // 移除當(dāng)前的toast的token,token就此失效 
  41.    mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY); 
  42.    ... 

問題驗(yàn)證

通過以上分析showNextToastLocked()被調(diào)用后,如果此時(shí)主線程由于其它原因被阻塞導(dǎo)致handleShow()不能及時(shí)調(diào)用,從而觸發(fā)超時(shí)邏輯導(dǎo)致token失效。主線程阻塞結(jié)束后,繼續(xù)執(zhí)行Toast的show方法時(shí),發(fā)現(xiàn)token已經(jīng)失效了,于是拋出BadTokenException異常從而導(dǎo)致上述Crash。

可以使用以下的代碼驗(yàn)證此異常:

  1. Toast.makeText(this, "測試Crash", Toast.LENGTH_SHORT).show(); 
  2. try { 
  3.    Thread.sleep(5000); 
  4. } catch (InterruptedException e) { 
  5.    e.printStackTrace(); 

解決方案

那么如何解決這個(gè)異常呢?首先想到就是對Toast加上try-catch,但是發(fā)現(xiàn)不起作用,原因是這個(gè)異常并非在當(dāng)前線程中立即被拋出的,而是添加到了消息隊(duì)列中,等待消息真正執(zhí)行時(shí)才會被拋出。Google在Android 8.0的代碼提交中修復(fù)了這個(gè)問題,把8.0的源碼和前一版本對比可以發(fā)現(xiàn),如同我們的分析,Google在消息執(zhí)行處將異常catch住了。那么針對8.0之前的版本發(fā)生的Crash怎么辦呢?美團(tuán)平臺使用了一個(gè)類似代理反射的通用解決方案,結(jié)構(gòu)如下圖:

基本原理:使用我們自己實(shí)現(xiàn)的ToastHandler替換Toast內(nèi)部的Handler,ToastHandler作用就是把異常catch住,這種修改思路和Android 8.0修復(fù)思路保持一致,只不過一個(gè)是在系統(tǒng)層面解決,一個(gè)是在用戶層面解決。

斗爭三:token null is not valid

在Android 7.1.1、7.1.2和去年8月發(fā)布的Android 8.0系統(tǒng)中,我們的方案出現(xiàn)了另一個(gè)異常token null is not valid,這個(gè)異常堆棧如下:

  1. android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running? 
  2.    at android.view.ViewRootImpl.setView(ViewRootImpl.java:683) 
  3.    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342) 
  4.    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94) 

token null is not valid原因分析

這個(gè)異常其實(shí)并非是Toast的異常,而是Google對WindowManage的一些限制導(dǎo)致的。Android從7.1.1版本開始,對WindowManager做了一些限制和修改,特別是TYPE_TOAST類型的窗口,必須要傳遞一個(gè)token用于權(quán)限校驗(yàn)才允許添加。Toast源碼在7.1.1及以上也有了變化,Toast的WindowManager.LayoutParams參數(shù)額外添加了一個(gè)token屬性,這個(gè)屬性的來源就已經(jīng)在上文分析過了,它是在NMS中被初始化的,用于對添加的窗口類型進(jìn)行校驗(yàn)。當(dāng)用戶禁掉通知權(quán)限時(shí),由于AspectJ的存在,最終會調(diào)用我們封裝的MToast,但是MToast沒有經(jīng)過NMS,因此無法獲取到這個(gè)屬性,另外就算我們按照NMS的方法自己生成一個(gè)token,這個(gè)token也是沒有添加TYPE_TOAST權(quán)限的,最終還是無法避免這個(gè)異常的發(fā)生。

源碼中關(guān)鍵代碼如下:

  1. // 方法簽名多了一個(gè)IBinder類型的token,它是在NMS中創(chuàng)建的 
  2. public void handleShow(IBinder windowToken) { 
  3.  ... 
  4.  if (mView != mNextView) { 
  5.      ... 
  6.      mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); 
  7.      mParams.x = mX; 
  8.      mParams.y = mY; 
  9.      mParams.verticalMargin = mVerticalMargin; 
  10.      mParams.horizontalMargin = mHorizontalMargin; 
  11.      mParams.packageName = packageName; 
  12.      mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; 
  13.  
  14.      // 這里添加了token 
  15.      mParams.token = windowToken; 
  16.  
  17.      if (mView.getParent() != null) { 
  18.          if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); 
  19.          mWM.removeView(mView); 
  20.      } 
  21.      ... 
  22.  
  23.      try { 
  24.          // 8.0版本的系統(tǒng),將這里的異常catch住了 
  25.          mWM.addView(mView, mParams); 
  26.          trySendAccessibilityEvent(); 
  27.      } catch (WindowManager.BadTokenException e) { 
  28.          /* ignore */ 
  29.      } 
  30.  } 

解決方案

經(jīng)過調(diào)研,發(fā)現(xiàn)Google對WindowManager的限制,讓我們不得不放棄使用TYPE_TOAST類型的窗口替代Toast,也代表了我們上述使用WindowManager方案的終結(jié)。

斗爭總結(jié)

我們的核心目標(biāo)只是希望在用戶關(guān)閉通知消息開關(guān)的情況下,能繼續(xù)看到通知,所以我們使用了WindowManager添加自定義window的方式來替換Toast,但是在替換的過程中遇到了一些Toast的Crash異常,為了解決這些Crash,我們提出了使用自定義ToastHandler的方式來catch住異常,確保app正常運(yùn)行。在方案推廣上,為了能用更少的人力,更高的效率完成替換,我們使用了AspectJ的方案。***,在Android 7.1.1版本開始,由于Google對WindowManager的限制,導(dǎo)致這種使用自定義window的替換Toast的方式不再可行,我們便開始尋找替換Toast的其它可行方案。

替換Toast的可行方案

為了繼續(xù)能讓用戶在禁掉通知權(quán)限的情況下,也能看到通知以及屏蔽上述Toast帶來的Crash,我們經(jīng)過調(diào)研、分析并嘗試了以下幾種方案。

  1. 在7.1.1以上系統(tǒng)中繼續(xù)使用WindowManager方式,只不過需要把type改為TYPE_PHONE等懸浮窗權(quán)限。
  2. 使用Dialog、DialogFragment、PopupWindow等彈窗控件來實(shí)現(xiàn)一個(gè)通知。
  3. 按照Snackbar的實(shí)現(xiàn)方式,找到一個(gè)可以添加布局的父布局,采用addView的方式添加通知。

以上幾種方案的共同點(diǎn)是為了繞過通知權(quán)限的檢查,即使用戶禁掉了通知權(quán)限,我們自定義的通知依然可以不受影響的彈出來,但是也有很明顯的缺陷,如下圖:

經(jīng)過對比,我們也采用了Snackbar替換Toast的方案,原因是Snackbar是Android自5.0系統(tǒng)推出MaterialDesign后官方推薦的控件,在交互友好性方面比Toast要好,例如:支持手勢操作,支持與CoordinatorLayout聯(lián)動(dòng)等,Snackbar作為提示控件目前在市面上也被廣泛使用,而其它方案有明顯的缺陷如下:

首先,使用WindowManager添加懸浮窗的方式,雖然這種方式能和原生的Toast保持***的一致性,但是需要的權(quán)限太高,坑也太多。TYPE_PHONE的權(quán)限要比TYPE_TOAST權(quán)限敏感太多,而且在Android 8.0系統(tǒng)上必須使用TYPE_APPLICATION_OVERLAY這個(gè)type,并且要申請以下兩個(gè)權(quán)限,這兩個(gè)權(quán)限不僅需要在清單文件中聲明,而且絕大部分手機(jī)默認(rèn)是關(guān)閉狀態(tài),需要我們引導(dǎo)用戶開啟,如果用戶選擇不開啟,那么Toast還是不能彈出。同時(shí)還需要適配眾多定制化ROM的國產(chǎn)機(jī)型。繞過了通知權(quán)限的坑,又跳入了懸浮窗權(quán)限的坑,這是不可取的。

  1. <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> 
  2. <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW"/> 

其次,使用Dialog方式也有明顯的缺陷,Dialog、DialogFragment、PopupWindow都嚴(yán)重依賴于Activity,沒有Activity作為上下文時(shí),它們是無法創(chuàng)建和顯示的,并且簡單的通知使用這種控件過重。此外,在UI展示和API一致性上,幾乎和Toast沒有什么關(guān)系,需要額外做封裝的成本比較大。

遇到問題

我們在使用Snackbar替換Toast時(shí)遇到了以下兩個(gè)問題:

  1. Snackbar彈出的時(shí)候,被Dialog,PopupWindow等控件遮住。
  2. Snackbar無法進(jìn)行跨頁面展示,這是Snackbar實(shí)現(xiàn)原理決定的。

解決方案

首先,為了滿足自身業(yè)務(wù)的擴(kuò)展性、靈活性,我們參照系統(tǒng)Snackbar的源碼,進(jìn)行了按需定制,比如多樣化的樣式擴(kuò)展、進(jìn)入進(jìn)出的動(dòng)畫擴(kuò)展、支持自定義布局的擴(kuò)展等,接口更加豐富。一方面是為了解決以上遇到的問題,另一方面也是為了在業(yè)務(wù)的迭代過程中能快速開發(fā)和適配。以下是基本的類圖依賴關(guān)系:

問題一解決

針對Snackbar彈出的時(shí)候,被Dialog,PopupWindow等控件遮住的問題,原因在于Snackbar依賴于View,當(dāng)把Activity布局的View傳給Snackbar做為Snackbar展示依賴的父View時(shí),后面再彈Dialog,PopupWindow等控件,Snackbar就會被控件遮擋。正確的做法是直接把PopupWindow和Dialog所依賴的View傳給Snackbar。那么我們定制化的Snackbar不僅支持傳遞這個(gè)View,也支持直接傳遞PopupWindow和Dialog的實(shí)例,上圖中SnackbarBuilder的方法反應(yīng)了這個(gè)改動(dòng)。

問題二解決

比較復(fù)雜的問題是Snackbar不支持跨頁面展示,我們在項(xiàng)目中有大量這樣的代碼:

  1. Toast.makeText(this, "彈出消息", Toast.LENGTH_SHORT).show(); 
  2. finish(); 

當(dāng)直接把Toast替換成Snackbar后,這個(gè)消息會一閃而過,用戶來不及查看,因?yàn)镾nackbar依賴的Activity被銷毀了,為了解決這個(gè)問題,我們一共探討了三種方案:

方案一:

使用startActivityForResult替換所有跨頁面展示的通知,也就是在A頁面使用startActivityForResult跳轉(zhuǎn)到B頁面,把原本在B頁面彈出Toast的邏輯,改寫到A頁面自己彈出Snackbar。

這種方案:優(yōu)點(diǎn)在于責(zé)任清晰明確,頁面被finish后應(yīng)該展示什么通知以及應(yīng)該由誰觸發(fā)這個(gè)通知的展示,這個(gè)責(zé)任本身就在調(diào)用方;缺點(diǎn)在于代碼改動(dòng)比較大。因此我們舍棄了這種方案。

方案二:

使用Application.ActivityLifecycleCallbacks全局監(jiān)聽Activity的生命周期,當(dāng)一個(gè)頁面關(guān)閉的時(shí)候,記錄下Snackbar剩余需要展示的時(shí)間,在進(jìn)入下一個(gè)Activity后,讓沒有展示完的Snackbar繼續(xù)展示。

這種方案:優(yōu)點(diǎn)在于代碼改動(dòng)量?。蝗秉c(diǎn)在于在頁面切換過程中,如果Snackbar沒有展示結(jié)束,會出現(xiàn)一次閃爍。雖然在技術(shù)上這種方案很好,代碼的侵入性極低,但是這個(gè)閃爍對于產(chǎn)品來說無法接受,因此這種方案也不做考慮。

方案三:

使用本地廣播進(jìn)行跨頁面展示,這也是美團(tuán)最終使用的解決方案,具體原理如下

  1. 在A頁面跳轉(zhuǎn)B頁面前,使用當(dāng)前傳入的Context注冊一個(gè)廣播。
  2. 在B頁面finish之前,發(fā)送A在跳轉(zhuǎn)前注冊的廣播,并把需要展示的消息使用Intent返回。
  3. 在廣播中獲取A頁面的實(shí)例,使用Snackbar展示B頁面回傳的消息,并把當(dāng)前廣播unRegister反注冊掉。

這是方案一的自動(dòng)化版本,為了達(dá)到自動(dòng)化的效果和對原有代碼的最小侵入性,我們設(shè)計(jì)了一個(gè)輔助類,就是上圖中的SnackbarHelper,原理圖如下:

SnackbarHelper提供統(tǒng)一的入口,接入成本低,只需要將原有使用context.startActivity()、context.startActivityForResult()、context.finish()的地方改成SnackBarHelper下面的同名方法即可。這樣通過廣播的方法完成了Snackbar的跨頁面展示,業(yè)務(wù)方的代碼修改量僅僅是改一下調(diào)用方式,改動(dòng)極小。

結(jié)語

目前這套解決方案在美團(tuán)業(yè)務(wù)中被廣泛使用,能覆蓋到絕大部分場景。通知的展現(xiàn)形式基本與Toast沒有區(qū)別,不僅解決了用戶在禁掉通知的情況下無法看到通知的困境,也降低了客訴率。

【本文為51CTO專欄機(jī)構(gòu)“美團(tuán)點(diǎn)評技術(shù)團(tuán)隊(duì)”的原創(chuàng)稿件,轉(zhuǎn)載請通過微信公眾號聯(lián)系機(jī)構(gòu)獲取授權(quán)】

戳這里,看該作者更多好文

責(zé)任編輯:武曉燕 來源: 51CTO專欄
相關(guān)推薦

2013-04-28 09:50:02

PHPMySQL

2017-07-21 09:48:34

SnackBarToastGoogle

2018-03-15 15:12:00

潤乾報(bào)表集成

2011-04-14 14:23:06

軟件測試測試

2012-02-22 09:32:58

云計(jì)算微軟Azure

2013-10-12 13:26:08

設(shè)計(jì)加載

2015-09-01 15:12:45

JavaHashMap那點(diǎn)事

2023-12-21 20:53:15

2011-05-25 19:37:47

2021-07-30 07:28:15

Kafka消息引擎

2011-08-31 10:15:48

桌面管理軟件

2009-07-03 14:16:30

JSP Servlet

2010-08-10 15:08:17

UPS電源評測

2010-06-07 14:07:18

IPv4與IPv6

2012-06-11 15:02:53

ASP.NET

2013-04-09 10:03:29

iOS6.0旋轉(zhuǎn)兼容

2011-02-22 09:47:58

WatchStor 征

2019-07-01 14:55:44

應(yīng)用安全web安全滲透測試

2018-10-22 13:34:24

SD-WAN運(yùn)維網(wǎng)絡(luò)

2010-07-22 10:07:01

SharePoint
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號