Handler消息机制三——在子线程中使用Toast

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/haoyuegongzi/article/details/82495669

上一篇文章末尾我们提到了——在子线程中使用Toast需要做以下处理:

newThread(new Runnable() {
   @Override
   public void run() {
       Looper.prepare();
       Toast.makeText(getApplicationContext(), "子线程显示", Toast.LENGTH_SHORT).show();
       Looper.loop();
   }
}).start();

根据我们的标题,除非Toast使用到了Handler通信机制,否则在子线程中使用toast,不必调用Looper.prepare()和Looper.loop()。
那Toast到底有没有使用到Handler通信机制呢?我们从使用Toast的步骤,结合源码一步步的来分析。

步骤一、Toast
步骤二、makeText(getApplicationContext(), "子线程显示", Toast.LENGTH_SHORT)
步骤三、.show();

首先,我们看步骤一Toast涉及到的源码:

Toast的构造方法有以下几个:

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

这个是我们通过关键字new得到一个Toast对象时使用的方法,但这里并没有与handler扯上联系。

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

这里构造方法里面就要去我传递一个Looper对象的参数,到这里我们初步获得了Toast跟handler有关的信息。但不急,我们继续看源码,看看这里Looper对象或者handler对象的作用是什么,是不是通信。
在源码里面我们看到Looper对象传递进去后就没有在下一步的操作,而与此有关联的对象是mTN,这里从源码上看,这里是在设置mTN的位置和尺寸大小。到此我们基本可以判定,Toast的构造方法里面handler对象没有起到通信的作用。

然后,我们再看看步骤二makeText(Context context, CharSequence text, Duration int duration)涉及到的源码

步骤二makeText的源码分三部分,但三个方法都是层层的调用,最终归集到一个方法上来:

方法1、
public static Toast makeText(Context context, @StringRes int resId, @Duration int duration) throws Resources.NotFoundException {
    return makeText(context, context.getResources().getText(resId), duration);
}
该方法我们看不出什么端倪,也看不到与handler有什么关系,直接的反应是他调用了makeText()的方法,那我们继续看他调用的makeText()方法。

方法2:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    return makeText(context, null, text, duration);
}
感觉很扯蛋吧,这里也是没什么有用的事情,还是直接调用了另一个makeText()方法。好吧,我们再看看他调用的makeText()方法:

方法3:
public static Toast makeText(@NonNull Context context, @Nullable Looper looper, @NonNull CharSequence text, @Duration int duration) {
    Toast result = new Toast(context, looper);
    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;
}
这里我们终于看到了干了点实事的代码。

首先,从参数上看,涉及到了Looper对象,但是前面的方法1压根儿就没有传Looper参数过来,方法二传了一个空对象过来,所以这里依然不涉及到handler的实际上的使用。

然后,从代码上看,这里是在布局并渲染Toast的UI界面,设置相关的参数。
因此步骤二也不涉及到handler的实际使用。

最后我们再来看看步骤三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
    }
}

这里,很奇怪我的源码里面INotificationManager、getOpPackageName()、enqueueToast()三处爆红,无法进一步看到内部具体实现。
无法看到具体实现,那我们先看看这个方法做了些什么事情。

这里,先是获取INotificationManager的服务对象,然后是获取到当前应用的包名,再是两个赋值操作。貌似也没什么特别的地方。最后是try……catch下的enqueueToast(pkg, tn, mDuration)操作。

等等,等等,enqueueToast()?是不是有点熟悉?enqueue?入队的意思?enqueueToast?吐司入队?但遗憾,由于爆红的缘故,无法查看到enqueueToast()方法的源码。

但发现另外一个很有趣的事情,那就是第一步和这一步都不断出现的一个参数:mTN。我们知道,在handler的入队操作的时候是要传入一个handler对象的,这里进行enqueueToast()操作使用到了mTN参数,莫非mTN就是handler或者是一个与handler有关的参数?

走吧,去瞧瞧他的构成。mTN的是TN.class的对象。他的源码不多,具体如下:

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

    @Override
    public void show(IBinder windowToken) {
        if (localLOGV) Log.v(TAG, "SHOW: " + this);
        mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
    }


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

在这里面,我们很容易的发现了Handler的存在,以及handler对象相关的方法:SHOW、HIDE、CANCEL等信息,我们简单的看下,不扯太多把子。

show()方法:
public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
        mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
    }
}

hide()方法:
public void hide() {
    if (localLOGV) Log.v(TAG, "HIDE: " + this);
        mHandler.obtainMessage(HIDE).sendToTarget();
    }
}

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

这里三个方法里面,都调用了mHandler.obtainMessage()方法,查源码可以发现:

public final Message obtainMessage(int what, Object obj) {
    return Message.obtain(this, what, obj);
}

这里调用的是Handler下四大金刚之Message的obtain()方法:

public static Message obtain(Handler h, int what, Object obj) {
    Message m = obtain();
    m.target = h;
    m.what = what;
    m.obj = obj;
    return m;
}

其作用是封装消息进入Message对象。然后我们再来看后面的sendToTarget()方法:

public void sendToTarget() {
    target.sendMessage(this);
}

通过前面两篇关于Handler文章的分析,我们知道这里的target其实就是handler对象,他调用sendMessage方法,该方法先调用sendMessageDelayed()方法,然后sendMessageDelayed()方法调用sendMessageAtTime()方法,sendMessageAtTime()方法最终调用enqueueMessage()方法,将我们要发送的吐司消息入队。至此,我们完全获得了Toast通知使用Handler的过程。

由该过程我们也清除的了解到为什么我们在子线程里面使用Toast,需要先后调用Looper.prepare()和Looper.loop()方法。因为Toast通知在本质上是利用Handler将消息发送到UI线程然后刷新UI。其本质上跟我们的前一篇文章《Handler消息机制二——子线程下如何使用Handler》是一样的。

真相明了,不再累述。

猜你喜欢

转载自blog.csdn.net/haoyuegongzi/article/details/82495669