1. 程式人生 > >簡單原始碼分析之小小的Toast

簡單原始碼分析之小小的Toast

前言:toast再常見不過,但是一個小小的toast居然內有乾坤,呵(w)呵(t)呵(f)

原始碼如下:
public class Toast {

//toast顯示時間    註釋控制了下輸入內容
@IntDef({LENGTH_SHORT, LENGTH_LONG})
@Retention(RetentionPolicy.SOURCE)
public @interface Duration {}
public static final int LENGTH_SHORT = 0;
public static final int LENGTH_LONG = 1;

public void setDuration(@Duration int duration) { 《=== 註釋作用處  不按套路編譯報錯 Must be one of: Toast.LENGTH_SHORT, Toast.LENGTH_LONG
    mDuration = duration;
    mTN.mDuration = duration;
}

@Duration
public int getDuration() {
    return mDuration;
}

//通常用法中乾坤Toast.makeText(context, "hello world", Toast.LENGTH_SHORT).show();
/**
 * @param context  The context to use.  Usually your {@link android.app.Application}       《=== 註解只留這一句,上下文的型別
 *                 or {@link android.app.Activity} object.
 */
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    return makeText(context, null, text, duration);
}

public static Toast makeText(Context context, @StringRes int resId, @Duration int duration) 《=== stringId youknow
                            throws Resources.NotFoundException {
    return makeText(context, context.getResources().getText(resId), duration);
}

public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
        @NonNull CharSequence text, @Duration int duration) {
    Toast result = new Toast(context, looper);   《=== 完全就是一個有參構造的簡單類,自身本不是什麼view,Window(畫外音:顯示出來那個玩意一定是wm.add進入的)

    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;                     《=== 存為全域性為了下面TN使用
    result.mDuration = duration;

    return result;
}

public Toast(Context context) {
    this(context, null);
}

public Toast(@NonNull Context context, @Nullable Looper looper) {
    mContext = context;
    mTN = new TN(context.getPackageName(), looper);          《=== 伏筆(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);
}

public void show() {
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }

    INotificationManager service = getService();              《=== 好戲登場 
                                                               ==== 經過分析 ok INotificationManager這貨終於知道了,就是一個註冊的遠端服務,我們拿到一個他的代理(可以呼叫它的實現方法)
                                                               ==== 下面是INotificationManager內部實現
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;

    try {
        service.enqueueToast(pkg, tn, mDuration);              《=== toast任務,加入window,並加入到toast管理佇列
    } catch (RemoteException e) {
        // Empty
    }
}


/*********INotificationManager由來*/
private static INotificationManager sService;               
static private INotificationManager getService() {
    if (sService != null) {
        return sService;
    }
    sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));   《=== 證明了一切,INotificationManager是註冊在ServiceManager的一個服務
    =====================================
    補充知識如下:
    1.android系統是一個多程序的系統,程序之間相互獨立
    2.程序間通訊方式方法之一 “Binder”, 這裡就是使用的它
    3.彈toast為什麼要跨程序通訊?自我理解:比如我需要在遠端服務裡彈toast,(有大神給答案那就太好了)一定得跨程序,關於mWM.addView暫留1
    4.binder機制,去看看羅神的部落格吧
    return sService;
}


//ServiceManager中的方法,c的過來,方便理解
public static IBinder getService(String name) {
    try {
        IBinder service = sCache.get(name);  《=== sCache是一個hashmap private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>();
        if (service != null) {
            return service;
        } else {
            return Binder.allowBlocking(getIServiceManager().getService(name));   《=== 記憶體沒有,去本地取 (從下面的分析看完,返回上面流程繼續)
        }
    } catch (RemoteException e) {
        Log.e(TAG, "error in getService", e);
    }
    return null;
}

private static IServiceManager getIServiceManager() {
    if (sServiceManager != null) {
        return sServiceManager;
    }

    // Find the service manager
    sServiceManager = ServiceManagerNative
            .asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));   《=== 經過本地一些列查詢,反正返回了這個服務
    return sServiceManager;
}

/**
 * BinderInternal中的方法,native的,愛莫能助了,並不知道在哪System.load(“cpp”);
 * 但是可以看註解啊!
 * Return the global "context object" of the system.  This is usually
 * an implementation of IServiceManager, which you can use to find
 * other services.       《=== 菜雞英語為您翻譯:返回給你一個整個系統全域性的上下文,這個東東實現了IServiceManager,用這個東東就可以查詢你需要的service
 * 補充: 根據返回值IBinder,可見系統內部也是用的Binder通訊,羅神講的詳細,什麼本地服務,代理服務,什麼使用者空間,系統空間的。
 */
public static final native IBinder getContextObject();

//Binder內的方法
public static IBinder allowBlocking(IBinder binder) {
    try {
        if (binder instanceof BinderProxy) {
            ((BinderProxy) binder).mWarnOnBlocking = false;        《===  要查詢的service本身就是一個代理服務ps
        } else if (binder != null
                && binder.queryLocalInterface(binder.getInterfaceDescriptor()) == null) {   《=== 判定自身不是空,還在本地沒有
            Log.w(TAG, "Unable to allow blocking on interface " + binder);
        }
    } catch (RemoteException ignored) {
    }
    return binder;
}

// 猜想這個描述可能就是羅神分析裡ProcessState中的fd,檔案描述符
public String getInterfaceDescriptor() {
    return mDescriptor;
}
//賦值方法
public void attachInterface(IInterface owner, String descriptor) {
    mOwner = owner;
    mDescriptor = descriptor; //賦值過程暫留2
}


/******INotificationManager內部實現*/
//1.INotificationManager.aidl
void enqueueToast(String pkg, ITransientNotification callback, int duration);
//2.實現NotificationManagerService
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
    ==========刪除日誌和安檢

    final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
    final boolean isPackageSuspended =
            isPackageSuspendedForUser(pkg, Binder.getCallingUid());           《=== 猜測最終返回當前應用是否正在執行吧

    if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
            (!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
                    || isPackageSuspended)) {                                 《=== 日誌不看,這個判斷就是如果當前這個toast請求,既不是遠端代理,又不是系統,還不是應用互動期間,那你彈個毛
                                                                               ==== 多所一句,遠端代理,遠端服務彈toast
        return;
    }

    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) {                                          《=== toast數量限制,除了系統toast,防止記憶體洩露
                    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) {         《=== 我擦一個應用才能彈50個吐司?
                                 Slog.e(TAG, "Package has already posted " + count
                                        + " toasts. Not showing more. Package=" + pkg);
                                 return;
                             }
                         }
                    }
                }

                Binder token = new Binder();
                mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);    《=== 加入window
                record = new ToastRecord(callingPid, pkg, callback, duration, token);
                mToastQueue.add(record);                                                      《=== 加入管理佇列
                index = mToastQueue.size() - 1;
                keepProcessAliveIfNeededLocked(callingPid);
            }
            // If it's at index 0, it's the current toast.  It doesn't matter if it's
            // new or just been updated.  Call back and tell it to show itself.
            // If the callback fails, this will remove it from the list, so don't
            // assume that it's valid after this.
            if (index == 0) {
                showNextToastLocked();                                                        《=== fuck 終於找到show的邏輯了                                                    
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}


// 檢查是不是系統toast
protected boolean isCallerSystemOrPhone() {
    return isUidSystemOrPhone(Binder.getCallingUid());     《=== 其實這個地方就證明Binder跨程序,它內部儲存了程序id,包名等等資訊,用來操作和判斷
}
protected boolean isUidSystemOrPhone(int uid) {
    final int appid = UserHandle.getAppId(uid);
    return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);  《=== 系統uid 1000, phoneid 1001 ,證明0~1000都是系統應用
}

//檢測當前包是否正在使用
private boolean isPackageSuspendedForUser(String pkg, int uid) {
    int userId = UserHandle.getUserId(uid);
    try {
        return mPackageManager.isPackageSuspendedForUser(pkg, userId);        《=== 實現過程
    } catch (RemoteException re) {
        throw new SecurityException("Could not talk to package manager service");
    } catch (IllegalArgumentException ex) {
        // Package not found.
        return false;
    }
}

//interface IPackageManager.aidl  《=== 也是binder方式,要不我們放過它實現的過程?按照谷歌的性格應該有個PackageManagerService是實現類,如果查不到就放過他
//好吧一點驚喜都不給我PackageManagerService
 @Override
public boolean isPackageSuspendedForUser(String packageName, int userId) {
    final int callingUid = Binder.getCallingUid();
    enforceCrossUserPermission(callingUid, userId,
            true /* requireFullPermission */, false /* checkShell */,
            "isPackageSuspendedForUser for user " + userId);
    synchronized (mPackages) {
        final PackageSetting ps = mSettings.mPackages.get(packageName);
        if (ps == null || filterAppAccessLPr(ps, callingUid, userId)) {
            throw new IllegalArgumentException("Unknown target package: " + packageName);
        }
        return ps.getSuspended(userId);
    }
}
//。。。找啊找啊找朋友
//PackageSetting extends PackageSettingBase.getSuspended()
boolean getSuspended(int userId) {
    return readUserState(userId).suspended;          《==== 所以這個就是包使用者狀態的一個屬性 懸浮,暫停
}
public PackageUserState readUserState(int userId) {
    PackageUserState state = userState.get(userId);
    if (state == null) {
        return DEFAULT_USER_STATE;                   《=== 預設false
    }
    state.categoryHint = categoryHint;
    return state;
}
private final SparseArray<PackageUserState> userState = new SparseArray<PackageUserState>();
//.....跑偏的太厲害了,PackageUserState這個玩意原始碼先暫留3, 直譯:包使用者狀態


//Toast佇列,記錄Toast
final ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>();


//show的邏輯
@GuardedBy("mToastQueue")
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(record.token);              《====  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);                   《===  異常移除本次toast
            }
            keepProcessAliveIfNeededLocked(record.pid);
            if (mToastQueue.size() > 0) {
                record = mToastQueue.get(0);                 《===  上頂一個toast任務
            } else { 
                record = null;                               《===  退出顯示
            }
        }
    }
}


/*** 好累啊,終於到了第二場好戲,就是前面暫留的1 TN*/
//其實經過上面分析,TN的快感就少了很多
//extends ITransientNotification.Stub  《===  Binder通訊,一定會有一個ITransientNotification.aidl檔案外露介面,實現就在這裡
//ITransientNotification.aidl中定義
void show(IBinder windowToken);
void hide();
//實現,發現其實cancel不是複寫
private static class TN extends ITransientNotification.Stub {
    private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

    private static final int SHOW = 0;
    private static final int HIDE = 1;
    private static final int CANCEL = 2;
    final Handler mHandler;

    int mGravity;
    int mX, mY;
    float mHorizontalMargin;
    float mVerticalMargin;


    View mView;
    View mNextView;
    int mDuration;

    WindowManager mWM;

    String mPackageName;

    static final long SHORT_DURATION_TIMEOUT = 4000;
    static final long LONG_DURATION_TIMEOUT = 7000;

    TN(String packageName, @Nullable Looper looper) {
        // 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;

        mPackageName = packageName;

        if (looper == null) {
            // Use Looper.myLooper() if looper is not specified.
            looper = Looper.myLooper();
            if (looper == null) {
                throw new RuntimeException(
                        "Can't toast on a thread that has not called Looper.prepare()");
            }
        }
        mHandler = new Handler(looper, null) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case SHOW: {
                        IBinder token = (IBinder) msg.obj;
                        handleShow(token);
                        break;
                    }
                    case HIDE: {
                        handleHide();
                        // Don't do this in handleHide() because it is also invoked by
                        // handleShow()
                        mNextView = null;
                        break;
                    }
                    case CANCEL: {
                        handleHide();
                        // Don't do this in handleHide() because it is also invoked by
                        // handleShow()
                        mNextView = null;
                        try {
                            getService().cancelToast(mPackageName, TN.this);
                        } catch (RemoteException e) {
                        }
                        break;
                    }
                }
            }
        };
    }

    /**
     * schedule handleShow into the right thread
     */
    @Override
    public void show(IBinder windowToken) {
        if (localLOGV) Log.v(TAG, "SHOW: " + this);
        mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
    }

    /**
     * schedule handleHide into the right thread
     */
    @Override
    public void hide() {
        if (localLOGV) Log.v(TAG, "HIDE: " + this);
        mHandler.obtainMessage(HIDE).sendToTarget();
    }

    public void cancel() {
        if (localLOGV) Log.v(TAG, "CANCEL: " + this);
        mHandler.obtainMessage(CANCEL).sendToTarget();
    }

    public void handleShow(IBinder windowToken) {
        if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                + " mNextView=" + mNextView);
        // If a cancel/hide is pending - no need to show - at this point
        // the window token is already invalid and no need to do any work.
        if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
            return;
        }
        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);
            // 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;
            mParams.packageName = packageName;
            mParams.hideTimeoutMilliseconds = mDuration ==
                Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
            mParams.token = windowToken;
            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);
            // Since the notification manager service cancels the token right
            // after it notifies us to cancel the toast there is an inherent
            // race and we may attempt to add a window after the token has been
            // invalidated. Let us hedge against that.
            try {
                mWM.addView(mView, mParams);                《===   他強任他強,清風拂山崗  最終還是WindowManager.addView
                trySendAccessibilityEvent();
            } catch (WindowManager.BadTokenException e) {
                /* ignore */
            }
        }
    }

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

            mView = null;
        }
    }
}


//剩下的只要自定義過toast都知道,我就不胡說了~
final Context mContext;
final TN mTN;
int mDuration;
View mNextView;


public void cancel() {
    mTN.cancel();
}

public void setView(View view) {
    mNextView = view;
}

public View getView() {
    return mNextView;
}

public void setMargin(float horizontalMargin, float verticalMargin) {
    mTN.mHorizontalMargin = horizontalMargin;
    mTN.mVerticalMargin = verticalMargin;
}

public float getHorizontalMargin() {
    return mTN.mHorizontalMargin;
}

public float getVerticalMargin() {
    return mTN.mVerticalMargin;
}

public void setGravity(int gravity, int xOffset, int yOffset) {
    mTN.mGravity = gravity;
    mTN.mX = xOffset;
    mTN.mY = yOffset;
}

public int getGravity() {
    return mTN.mGravity;
}

public int getXOffset() {
    return mTN.mX;
}

public int getYOffset() {
    return mTN.mY;
}

public WindowManager.LayoutParams getWindowParams() {
    return mTN.mParams;
}


/**
 * Update the text in a Toast that was previously created using one of the makeText() methods.
 * @param resId The new text for the Toast.
 */
public void setText(@StringRes int resId) {
    setText(mContext.getText(resId));
}

/**
 * Update the text in a Toast that was previously created using one of the makeText() methods.
 * @param s The new text for the Toast.
 */
public void setText(CharSequence s) {
    if (mNextView == null) {
        throw new RuntimeException("This Toast was not created with Toast.makeText()");
    }
    TextView tv = mNextView.findViewById(com.android.internal.R.id.message);
    if (tv == null) {
        throw new RuntimeException("This Toast was not created with Toast.makeText()");
    }
    tv.setText(s);
}

}

原始碼貼不上了,刪了

小結:
1.toast本質是inflate了一個View(有預設,也可以設定),然後通過WindowManager.addView()進行顯示
2.由系統中NotificationManagerService管理和維護這一個ToastQueue(toast佇列)
3.NotificationManagerService又通過Toast.TN,輪循回撥,執行show的操作(即WindowManager.addView())
4.toast是有數量限制的

至此還剩三個遺留:
1.wm.addView流程
2.descriptor描述怎麼來的
3.packageUserState分析
下回分解吧