Android 高級自定義Toast及源碼解析

neusoft1 8年前發布 | 10K 次閱讀 Android開發 移動開發 TOAST

Toast概述

Toast的作用

不需要和用戶交互的提示框。

Toast的簡單使用

Toast.makeText(MainActivity.this.getApplicationContext(),"沉迷學習,日漸消瘦",Toast.LENGTH_SHORT).show()

自定義Toast

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 。代碼就不貼了。

高級自定義Toast

產品狗的需求:點擊一個 Button ,網絡請求失敗的情況下使用 Toast 的方式提醒用戶。
程序猿:ok~大筆一揮。

Toast.makeText(MainActivity.this.getApplicationContext(),"沉迷學習,日漸消瘦",Toast.LENGTH_SHORT).show()

測試:你這程序寫的有問題。每次點擊就彈出了氣泡,連續點擊20次,居然花了一分多鐘才顯示完。改!
程序猿:系統自帶的就這樣。愛要不要。
測試:那我用單元測試模擬點擊50次之后,它就不顯示了,這個怎么說。
程序猿:…
這個時候,高級自定義 Toast 就要出場了~

activity_main.xml —->上下兩個按鈕,略。

MainActivity.java

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();
}

}</code></pre>

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;
}

}</code></pre>

那么有的同學會問了:你這樣不就是加了個單例嗎,好像也沒有什么區別。區別大了。僅僅一個單例,既實現了產品狗的需求,又不會有單元測試快速點擊50次的之后不顯示的問題。為什么?Read The Fucking Source Code。

Toast源碼解析

這里以 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="

<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></code></pre>

可以發現,里面只有一個 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 的精華就在這三個問題里,接下來的內容全部圍繞上述三個問題,尤其是第三個。已經全部了解的同學可以去看別的博客了~

1. 通過 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") 。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() 。具體可以看下這篇博客。

2. 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 端。這個暫且按下不提,下文會詳細講述。

3. 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);
            }
        }
    }
    ...
}</code></pre> 

在 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/Toast . j a v a o a s t

TN#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 掉。

 

 

來自:http://blog.csdn.net/qq_17250009/article/details/52753929

 

 本文由用戶 neusoft1 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!