深入分享一下android.widget.Toast

问题来源

  Toast是我们Android大开发中比不可少的显示部分,相信大家很熟悉Toast的使用方式。之所以写这篇文章,主要是我在项目中遇到了这样一个问题,PM说的Toast在某个特定的页面显示的时长能不能合适一点,什么意思?我们大Toast只有Toast.LENGTH_SHORTToast.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显示过程

Toast.show()其实远程调用了NotificationManageService的enqueueToast方法,在该方法中,存在一个Handler遍历ToastQueue,ToastQueue中每一个ToastRecord将会调用callback.show()callback.hide()方法,此方法最终将调用Toast的内部类TN对象的show()hide()方法,而showhide将分别调用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()

结果如下:

all_time_to_show

当然了,这里Toast也只是一次性的,show()一次,hide()一次之后,就相当于废了。如果要想重新show,那么需要重新new Object了。
好了,这篇文章好长,也差不多写完了,基本上把Toast流程分析了一遍。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值