。客户反馈我司的指纹解锁机器冷屏(息屏)解锁下速度太慢,体验很差,而对比机却非常快
。对比发现,我司机器跟市面品牌机的冷屏解锁速度差了不是一个等级, 急待改善.
既然要优化功能,首先要做到理解功能实现原理,如此才能找到性能瓶颈,打开突破点.
Google默认的冷屏指纹解锁基本框架流程如下:
(1)指纹IC检测到手指触摸模组,HW触发irq,被Linux kernel接收到;
(2)fingerprintd守护进程接到kernel上报的irq后调用TZ的系统调用接口发起指纹数据采图、比对auth请求;
(3)指纹数据auth比对成功,从TZ返回kernel再返回fingerprintd,此处耗费约180-260ms;
(4)fingerpritnd接到auth成功消息,binder回调FingerprintService对象onAuthenticated接口;
(5)auth success结果回调到keyguard,准备wakeup system;
(6)keyguard调用PowerManager.wakeUp() 唤醒系统,耗费约350+200 ms
(7)Wakeup completed,keyguard调用keyguardDone解锁完成后,耗费约200-300ms
(8)解锁完成,调用系统接口点亮背光,此刻完成整个解锁全过程。
从以上流程可以看出,整个息屏解锁流程的大部分时间集中在三个过程:指纹运算比对、系统和外设wakeup、解锁绘图,这三块时间之和即为
整个解锁流程完成的理论时间之和约:900 ~1100 ms
从上面的流程看到几个信息:wakeup、authenticated、keyguard unlock属于最耗时的三个部分,而且是串行执行的,下面分别分析这几个部分是否有改善空间.
指纹比对成功后keyguard进程会通过PowerManagerService的wakeup()接口执行唤醒系统的操作,该函数最耗时部分在于底层驱动设备的resume过程,其中占比重最大的属LCD,TP的resume,所以此处属于重点优化部分内容。另外,系统要显示数据,肯定要等LCD resume完成才可以,这个可以理解,但是为什么TP 的resume会影响到系统亮屏呢?这个原因其实可以从代码中找到,后面源码分析将会介绍,TP resume的动作是放在fb unblank的线程中的,而且会发现系统要亮屏其实是可以不等TP resume的。
fingerprintd收到指纹IRQ后,会通过系统调用接口向Trustzoon端的指纹TA(trust app)发起指纹图像采集、模板对比操作,而这个耗时业内标准是200ms左右,这块各个方案供应商表现差别不大,此部分的优化完全依靠方案提供商。
现有的方案是要等执行解锁动作完成后再去点亮背光,如果不等解锁完成就去点亮背光会看到解锁退去的动画闪动,用户体验不佳。这部分的耗时比较多,从log看至 200-300 ms的delay,属于重点优化部分。若能解决解锁时候不出现“动画退去的闪动”也是个不错的方法。
既然对比机比我们的速度快这么多,抛开客观条件(CPU,flash读写速度等),还是可以认真的研究一番,看看优秀的产品是怎么去做的,我们可以尝试参考优秀做的实现方式用到我们的产品上,师夷长技以制夷,下面简单描述下两款对比机的分析过程.
老实说,要不是因为这个分析我还真没听过这个牌子的手机,不过人家的冷屏解锁做的如此之快,确实很不错,很值得我们研究学习一番。经过log分析,机器代码逆向发现,该机器不走寻常路,简单来说就是:
。收到指纹中断后就另外开线程直接去wakup系统,而不是等指纹auth success再去做,这样做的好处就是wakeup系统跟auth同时进行,由原来串行改为并行,重叠部分的时间就相当于省下了.
。系统休眠的时候先自动上锁,然后再将keyguard锁解掉!这样系统灭屏的时候相当于没有上锁状态,指纹冷屏解锁就相当于只有wakeup 、指纹auth,点亮背光的操作了,没有keyguard unlock的操作,这样keyguard unlock的200-300ms直接为0 了!一想这样明显会有问题啊,息屏了没上锁,我按power键类似的亮屏不是不用解锁就进入系统了么?对没错,所以需要把这些地方堵住,在非指纹冷屏解锁亮屏的条件下亮屏会先去给keyguard上锁再亮屏,这样非指纹冷屏解锁的亮屏就一样会看到keyguard锁。
收到指纹中断:
08-30 10:09:20.251 2054 6017 D btl_algo: -- btl_api_waitSignal 08-30 10:09:20.251 2054 6017 D btl_algo: Finger is pressed 08-30 10:09:20.251 2054 6017 D fingerprintd: onAcquired(0)
...
比对成功,耗时:217ms
08-30 10:09:20.468 2054 6017 D fingerprintd: onAuthenticated(fid=2, gid=0) 08-30 10:09:20.469 2399 2399 D PowerManagerNotifier: onUserActivity: event=2, uid=1000 ...
亮背光: 08-30 10:09:20.472 3447 3461 D btl_jni : FpEnBlackLight 08-30 10:09:20.472 3447 3461 D btl_jni : User_EnBlackLight enable: 1
整个流程时间耗费:221ms,非常之快.
这里看到它自己调用jni去亮背光,我们知道要点亮背光led之前需要wakeup显示系统才可以成功,所以这里推断它是在收到指纹中断的时候就去执行wakeup的操作,可由于log太少,所以没有看到相关的log信息.
某品牌机2也做了很多优化跟某品牌机1类似,收到irq后报个key==304上来,然后直接wakeup系统:
01-03 02:40:38.105 1470 1856 D WindowManager: interceptKeyTq keycode=304 interactive=false keyguardActive=true policyFlags=2000000 01-03 02:40:38.105 1470 1856 D WindowManager: isPhicalHomeKey = false ,result = 0 01-03 02:40:38.105 2151 2161 V PowerEffectManager: noteInputState key = 304 01-03 02:40:38.105 1470 1856 D WindowManager: before sendMessage MSG_WAKEUP_BY_WHO_FINGERPRINT 01-03 02:40:38.105 1470 1854 D FingerprintWakeHook: onWakeUpByWho: eventTime:6238453 who:FingerPrint uid:1000 ident:4294967297470
...
01-03 02:40:38.275 787 1912 D fingerprintd: onAuthenticated(fid=265437463, gid=0) --- Auth success!
01-03 02:40:38.285 1983 2507 D KeyguardViewMediator: keyguardDone(true)--- 开始解锁 .... 01-03 02:40:38.305 1470 1470 I frontfingerprintkey: fingerprintReceiver Receiver action: android.intent.action.USER_PRESENT --解锁完成 ... 01-03 02:40:38.395 1470 1941 D DisplayPowerState: Updating screen state: state=ON, backlight=39, backlightChanged=true mPowerAssistantMode=0 01-03 02:40:38.395 1470 1941 D BBKTouchScreenService: Set LCD backlight state ON ---- 点亮背光
整体耗时:
指纹数据比对耗时:275-105 == 170 ms
解锁完成到亮屏耗时:395-285 == 110 ms
整个周期耗时:170 + 110 == 280 ms
解锁时间总体很快,但比某品牌机1的221ms略低,可是没有不上锁啊,说明它还是有解锁keyguard的时间耗时的,但是它是如何做到这么快的呢?带着疑问,尝试逆向代码看会发现:
com/android/server/wm/WindowManagerService.java:
指纹认证成功后会先执行hideKeyguardLocked函数,从上面代码猜测是1939行设置取消动画,1942-1946,1952行设置keyguardWindow为透明,如果是这样那么就可以不用管解锁的“闪动”导致的用户体验差的问题了,因为已经透明了,对用户不可见了,这是个很不错的优化keyguard解锁时间的思路。
此部分属于耗时大头,跟系统亮屏速度强关联,同时关系到wakeup跟sleep的速度,影响很大,目前我司v12xx系列的机器优化的还不错,大概是190-240 ms左右,亮屏速度体验较好。7201/6901系列就非常慢,大概需要330-350 ms,亮屏体验差。此部分的优化需要对应的LCD驱动工程师配合IC厂工程师联合调试.
系统要正常亮屏显示,lcd肯定要先准备好,所以lcd resume会影响亮屏时间,但是tp resume跟系统亮灭屏又有什么关系呢?要知道原因,分析代码,看tp resume是如何被call到的。首先找到你手机用的TP驱动是哪一个,这里是用ft5364i,代码在如下位置:
drivers/input/touchscreen/ft5364i/focaltech_core.c
搜索resume发现执行tp resume的是这个函数:
从函数名字可以猜到,是在执行fb blank的unblank,也就是亮屏的线程被“通知”执行的,那为什么会这样执行呢?继续看fb_notifier_callback在哪里调用,首先看到会再tp驱动的probe函数中注册到fb 的通知链中,
那注册的过程做了什么呢?进一步深究,如下代码:
我们可以看看这个 fb_notifier_list 的定义:
static BLOCKING_NOTIFIER_HEAD(fb_notifier_list);
典型的订阅-发布模式设计,从宏定义名字可以知道就是一个通知链的头指针,也就是说上面fb_register_client的作用就是将新的这个node(nb)加入到fb_notifier_list头指针的链表中管理起来,这个是我们从函数名字上猜想的结果,实际上是如何呢?继续看代码看具体做了什么,如下最终会调用到这个函数:
其中nl就是上面传下来的fb_notifier_list头指针,n就是需要新加入的节点指针,从上面代码可以看到一个细节,新节点的插入会根据优先级来按顺序实现,而不是单单从调用关系先后而决定的.
上面的分析搞清楚了fb resume是注册到fb_notifier_list链表里面,那什么时候执行呢?看如下代码:
drivers/video/fbdev/core/fbmem.c
我们知道系统亮灭屏的时候SurfaceFlinger经常会调用驱动跑fb_blank的代码,亮屏就是unblank参数,灭屏就是blank参数,而这里就会去调用fb_notifier_call_chain函数,看看这个函数:
是不是很眼熟?fb_notifier_list,就是上面看到的注册试试的那个头指针,理解了上面的注册过程,那么这个也就很容易理解了,就是去依次遍历每一个注册到该链表的节点,依次回调notifier_call函数,而TP 的resume就是在这个函数里面的,所以自然就会影响到fb blank整个函数的执行周期,如果tp resume耗时太久也就自然对亮屏会有影响了。
默认的解锁流程都是串行执行的,没有有效的利用相同的时间内做更多的事情,现在都是多核CPU,完全可以在收到指纹irq立即去执行wakeup动作,然后等Auth success就直接去点亮背光,这样做就可以在指纹auth的200ms内一起把wakeup的事情也干了,而重叠的时间就是省下来来的时间。
直接调用Power.wakeu默认就会点亮背光,而触发irq的时候并不知道auth是否成功,所以wakeup之前需要把背光拦截掉,不能点亮,只有等指纹auth success后才点亮,所以这里需要更改点亮背光的策略.
背光提早到keyguard unlock之前亮会引入一个副作用就是会看到keyguard消退的动画跳变过程,用户体验不佳,这个时候就可以借助vivo-x9的做法了,直接设置为透明解锁,这样就可以巧妙的避开这个问题,当然实际开发过程可能会有各种状态考虑需要细细调整.
上面分析了tp resume是跟fb blank同步进行,那么事实上tp resume时间大概是200ms以下,人的反应时间大概是100-200ms,人眼看到亮屏在到用手去触摸屏幕时间需要至少200+ms以上了,所以可以尝试改为异步执行,这样就可以节省掉tp resume的200ms时间了。代码实现很简单如下:
首先增加一个指针fb_notify_work,在probe函数中创建一个workqueue,INIT_WORK该宏的本质是创建一个新的内核线程,然后再使之与指针fb_notify_work绑定,这样就可以操作该指针来实现启动、关闭该线程的执行了。
实现workqueue的callback:
修改notifier通知链回调代码:
入行代码中同步调用tp resume改为schedule_work调用,该函数本质是执行新的内核线程来实现异步执行,这样tp resume的执行就跟fb blank不在同一个线程了,也就不会影响到亮屏的时间了.
分两步,其一就是IRQ(中断)要能触发,其二就是system_server进程需要接受到这个IRQ信号,在指纹认证等待模式下,只要手指有触摸指纹模块,那么自动就会触发指纹IRQ信号,所以我们需要做的就是需要新建一个线程用于监听来自指纹驱动上报的IRQ信号,然后再通过PowerManagerService执行wakeup的动作唤醒系统。
图解:
(1-3)FingerprintService启动的时候初始化本地封装类FingerprintNative,然后通过jni调用本地函数native_init();
(4)执行路径路由熬fp_dev_init(),调用fp_dev_open();
(5)open(“/dev/fp_drv") 设备节点,拿到句柄dev_fd,注册异步监听该设备来自内核的消息,fp_input_handler()是回调函数;
(6)return;
(7)传入new FingerprintNative()对象到native层;
(8)通过pthread_create()创建监听线程,监听来自“/dev/fp_drv”设备的消息,该监听是阻塞执行的。
图解:
(1)触摸指纹模块,硬件触发中断,被kernel接到,回调中断处理函数;
(2)调用fp_drv.c里面的接口,内核空间使用kill_fasync()发送异步消息;
(3)消息通过系统调用接口传入到用户空间,被native层的监听线程监听到,通过JNI回到到java层;
(4)JNI回调Java层的onReport() 函数;
(5)调用Power的wakeUp()函数执行唤醒系统;
默认情况下执行Power的wakeup()就会自动亮背光,但是事实上我们需要改成指纹Auth success才亮背光,所以需要在调用wakeup前拦截掉背光控制流程,可以通过改上层或者底层来实现,经过实际测试综合考虑还是改kernel层更合适,如下内核led接口处添加拦截判断:
kernel-3.18/drivers/leds/leds.h
kernel-3.18/drivers/input/fingerprint/fp_drv/fp_drv.c
get_bl_ctr_flag()函数实现在fp_drv.c中:
这里会判断传入的led dev,如果是背光 "lcd-backlight" 则需要判断fp_backlight_control的值是否为1,为1则拦截,否则不拦截,如此通过控制全局变量fp_backlight_control的值就可以实现背光的拦截操作。
这个就比较简单了,有两种方式实现,一种在用户空间,另外一种在内核空间:
先看用户空间的实现方式:
#define BL_DEV_ATTR "/sys/class/leds/lcd-backlight/brightness"
也可以再内核空间操作,内核空间实现方法如下:
所有的led设备初始化的时候都会挂到一个链表中,头指针是leds_list,遍历这个链表找到“lcd-backlight" 那就是背光驱动,设置需要的值即可.
实现涉及文件:
SystemUI/src/com/android/systemui/statusbar/phone/FingerprintUnlockController.java
SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
Keyguard/src/com/android/keyguard/KeyguardUpdateMonitor.java
关键代码:
public void setStatusBarWindowAlpha(int mode ){
2973 synchronized(PhoneStatusBar.this){
2975 fingerHandler.removeMessages(FINGFERPRINT_QUICK_UNLOCK_MSG);
2976 recoveryAlphaState(true);
2977 if(mode == KeyguardUpdateMonitor.MSG_POWER_KEY_PRESS ) {
2979 recoveryAlphaState(false);
2980 return;
2981 }
2983 try {
2984 mScaleFlag = true;
2985 mScale = mWindowManagerService.getAnimationScale(0);
2986 mWindowManagerService.setAnimationScale(0, 0.0f);
2987 } catch (Exception e) {
2988
2989 }
2991 int count = mStatusBarWindow.getChildCount();
2992 for(int i=0 ; i< count ;i++){
2993 View v = mStatusBarWindow.getChildAt(i);
2994 if (v instanceof PhoneStatusBarView) {
2995
2996 } else {
2997 Object obj = v.getTag();
2999 if (obj == null) {
3000 float f;
3001 if(v instanceof BackDropView){
3002 //Some time BackDropView getAlpha= 0.001999,cause bug
3004 f = 1.0f;
3005 } else {
3006 f = v.getAlpha();
3007 }
3008 v.setTag(new Float(f));
3010 }
3011 v.setAlpha(0.0f);
3012 }
3013 }
3014 fingerHandler.sendEmptyMessageDelayed(FINGFERPRINT_QUICK_UNLOCK_MSG, 1500);
3015 }
3016 }
简析:
LINE 2985-2986:获取keyguard窗口的动画值备份mScale,然后设置为0,关闭窗口动画;
LINE 2994:如果是状态栏或者导航栏的view,则不纳入视图隐藏,保持不变;
LINE 3011:设置透明度为0.0f,隐藏显示;
LINE 2979:若是按下Power键亮屏,则需要恢复动画显示,恢复keyguard显示;
使用错误指纹触摸模组,此刻会wakup系统,此时如果按power键,系统是不会亮屏的,因为当前已经wakeup了,这明显是引入的问题点,所以需要特别针对这样的情况特殊处理下,主要涉及修改如下:
services/core/java/com/android/server/policy/PhoneWindowManager.java:
private void interceptPowerKeyDown(KeyEvent event, boolean interactive) {
..
1192 if (isFpQuickWakeUpSupport) {
1193 if (mFpManager == null) {
1194 mFpManager = (FingerprintManager) mContext.getSystemService(Context.FINGERPRINT_SERVICE);
1195 }
1196 if (mFpManager != null && mFpManager.isFingerprintQuickWakeup(1, "power key down")) {
1197 mFpWakeupFlag = true;
1198 return;
1199 }
1200 }
..
private void powerPress(long eventTime, boolean interactive, int count) {
..
1293 if (isFpQuickWakeUpSupport) {
1294 if (mFpWakeupFlag) {
1295 mFpWakeupFlag = false;
1296 return;
1297 }
1298 }
..
services/core/java/com/android/server/fingerprint/FingerprintService.java:
1299 @Override // Binder call
1300 public boolean isFingerprintQuickWakeup(int arg0, String reason) {
1301 if (arg0 == 1 && mCurrFingerprintState == FingerprintManager.FP_TRIGGER_IRQ) {
1302 boolean on = mFpEvent.isBackLightOn();
1303 if (on == false) {
1304 mFpEvent.userActivity("power key press");
1305 mHandler.postDelayed(new Runnable() {
1306 @Override
1307 public void run() {
1308 mFpEvent.bl_ctl_enable(mFpEvent.FP_BL_UNLOCK_AND_TRIGGER, reason);
1309 }
1310 }, 100);
1311 mCurrFingerprintState = 0;
1312 return true;
1313 }
简析:
LINE 1196:Power down事件会调interceptPowerKeyDown函数,这个时候判断是否是属于使用过错误指纹触摸过模组,如果是,就直接通过isFingerprintQuickWakeup接口内部实现点亮背光,同时设置标记mFpWakeupFlag=true;
LINE 1294:Power up事件的时会调此函数,此函数主要作用是gotosleep(),如果mFpWakeupFlag=true说明是上面说的这种情况就不执行休眠,直接返回,如此就可以解决前面说的按power无法亮屏的问题.
729 @Override
730 public void onReceive(final Context context, final Intent intent) {
731 final String action = intent.getAction();
732 if (Intent.ACTION_POWER_CONNECTED.equals(action)
741 || Intent.ACTION_POWER_DISCONNECTED.equals(action)) {
742 boolean on = isBackLightOn();
743 if(DEBUG) Log.i(TAG, "#onReceive: ACTION_POWER_CONNECTED/DISCONNECTED: on:"+ on);
744 if (!on) {
745 userActivity("ACTION_POWER_CONNECTED/DISCONNECTED");
746 bl_ctl_enable(FP_BL_UNLOCK_AND_TRIGGER, "ACTION_POWER_CONNECTED/DISCONNECTED");
747 }
748 }
749 else if (TelephonyManager.ACTION_PHONE_STATE_CHANGED.equals(action)) {
750 int state = mTelemanager.getCallState();
751 if(DEBUG) Log.i(TAG, "#onReceive: ACTION_PHONE_STATE_CHANGED:"+state);
752 switch (state) {
753 case TelephonyManager.CALL_STATE_RINGING:
754 case TelephonyManager.CALL_STATE_IDLE:
755 boolean on = isBackLightOn();
756 if (!on) {
757 userActivity("ACTION_PHONE_STATE_CHANGED");
758 bl_ctl_enable(FP_BL_UNLOCK_AND_TRIGGER, "ACTION_PHONE_STATE_CHANGED");
759 }
760 break;
761 }
762 }
..
可以看到,代码其实很简单,就是监听相关广播,然后直接设置为亮背光,不再解释.
一系列优化调试后,息屏指纹解锁亮屏整个耗时在400-500ms左右,提升一倍速率。