Android 高级自定义Toast及源码解析
本文已授权微信公众号《非著名程序员》原创首发,转载请务必注明出处。
Toast概述
Toast的作用
不需要和用户交互的提示框。
更多参见官网:https://developer.android.com/guide/topics/ui/notifiers/toasts.html
Toast的简单使用
Toast.makeText(MainActivity.this.getApplicationContext(),"沉迷学习,日渐消瘦",Toast.LENGTH_SHORT).show()
自定义Toast
Toast customToast = new Toast(MainActivity.this.getApplicationContext());View customView = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_toast,null);ImageView img = (ImageView) customView.findViewById(R.id.img);TextView tv = (TextView) customView.findViewById(R.id.tv);img.setBackgroundResource(R.drawable.daima);tv.setText("沉迷学习,日渐消瘦");customToast.setView(customView);customToast.setDuration(Toast.LENGTH_SHORT);customToast.setGravity(Gravity.CENTER,0,0);customToast.show();
布局文件中根元素为LinearLayout
,垂直放入一个ImageView
和一个TextView
。代码就不贴了。
高级自定义Toast
产品狗的需求:点击一个Button
,网络请求失败的情况下使用Toast
的方式提醒用户。
程序猿:ok~大笔一挥。
Toast.makeText(MainActivity.this.getApplicationContext(),"沉迷学习,日渐消瘦",Toast.LENGTH_SHORT).show()
测试:你这程序写的有问题。每次点击就弹出了气泡,连续点击20次,居然花了一分多钟才显示完。改!
程序猿:系统自带的就这样。爱要不要。
测试:那我用单元测试模拟点击50次之后,它就不显示了,这个怎么说。
程序猿:…
这个时候,高级自定义Toast
就要出场了~
activity_main.xml
—->上下两个按钮,略。
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener{public static final String TAG = "MainActivity";private Button customToastBtn;private Button singleToastBtn;private static int num;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initView();initClick();performClick(100);}private void initView() {customToastBtn = (Button) findViewById(R.id.customToastBtn);singleToastBtn = (Button) findViewById(R.id.singleToastBtn);}private void initClick() {customToastBtn.setOnClickListener(this);singleToastBtn.setOnClickListener(this);}/*** 点击singleToastBtn按钮* @param clickFrequency 点击的次数*/private void performClick(int clickFrequency) {for (int i = 0; i < clickFrequency; i++){singleToastBtn.performClick();}}@Overridepublic void onClick(View view) {switch (view.getId()){case R.id.customToastBtn:showCustomToast();break;case R.id.singleToastBtn:showSingleToast();break;default:break;}}private void showCustomToast() {Toast customToast = new Toast(MainActivity.this.getApplicationContext());View customView = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_toast,null);ImageView img = (ImageView) customView.findViewById(R.id.img);TextView tv = (TextView) customView.findViewById(R.id.tv);img.setBackgroundResource(R.drawable.daima);tv.setText("沉迷学习,日渐消瘦");customToast.setView(customView);customToast.setDuration(Toast.LENGTH_SHORT);customToast.setGravity(Gravity.CENTER,0,0);customToast.show();}private void showSingleToast() {Toast singleToast = SingleToast.getInstance(MainActivity.this.getApplicationContext());View customView = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_toast,null);ImageView img = (ImageView) customView.findViewById(R.id.img);TextView tv = (TextView) customView.findViewById(R.id.tv);img.setBackgroundResource(R.drawable.daima);tv.setText("沉迷学习,日渐消瘦 第"+num+++"遍 toast="+singleToast);singleToast.setView(customView);singleToast.setDuration(Toast.LENGTH_SHORT);singleToast.setGravity(Gravity.CENTER,0,0);singleToast.show();}
}
SingleToast.java
public class SingleToast {private static Toast mToast;/**双重锁定,使用同一个Toast实例*/public static Toast getInstance(Context context){if (mToast == null){synchronized (SingleToast.class){if (mToast == null){mToast = new Toast(context);}}}return mToast;}
}
那么有的同学会问了:你这样不就是加了个单例吗,好像也没有什么区别。区别大了。仅仅一个单例,既实现了产品狗的需求,又不会有单元测试快速点击50次的之后不显示的问题。为什么?Read The Fucking Source Code。
Toast源码解析
这里以Toast.makeText().show
为例,一步步追寻这个过程中源码所做的工作。自定义Toast
相当于自己做了makeText()
方法的工作,道理是一样一样的,这里就不再分别讲述了~
源码位置:frameworks/base/core/java/android/widght/Toast.java
Toast#makeText()
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {// 获取Toast对象Toast result = new Toast(context);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);// 设置View和duration属性result.mNextView = v;result.mDuration = duration;return result;}
这里填充的布局transient_notification.xml
位于frameworks/base/core/res/res/layout/transient_notification.xml。加分项,对于XML布局文件解析不太了解的同学可以看下这篇博客。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:background="?android:attr/toastFrameBackground"><TextView
android:id="@android:id/message"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_weight="1"android:layout_gravity="center_horizontal"android:textAppearance="@style/TextAppearance.Toast"android:textColor="@color/bright_foreground_dark"android:shadowColor="#BB000000"android:shadowRadius="2.75"/></LinearLayout>
可以发现,里面只有一个TextView
,平日设置的文本内容就是在这里展示。接下来只有一个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}}
这里有三个问题。
1. 通过getService()
怎么就获得一个INotificationManager
对象?
2. TN
类是个什么鬼?
3. 方法最后只有一个service.enqueueToast()
,显示和隐藏在哪里?
Toast
的精华就在这三个问题里,接下来的内容全部围绕上述三个问题,尤其是第三个。已经全部了解的同学可以去看别的博客了~
1. 通过getService()
怎么就获得一个INotificationManager
对象?
static private INotificationManager getService() {if (sService != null) {return sService;}sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));return sService;}
对Binder
机制了解的同学看见XXX.Stub.asInterface
肯定会很熟悉,这不就是AIDL
中获取client
嘛!确实是这样。
tips: 本着追本溯源的精神,先看下ServiceManager.getService("notification")
。在上上上上篇博客SystemServer启动流程源码解析中startOtherServices()
涉及到NotificationManagerService
的启动,代码如下,这里不再赘述。
mSystemServiceManager.startService(NotificationManagerService.class);
Toast
中AIDL
对应文件的位置。
源码位置:frameworks/base/core/java/android/app/INotificationManager.aidl
Server
端:NotificationManagerService.java
源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
篇幅有限,这里不可能将AIDL
文件完整的叙述一遍,不了解的同学可以理解为:经过进程间通信(AIDL
方式),最后调用NotificationManagerService#enqueueToast()
。具体可以看下这篇博客。
2. TN
类是个什么鬼?
在Toast#makeText()
中第一行就获取了一个Toast
对象
public Toast(Context context) {mContext = context;mTN = new 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);}
源码位置:frameworks/base/core/java/android/widght/Toast$TN.java
private static class TN extends ITransientNotification.Stub {...TN() {final WindowManager.LayoutParams params = mParams;params.height = WindowManager.LayoutParams.WRAP_CONTENT;params.width = WindowManager.LayoutParams.WRAP_CONTENT;...}...}
源码中的进程间通信实在太多了,我不想说这方面的内容啊啊啊~。有时间专门再写一片博客。这里提前剧透下TN
类除了设置参数的作用之外,更大的作用是Toast
显示与隐藏的回调。TN
类在这里作为Server
端。NotificationManagerService$NotificationListeners
类作为client
端。这个暂且按下不提,下文会详细讲述。
3. show()
方法最后只有一个service.enqueueToast()
,显示和隐藏在哪里?
源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
private final IBinder mService = new INotificationManager.Stub() {@Overridepublic void enqueueToast(String pkg, ITransientNotification callback, int duration){if (pkg == null || callback == null) {Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);return ;}final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));...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) {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) {Slog.e(TAG, "Package has already posted " + count+ " toasts. Not showing more. Package=" + pkg);return;}}}}record = new ToastRecord(callingPid, pkg, callback, duration);mToastQueue.add(record);index = mToastQueue.size() - 1;// 将Toast所在的进程设置为前台进程keepProcessAliveLocked(callingPid);}if (index == 0) {showNextToastLocked();}} finally {Binder.restoreCallingIdentity(callingId);}}}...}
在Toast#show()
最终会进入到这个方法。首先通过indexOfToastLocked()
方法获取应用程序对应的ToastRecord
在mToastQueue
中的位置,Toast
消失后返回-1,否则返回对应的位置。mToastQueue
明明是个ArratList
对象,却命名Queue
,猜测后面会遵循“后进先出”的原则移除对应的ToastRecord
对象~。这里先以返回index=-1
查看,也就是进入到else
分支。如果不是系统程序,也就是应用程序。那么同一个应用程序瞬时在mToastQueue
中存在的消息不能超过50条(Toast
对象不能超过50个)。否则直接return
。这也是上文中为什么快速点击50次之后无法继续显示的原因。既然瞬时Toast
不能超过50个,那么运用单例模式使用同一个Toast
对象不就可以了嘛?答案是:可行。消息用完了就移除,瞬时存在50个以上的Toast
对象相信在正常的程序中也用不上。而且注释中也说这样做是为了放置DOS攻击和防止泄露。其实从这里也可以看出:为了防止内存泄露,创建Toast
最好使用getApplicationContext
,不建议使用Activity
、Service
等。
回归主题。接下来创建了一个ToastRecord
对象并添加进mToastQueue
。接下来调用showNextToastLocked()
方法显示一个Toast
。
源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
NotificationManagerService#showNextToastLocked()
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();scheduleTimeoutLocked(record);
return;} catch (RemoteException e) {int index = mToastQueue.indexOf(record);if (index >= 0) {mToastQueue.remove(index);}keepProcessAliveLocked(record.pid);if (mToastQueue.size() > 0) {record = mToastQueue.get(0);} else {record = null;}}}}
这里首先调用record.callback.show()
,这里的record.callback
其实就是TN
类。接下来调用scheduleTimeoutLocked()
方法,我们知道Toast
显示一段时间后会自己消失,所以这个方法肯定是定时让Toast
消失。跟进。
private void scheduleTimeoutLocked(ToastRecord r){mHandler.removeCallbacksAndMessages(r);Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;mHandler.sendMessageDelayed(m, delay);}
果然如此。重点在于使用mHandler.sendMessageDelayed(m, delay)
延迟发送消息。这里的delay
只有两种值,要么等于LENGTH_LONG
,其余统统的等于SHORT_DELAY
,setDuration
为其他值用正常手段是没有用的(可以反射,不在重点范围内)。
handler
收到MESSAGE_TIMEOUT
消息后会调用handleTimeout((ToastRecord)msg.obj)
。跟进。
private void handleTimeout(ToastRecord record){if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);synchronized (mToastQueue) {int index = indexOfToastLocked(record.pkg, record.callback);if (index >= 0) {cancelToastLocked(index);}}}
啥也不说了,跟进吧~
void cancelToastLocked(int index) {ToastRecord record = mToastQueue.get(index);try {record.callback.hide();} catch (RemoteException e) {...}mToastQueue.remove(index);keepProcessAliveLocked(record.pid);if (mToastQueue.size() > 0) {showNextToastLocked();}}
延迟调用record.callback.hide()
隐藏Toast
,前文也提到过:record.callback
就是TN
对象。到这,第三个问题已经解决一半了,至少我们已经直到Toast
的显示和隐藏在哪里被调用了,至于怎么显示怎么隐藏的,客观您接着往下看。
源码位置:frameworks/base/core/java/android/widght/ToastTN.javaToastTN.java ToastTN#show()
final Handler mHandler = new Handler(); @Overridepublic void show() {if (localLOGV) Log.v(TAG, "SHOW: " + this);mHandler.post(mShow);}final Runnable mShow = new Runnable() {@Overridepublic void run() {handleShow();}};
注意下这里直接使用new Handler
获取Handler
对象,这也是为什么在子线程中不用Looper
弹出Toast会出错的原因。跟进handleShow()
。
public void handleShow() {if (mView != mNextView) {// remove the old view if necessaryhandleHide();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);...mParams.packageName = packageName;if (mView.getParent() != null) {mWM.removeView(mView);}mWM.addView(mView, mParams);trySendAccessibilityEvent();}}
原来addView
到WindowManager
。这样就完成了Toast
的显示。至于隐藏就更简单了。
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.removeView(mView);}mView = null;}}
直接remove
掉。
题外话
今天周末,一天时间完成这篇简单的源码阅读加写作。每次写完源码解析总是成就感伴随着失落感, 成就感来源于我又get到一个原理或者新技能,失落感来自源码也就是那么回事,但是回头想想我得到了什么?其实并不多。但我仍然在乐此不疲的追寻着。或许是我还没“开窍”,没有到那种融会贯通的境界。但我清楚的知道,我在进步。我在努力变的更加优秀。
更多Framework源码解析,请移步 Android6.0 Framework源码解析系列[目录]
Android 高级自定义Toast及源码解析相关推荐
- 你连《Android高级UI与FrameWork源码》都搞不懂学什么Android?还敢面试阿里P7!
Android高级UI与FrameWork源码 重要性? 这块知识是现今使用者最多的,我们称之为Android2013~2016年的技术,但是,即使是这样的技术,Android开发者也往往因为网上Co ...
- Android多线程之ArrayBlockingQueue源码解析
阻塞队列系列 Android多线程之LinkedBlockingQueue源码解析 Android多线程之SynchronousQueue源码解析 Andorid多线程之DelayQueue源码分析 ...
- Android多线程之IntentService源码解析
想要了解 IntentService 的工作原理需要先对 Android 系统中以 Handler.Looper.MessageQueue 组成的异步消息处理机制以及 HandlerThread 有所 ...
- Android Glide 3.7.0 源码解析(八) , RecyclableBufferedInputStream 的 mark/reset 实现
个人博客传送门 一.mark / reset 的作用 Android Glide 3.7.0 源码解析(七) , 细说图形变换和解码有提到过RecyclableBufferedInputStream ...
- 【Android应用开发】EasyDialog 源码解析
示例源码下载 : http://download.csdn.net/detail/han1202012/9115227 EasyDialog 简介 : -- 作用 : 用于在界面进行一些介绍, 说明; ...
- 【Android 控件使用及源码解析】 GridView规则显示图片仿微信朋友圈发图片
今天闲下来想用心写一点东西,发现没什么可写的,就写一下最近项目上用到的一些东西吧.最近项目要求上传多图并且多图显示,而且要规则的显示,就像微信朋友圈的图片显示一样. 想了一下用GridView再适合不 ...
- Android 网络框架之Retrofit源码解析,flutter边框特效
Retrofit的构建使用了建造者模式,这个模式的优点就是可以构造复杂的对象,方便扩展,并且看起来代码比较简洁,美观: 在开始之前,我们先来看一下Retrofit的成员变量: 这里的变量并不是很多,我 ...
- android handlerthread 线程管理,Android多线程之HandlerThread源码解析
一.概述 先来了解一下HandlerThread的几个特性 HandlerThread继续于Thread,本身就是一个线程类 HandlerThread在内部维护了自己的Looper对象,所以可以进行 ...
- android输入法01:SoftKeyboard源码解析01
本文主要介绍android自带输入法实例SoftKeyboard的源码,共分为两篇:第一篇为SoftKeyboard框架概述,第二篇为源码注释. 1.IMF简介 一个IMF结构中包含三个主要的部分 ...
- Android 网络编程之OkHttp源码解析
前言:OkHttp框架是Android的网络请求框架,无数的项目都在使用着这个框架,重要性不言而喻; 本文会将OKHTTP的源码进行拆解,每个部分来单独学习,由简入深,循序渐进,篇幅较长,建议收藏,慢 ...
最新文章
- Java渐变进度条_Android ProgressBar自定义图片进度,自定义渐变色进度条
- Java消息中间件(activeMQ)
- 七十、Vue城市页面Ajax动态渲染和兄弟组件数据传递
- 企业运维笔试考题(1)
- Android 图文混排 通过webview实现并实现点击图片
- win10开机之后任务栏卡住了怎么办
- 如何安装和使用纯文本编辑器 vi/vim
- Android7.1 Audio的FW和HAL层dump PCM数据(三十七)
- java 开发小记:如何使用 MyEclipse 开发自己的类库(mylib.jar)以及引用(使用)她...
- HTTP Gzip压缩问题总结
- FLUKE 754过程校准器带HART协议
- Siebel Open UI
- 20200804自编译openwrtx86_64固件,源码来自L大
- win10的一些用法
- 连锁加盟2-3事~实录
- element ui 前台模板_一个干净优雅的 Element UI Admin 模板
- linux启动分区丢,Linux装机因为分区丢失引导解决办法
- Python中面向对象封装案例——小明爱跑步、摆放家具
- 一文梳理人脸识别,看完全都懂了!
- String和数据库类型的相互转化
热门文章
- C++定义一个描述员工(Employee)基本情况的类
- 看一篇就学会系列,mysql慢日志查看,本地mysql是使用phpstudy安装的
- unity3d:粒子随摄像机远近有大小变化bug
- 什么是PID,PID的作用。
- 信息安全技术--轮转机密码
- 自锁时间电路plc_自锁与互锁电路的plc梯形图程序【图】
- axure 倒计时_Axure 8.0实例 |自定义倒计时制作流程
- HikariPool-1 - Connection is not available, request timed out after 6000ms
- 含指数函数的不定积分方法归纳
- FAT16 FAT32 NTFS exFAT ReFS