- Activity的启动流程是怎样的?
面试官视角:这道题想考察什么?
1. 是否熟悉Activity启动过程中与AMS的交互过程(高级)
2. 是否熟悉 Binder 通信机制(高级)
3. 是否了解插件化框架如何Hook Activity启动(高级)
4. 阐述 Activity 转场动画的实现原理可加分(中级)
5. 阐述 Activity 的窗口显示流程可加分(高级)
题目剖析:
1. 与AMS交互
2. Activity的参数和结果如何传递
3. Activity如何实例化
4. Activity生命周期如何流转
5. Activity的窗口如何展示
6. Activity转场动画的实现机制
题目结论:
Activity跨进程启动:
请求进程A:startActivity——(AMP,ActivityManager代理对象)——>
system_server进程:AMS(ActivityManagerService)
解析Activity信息、处理启动参数
启动目标进程——> Zygote --> 进程B --> 绑定新进程
ATP(ApplicationThread在system_server进程的代理) --scheduleLaunchActivity/mH中EXECUTE_TRANSACTION消息执行任务(Android P)-->
新进程B:ApplicationThread --> ActivityThread --> Activity生命周期
Activity进程内启动:
请求进程A:startActivity—(hook插入点1)—(AMP,ActivityManager代理对象)——>
system_server进程:AMS(ActivityManagerService)
解析Activity信息、处理启动参数、scheduleLaunchActivity/mH中EXECUTE_TRANSACTION消息处理(Android P)-->
回到请求进程A:ApplicationThread --> ActivityThread -(hook插入点2)-> Activity生命周期
Activity的参数传递:Activity之间切换交互,需要system_server进程作为中介,而两者之间的交流是通过Binder机制,而Binder机制依赖于Android内核中的Binder缓冲区,因此参数传递的大小依赖于Binder缓冲区的大小并且数据必须是可序列化的。
传递大数据的方法:
EventBus
单例数据格式对象(注意内存泄露或者内存溢出的,考虑使用WeakReferences将数据包装起来)
持久化 数据库、ACache(ASimpleCache)、文件之类的(缺点:数据量很大的时候读写时间慢,效率低下,IO容易出问题)
Activity实例化:Activity实际上是在nstrumentation类的newActivity方法中被反射创建的。
Fragment为什么不能添加有参数的构造方法?虽然Fragment可以通过new的方式创建,但是若涉及Activity状态的保存和恢复则可能会出问题。比如:Activity A可能由于长时间处于不可见而被杀死,则此时就涉及Activity状态的保存和恢复问题,而Activity中的FragmentManager会在Activity被销毁时,将所有Fragment按照android:fragments为key的数据里存储现在有哪些fragment显示、顺序、位置如何等等,当Activity需要恢复时则还是通过反射创建所以根本不知道需要构造参数如何赋值,因此无法给Activity或者Fragment添加有参数的构造方法,若fragment存在有参构造则最好有默认值处理。
Activity窗口如何展示:
newActivity
activity-attach--> createPhoneWindow
activity-create--> installDecor\addContentView\setContentView
activity-start-->
activity-restoreState
activity-postCreate
activity-resume--> 测量、布局、绘制
activity-makeVisible--> 显示DecorView
Activity转场动画的实现机制:参考于https://www.jianshu.com/p/69d48f313dc4
1. 内容过渡动画的原理
1). Activity A 调用 startActivity()
系统遍历 A 的视图节点,找到将要运行退退出转换的所有过渡视图
A 的退出转换记录所有过渡视图的开始状态
系统将所有过渡视图的可见性设置为INVISIBLE
在下一帧,A 的退出转换记录所有过渡视图的结束状态
A 的退出转换比较每个过渡视图的开始状态和结束状态,然后创建 Animator 作为退出动画,运行该动画。
2). Activity B 启动了
系统遍历 B 的视图节点,找到将要运行进入转换的所有过渡视图,设置这些过渡视图的可见性为INVISIBLE
B 的进入转换记录所有过渡视图的开始状态
系统将所有过渡视图的可见性设置为VISIBLE
在下一帧,B 的进入转换记录所有过渡视图的结束状态
B 的进入转换比较每个过渡视图的开始状态和结束状态,然后创建 Animator 作为进入动画,运行该动画。
注:所有的内容转换都需要记录每个过渡视图的开始状态和结束状态。而抽象类Visibility已经做了这部分内容了,Visibility的子类只需要实现 onAppear() 和 onDisappear() 方法,创建过渡视图进入或退出场景的 Animator。Android 5.0 中Visibility有三个子类 -- Fade、Slide、Explode,如果有需要的话也可以自定义Visibility子类。
2. 共享元素过渡动画的原理
A 调用startActivity(intent, bundle)后,B 启动时,窗口的背景是透明的。
系统以 A 为标准重新设置 B 的每个共享元素视图的大小和位置,过一会 B 的进入转换会记录 B 中所有共享元素的开始状态,而对于内容过渡来说,其他的 transitioning view 的可见性都是 INVISIBLE。
系统再重新将 B 的每个共享元素视图的大小和位置设置为原来的样子,过一会 B 的进入转换会记录 B 中所有共享元素的结束状态。
B 的进入转换比较每个共享元素的开始状态和结束状态,创建 Animator 作为共享元素动画。
系统将隐藏 A 的所有共享元素视图,然后开始运行 B 的共享元素动画。在 B 的共享元素动画过程中,B 的窗口背景会逐渐变为不透明的。
注:对比内容过渡动画,内容过渡动画中系统会修改 transition views 的可见性,而共享元素过渡动画中系统会修改 shared element views 的位置、大小和显示。而且我们也可以看出实际上共享元素的 view 其实并没有在 Activity/ Fragment 之间共享,事实上,我们看到的进入或者返回的共享元素过渡动画都是直接在 B 的视图中运行的。
注:Android P中创建新Activity由以前的scheduleLaunchActivity方法变成mH中EXECUTE_TRANSACTION消息执行ClientTransaction类型任务(实际为LaunchActivityItem类型),继而执行client.handleLaunchActivity。
client实际类型为ClientTransactionHandler,而在Android P中,ActivityThread extends ClientTransactionHandler,而ClientTransactionHandler封装了handlexxxActivity的方法。因此Android P中最后也是执行ActivityThread中的handleLaunchActivity方法执行创建Activity。
EXECUTE_TRANSACTION消息由ActivityThread中sendActivityResult方法调用mAppThread.scheduleTransaction(clientTransaction)--> ActivityThread.this.scheduleTransaction(transaction) --> ClientTransactionHandler(隐藏抽象类,ActivityThread是其子类)中scheduleTransaction方法--> sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);
简单列以下Android P的流程:
1. startActivity--> Activity(startActivity-> startActivityForResult)--> Instrumentation(execStartActivity)--> ActivityManager(getService.startActivity)--> ActivityManagerService(startActivity)--> ActivityStartController(obtainStarter工厂方法模式)--> ActivityStarter(execute--> startActivityMayWait--> startActivity--> startActivityUnchecked)
2. --> ActivityStackSupervisor(resumeTopActivityUncheckedLocked)--> ActivityStack(resumeTopActivityUncheckedLocked--> resumeTopActivityInnerLocked)--> ActivityStartSupervisor(startSpecificActivityLocked--> realStartActivityLocked)--> ClientLifecycleManager(scheduleTransation)--> ClientTransation(schedule)
3. --> ActivityThread(ApplicationThread(scheduleTransation)--> scheduleTransation)--> ClientTransationHandler(scheduleTransation--> sendMessage(ActivityThread.H.EXECUTE_TRANSATION))--> ActivityThread(H(handleMessage))--> TransationExceutor(execute)--> LaunchActivityItem(excute)--> ClientTransationHandler(handleLaunchActivity)
4(最后使用反射创建Activity). --> ActivityThread(handleLaunchActivity--> performLaunchActivity)--> Instrumentation(newActivity--> getFactory(pkg))--> ActivityThread(peekPackageInfo)--> LoadedApk(getAppFactory)--> AppComponentFactory(instantiateActivity(cl, className, intent)--> (Activity) cl.loadClass(className).newInstance())--> Activity(performCreate--> onCreate)
小技巧:若在代码中无法找到隐藏类,则可在文件顶部,先查找此类包名,再想办法进行查找。
- 如何跨App启动Activity?有哪些注意事项?
面试官视角:这道题想考察什么?
1. 是否了解如何启动外部应用的Activity(初级)
2. 是否了解如何防止自己的Activity被外部非正常启动(中级)
3. 是否对拒绝服务漏洞有了解(高级)
4. 如何在开发时规避拒绝服务漏洞(高级)
题目结论:
初级:
1. 共享uid的App(应用于系统级应用或者"全家桶"应用),示例:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.android.customwidget"
android:sharedUserId="com.demo">
...
</manifest>
共享uid不仅能启动其Activity,系统对于流量的计算等等都是共享的。
2. 使用exported,示例:
<activity android:name=".BActivity" android:exported="true"/>
3. 使用IntentFilter,配置action等
<activity android:name=".BActivity"
android:permission="com.demo.b">
<intent-filter>
<action android:name="com.demo.intnet.Test"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
使用:
Intent it = new Intent();
it.setAction("com.demo.intnet.Test");
startActivity(it);
中级:
App B为允许外部启动的Activity B加权限,示例:
<activity android:name=".BActivity"
android:permission="com.demo.b">
<intent-filter>
<action android:name="com.demo.intnet.Test"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
App A若想启动App B的ActivityB,则需要声明权限:<uses-permission android:name="com.demo.b">
高级:
什么是服务漏洞?
答:说App A的ActivityA启动App B的ActivityB时,传过来一个Bundle数据,此数据是一个被Serializable修饰的类SerializableA。
若App B中没有SerializableA这个类,只要App B的ActivityB中访问了Intent的Extra(getIntent().getExtras())则就会发生类找不到异常。此种情况就是服务漏洞
如何解决服务漏洞?
答:try{}catch(Exception e){}
- 如何解决Activity参数的类型安全及接口繁琐的问题?
面试官视角:这道题想考察什么?
1. 是否有代码优化和代码重构的意识(高级)
2. 是否对反射、注解处理器有了解(高级)
3. 是否具备一定的框架设计能力(高级)
题目剖析:
1. 类型安全:Bundle的Key-Value不能在编译期保证类型
2. 接口繁琐:启动Activity时参数和结果传递都依赖Intnet
3. 等价的问法:设计一个框架,解决上述问题
4. 面试不需要实现,只管合理大胆的想
题目结论:
初级:为什么Activity的参数存在类型安全问题?
设置值:intent.putExtra("id", 0);
获取值:String id = getIntent().getStringExtra("id");
参数类型安全需要人工保证,容易出错。
中高级:
常规写法:
intent.putExtra("id", 0);
intent.putExtra("name", 0);
intent.putExtra("age", 0);
intent.putExtra("title", 0);
...
参数若很多时,则需要写一堆代码,若此时又多一个参数,则又需要维护一遍。
期望写法:
UserActivityBuilder.builder(age, name).title(title).start(context);
必传参数直接当作builder参数传入,否则通过方法传入。通过注解处理器生成Builder
注入逻辑调用时机:ActivityLifrcycleCallbacks的onActivityCreated方法中
注意需要手动处理onNewIntent方法:onNewIntent没有对应的生命周期回调
注解处理器程序的开发注意事项:
1. 注意注解标注的类的继承关系
2. 注意注解标注的类为内部类的情况
3. 注意kotlin与Java的类型映射问题
4. 把握好代码生成和直接依赖的边界
满分答案:框架设计,元编程(用代码写代码)
compile编译期:
1. APT:即注解处理器
2. Bytecode:RePlugin
3. Generic泛型:介于编译和运行期之间
runtime运行期:
4. Reflect:反射
5. Proxy:动态代理
- 如何在代码的任意位置为当前Activity添加View?
面试官视角:这道题想考察什么?
1. 如何在任意位置获取当前Activity(中级)
2. 是否对Activity的窗口有深入认识(高级)
3. 潜在的内存泄漏的风险以及内存回收机制(高级)
4. 是否能够深入需求评估技术方案的合理性(高级)
题目剖析:
1. 如何获取当前Activity?
2. 如何在不影响正常View展示的情况下添加View?
3. 既然能添加,就应当能移除,如何移除?
4. 这样作的目的是什么?添加全局View是否更合适?
题目结论:
1. 获取当前Activity:Application.ActivityLifecycleCallbacksgais回调中获取
注意内存泄漏:private static WeakReference<Activity> currentActivityRef;
onActivityCreated回调中:currentActivityRef = new WeakReference<>(activity);
2. 内存回收机制
GC Roots包括:虚拟机栈帧(栈桢中的本地变量表)引用的对象、类静态属性引用的对象、常量引用的对象、Native方法引用的对象
对象若无 GC Roots引用,则表示可以被回收。软引用SoftRef内存不足时回收,弱引用WeakRef发生gc时(内存即便充足)回收。
3. 添加View
Activity中真实的根布局为DecorView(FrameLayout子类)
DecorView包含一个线性布局LinearLayout,LinearLayout其分为上下两部分:titleBar和mContentParent。
而mContentParent实际上就是我们在布局文件中绘制布局显示的区域。mContentParent的id即android.R.id.content
示例扩展类:https://github.com/Endless5F/JcyDemoList/blob/master/CustomWidget/src/main/java/com/android/customwidget/ext/ActivityExt.java
4. 添加全局View:https://github.com/yhaolpz/FloatWindow
Android全局悬浮窗:github.com/yhaolpz/Flo…
- 如何实现类似微信右滑返回的效果?
面试官视角:这道题想考察什么?
1. 是否熟练掌握手势和动画的运用(中级)
2. 是否了解窗口绘制的内部原理(高级)
3. 是否对Activity的窗口有深入了解(高级)
题目剖析:
1. 没有明说UI的类型,Activity还是Fragment?
2. Fragment实现简单,重点回答Activity
3. 考虑如何设计这样一个组件
4. 考虑如何降低接入成本
题目结论:
一星:Fragment的实现
1. 对于Fragment的控制相对简单
2. 不涉及Window的控制,只是View级别的操作
3. 实现View跟随手势滑动移动的效果
4. 实现手势结束后判断取消或返回执行归位动画
二星:Activity的实现
1. 首先需要将最上层Activity的window设置成透明色
<style name="AppTranslucentTheme" parent="AppTheme">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item> <!--窗口是半透明的-->
</style>
若上层Activity不设置成半透明,则下层Activity则不会被绘制,就会显示黑色
2. Activity联动--多Task(不同堆栈的Activity)
多Task之间Activity切换时,会先切换一个任务堆栈,然后再显示Activity
场景举例:ActivityA和ActivityC是同一个任务栈Task
若此时ActivityC的window的背景是透明的,而ActivityA在ActivityC下面,此时若由ActivityB切换到ActivityC,
则就会出现,先显示ActivityA再显示ActivityC,这是由于ActivityB和C之间是两个Task栈,因此先栈切换,而ActivityC是透明的,因此先显示了一下A,最后才显示C。
此种场景可以先判断跳转之间是否是同一Task栈,然后给ActivityC“拍照”,然后放在ActivityC下面,给用户一种假象。
如何获取Activity栈?
答:根据Application.ActivityLifecycleCallbacksgais回调中,使用Activity
3. Activity透明对生命周期的影响(为了性能)
若上层Activity的window背景是透明的,则该Activity下面的Activity生命周期则为Started的状态,以此类推,直到一个Activity的window背景不透明,则此下面的Activity的生命周期为Created。
三星:设计SDK
1. 现有方案(SwipeBackLayout):Activity需要继承自SwipeBackActivity,若此时也需要继承业务的BaseActivity则会产生冲突。
2. 用接口代替父类:通过实现接口把逻辑移到外部类中,通过组合而不是继承来实现。
3. 动态切换窗口透明状态:滑动过程中可通过反射调用
// @hide
@SystemApi
convertToTranslucent(TranslucentConversionListener callback, ActivityOptions options) // 转换为半透明
// @hide
@SystemApi
public void convertFromTranslucent() // 从半透明转换
SwipeBackLayout:github.com/ikew0ng/Swi…
- Android中为什么非UI线程不能更新UI?
概念引入1:ViewRootImpl,ViewRoot和View关系
ViewRoot对应ViewViewRootImpl类,它是连接WindowManager和DecorView的纽带,
View的三大流程(measure,layout,draw)均是通过ViewRoot来完成的。ViewRootIml是View的根类,其控制着View的测量、绘制等操作
ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象并和DecorView建立联系。
DecorView作为顶级的View一般情况下它内部会包含一个竖向的LinearLayout,这个LinearLayout里面有上下两个部分(具体情况和Android版本及主题有关),上面是标题栏,下面是内容栏。
概念引入2:SurfaceView
SurfaceFlinger服务负责绘制Android应用程序的UI,它的实现相当复杂,SurfaceFlinger服务运行在Android系统的System进程中,它负责管理Android系统的帧缓冲区(Frame Buffer)。
Android设备的显示屏被抽象为一个帧缓冲区,而Android系统中的SurfaceFlinger服务就是通过向这个帧缓冲区写入内容来绘制应用程序的用户界面的。Android系统在硬件抽象层中提供了一个Gralloc模块,封装了对帧缓冲区的所有访问操作。
Linux内核在启动的过程中会创建一个类别和名称分别为“graphics”和“fb0”的设备,用来描述系统中的第一个帧缓冲区,即第一个显示屏,其中,数字0表示从设备号。注意,系统中至少要存在一个显示屏,因此,名称为“fb0”的设备是肯定会存在的,否则的话,就是出错了。Android系统和Linux内核本身的设计都是支持多个显示屏的,不过,在Android目前的实现中,只支持一个显示屏。
init进程在启动的过程中,会启动另外一个进程ueventd来管理系统的设备文件。当ueventd进程启动起来之后,会通过netlink接口来Linux内核通信,以便可以获得内核中的硬件设备变化通知。而当ueventd进程发现内核中创建了一个类型和名称分别为“graphics”和“fb0”的设备的时候,就会这个设备创建一个/dev/graphics/fb0设备文件。这样,用户空间的应用程序就可以通过设备文件/dev/graphics/fb0来访问内核中的帧缓冲区,即在设备的显示屏中绘制指定的画面。注意,用户空间的应用程序一般是通过内存映射的方式来访问设备文件/dev/graphics/fb0的。
用户空间的应用程序在使用帧缓冲区之间,首先要加载Gralloc模块,并且获得一个gralloc设备和一个fb设备。有了gralloc设备之后,用户空间中的应用程序就可以申请分配一块图形缓冲区,并且将这块图形缓冲区映射到应用程序的地址空间来,以便可以向里面写入要绘制的画面的内容。最后,用户空间中的应用程序就通过fb设备来将前面已经准备好了的图形缓冲区渲染到帧缓冲区中去,即将图形缓冲区的内容绘制到显示屏中去。相应地,当用户空间中的应用程序不再需要使用一块图形缓冲区的时候,就可以通过gralloc设备来释放它,并且将它从地址空间中解除映射
每一个Android应用程序与SurfaceFlinger服务都有一个连接,这个连接都是通过一个类型为Client的Binder对象来描述的。这些Client对象是Android应用程序连接到SurfaceFlinger服务的时候由SurfaceFlinger服务创建的,而当Android应用程序成功连接到SurfaceFlinger服务之后,就可以获得一个对应的Client对象的Binder代理接口了。有了这些Binder代理接口之后,Android应用程序就可以通知SurfaceFlinger服务来绘制自己的UI了。
Android应用程序在通知SurfaceFlinger服务来绘制自己的UI的时候,需要将UI元数据传递给SurfaceFlinger服务,例如,要绘制UI的区域、位置等信息。一个Android应用程序可能会有很多个窗口,而每一个窗口都有自己的UI元数据,因此,Android应用程序需要传递给SurfaceFlinger服务的UI元数据是相当可观的。在这种情况下,通过Binder进程间通信机制来在Android应用程序与SurfaceFlinger服务之间传递UI元数据是不合适的,这时候Android系统的匿名共享内存机制(Anonymous Shared Memory)就派上用场了。
一般来说,每一个窗口在SurfaceFlinger服务中都对应有一个Layer,用来描述它的绘图表面。对于那些具有SurfaceView的窗口来说,每一个SurfaceView在SurfaceFlinger服务中还对应有一个独立的Layer或者LayerBuffer,用来单独描述它的绘图表面,以区别于它的宿主窗口的绘图表面。
无论是LayerBuffer,还是Layer,它们都是以LayerBase为基类的,也就是说,SurfaceFlinger服务把所有的LayerBuffer和Layer都抽象为LayerBase,因此就可以用统一的流程来绘制和合成它们的UI。
注意,用来描述SurfaceView的Layer或者LayerBuffer的Z轴位置是小于用来其宿主Activity窗口的Layer的Z轴位置的,但是前者会在后者的上面挖一个“洞”出来,以便它的UI可以对用户可见。实际上,SurfaceView在其宿主Activity窗口上所挖的“洞”只不过是在其宿主Activity窗口上设置了一块透明区域。
SurfaceView有以下三个特点:
A. 具有独立的绘图表面;
B. 需要在宿主窗口上挖一个洞(即需要在宿主窗口的绘图表面上设置一块透明区域)来显示自己;
C. 它的UI绘制可以在独立的线程中进行,这样就可以进行复杂的UI绘制,并且不会影响应用程序的主线程响应用户输入。
概念引入3:SurfaceTexture,TextureView, SurfaceView和GLSurfaceView的区别(https://blog.csdn.net/m475664483/article/details/52998445)
SurfaceView:
从Android 1.0(API level 1)时就有 。它继承自类View,因此它本质上是一个View。但与普通View不同的是,它有自己的Surface。我们知道,一般的Activity包含的多个View会组成View hierachy的树形结构,只有最顶层的DecorView,也就是根结点视图,才是对WMS可见的。这个DecorView在WMS中有一个对应的WindowState。相应地,在SF中对应的Layer。而SurfaceView自带一个Surface,这个Surface在WMS中有自己对应的WindowState,在SF中也会有自己的Layer。
虽然在App端它仍在View hierachy中,但在Server端(WMS和SF)中,它与宿主窗口是分离的。这样的好处是对这个Surface的渲染可以放到单独线程去做,渲染时可以有自己的GL context。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。但它也有缺点,因为这个Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中,一些View中的特性也无法使用。
GLSurfaceView:
从Android 1.5(API level 3)开始加入,作为SurfaceView的补充。它可以看作是SurfaceView的一种典型使用模式。在SurfaceView的基础上,它加入了EGL的管理,并自带了渲染线程。另外它定义了用户需要实现的Render接口,提供了用Strategy pattern更改具体Render行为的灵活性。作为GLSurfaceView的Client,只需要将实现了渲染函数的Renderer的实现类设置给GLSurfaceView即可。
其中SurfaceView中的SurfaceHolder主要是提供了一坨操作Surface的接口。GLSurfaceView中的EglHelper和GLThread分别实现了上面提到的管理EGL环境和渲染线程的工作。GLSurfaceView的使用者需要实现Renderer接口。
SurfaceTexture:
从Android 3.0(API level 11)加入。和SurfaceView不同的是,它对图像流的处理并不直接显示,而是转为GL外部纹理,因此可用于图像流数据的二次处理(如Camera滤镜,桌面特效等)。比如Camera的预览数据,变成纹理后可以交给GLSurfaceView直接显示,也可以通过SurfaceTexture交给TextureView作为View heirachy中的一个硬件加速层来显示。首先,SurfaceTexture从图像流(来自Camera预览,视频解码,GL绘制场景等)中获得帧数据,当调用updateTexImage()时,
根据内容流中最近的图像更新SurfaceTexture对应的GL纹理对象,接下来,就可以像操作普通GL纹理一样操作它了。从下面的类图中可以看出,它核心管理着一个BufferQueue的Consumer和Producer两端。Producer端用于内容流的源输出数据,Consumer端用于拿GraphicBuffer并生成纹理。SurfaceTexture.OnFrameAvailableListener用于让SurfaceTexture的使用者知道有新数据到来。JNISurfaceTextureContext是OnFrameAvailableListener从Native到Java的JNI跳板。其中SurfaceTexture中的attachToGLContext()和detachToGLContext()可以让多个GL context共享同一个内容源。
Android 5.0中将BufferQueue的核心部分分离出来,放在BufferQueueCore这个类中。BufferQueueProducer和BufferQueueConsumer分别是它的生产者和消费者实现基类(分别实现了IGraphicBufferProducer和IGraphicBufferConsumer接口)。它们都是由BufferQueue的静态函数createBufferQueue()来创建的。Surface是生产者端的实现类,提供dequeueBuffer/queueBuffer等硬件渲染接口,和lockCanvas/unlockCanvasAndPost等软件渲染接口,使内容流的源可以往BufferQueue中填graphic buffer。GLConsumer继承自ConsumerBase,是消费者端的实现类。它在基类的基础上添加了GL相关的操作,如将graphic buffer中的内容转为GL纹理等操作。
TextureView:
在4.0(API level 14)中引入。它可以将内容流直接投影到View中,可以用于实现Live preview等功能。和SurfaceView不同,它不会在WMS中单独创建窗口,而是作为View hierachy中的一个普通View,因此可以和其它普通View一样进行移动,旋转,缩放,动画等变化。值得注意的是TextureView必须在硬件加速的窗口中。它显示的内容流数据可以来自App进程或是远端进程。
TextureView继承自View,它与其它的View一样在View hierachy中管理与绘制。TextureView重载了draw()方法,其中主要把SurfaceTexture中收到的图像数据作为纹理更新到对应的HardwareLayer中。SurfaceTexture.OnFrameAvailableListener用于通知TextureView内容流有新图像到来。SurfaceTextureListener接口用于让TextureView的使用者知道SurfaceTexture已准备好,这样就可以把SurfaceTexture交给相应的内容源。Surface为BufferQueue的Producer接口实现类,使生产者可以通过它的软件或硬件渲染接口为SurfaceTexture内部的BufferQueue提供graphic buffer。
面试官视角:这道题想考察什么?
1. 是否理解线程安全的概念(中级)
2. 是否能够理解UI线程的工作机制(高级)
3. 是否熟悉SurfaceView实现高帧率的原理(高级)
题目剖析:
1. UI线程的工作机制
2. 为什么UI设计成线程不安全的?
3. 非UI线程一定不能更新UI吗?
题目结论:
1. 首先需要了解UI更新是非线程安全的。
2. 非UI线程更新UI的异常是从哪抛出的呢?
答:android.view.ViewRootImpl
一星:UI线程是什么?
答:Android的App进程是由zygote进程fork出的新进程,此进程会执行Android的入口函数ActivityThread
二星:主线程如何工作?
答:主线程通过Looper
三星:
UI为什么不设计成线程安全的?
1. UI具有可变性,甚至是高频可变性
2. UI对响应时间的敏感性要求UI操作必须高效
3. UI组件必须批量绘制来保证效率
4. 若设计成线程安全,则需要频繁的加锁,开销太大
非UI线程一定不能更新UI吗?
场景:IO线程(网络请求) UI线程(刷新UI)
0. 正常操作:网络请求回来后,通过Handler
1. 间接在非UI线程刷新:调用View
2. ViewRootImpl未初始化前在非UI线程更新:ViewRootIml是View的根类,是在onResume生命周期创建,因此在onCreate和onStart生命周期中在子线程(线程不能睡眠)里可以更改UI。
3. 通过Looper实现在子线程使用Toast:
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Looper.prepare();
Toast.makeText(mContext, "子线程弹Toast", Toast.LENGTH_SHORT).show();
Looper.loop();
}
}).start();
SurfaceView非UI线程刷新及绘制:SurfaceView一方面可以实现复杂而高效的UI,另一方面又不会导致用户输入得不到及时响应。常用于画面内容更新频繁的场景,比如游戏、视频播放和相机预览。
使用SurfaceView的三步骤:
1、获取SurfaceHolder对象,其是SurfaceView的内部类。添加回调监听Surface生命周期。
mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
2、surfaceCreated 回调后启动绘制线程。只有当native层的Surface创建完毕之后,才可以调用lockCanvas(),否则失败。
@Override
public void surfaceCreated(SurfaceHolder holder) {
mDrawThread = new DrawThread();
mDrawThread.start();
}
3、绘制
Canvas canvas = mSurfaceHolder.lockCanvas();
// 使用canvas绘制内容
mSurfaceHolder.unlockCanvasAndPost(canvas);
使用SurfaceView不显示问题:发生这种问题的原因是多层嵌套被遮挡
setZOrderOnTop(boolean onTop) // 在最顶层,会遮挡一切view
setZOrderMediaOverlay(boolean isMediaOverlay)// 如已绘制SurfaceView则在surfaceView上一层绘制。
黑色背景问题:mHolder.setFormat(PixelFormat.TRANSPARENT); //设置背景透明
- Handler发送消息的delay可靠吗?
概念引入:Linux的epoll模型
epoll模型 :当没有消息的时候会epoll.wait,等待句柄写的时候再唤醒,这个时候其实是阻塞的。
面试官视角:这道题想考察什么?
1. 是否清楚UI时间相关的任务如动画的设计实现原理(中级)
2. 是否对Looper的消息机制有深刻的理解(高级)
3. 是否做过UI过度绘制(UI消息过多)或者其它消息机制的优化(高级)
题目剖析:
1. 答案肯定不可靠,但需要深入分析原理
2. 给出基于原理的实践案例
题目结论:
一星:主线程压力过大(待处理消息过多)
若发送的消息过多,主线程处理较慢,导致堆积很多待处理消息,会导致主线程卡顿
二星:
1. handler.postDelayed(run, delay)的消息,调用时间非delay
2. MessageQueue如何处理消息:
Working Thread(工作线程)-->enquequeMessage(MessageQueue,入队一条消息)-->wake(Native层:NativeMessageQueue,唤醒)-->write(mWakeEventFd,写入消息)--↓(唤醒mEpollFd)
Looper-->next(MessageQueue,处理下一条消息)-->pollOnce(Native层:NativeMessageQueue,轮询一条)-->epoll_wait(mEpollFd,若此时消息队列中无消息,则在此等待,唤醒后返回一条消息)
三星:队列优化
重复消息过滤:主要针对运行时高频发送的事件类型。通过一定手段判断一个合适的频率,通过handler.removeCallbacksAndMessages(msg)移除重复消息。
互斥消息取消:主要针对后面的事件与前面消息的互斥。通过handler.removeCallbacksAndMessages(msg)移除前面互斥的消息。
复用消息:Message.obtain();防止消息对象创建过多引发gc。
消息空闲IdleHandler:
class RunOnceHandler implements MessageQueue.IdleHandler {
@Override
public boolean queueIdle() {
Log.d(TAG, "RunOnceHandler.queueIdle()...只运行一次");
return false;
}
}
Looper.myQueue().addIdleHandler(new RunOnceHandler());
完整示例:https://github.com/Endless5F/JcyDemoList/blob/master/PerformanceAnalysis/src/main/java/com/android/performanceanalysis/activity/IdleHandlerActivity.java
使用独享的Looper:
Handler只能在主线程中创建吗?不是的只要有Looper就可以,比如HandlerThread
private HandlerThread handlerThread = new HandlerThread("独享Looper");
{handlerThread.start();}
private Handler sigleLooperHandler = new Handler(handlerThread.getLooper);
- 主线程的Looper为什么不会导致应用ANR?
概念引入1:进程/线程
进程:每个app运行时前首先创建一个进程,该进程是由Zygote fork出来的,用于承载App上运行的各种Activity/Service等组件。
进程对于上层应用来说是完全透明的,这也是google有意为之,让App程序都是运行在Android Runtime。
大多数情况一个App就运行在一个进程中,除非在AndroidManifest.xml中配置Android:process属性,或通过native代码fork进程。
线程:线程对应用来说非常常见,比如每次new Thread().start都会创建一个新的线程。
该线程与App所在进程之间资源共享,从Linux角度来说进程与线程除了是否共享资源外,并没有本质的区别,
都是一个task_struct结构体,在CPU看来进程或线程无非就是一段可执行的代码,CPU采用CFS调度算法,保证每个task都尽可能公平的享有CPU时间片。
概念引入2:为什么主线程不会因为Looper.loop()方法造成阻塞
1. epoll模型:当没有消息的时候会epoll.wait,等待句柄写的时候再唤醒,这个时候其实是阻塞的。
2. 所有的ui操作都通过handler来发消息操作。比如屏幕刷新16ms一个消息,你的各种点击事件,所以就会有句柄写操作,唤醒上文的wait操作,所以不会被卡死了。
死循环问题:
对于线程既然是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。
而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,
死循环便能保证不会被退出,例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。
这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。没看见哪里有相关代码为这个死循环准备了一个新线程去运转?
事实上,会在进入死循环之前便创建了新binder线程,在代码ActivityThread.main()中:
thread.attach(false);便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件)
,该Binder线程通过Handler将Message发送给主线程,具体过程可查看http://gityuan.com/2016/03/06/start-service/,简单说Binder用于进程间通信,采用C/S架构。
另外,ActivityThread实际上并非线程,不像HandlerThread类,ActivityThread并没有真正继承Thread类,只是往往运行在主线程,给人以线程的感觉,其实承载ActivityThread的主线程就是由Zygote fork而创建的进程。
面试官视角:这道题想考察什么?
1. 是否了解ANR产生的条件(中级)
2. 是否对Looper的消息机制有深刻的理解(高级)
3. 是否对Android App的进程运行机制有深入了解(高级)
4. 是否对IO多路复用有一定的认识(高级)
题目剖析:
1. ANR是如何产生的
2. Looper的工作机制是什么?
3. Looper不会导致ANR本质原因是什么?
4. Looper的死循环为什么不会导致CPU占用率高?
题目结论:
一星:ANR类型
Service超时:前台服务 20s / 后台服务 200s
BroadcaseQueue超时:前台广播 10s / 后台广播 60s
ContentProvider超时:10s
InputDispatching(输入事件,最常见)超时:5s
二星:
主线程究竟在干什么?
ActivityThread的main方法主要就是做消息循环,一旦退出消息循环,那么你的程序也就可以退出了。
从消息队列中取消息可能会阻塞,取到消息会做出相应的处理。如果某个消息处理时间过长,就可能会影响UI线程的刷新速率,造成卡顿的现象。
Looper和ANR的关系:Looper是针对整个应用进程,而ANR只针对Looper循环中对应消息的一环而已。
三星:主线程的死循环一直运行是不是特别消耗CPU资源呢?
其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,
便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,
通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,
当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
概念引入参考链接:www.cnblogs.com/chenxibobo/…
- 如何自己实现一个简单的Handler - Looper框架?
面试官视角:这道题想考察什么?
1. 是否对Looper的消息机制有深刻的理解(高级)
2. 是否对Java并发包中提供的队列有较为清楚的认识(高级)
题目剖析:
1. “简单”表明可以运用Java标准库当中的组件
2. 覆盖实现的关键路径即可,突出重点
3. 分析Android为什么要单独实现一套
4. 仍然着眼于阐述Handler-Looper的原理
题目结论:
Handler的核心能力:
1. 线程间通信
2. 延迟任务执行
Looper的核心能力:
1. 准备prepare
2. 获取消息队列
3. loop轮询分发处理消息
MessageQueue的核心能力:
1. 持有消息
2. 消息按时间排序(优先级)
3. 队列为空时阻塞读取
4. 头结点有延时可以定时阻塞(DealyQueue)
Message的实现:
1. 仿照Android即可,保存信息,持有handler等等
2. 若MessageQueue使用DealyQueue,则此Message需要实现Delayed接口
Android为什么不直接复用DelayQueue?
1. DelayQueue没有提供合适的remove机制
2. 更大的自由度,可以定制许多功能,特别是与Native层结合
3. Android的MessageQueue可以针对单线程读取的场景做优化(DelayQueue很多地方加了锁,而MessageQueue只需要在入队时加锁,因为读时只是自己的Looper读)