欢迎你们前往云+社区,获取更多腾讯海量技术实践干货哦~java
做者: QQ音乐技术团队 android
Toast
做为 Android
系统中最经常使用的类之一,因为其方便的api设计和简洁的交互体验,被咱们所普遍采用。可是,伴随着咱们开发的深刻,Toast
的问题也逐渐暴露出来。 本系列文章将分红两篇: 第一篇,咱们将分析 Toast
所带来的问题 第二篇,将提供解决 Toast
问题的解决方案 (注:本文源码基于Android 7.0)api
上一篇 [[Android] Toast问题深度剖析(一)] 笔者解释了:安全
Toast
系统如何构建窗口(经过系统服务NotificationManager来生成系统窗口)Toast
异常出现的缘由(系统调用 Toast
的时序紊乱)而本篇的重点,在于解决咱们第一章所说的 Toast
问题。bash
基于第一篇的知识,咱们知道,Toast
的窗口属于系统窗口,它的生成和生命周期依赖于系统服务 NotificationManager
。一旦 NotificationManager
所管理的窗口生命周期跟咱们本地的进程不一致,就会发生异常。那么,咱们能不能不使用系统的窗口,而使用本身的窗口,而且由咱们本身控制生命周期呢?事实上, SnackBar
就是这样的方案。不过,若是不使用系统类型的窗口,就意味着你的Toast
界面,没法在其余应用之上显示。(好比,咱们常常看到的一个场景就是你在你的应用出调用了屡次 Toast.show
函数,而后退回到桌面,结果发现桌面也会弹出 Toast
,就是由于系统的 Toast
使用了系统窗口,具备高的层级)不过在某些版本的手机上,你的应用能够申请权限,往系统中添加 TYPE_SYSTEM_ALERT
窗口,这也是一种系统窗口,常常用来做为浮层显示在全部应用程序之上。不过,这种方式须要申请权限,并不能作到让全部版本的系统都能正常使用。 若是咱们从体验的角度来看,当用户离开了该进程,就不该该弹出另一个进程的 Toast
提示去干扰用户的。Android
系统彷佛也意识到了这一点,在新版本的系统更新中,限制了不少在桌面提示窗口相关的权限。因此,从体验上考虑,这个状况并不属于问题。ide
“那么咱们能够选择哪些窗口的类型呢?”函数
Android
进程内,咱们能够直接使用类型为子窗口类型的窗口。在 Android
代码中的直接应用是 PopupWindow
或者是 Dialog
。这固然能够,不过这种窗口依赖于它的宿主窗口,它可用的条件是你的宿主窗口可用View
系统: 使用 View
系统去模拟一个 Toast
窗口行为,作起来不只方便,并且能更加快速的实现动画效果,咱们的 SnackBar
就是采用这套方案。这也是咱们今天重点讲的方案“若是采用 View 系统方案,那么我要往哪一个控件中添加个人 Toast 控件呢?”布局
在Android
进程中,咱们全部的可视操做都依赖于一个 Activity
。 Activity
提供上下文(Context)和视图窗口(Window) 对象。咱们经过Activity.setContentView
方法所传递的任何 View
对象 都将被视图窗口( Window
) 中的 DecorView
所装饰。而在 DecorView
的子节点中,有一个 id
为android.R.id.content
的 FrameLayout
节点(后面简称 content
节点) 是用来容纳咱们所传递进去的 View
对象。通常状况下,这个节点占据了除了通知栏的全部区域。这就特别适合用来做为 Toast
的父控件节点。post
“我什么时机往这个content
节点中添加合适呢?这个 content
节点何时被初始化呢?”性能
根据不一样的需求,你可能会关注如下两个时机:
Content
节点生成Content
内容显示实际咱们只须要将咱们的 Toast
添加到 Content
节点中,只要知足第一条便可。若是你是为了完成性能检测,测量或者其余目的,那么你可能更关心第二条。 那么什么状况下 Content
节点生成呢?刚才咱们说了,Content
节点包含在咱们的 DecorView
控件中,而 DecorView
是由 Activity
的 Window
对象所持有的控件。Window
在 Android
中的实现类是 PhoneWindow
,(这部分代码有兴趣能够自行阅读) 咱们来看下源码:
//code PhoneWindow.java
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) { //mContentParent就是咱们的 content 节点
installDecor();//生成一个DecorView
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}复制代码
PhoneWindow
对象经过 installDecor
函数生成 DecorView
和 咱们所须要的 content
节点(最终会存到 mContentParent
) 变量中去。可是, setContentView
函数须要咱们主动调用,若是我并无调用这个 setContentView
函数,installDecor
方法将不被调用。那么,有没有某个时刻,content
节点是必然生成的呢?固然有,除了在 setContentView
函数中调用installDecor
外,还有一个函数也调用到了这个,那就是:
//code PhoneWindow.java
@Override
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}复制代码
而这个函数,将在 Activity.findViewById
的时候调用:
//code Activity.java
public View findViewById(@IdRes int id) {
return getWindow().findViewById(id);
}
//code Window.java
public View findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}复制代码
所以,只要咱们只要调用了 findViewById
函数,同样能够保证 content
被正常初始化。这样咱们解释了第一个”就绪”(Content
节点生成)。咱们再来看下第二个”就绪”,也就是 Android
界面何时显示呢?相信你可能火烧眉毛的回答不是 onResume
回调的时候么?实际上,在 onResume
的时候,根本还没处理跟界面相关的事情。咱们来看下 Android
进程是如何处理 resume
消息的: (注: AcitivityThread
是 Android
进程的入口类, Android
进程处理 resume
相关消息将会调用到 AcitivityThread.handleResumeActivity
函数)
//code AcitivityThread.java
void handleResumeActivity(...) {
...
ActivityClientRecord r = performResumeActivity(token, clearHide);
// 以后会调用call onResume
...
View decor = r.window.getDecorView();
//调用getDecorView 生成 content节点
decor.setVisibility(View.INVISIBLE);
....
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();//add to WM 管理
}
...
}
//code Activity.java
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}复制代码
Android
进程在处理 resume
消息的时候,将走如下的流程:
performResumeActivity
回调 Activity
的 onResume
函数Window
的 getDecorView
生成 DecorView
对象和 content
节点DecorView
归入 WindowManager
(进程内服务)的管理Activity.makeVisible
显示当前 Activity
按照上述的流程,在 Activity.onResume
回调以后,才将控件归入本地服务 WindowManager
的管理中。也就是说, Activity.onResume
根本没有显示任何东西。咱们不妨写个代码验证一下:
//code DemoActivity.java
public DemoActivity extends Activity {
private View view ;
@Override
protected void onCreate( Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new View(this);
this.setContentView(view);
}
@Override
protected void onResume() {
super.onResume();
Log.d("cdw","onResume :" +view.getHeight());// 有高度是显示的必要条件
}
}复制代码
这里,咱们经过在 onResume
中获取高度的方式验证界面是否被绘制,最终咱们将输出日志:
D cdw : onResume :0复制代码
那么,界面又是在何时完成的绘制呢?是否是在 WindowManager.addView
以后呢?咱们在 onResume
以后会调用Activity.makeVisible
,里面会调用WindowManager.addView
。所以咱们在onResume
里post
一个消息就能够检测WindowManager.addView
以后的状况:
@Override
protected void onResume() {
super.onResume();
this.runOnUiThread(new Runnable() {
@Override
public void run() {
Log.d("cdw","onResume :" +view.getHeight());
}
});
}
//控制台输出:
01-02 21:30:27.445 2562 2562 D cdw : onResume :0复制代码
从结果上看,咱们在 WindowManager.addView
以后,也并无绘制界面。那么,Android的绘制是何时开始的?又是到何时结束?
在 Android
系统中,每一次的绘制都是经过一个 16ms
左右的 VSYNC
信号控制的,这种信号可能来自于硬件也可能来自于软件模拟。每一次非动画的绘制,都包含:测量,布局,绘制三个函数。而通常触发这一事件的的动做有:
View
的某些属性的变动View
从新布局LayoutView
节点当调用 WindowManager.addView
将空间添加到 WM
服务管理的时候,会调用一次Layout请求,这就触发了一次 VSYNC
绘制。所以,咱们只须要在 onResume
里post
一个帧回调就能够检测绘制开始的时间:
@Override
protected void onResume() {
super.onResume();
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
//TODO 绘制开始
}
});
}复制代码
咱们先来看下 View.requestLayout
是怎么触发界面从新绘制的:
//code View.java
public void requestLayout() {
....
if (mParent != null) {
...
if (!mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
}复制代码
View
对象调用 requestLayout
的时候会委托给本身的父节点处理,这里之因此不称为父控件而是父节点,是由于除了控件外,还有 ViewRootImpl
这个非控件类型做为父节点,而这个父节点会做为整个控件树的根节点。按照咱们上面说的委托的机制,requestLayout
最终将会调用到 ViewRootImpl.requestLayout
。
//code ViewRootImpl.java
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();//申请绘制请求
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
....
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//申请绘制
....
}
}复制代码
ViewRootImpl
最终会将 mTraversalRunnable
处理命令放到 CALLBACK_TRAVERSAL
绘制队列中去:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();//执行布局和绘制
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
...
performTraversals();
...
}
}复制代码
mTraversalRunnable
命令最终会调用到 performTraversals()
函数:
private void performTraversals() {
final View host = mView;
...
host.dispatchAttachedToWindow(mAttachInfo, 0);//attachWindow
...
getRunQueue().executeActions(attachInfo.mHandler);//执行某个指令
...
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
host.measure(childWidthMeasureSpec, childHeightMeasureSpec);//测量
....
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//布局
...
draw(fullRedrawNeeded);//绘制
...
}复制代码
performTraversals
函数实现了如下流程:
dispatchAttachedToWindow
通知子控件树当前控件被 attach
到窗口中getRunQueue
meausre
测量指令layout
布局函数draw
这里咱们看到一句方法调用:
getRunQueue().executeActions(attachInfo.mHandler);复制代码
这个函数将执行一个延时的命令队列,在 View
对象被 attach
到 View
树以前,经过调用 View.post
函数,能够将执行消息命令加入到延时执行队列中去:
/code View.java
public boolean post(Runnable action) {
Handler handler;
AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
handler = attachInfo.mHandler;
} else {
// Assume that post will succeed later
ViewRootImpl.getRunQueue().post(action);
return true;
}
return handler.post(action);
}复制代码
getRunQueue().executeActions
函数执行的时候,会将该命令消息延后一个UI线程消息执行,这就保证了执行的这个命令消息发生在咱们的绘制以后:
//code RunQueue.java
void executeActions(Handler handler) {
synchronized (mActions) {
...
for (int i = 0; i < count; i++) {
final HandlerAction handlerAction = actions.get(i);
handler.postDelayed(handlerAction.action, handlerAction.delay);//推迟一个消息
}
}
}复制代码
因此,咱们只须要在视图被 attach
以前经过一个 View
来抛出一个命令消息,就能够检测视图绘制结束的时间点:
//code DemoActivity.java
@Override
protected void onResume() {
super.onResume();
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
start = SystemClock.uptimeMillis();
log("绘制开始:height = "+view.getHeight());
}
});
}
@Override
protected void onCreate( Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new View(this);
view.post(new Runnable() {
@Override
public void run() {
log("绘制耗时:"+(SystemClock.uptimeMillis()-start)+"ms");
log("绘制结束后:height = "+view.getHeight());
}
});
this.setContentView(view);
}
//控制台输出:
01-03 23:39:27.251 27069 27069 D cdw : --->绘制开始:height = 0
01-03 23:39:27.295 27069 27069 D cdw : --->绘制耗时:44ms
01-03 23:39:27.295 27069 27069 D cdw : --->绘制结束后:height = 1232复制代码
咱们带着咱们上面的知识储备,来看下SnackBar是如何作的呢:
SnackBar
系统主要依赖于两个类:
SnackBar
做为门面,与业务程序交互SnackBarManager
做为时序管理器, SnackBar
与 SnackBarManager
的交互,经过 Callback
回调对象进行SnackBarManager
的时序管理跟 NotifycationManager
的很相似再也不赘述
SnackBar
经过静态方法 make
静态构造一个 SnackBar
:
public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
@Duration int duration) {
Snackbar snackbar = new Snackbar(findSuitableParent(view));
snackbar.setText(text);
snackbar.setDuration(duration);
return snackbar;
}复制代码
这里有一个关键函数 findSuitableParent
,这个函数的目的就至关于咱们上面的 findViewById(R.id.content)
同样,给 SnackBar
所定义的 Toast
控件找一个合适的容器:
private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
do {
if (view instanceof CoordinatorLayout) {
return (ViewGroup) view;
} else if (view instanceof FrameLayout) {
if (view.getId() == android.R.id.content) {//把 `Content` 节点做为容器
...
return (ViewGroup) view;
} else {
// It's not the content view but we'll use it as our fallback
fallback = (ViewGroup) view;
}
}
...
} while (view != null);
// If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
return fallback;
}复制代码
咱们发现,除了包含 CoordinatorLayout
控件的状况, 默认状况下, SnackBar
也是找的 Content
节点。找到的这个父节点,做为 Snackbar
构造器的形参:
private Snackbar(ViewGroup parent) {
mTargetParent = parent;
mContext = parent.getContext();
...
LayoutInflater inflater = LayoutInflater.from(mContext);
mView = (SnackbarLayout) inflater.inflate(
R.layout.design_layout_snackbar, mTargetParent, false);
...
}复制代码
Snackbar
将生成一个 SnackbarLayout
控件做为 Toast
控件。最后当时序控制器 SnackBarManager
回调返回的时候,通知 SnackBar
显示,即将SnackBar.mView
增长到 mTargetParent
控件中去。
这里有人或许会有疑问,这里使用强引用,会不会形成一段时间内的内存泄漏呢? 假如你如今弹了 10
个 Toast
,每一个 Toast
的显示时间是 2s
。也就是说你的最后一个 SnackBar
将被 SnackBarManager
持有至少 20s
。而 SnackBar
中又存在有父控件 mTargetParent
的强引用。至关于在这20s内, 你的mTargetParent
和它所持有的 Context
(通常是 Activity
)没法释放
这个实际上是不会的,缘由在于 SnackBarManager
在管理这种回调 callback
的时候,采用了弱引用。
private static class SnackbarRecord {
final WeakReference<Callback> callback;
....
}复制代码
可是,咱们从 SnackBar
的设计能够看出,SnackBar
没法定制具体的样式: SnackBar
只能生成 SnackBarLayout
这种控件和布局,可能并不知足你的业务需求。固然你也能够变动 SnackBarLayout
也能达到目的。不过,有了上面的知识储备,咱们彻底能够写一个本身的 Snackbar
。
从第一篇文章咱们知道,咱们直接在 Toast.show
函数外增长 try-catch
是没有意义的。由于 Toast.show
实际上只是发了一条命令给 NotificationManager
服务。真正的显示须要等 NotificationManager
通知咱们的 TN
对象 show
的时候才能触发。NotificationManager
通知给 TN
对象的消息,都会被 TN.mHandler
这个内部对象进行处理
//code Toast.java
private static class TN {
final Runnable mHide = new Runnable() {// 经过 mHandler.post(mHide) 执行
@Override
public void run() {
handleHide();
mNextView = null;
}
};
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);// 处理 show 消息
}
};
}复制代码
在NotificationManager
通知给 TN
对象显示的时候,TN
对象将给 mHandler
对象发送一条消息,并在 mHandler
的 handleMessage
函数中执行。 当NotificationManager
通知 TN
对象隐藏的时候,将经过 mHandler.post(mHide)
方法,发送隐藏指令。不论采用哪一种方式发送的指令,都将执行 Handler
的dispatchMessage(Message msg)
函数:
//code Handler.java
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);// 执行 post(Runnable)形式的消息
} else {
...
handleMessage(msg);// 执行 sendMessage形式的消息
}
}复制代码
所以,咱们只须要在 dispatchMessage
方法体内加入 try-catch
就能够避免 Toast
崩溃对应用程序的影响:
public void dispatchMessage(Message msg) {
try {
super.dispatchMessage(msg);
} catch(Exception e) {}
}复制代码
所以,咱们能够定义一个安全的 Handler
装饰器:
private static class SafelyHandlerWarpper extends Handler {
private Handler impl;
public SafelyHandlerWarpper(Handler impl) {
this.impl = impl;
}
@Override
public void dispatchMessage(Message msg) {
try {
super.dispatchMessage(msg);
} catch (Exception e) {}
}
@Override
public void handleMessage(Message msg) {
impl.handleMessage(msg);//须要委托给原Handler执行
}
}复制代码
因为 TN.mHandler
对象复写了 handleMessage
方法,所以,在 Handler
装饰器里,须要将 handleMessage
方法委托给 TN.mHandler
执行。定义完装饰器以后,咱们就能够经过反射往咱们的 Toast
对象中注入了:
public class ToastUtils {
private static Field sField_TN ;
private static Field sField_TN_Handler ;
static {
try {
sField_TN = Toast.class.getDeclaredField("mTN");
sField_TN.setAccessible(true);
sField_TN_Handler = sField_TN.getType().getDeclaredField("mHandler");
sField_TN_Handler.setAccessible(true);
} catch (Exception e) {}
}
private static void hook(Toast toast) {
try {
Object tn = sField_TN.get(toast);
Handler preHandler = (Handler)sField_TN_Handler.get(tn);
sField_TN_Handler.set(tn,new SafelyHandlerWarpper(preHandler));
} catch (Exception e) {}
}
public static void showToast(Context context,CharSequence cs, int length) {
Toast toast = Toast.makeText(context,cs,length);
hook(toast);
toast.show();
}
}复制代码
咱们再用第一章中的代码测试一下:
public void showToast(View view) {
ToastUtils.showToast(this,"hello", Toast.LENGTH_LONG);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {}
}复制代码
等 10s 以后,进程正常运行,不会由于 Toast
的问题而崩溃。
此文已由做者受权云加社区发布,转载请注明文章出处