目录介绍
-
01.整体概述
- 1.1 我的项目背景
- 1.2 遇到问题
- 1.3 根底概念
- 1.4 设计指标
- 1.5 收益剖析
-
02.Window概念
- 2.1 Window增加View
- 2.2 Window的概念
- 2.3 LayoutParams
- 2.4 WMS流程梳理
-
03.悬浮窗技术要点
- 3.1 业务思考点剖析
- 3.2 关键技术要点
- 3.3 利用悬浮窗
- 3.4 增加浮窗源码流程
- 3.5 了解WMS原理
- 3.6 拖拽回弹吸附
-
04.开发重要步骤
- 4.1 悬浮窗实现流程
- 4.2 申请悬浮窗权限
- 4.3 初始化悬浮窗
- 4.4 设置悬浮窗参数
- 4.5 增加View到悬浮窗
- 4.6 悬浮窗拖拽实现
- 4.8 悬浮窗权限适配
- 4.9 LayoutParam坑
-
05.计划根底设计
- 5.1 整体架构图
- 5.2 UML设计图
- 5.3 要害流程图
- 5.4 接口设计图
- 5.5 模块间依赖关系
-
06.其余设计说明
- 6.1 性能设计
- 6.2 稳定性设计
- 6.3 异样设计
- 6.4 事件上报设计
-
07.遇到的问题和坑
- 7.1 解决输入法层级关系
- 7.2 边界逻辑敞开悬浮窗
- 7.3 点击屡次关上页面
- 7.4 Home键遇到的问题
01.整体概述
1.1 我的项目背景
-
业务场景剖析
- 以视频通话为例,在视频通话时,咱们关上其余利用或点击Home键退出时或点击缩放图标,悬浮窗会显示在其余利用之上,给人的假象是通话页面变小了,点击悬浮窗回到通过页面,悬浮窗隐没。退出通话页面悬浮窗隐没。
-
市面上常见的悬浮窗,如微信视频通话性能,有如下特点:
- 整屏页面能切换到一个小的悬浮窗;悬浮窗能运行在其余app上方;悬浮窗能跳回整屏页面,并且悬浮窗隐没
-
需要悬浮窗成果
- 点击放大按钮,将以后远端视屏加载进悬浮窗,且悬浮窗可拖拽,不影响其余界面焦点;点击悬浮窗可返回原来的Activity
1.2 遇到问题
-
什么是悬浮窗
- 全局悬浮窗在许多利用中都能见到,点击Home键,小窗口依然会在屏幕上显示。留神:悬浮窗留神申请权限!
-
那么开发全局悬浮窗属于那一类呢?
- 属于零碎窗口,相当于跟Toast是一个级别的。针对悬浮窗的展现和移除,则能够模拟Toast中addView和removeView操作……
-
视频通话Activity如何最小化
- Activity自身自带了一个moveTaskToBack(boolean nonRoot),咱们要实现最小化只须要调用moveTaskToBack(true)传入一个true值就能够了,然而这里有一个前提,就是须要设置Activity的启动模式为singleInstance模式,两步搞定。
- 注:activity最小化后从新从后盾回到前台会回调onRestart()办法。点击悬浮窗开启activity会回调onNewIntent(留神能够setIntent(intent)一下)
1.3 根底概念
-
Window 有三种类型,别离是利用 Window、子 Window 和零碎 Window。
- 利用Window:z-index在1~99之间,它往往对应着一个Activity。
- 子Window:z-index在1000~1999之间,它往往不能独立存在,须要附丽在父Window上,例如Dialog等。
- 零碎Window:z-index在2000~2999之间,它往往须要申明权限能力创立,例如Toast、状态栏、零碎音量条、谬误提示框都是零碎Window。
-
这些层级范畴对应着 WindowManager.LayoutParams 的 type 参数
- 如果想要 Window 位于所有 Window 的最顶层,那么采纳较大的层级即可,很显然零碎 Window 的层级是最大的。
-
Android显示零碎分为3层
- UI框架层:负责管理窗口中View组件的布局与绘制以及响应用户输出事件
- WindowManagerService层:负责管理窗口Surface的布局与秩序
- SurfaceFlinger层:将WindowManagerService治理的窗口依照肯定的秩序显示在屏幕上
-
WMS(WindowManagerService)相干概念
- Window:它是一个抽象类,具体实现类为 PhoneWindow ,它对 View 进行治理。Window是View的容器,View是Window的具体表现内容;
- WindowManager:是一个接口类,继承自接口 ViewManager ,从它的名称就晓得它是用来治理 Window 的,它的实现类为 WindowManagerImpl;
- WMS:是窗口的管理者,它负责窗口的启动、增加和删除。另外窗口的大小和层级也是由它进行治理的;
1.4 设计指标
-
目前开发悬浮窗的计划有以下几种
- 第一种:写在base外面或者监听所有activity生命周期,这样每次启动一个新的Activity都要往页面上addView一次,耦合性比拟强。
- 第二种:采纳在Window上增加View的模式,相当于是全局性的悬浮窗。封装成库,裸露Api给开发者调用。
- 第三种:采纳服务Service,而后在Service中采纳WindowManager增加和移除View操作。那么在Activity中想要展现弹窗则须要通过播送通信,让Service收到播送解决逻辑。移植性比拟弱!
-
悬浮窗设计指标
- 良好的接口设计,能够设置各种自定义视图,反对拖动和拖拽吸附到边缘。弱小的Api办法和傻瓜式调用链路。
-
展现悬浮窗是否想Popup那样附丽在某控件地位
- 我在写悬浮窗库时,思考是否想Popup那种有showAsDropDown办法Api,能够显示在某个View的重心地位,而后在设置x和y偏移量。这个是能够做到的,加上这个Api不便库的弱小应用!
1.5 收益剖析
-
悬浮窗收益
- 进步产品的用户体验,app推到后盾,或者推出页面做其余操作(比方查看信息),这个时候浮窗性能次要是减少通话的敌对
-
技能收益
- 下沉为性能根底库,能够不便各个产品线应用,进步开发的效率。防止跟业务解耦合。应用场景有:音视频,直播,debug悬浮工具等……
-
悬浮窗库代码
- https://github.com/yangchong2…
02.Window概念
2.1 Window增加View
-
先看一个简略的案例。在主屏幕上增加一个TextView并展现,并且这个TextView独占一个窗口。
TextView mview = new TextView(context); WindowManager mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); WindowManager.LayoutParams wmParams = new WindowManager.LayoutParams(); wmParams.type = WindowManager.LayoutParams.TYPE_TOAST; wmParams.width = 800; wmParams.height = 800; mWindowManager.addView(mview, wmParams);
-
对Window增加View的流程步骤剖析
- WindowManager.addView增加窗口之前,TextView的onDraw不会被调用,也就说View必须被增加到窗口中,才会被绘制。只有申请了附丽窗口,View才会有能够绘制的指标内存。
- 当APP通过WindowManagerService的代理向其增加窗口的时候,WindowManagerService除了本人进行注销整顿,还须要向SurfaceFlinger服务申请一块Surface画布,其实次要是画布背地所对应的一块内存,只有这一块内存申请胜利之后,APP端才有绘图的指标,并且这块内存是APP端同SurfaceFlinger服务端共享的,这就省去了绘图资源的拷贝。
- APP端是能够通过unLockCanvasAndPost间接同SurfaceFlinger通信进行重绘的,就是说图形的绘制同WMS没有关系,WMS只是负责窗口的治理,并不负责窗口的绘制。
2.2 Window的概念
-
Window是个抽象类,PhoneWindow是Window惟一的实现类。PhoneWindow像是一个工具箱,封装了三种工具:
- DecorView、WindowManager.LayoutParams、WindowManager。
- 其中DecorView和WindowManager.LayoutParams负责窗口的动态属性,比方窗口的题目、背景、输入法模式、屏幕方向等等。WindowManager负责窗口的动静操作,比方窗口的增、删、改。
- Window抽象类对WindowManager.LayoutParams相干的属性(如:输入法模式、屏幕方向)都提供了具体的办法。而对DecorView相干的属性(如:题目、背景),只提供了形象办法,这些形象办法由PhoneWindow实现。
-
Window并不是实在地存在着的,而是以View的模式存在。
- Window自身就只是一个形象的概念,而View是Window的表现形式。要想显示窗口,就必须调用WindowManager.addView(View view, ViewGroup.LayoutParams params)。
- 参数view就代表着一个窗口。在Activity和Dialog的显示过程中都会调用到wm.addView(decor, l);所以Activity和Dialog的DecorView就代表着各自的窗口。
2.3 WindowManager
-
在理解WindowManager治理View实现之前,先理解下WindowManager相干类图以及Activity界面各层级显示关系;
2.4 LayoutParams
-
WindowManager.LayoutParams这个类用于提供悬浮窗所需的参数,其中有几个常常会用到的变量:
- type值用于确定悬浮窗的类型,个别设为2002,示意在所有应用程序之上,但在状态栏之下。
- flags值用于确定悬浮窗的行为,比如说不可聚焦,非模态对话框等等,属性十分多,大家能够查看文档。
- gravity值用于确定悬浮窗的对齐形式,个别设为左上角对齐,这样当拖动悬浮窗的时候不便计算坐标。
- x值用于确定悬浮窗的地位,如果要横向挪动悬浮窗,就须要扭转这个值。
- y值用于确定悬浮窗的地位,如果要纵向挪动悬浮窗,就须要扭转这个值。
- width值用于指定悬浮窗的宽度。
- height值用于指定悬浮窗的高度。
-
那么这个外面如何计算悬浮窗上下左右的地位呢?比方有个场景悬浮窗和音视频页面放大和放大就须要拿到悬浮窗地位
- 一般View如何拿到上下左右地位,能够采纳sourceView.getGlobalVisibleRect(visibleRect),简略来说就是对指标view在父view映射,而后从屏幕左上角开始计算,而后保留到rect中。
- 悬浮窗View如何拿到上下左右地位,left = layoutParams.x;top = y,right = x + layoutParams.width;bottom = y + layoutParams.height
03.悬浮窗技术要点
3.1 业务思考点剖析
-
针对窗口放大或者悬浮窗须要思考几个重要的点:
- 悬浮窗体的比例以及层级,层级要在statusBar之下且在activity之上,这样能力保障其不会被其余业务界面笼罩;
- 悬浮框显示后,外部的内容如何无缝连接持续显示;
3.2 关键技术要点
-
悬浮窗权限判断
- 这个须要留神针对不同的版本须要适配权限。留神网上说有什么办法能够绕过权限申请,这个是不可能的事件。同时要留神,局部手机判断悬浮窗权限Api可能生效……
-
将view增加到悬浮窗上
- 利用addView将View增加在window上,同样的,WindowManager.LayoutParams.type能够设置View的层级,避免被其余业务界面所笼罩。
3.3 利用悬浮窗
-
利用内悬浮窗实现流程
- 1.获取WindowManager;2.创立悬浮View;3.设置悬浮View的拖拽事件;4.增加View到WindowManager中
-
对于利用悬浮窗来说,Android版本对其影响不大。
- Type为TYPE_APPLICATION:只有Activity建设了,就能够增加。
- Type为TYPE_APPLICATION_ATTACHED_DIALOG:须要在Activity获取焦点,并且用户可操作时才可增加。
3.4 增加浮窗源码流程
-
悬浮窗增加流程:
- -> WindowManager.addView 这个是调用ViewManager接口的addView办法增加视图
- -> WindowManagerImpl.addView 接着会调用具体实现类
- -> WindowManagerGlobal.addView 在这个办法中会找到外围的ViewRootImpl,这个Impl相当于是root根
- -> ViewRootImpl.setView 最初会调用setView将view设置进去,mWindowSession在创立ViewRootImpl对象时被实例化
- -> WindowSession.addToDisplay(AIDL进行IPC)
- -> WindowManagerService.addWindow()
- -> ViewRootImpl.setView
-
从 WindowManager 到 WMS 的具体流程如下所示:
-
这里解说一下AIDL交互的流程逻辑
- 次要剖析是ViewRootImpl#setView()到WindowManagerService.addWindow()的这个过程,波及到跨过程通信。
- 1.ViewRootImpl#setView()过程。mWindowSession是IWindowSession对象。在创立ViewRootImpl对象时被实例化。
- 2.WindowManagerGlobal#getWindowSession()过程。getWindowManagerService()通过AIDL返回WindowManagerService实例。之后调用WindowManagerService#openSession()。
- 3.WindowManagerService#openSession()过程。返回一个Session对象。也就是说在ViewRootImpl#setView()中调用的是mWindowSession.addToDisplay,其实就是Session#addToDisplay()。
- 4.Session#addToDisplay()过程。mService是个WindowManagerService对象,也就是说最初调用的是WindowManagerService#addWindow()
- 5.WindowManagerService#addWindow()过程。mWindowMap是个Map实例,将WindowManager增加进WindowManagerService对立治理。至此,整个增加视图操作解析结束。
-
WindowManager.updateViewLayout()解析
- 和addView()过程一样,最终会进入到WindowManagerGlobal#updateViewLayout()。将传入的View设置参数之后,更新mRoot中View的参数。
-
WindowManager.removeView()解析
- 和下面过程一样,最终会进入到WindowManagerGlobal#removeView()。这个过程要略微麻烦点,首先调用root.die(),接着将View增加进mDyingViews。
- ViewRootImpl#die()中,参数immediate默认为false,也就是说这里只是发送了一个what=MSG_DIE的空音讯。ViewRootHandler收到这条音讯会执行doDie()。
- 通过一圈效验最终还是回到WindowManagerGlobal中移除View
3.6 拖拽回弹吸附
-
先看微信成果
- 当你拖动微信悬浮窗的时候,手指松开,这个时候悬浮窗回到边缘,会有一个很敌对的动画过渡成果。而并非是扭转地位那么僵硬。
-
为何做该性能
- 拖拽回到边缘,如果是间接调用updateLocation,那太僵硬了。
-
如何做敌对动画
- 这里能够增加属性动画,给动画设置工夫,而后在动画执行获取坐标值。而后再更改地位,这样就比拟连贯,成果更好一些。
04.开发重要步骤
4.1 悬浮窗实现流程
-
利用内悬浮窗实现流程
- 第一个是获取WindowManager,而后设置相干params参数。留神配置参数的时候须要留神type
- 第二个是增加xml或者自定义view到windowManager上
- 第三个是解决拖拽更改view地位的监听逻辑,别离在down,move,up三个事件处理业务
- 第四个是吸附右边或者左边,大略的思路是判断手指抬起时候的点是在屏幕右边还是左边
4.2 申请悬浮窗权限
-
对于悬浮窗的权限
- 当API<18时,零碎默认是有悬浮窗的权限,不须要去解决;
-
当API >= 23时,须要在AndroidManifest中申请权限,为了避免用户手动在设置中勾销权限,须要在每次应用时check一下是否有悬浮窗权限存在;
Settings.canDrawOverlays(this)
-
当API > 25时,零碎间接禁止用户应用TYPE_TOAST创立悬浮窗。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
4.3 初始化悬浮窗
-
第一步:首先创立WindowManager
//创立WindowManager windowManager = (WindowManager)applicationContext.getSystemService(Context.WINDOW_SERVICE); layoutParams = new WindowManager.LayoutParams();
4.4 设置悬浮窗参数
-
第一步:创立LayoutParams
layoutParams = new WindowManager.LayoutParams();
-
第二步:LayoutParam设置
wmParams.type = WindowManager.LayoutParams.TYPE_TOAST; wmParams.width = 800; wmParams.height = 800; mWindowManager.addView(mview, wmParams);
4.5 增加View到悬浮窗
-
界面触发悬浮窗代码如下:
// 新建悬浮窗控件 View view = LayoutInflater.from(this).inflate(R.layout.float_window, null); view.setOnTouchListener(new FloatingOnTouchListener()); // 将悬浮窗控件增加到WindowManager windowManager.addView(view, layoutParams);
- 须要留神的是,在暗藏悬浮窗的时候,最好是移除一下,下次须要显示的时候再增加。
4.6 悬浮窗拖拽实现
-
如何实现悬浮窗可随手指拖动?
- 思路非常简单,监听悬浮窗那个onTouchListener即可,在刚点击的ACTION_DOWN(手指按下)事件中记录以后的x,y地位,而后在每次挪动(ACTION_MOVE事件)后获取到本次挪动的地位,二者相减就是须要挪动的地位,这是自定义view的最基本操作了。
-
如何实现悬浮窗左右边的吸顶成果?
- 监听到手指抬起(UP事件)的动作后,判断以后地位是凑近右边还是左边,凑近右边就以地位动画的形式平移到右边,凑近左边就平移到左边。
4.8 悬浮窗权限适配
-
权限配置和申请,这一块倒是没什么坑
-
在当Android7.0以上的时候,须要在AndroidManifest.xml文件中申明SYSTEM_ALERT_WINDOW权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
-
4.9 LayoutParam坑
-
LayoutParam的坑!!!!
- WindowManager的addView办法有两个参数,一个是须要退出的控件对象View,另一个参数是WindowManager.LayoutParam对象。
-
LayoutParam里的type变量。须要留神一个坑!!!!!!这个变量是用来指定窗口类型的。在设置这个变量时,须要对不同版本的Android零碎进行适配。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else { layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE; }
-
在Android 8.0之前,悬浮窗口设置能够为TYPE_PHONE,这种类型是用于提供用户交互操作的非应用窗口。
- 而Android 8.0对系统和API行为做了批改,包含应用SYSTEM_ALERT_WINDOW权限的利用无奈再应用一下窗口类型来在其余利用和窗口上方显示揭示窗口:
- 如果须要实现在其余利用和窗口上方显示揭示窗口,那么必须该为TYPE_APPLICATION_OVERLAY的新类型。
-
如果在Android 8.0以上版本依然应用TYPE_PHONE类型的悬浮窗口,则会呈现如下异样信息:
android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@f8ec928 -- permission denied for window type 2002
05.计划根底设计
5.1 整体架构图
5.2 UML设计图
-
悬浮窗整体UML类图
06.其余设计说明
6.1 性能设计
-
性能设计在该库中次要波及两点
- 第一个如果是用在activity中,那么则须要留神内存透露的问题,须要开释activity上下文的援用
- 第二个如果是用在全局,那么须要留神增加view防止反复增加(如果曾经增加则首先要移除),而后销毁的时候把FloatWindow各种属性设置成null清理
6.2 稳定性设计
-
如何防止窗口挪动,挪动后松手的霎时触发了点击事件
- 首先设置一个布尔标记值(触摸挪动标记),在手指按上来(ACTION_DOWN)的时候设置为false。
- 而后在挪动(ACTION_MOVE)的时候,如果用户挪动了手指,那么就拦挡本次触摸事件,从而不让点击事件失效。
- 最初在手指抬起(ACTION_UP,ACTION_CANCEL)的时候,返回记录的触摸挪动标记。如果是true示意本人生产事件,则不会让点击事件失效。
-
这个中央须要留神两点
- 第一点:为何要监听ACTION_CANCEL事件,是因为手指拖动,疾速拖动到窗口外,这个时候没有手指抬起操作,那么监听事件完结次要是加强边界逻辑。
- 第二点:怎么判断滑动?因为点击click也会执行down,move,up等一连串事件。这个时候就要判断最小move间隔是否大于零碎最小触摸间隔,如果是则为拖动,否则是点击!
-
如何解决滑出指定间隔又滑入当作是点击事件bug
- 这个这个,能够当作一种加强逻辑,然而然而手指操作不进去,先放着……
6.3 异样设计
-
针对悬浮窗的增加,移除和更新操作须要减少catch操作。那么为何要这样操作,模拟吐司。如下所示:
try { mWindowManager.addView(mDecorView, mWindowParams); } catch (NullPointerException | IllegalStateException | IllegalArgumentException | WindowManager.BadTokenException e) { // 如果这个 View 对象被反复增加到 WindowManager 则会抛出异样 // java.lang.IllegalStateException: View has already been added to the window manager. } //上面这个是更新view try { mWindowManager.updateViewLayout(mDecorView, mWindowParams); } catch (IllegalArgumentException e) { // 当 WindowManager 曾经隐没时调用会产生解体 // IllegalArgumentException: View not attached to window manager }
-
参考零碎级别的Toast,其实悬浮窗跟吐司一样,设置零碎层级后,对addView减少catch操作。
try { mWM.addView(mView, mParams); } catch (WindowManager.BadTokenException e) { /* ignore */ }
-
6.4 事件上报设计
-
在悬浮窗中,有一部分代码增加上了catch操作。那么是否把这一部分的异样当作事件上报到APM上来
- 第一种计划:依赖APM,而后调用api进行事件上报,显然这种是不可行的。因为该性能库是不想依赖太大的内部库。
- 第二种计划:采纳接口+实现类,通过反射的模式去调用。但这样又感觉不太好,采纳Class.forName要防止混同导致类找不到。
- 第三种计划:采纳抽象类+实现类,将实现类的对象设置到抽象类中调用,实现类在壳工程做具体操作。
-
具体实现步骤如下所示
-
举一个简略的例子阐明该思路,比方,我在悬浮窗依赖接口层,而后调用代码如下所示
ExceptionReporter.reportCrash("Float FloatWindow updateViewLayout", e);
-
-
而后,在app壳工程中具体操作如下所示
ExceptionReporter.setExceptionReporter(ExceptionHelperImpl()) public class ExceptionReporterImpl extends ExceptionReporter { @Override protected void reportCrash(Throwable throwable) { //壳工程中能够拿到APM,比方上传到bugly平台上 } @Override protected void reportCrash(String tag, Throwable throwable) { } }
07.遇到的问题和坑
7.1 解决输入法层级关系
-
先看一下问题
- 微信里的悬浮窗是在输入法之下的,所以交互的同学也要求悬浮窗也要在输入法之下。查看了一下WindowManager源码,悬浮窗的优先级TYPE_APPLICATION_OVERLAY,下面大字写着明明是在输入法之下的,然而理论体现是在输入法之上。
7.2 边界逻辑敞开悬浮窗
-
先看一下问题
- 谷歌坑人的中央,都没中央设置这个悬浮窗是否只用到app内,所以默认在桌面上也会显示本人的悬浮窗。
- 比方在微信里显示其余app的悬浮窗,这种蹩脚的体验可想而知,用户不给你卸载就真是奇观了。
-
尝试解决这个问题
- 为了解决这个问题,最后的实现形式是对所有通过的activity进行记录,显示就加1,页面被挂起就减1,如果减到以后计数为0时阐明所有页面曾经敞开了,就能够暗藏悬浮窗了。
- 实际上这么做还是有问题的,在局部手机上如果是在首页按返回键的话依然不能暗藏,这个又是零碎级的兼容性问题。
- 为了解决这问题,前面又做了一个解决,通过注册registerActivityLifecycleCallbacks监听app的前后台回调,检测到如果以后首页被销毁时,应该将悬浮窗进行暗藏。
7.3 点击屡次关上页面
-
问题阐明一下
- 如果你的悬浮窗点击事件是关上页面的话,这里须要留神了,别忘了将这个关上的页面的启动模式设置为singleTop或者是singleTask,从而复用同一个,远离始终按返回的天堂操作。
7.4 Home键遇到的问题
-
先说一下遇到问题的场景
- 按home退到桌面从桌面点击利用图标又从启动页重新启动的,挺奇怪的。点击home键按道理说是不会推出MainActivity的呀
-
先说下代码逻辑
- 语音/视频通话界面activity 配置 android:launchMode=“singleInstance” 模式,切换到悬浮框调用 moveTaskToBack(true)办法,能启动小窗口,通话页面退到后盾。
-
调试中发现的问题
- 通话界面按home键,之前的activity销毁了,日志发现走了onDestroy,从新点击app图标,MainActivity相干页面从新onCreate(相当于重新启动app了)。
- 因为通话页面是singleInstance模式,此时有两个工作栈,按Home键后再从工作程序中切回,此时利用只保留了第二个工作栈,曾经失去了和第一个工作栈的关系,finish之后无奈在回到第一个工作栈。
-
该问题解决方案
- 给通话界面设置taskAffinity,如果不设置的话,按下home键时零碎会清理最近不流动的和application雷同的taskAffinity的所有处于后盾的栈,taskAffinity默认与application是同一个。
- 给通话页面设置taskAffinity之后,MainActivity所在后盾栈就不会被清理。须要留神:若想在taskAffinity属性失效,须要在启动该Activity时设置Flag为FLAG_ACTIVITY_NEW_TASK。