Toast 源码分析

思考

在分析源码之前有两个问题

  1. 子线程直接使用 Toast 时会抛出异常 “Can’t create handler inside thread that has not called Looper.prepare()”, 在 Toast 类里搜 “Looper” 并搜不到。是哪里抛出的这个异常呢?

  2. 在子线程中这样使用 Toast:

Looper.prepare();
Toast.makeText(getApplicationContext(), "test toast", Toast.LENGTH_SHORT).show();
Looper.loop();

像上面这种调用是在主线程显示 Toast 吗

Toast 源码分析

先看 Toast 的 makeText 和 show 方法:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    // 创建 TN(Toast 的内置对象),用于展示 Toast 的主要类
    Toast result = new Toast(context);

    // 加载 Toast 的展示内容
    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;
}

public void show() {
    // 校验显示内容
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }

    INotificationManager service = getService(); // NotificationManagerService 的成员变量 mService 是 INotificationManager.Stub 的实现类
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    // 展示内容属性赋值给 tn 的属性
    tn.mNextView = mNextView;

    try {
        // 调用 NotificationManagerService 中 mService 的 enqueueToast 方法
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        // Empty
    }
}

下面来看 NotificationManagerService 中 mService 的 enqueueToast 方法:

// callback 就是 TN 实例
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
    // ...

    synchronized (mToastQueue) {
        int callingPid = Binder.getCallingPid();
        long callingId = Binder.clearCallingIdentity();
        try {
            ToastRecord record;
            int index = indexOfToastLocked(pkg, callback);
            // If it's already in the queue, we update it in place, we don't
            // move it to the end of the queue.
            if (index >= 0) {
                record = mToastQueue.get(index);
                record.update(duration);
            } else {
                // ...

                Binder token = new Binder();
                mWindowManagerInternal.addWindowToken(token,
                        WindowManager.LayoutParams.TYPE_TOAST);
                // 创建 ToastRecord 对象
                record = new ToastRecord(callingPid, pkg, callback, duration, token);
                // mToastQueue 添加记录
                mToastQueue.add(record);
                index = mToastQueue.size() - 1;
                keepProcessAliveIfNeededLocked(callingPid);
            }
            if (index == 0) {
                showNextToastLocked();
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}

上面代码可以看到有往 mToastQueue 中添加记录,那么什么时候执行呢,全局搜 “mToastQueue.get(” 看到在 showNextToastLocked 方法中取出 mToastQueue 的第 0 个元素,调用 record.callback.show(record.token), record.callback 就是 TN 对象,也就是这里调了 TN 的 show 方法。下面就来看一下 TN 的 show 方法做了什么:

public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    // 交给 mHandler 处理
    mHandler.obtainMessage(0, windowToken).sendToTarget();
}

// TN 的 mHandler
final Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        // 所以 TN 的 show 方法处理的是这里
        IBinder token = (IBinder) msg.obj;
        handleShow(token);
    }
};

下面来看核心展示 Toast 的方法,也就是 TN 的 handleShow() 方法

public void handleShow(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
            + " mNextView=" + mNextView);
    if (mView != mNextView) {
        handleHide();
        mView = mNextView;
        // ...
        // 展示使用的是 WindowManager
        mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

        // 下面配置的是位置参数
        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);
        // 展示 Toast, 由此也可以看出,Toast 是被添加在 Window 下的
        mWM.addView(mView, mParams);
        trySendAccessibilityEvent();
    }
}

总结

Toast 是先调用 makeText 创建要展示的 view, 调用 show 方法时内部跨进程通知 NotificationManagerService, 再通知到 TN 执行 show 方法,最终通过 handler 添加到 Window 下。
再看下最开始的思考,

  1. makeText 会 new Toast, Toast 构造方法里创建 TN 对象,TN 有一个成员变量 mHandler, 在创建 Handler 时需要该线程有 Looper 对象,主线程的 ActivityThread 的 main() 方法有创建 Looper 对象,但子线程并没有 Looper 对象,所以会抛异常。

  2. 如果在子线程调 Looper.prepare(), 会是在主线程展示的 Toast 吗,看上面的代码很明显不是,因为 Toast, TN 都是在子线程创建的,所以 mHandler 也是在子线程创建的,处理消息时的 Looper 也是子线程创建的,所以展示也是在子线程的。

你可能感兴趣的:(Android)