问题来源
Toast是我们Android大开发中比不可少的显示部分,相信大家很熟悉Toast的使用方式。之所以写这篇文章,主要是我在项目中遇到了这样一个问题,PM说的Toast在某个特定的页面显示的时长能不能合适一点,什么意思?我们大Toast只有Toast.LENGTH_SHORT
和Toast.LENGTH_LONG
两种,前者是2s,后者是显示3.5s。嗯,有些页面的确显示有问题,short太短,long太短,搞得比较尴尬,所以需要分析一下,这Toast到底是怎么运行的,我能不能修改这个时间,让它要多长有多长呢? 我希望你读完这篇文章之后,能了解深层次的Toast运行原理。
深入源码
废话不多说,我们一般使用Toast的方式如下:
Toast.makeText(activity, "hello world", Toast.LENGTH_SHORT).show();
//or
Toast.makeText(activity, "hello world", Toast.LENGTH_LONG).show();
通过源码分析,makeText()方法中,主要是获取LayoutInflater获取一个TextView,这个TextView就是我们的Toast需要显示的View,这个很简单,就不说了,主要来看一下show()方法:
/**
* 显示特定时长的View
*/
public void show() {
//代码省略
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
通过上面可以看出,首先Toast获取了一个INotificationManager,因为后面叫service,看上去是一个服务,那么我们看一下getService()方法:
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
一看INotificationManager.Stub.asInterface,就知道使用了aidl
方法,这可有点扯淡了,一个简单的Toast,也要用上aidl
,我不太熟悉,怎么办?既然不熟悉,那么我们先放一步说话吧,先看看
service.enqueueToast(pkg, tn, mDuration);
方法,但是这个方法也是标红的:
以前,每次看到这里,都有些失望,都不知道接下来该怎么分析源码,因为这些源码都在framework层,也不好找,怎么办呢?别急,现在已经有办法了,现在有这么一个网站,专门介绍Android#framwork层的,叫http://androidxref.com/,大家可以有事没事上去看看。说到这里,我脑袋发热,直接搜了一下enqueueToast
方法,不搜不知道,一搜吓一跳:
我们居然搜到了INotificationManager.aidl
,Toast
还有NotificationManagerService.java
三个文件,想都不用想,真正的实现类一定是NotificationManagerService.java
,我们进去看看:
我稍微把代码整理一下,当然我建议你先不看这一大段代码,说实话没什么卵用,边看解释边回来看效果更好:
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration) {
//无关紧要的代码ignore
final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
final boolean isPackageSuspended =
isPackageSuspendedForUser(pkg, Binder.getCallingUid());
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);
}
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token,WindowManager.LayoutParams.TYPE_TOAST);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
}
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
代码还是很长,不过我们慢慢分析,我们先看一下enqueueToast(String pkg, ITransientNotification callback, int duration)
这个方法三个参数分别是:pkg,ITransientNotification接口的callback和duration,其实我现在还是最关心的就是这个duration,因为这个duration传入的就是决定显示的时间,见Toast.show()方法:
不过,更重要的是需要知道这个TN,即ITransientNotification这个callback,TN是Toast的一个静态内部类,继承了ITransientNotification.Stub (这是是aidl方法产生的,此时也实现了ITransientNotification接口,这篇不是扯aidl的,所以我就当大家都明白), 大家可以先不管这个TN到底是个什么玩意,先不扯,等用到了咋们在慢慢回来看。
对于上面的源码,我们就从最简单的方式分析吧,那么我们就直接分析这个方法:
if (index == 0) {
showNextToastLocked();
}
我们就认为现在mToastQueue
就一个ToastRecord
,在分析这个方法之前,我们先看一下ToastRecord
是怎么生成的:
record = new ToastRecord(callingPid, pkg, callback, duration, token);
可见,此时的callback和duration是我们在传入enqueueToast
方法的参数,此时记住就行,等会要用到,现在我们需要分析showNextToastLocked()
方法了:
方法不长,那我就直接截图了:
此时很明朗,其实是直接调用了record.callback.show(record.token)
通过刚才的分析,这个callback就是我们传入ITransientNotification
,当然实现者大家肯定都知道,就是Toast内部静态类TN,那么是时候看看TN的show()方法了:
@Override
public void show(IBinder windowToken) {
mHandler.obtainMessage(0, windowToken).sendToTarget();
}
这个一看就明白,直接mHandler看源码了:
这个简单,直接看handleShow(token)方法了:
@Override
public void handleShow(IBinder windowToken) {
if (mView != mNextView) {
WindowManager mWn = ... ;
//代码省
mWM.addView(mView, mParams);
}
}
哦,终于知道Toast也是通过WindowManager.addView
添加上去的,这个就叼了,最起码我们分析了一遍Toast的生成过程,还是比较曲折的。 好了,既然挂上去了,那怎么消失掉呢?消失的源码该怎么分析呢?别急,咋们忘回看:showNextToastLocked()
中还有一个scheduleTimeoutLocked(record)
方法,我们进去看看:
很简单,如果使用的事Toast.LENGTH_LONG,就延迟3.5秒,否则就延迟2秒,那么我们去看一下mHandler.MESSAGE_TIMEOUT:
那我们就去看一下handleTimeOut
方法:
这次执行的方法是cancelToastLocked(index)
方法,那么此时这个方法在做什么呢?
我们来看一下代码:
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
//调用record.callback
//即是刚刚认识的TN对象
try {
record.callback.hide();
} catch (RemoteException ignore) {
//...
}
ToastRecord lastToast = mToastQueue.remove(index);
mWindowManagerInternal.removeWindowToken(lastToast.token, true);
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
showNextToastLocked();
}
}
同样道理,我们看到了record.callback.hide()
方法,此时我们知道callback还是Toast中的TN对象,那么它的hide
方法为:
@Override
public void hide() {
mHandler.post(mHide);
}
对于mHander.post(mHide)
,其中的mHide
为一个Runnable对象:
final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
mNextView = null;
}
};
直接去看handleHide()
方法:
public void handleHide() {
if (mView != null) {
if (mView.getParent() != null) {
//最终还是WindowManager移除了该View
mWM.removeViewImmediate(mView);
}
mView = null;
}
}
最终还是熟悉的WindowManager移除了mView,那么此时我们的Toast就会消失在屏幕之上了。
当然了,事情还没有结束,在cancelToastLocked(index)
方法中还有一个方法值得我们看一下:
if (mToastQueue.size() > 0) {
showNextToastLocked();
}
如果ToastQueue不为空,那么将继续循环将ToastQueue每一个ToastRecord执行show()和hide()方法,直到所有Toast都显示掉。当然,如果你不适用特殊的手段,按照Toast的执行意向,你将不会同时看到两个Toast在一个屏幕上,因为前一个Toast没有show()完成,不会去调用后面ToastRecord的方法的。
上个图,把整个过程描述一下,如果有错误,请及时提出:
Toast.show()
其实远程调用了NotificationManageService的enqueueToast
方法,在该方法中,存在一个Handler遍历ToastQueue,ToastQueue中每一个ToastRecord将会调用callback.show()
和callback.hide()
方法,此方法最终将调用Toast的内部类TN对象的show()
和hide()
方法,而show
和hide
将分别调用WindowManager.addView()
和windowManager.removeView()
方法,直至将Toast显示或者移除在屏幕上。
结局
回到开头,到了这里,我们是否可以自己控制Toast的显示时长呢?如果还是要NotificationManagerService参与,那就没啥希望了,因为它内部的Handler.postDelay()
只有两种选择,要么是2s,要么是3.5s。那么我们不用NotificationManagerService
这这尊大佛,直接调用Toast.TN.show()
和Toast.TN.Hide()
是否可行呢?我的想法也是这样的,也实现过了,代码如下,很简单,只使用了简单的反射:
public class AllTimeShowToast {
private Toast mToast ;
private Object TN ;
private Method show ;
private Method hide ;
private TextView mLongTextView ;
public AllTimeShowToast(Context context) {
mToast = new Toast(context );
initTextView(context);
initTN();
}
private void initTextView(Context context) {
mLongTextView = new TextView(context);
mLongTextView.setText("all time show ");
}
private void initTN() {
try {
Field tnObj = mToast.getClass().getDeclaredField("mTN");
tnObj.setAccessible(true);
TN = tnObj.get(mToast);
show = TN.getClass().getMethod("show");
hide = TN.getClass().getMethod("hide");
Field tnParamsField = TN.getClass().getDeclaredField("mParams");
tnParamsField.setAccessible(true);
WindowManager.LayoutParams params = (WindowManager.LayoutParams) tnParamsField.get(TN);
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
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;
Field nextViewField = TN.getClass().getDeclaredField("mNextView");
nextViewField.setAccessible(true);
nextViewField.set(TN, mLongTextView);
}catch (Exception e) {
e.printStackTrace();
}
}
public void show() {
try {
show.invoke(TN);
} catch (Exception e) {
e.printStackTrace();
}
}
public void hide() {
try {
hide.invoke(TN) ;
}catch (Exception e) {
e.printStackTrace();
}
}
}
调用方式为:
var showToast = AllTimeShowToast(this);
//show方法展示
showToast!!.show()
//hide方法展示
showToast!!.hide()
结果如下:
当然了,这里Toast也只是一次性的,show()
一次,hide()
一次之后,就相当于废了。如果要想重新show
,那么需要重新new Object
了。
好了,这篇文章好长,也差不多写完了,基本上把Toast流程分析了一遍。。。