1. 程式人生 > >千人千面線上問題回放技術揭祕

千人千面線上問題回放技術揭祕

文章內容

導語
釋出app後,開發者最頭疼的問題就是如何解決交付後的使用者側問題的還原和定位,是業界缺乏一整套系統的解決方案的空白領域,閒魚技術團隊結合自己業務痛點提出一套全新的技術思路解決這個問題並在線上取得了比較滿意的實踐效果。

我們透過系統底層來捕獲ui事件流和業務資料的流動,並利用捕獲到的這些資料通過事件回放機制來複現線上的問題。本文先介紹錄製和回放的整體框架,接著介紹裡面涉及到的3個關鍵技術點,也是這裡最複雜的技術(模擬觸控事件,統一攔截器實現,統一hook block)

背景
現在的app基本都會提供使用者反饋問題的入口,然而提供給使用者反饋問題一般有兩種方式:

直接用文字輸入表達,或者截圖

直接錄製視訊反饋

這兩種反饋方式常常帶來以下抱怨:

使用者:輸入文字好費時費力

開發1:看不懂使用者反饋說的是什麼意思?

開發2:大概看懂使用者說的是什麼意思了,但是我線下沒辦法復現哈

開發3:看了使用者錄製的視訊,但是我線下沒辦法重現,也定位不到問題

所以:為了解決以上問題,我們用一套全新的思路來設計線上問題回放體系

線上問題回放體系的意義
使用者不需要輸入文字反饋問題,只需要重新操作一下app重現問題步驟即可

開發者拿到使用者反饋的問題指令碼後,通過線下回放對問題一目瞭然,跟錄製視訊效果一樣,是的,你沒看錯,就是跟看視訊一樣。

通過指令碼的回放實時獲取到app執行時相關資料(本地資料,網路資料,堆疊等等), 以便排查問題

為後續自動測試提供想象空間–你懂的

效果視訊

技術原理
1.app與外部環境的關係

從上面的關係圖可以看出,整個app的執行無非是使用者ui操作,然後觸發app從外界獲取資料,包括網路資料,gps資料等等,也包括從手機本地獲取資料,比如相簿資料,機器資料,系統等資料。
所以我們要實現問題回放只需要記錄使用者的UI操作和外界資料,app自身資料即可。

app錄製 = 使用者的UI操作 + 外界資料(手機內和手機外) + app自身資料

2.線上問題回放架構由兩部分組成:錄製和回放
錄製是為回放服務,錄製的資訊越詳細,回放成功率就越高,定位問題就越容易

錄製其實就是把ui和資料記錄下來,回放其實就是app自動

驅動UI操作並把錄製時的資料塞回相應的地方。

3.錄製架構圖

錄製流程

4.回放架構圖
回放跟錄製框架圖基本一樣,實際上錄製和回放的程式碼是在一起,邏輯也是統一的,為了便於表達,我人為劃分成兩個架構圖出來。

回放的流程:
回放流程圖在這裡省略

1.啟動app,點選回放按鈕
2.引擎載入回放指令碼
3.註冊事件(如ui事件,網路資料事件,本地檔案事件,頁面跳轉事件等等)
4.從指令碼中解析出一個個事件資料節點,並組成消費佇列
5.啟動播放器,從消費佇列裡讀取一個個事件來播放,如果是ui事件則直接播放,如果是靜態資料事件則直接按照指令要求替換資料值,如果是非ui執行時事件則通過事件指令規則來確定是主動播放還是等待攔截對應的事件,如果需要等待攔截對應的事件,則播放器會一直等待此事件直到此事件被app消費掉為止。只有此事件被消費了,播放器才能播放下一個事件。
6.當攔截到被註冊的事件後,根據此事件指令要求把相應的資料塞到相應的欄位裡
7.跳回6繼續執行,直到消費佇列裡的事件被消費

注意:回放每個事件時會實時自動打印出相應的堆疊資訊和事件資料,有利於排查問題

關鍵技術介紹
1.模擬觸控事件
從ui事件資料解析出被觸控的view,以及此view所在的檢視樹中的層級關係,並在當前回放介面上查詢到對應的view,然後往該view上傳送ui操作事件(點選,雙擊等等),並帶上觸控事件的座標資訊,其實這裡是模擬觸控事件。

我們先來介紹觸控事件的處理流程

等待觸控階段
手機螢幕處於待機狀態,等待觸控事件發生

手指開始觸控式螢幕幕

系統反應階段
螢幕感應器接收到觸控,並將觸控資料傳給系統IOKit(IOKit是蘋果的硬體驅動框架)

系統IOKit封裝該觸控事件為IOHIDEvent物件

接著系統IOKit把IOHIDEvent物件轉發給SpringBoard程序

SpringBoard程序就是iOS的系統桌面,它存在於iDevice的程序中,不可清除,它的執行原理與Windows中的explorer.exe系統程序相類似。它主要負責介面管理,所以只有它才知道當前觸控到底有誰來響應。

SpringBoard接收階段
SpringBoard收到IOHIDEvent訊息後,觸發runloop中的Source1回撥__IOHIDEventSystemClientQueueCallback()方法。

SpringBoard開始查詢前臺是否存在正在執行的app,如果存在,則SpringBoard通過程序通訊方式把此觸控事件轉發給前臺當前app,如果不存在,則SpringBoard進入其自己內部響應過程。

app處理階段
前臺app主執行緒Runloop收到SpringBoard轉發來的訊息,並觸發對應runloop 中的Source1回撥_UIApplicationHandleEventQueue()。

_UIApplicationHandleEventQueue()把IOHIDEvent處理包裝成UIEvent進行處理分發

Soucre0回撥內部UIApplication的sendEvent:方法,將UIEvent傳給UIWindow

在UIWindow為根節點的整棵檢視樹上通過hitTest(_:with:)和point(inside:with:)這兩個方法遞迴查詢到合適響應這個觸控事件的檢視。

找到最終的葉子節點檢視後,就開始觸發此檢視繫結的相應事件,比如跳轉頁面等等。

從上面觸控事件處理過程中我們可以看出要錄製ui事件只需要在app處理階段中的UIApplication sendEvent方法處截獲觸控資料,回放時也是在這裡把觸控模擬回去。

下面是觸控事件錄製的程式碼,就是把UITouch相應的資料儲存下來即可
這裡有一個關鍵點,需要把touch.timestamp的時間戳記錄下來,以及把當前touch事件距離上一個touch事件的時間間隔記錄下來,因為這個涉及到觸控引起慣性加速度問題。比如我們平時滑動列表檢視時,手指離開屏幕後,列表檢視還要慣性地滑動一小段時間。

- (
void
)handleUIEvent:(
UIEvent
 *)
event

{

    
if
 (!
self
.isEnabled) 
return
;

    
if
 (
event
.type != 
UIEventTypeTouches
) 
return
;


    
NSSet
 *allTouches = [
event
 allTouches];

    
UITouch
 *touch = (
UITouch
 *)[allTouches anyObject];


    
if
 (touch.view) {

        
if
 (
self
.filter && !
self
.filter(touch.view)) {

            
return
;

        }

    }


    
switch
 (touch.phase) {

        
case
 
UITouchPhaseBegan
:

        {

            
self
.machAbsoluteTime = mach_absolute_time();

            
self
.systemStartUptime = touch.timestamp;

            
self
.tuochArray = [
NSMutableArray
 array];

            [
self
 recordTouch:touch click:
self
.machAbsoluteTime];


            
break
;

        }

        
case
 
UITouchPhaseStationary
:

        {

            [
self
 recordTouch:touch click:mach_absolute_time()];

            
break
;

        }


        
case
 
UITouchPhaseCancelled
:

        {

            [
self
 recordTouch:touch click:mach_absolute_time()];

            [[
NSNotificationCenter
 defaultCenter] postNotificationName:@
"notice_ui_test"
 
object
:
self
.tuochArray];

            
break
;

        }


        
case
 
UITouchPhaseEnded
:

        {

            [
self
 recordTouch:touch click:mach_absolute_time()];

            [[
NSNotificationCenter
 defaultCenter] postNotificationName:@
"notice_ui_test"
 
object
:
self
.tuochArray];


            
break
;

        }


        
case
 
UITouchPhaseMoved
:

        {

            [
self
 recordTouch:touch click:mach_absolute_time()];

        }


        
default
:

            
break
;

    }


}

我們來看一下程式碼怎麼模擬單擊觸控事件(為了容易理解,我把有些不是關鍵,複雜的程式碼已經去掉)

接著我們來看一下模擬觸控事件程式碼
一個基本的觸控事件一般由三部分組成:

1.UITouch物件 - 將用於觸控

2.第一個UIEvent Began觸控

3.第二個UIEvent Ended觸控

實現步驟:

1.程式碼的前面部分都是一些UITouch和UIEvent私有介面,私有變數欄位,由於蘋果並不公開它們,為了讓其編譯不報錯,所以我們需要把這些欄位包含進來,回放是線上下,所以不必擔心私有介面被拒的事情。

2.構造觸控物件:UITouch和UIEvent,把記錄對應的欄位值塞回相應的欄位。塞回去就是用私有介面和私有欄位

3.觸控的view位置轉換為Window座標,然後往app裡傳送事件 [[UIApplication sharedApplication] sendEvent:event];

4.要回放這些觸控事件,我們需要把他丟到CADisplayLink裡面來執行

//

//  SimulationTouch.m

//

//  Created by 詩壯殷 on 2018/5/15.

//


#import "SimulationTouch.h"

#import <objc/runtime.h>

#include
 
<mach/mach_time.h>





@implementation
 
UITouch
 
(
replay
)



-
 
(
id
)
initPoint
:(
CGPoint
)
point window
:(
UIWindow
 
*)
window

{

    
NSParameterAssert
(
window
);


    
self
 
=
 
[
super
 init
];

    
if
 
(
self
)
 
{

        
[
self
 setTapCount
:
1
];

        
[
self
 setIsTap
:
YES
];

        
[
self
 setPhase
:
UITouchPhaseBegan
];

        
[
self
 setWindow
:
window
];

        
[
self
 _setLocationInWindow
:
point resetPrevious
:
YES
];

        
[
self
 setView
:[
window hitTest
:
point withEvent
:
nil
]];

        
[
self
 _setIsFirstTouchForView
:
YES
];

        
[
self
 setTimestamp
:[[
NSProcessInfo
 processInfo
]
 systemUptime
]];

    
}

    
return
 
self
;

}


@end





@interface
 
UIInternalEvent
 
:
 
UIEvent

-
 
(
void
)
_setHIDEvent
:(
IOHIDEventRef
)
event
;

@end


@interface
 
UITouchesEvent
 
:
 
UIInternalEvent

-
 
(
void
)
_addTouch
:(
UITouch
 
*)
touch forDelayedDelivery
:(
BOOL
)
delayedDelivery
;

-
 
(
void
)
_clearTouches
;

@end






typedef
 
enum
 
{


    kIOHIDDigitizerEventRange 
=
 
0x00000001
,


    kIOHIDDigitizerEventTouch 
=
 
0x00000002
,


    kIOHIDDigitizerEventPosition 
=
 
0x00000004
,

}
 
IOHIDDigitizerEventMask
;


IOHIDEventRef
 
IOHIDEventCreateDigitizerFingerEvent
(
CFAllocatorRef
 allocator
,

                                                   
AbsoluteTime
 timeStamp
,

                                                   
uint32_t
 index
,

                                                   
uint32_t
 identity
,

                                                   
IOHIDDigitizerEventMask
 eventMask
,

                                                   
IOHIDFloat
 x
,

                                                   
IOHIDFloat
 y
,

                                                   
IOHIDFloat
 z
,

                                                   
IOHIDFloat
 tipPressure
,

                                                   
IOHIDFloat
 twist
,

                                                   
Boolean
 range
,

                                                   
Boolean
 touch
,

                                                   
IOOptionBits
 options
);


@implementation
 
SimulationTouch


-
 
(
void
)
performTouchInView
:(
UIView
 
*)
view start
:(
bool
)
start

{

    
UIWindow
 
*
_window 
=
 view
.
window
;

    
CGRect
 fInWindow
;

    
if
 
([
view isKindOfClass
:[
UIWindow
 
class
]])

    
{

        fInWindow 
=
 view
.
frame
;

    
}

    
else

    
{

        fInWindow 
=
 
[
_window convertRect
:
view
.
frame fromView
:
view
.
superview
];

    
}


    
CGPoint
 point 
=

    
CGPointMake
(
fInWindow
.
origin
.
x 
+
 fInWindow
.
size
.
width
/
2
,

                fInWindow
.
origin
.
y 
+
 fInWindow
.
size
.
height
/
2
);


    
if
(
start
)

    
{

        
self
.
touch 
=
 
[[
UITouch
 alloc
]
 initPoint
:
point window
:
_window
];

        
[
self
.
touch setPhase
:
UITouchPhaseBegan
];


    
}

    
else

    
{

        
[
self
.
touch _setLocationInWindow
:
point resetPrevious
:
NO
];

        
[
self
.
touch setPhase
:
UITouchPhaseEnded
];

    
}


    
CGPoint
 currentTouchLocation 
=
 point
;


    
UITouchesEvent
 
*
event
 
=
 
[[
UIApplication
 sharedApplication
]
 _touchesEvent
];

    
[
event
 _clearTouches
];


    
uint64_t
 machAbsoluteTime 
=
 mach_absolute_time
();

    
AbsoluteTime
 timeStamp
;

    timeStamp
.
hi 
=
 
(
UInt32
)(
machAbsoluteTime 
>>
 
32
);

    timeStamp
.
lo 
=
 
(
UInt32
)(
machAbsoluteTime
);


    
[
self
.
touch setTimestamp
:[[
NSProcessInfo
 processInfo
]
 systemUptime
]];


    
IOHIDDigitizerEventMask
 eventMask 
=
 
(
self
.
touch
.
phase 
==
 
UITouchPhaseMoved
)

    
?
 kIOHIDDigitizerEventPosition

    
:
 
(
kIOHIDDigitizerEventRange 
|
 kIOHIDDigitizerEventTouch
);



    
Boolean
 isRangeAndTouch 
=
 
(
self
.
touch
.
phase 
!=
 
UITouchPhaseEnded
);

    
IOHIDEventRef
 hidEvent 
=
 
IOHIDEventCreateDigitizerFingerEvent
(
kCFAllocatorDefault
,

                                                                  timeStamp
,

                                                                  
0
,

                                                                  
2
,

                                                                  eventMask
,

                                                                  currentTouchLocation
.
x
,

                                                                  currentTouchLocation
.
y
,

                                                                  
0
,

                                                                  
0
,

                                                                  
0
,

                                                                  isRangeAndTouch
,

                                                                  isRangeAndTouch
,

                                                                  
0
);

    
if
 
([
self
.
touch respondsToSelector
:
@selector
(
_setHidEvent
:)])
 
{

        
[
self
.
touch _setHidEvent
:
hidEvent
];

    
}


    
[
event
 _setHIDEvent
:
hidEvent
];

    
[
event
 _addTouch
:
self
.
touch forDelayedDelivery
:
NO
];

    
[[
UIApplication
 sharedApplication
]
 sendEvent
:
event
];


}


@end

怎樣呼叫私有介面,以及使用哪些私有介面,這點不需要再解釋了,如果感興趣,請關注我們公眾號,後續我專門寫篇文章來揭露這方面的技術,總的來說就下載蘋果提供觸控事件的原始碼庫,分析原始碼,然後設定斷掉除錯,甚至反彙編來理解觸控事件的原理。

2.統一攔截器
錄製和回放都居於事件流來處理的,而資料的事件流其實就是對一些關鍵方法的hook,由於我們為了保證對業務程式碼無侵入和擴充套件性(隨便註冊事件),我們需要對所有方法統一hook,所有的方法由同一個鉤子來響應處理。如下圖所示

這個鉤子是用用匯編編寫,由於彙編程式碼比較多,而且比較難讀懂,所以這裡暫時不附上原始碼,彙編層主要把硬體裡面的一些資料統一讀取出來,比如通用暫存器資料和浮點暫存器資料,堆疊資訊等等,甚至前面的前面的方法引數都可以讀取出來,最後轉發給c語言層處理。

彙編層把硬體相關資訊組裝好後呼叫c層統一攔截介面,彙編層是為c層服務。c層無法讀取硬體相關資訊,所以這裡只能用匯編來讀取。c層介面通過硬體相關資訊定位到當前的方法是屬於哪個事件,知道了事件,也意味著知道了事件指令,知道了事件指令,也知道了哪些欄位需要塞回去,也知道了被hook的原始方法。

c層程式碼介紹如下:
由於是統一呼叫這個攔截器,所以攔截器並不知道當前是哪個業務程式碼執行過來的,也不知道當前這個業務方法有多少個引數,每個引數型別是什麼等等,這個介面程式碼處理過程大概如下

通過暫存器獲取物件self

通過暫存器獲取方法sel

通過self和sel獲取對應的事件指令

通過事件指令回撥上層來決定是否往下執行

獲取需要回放該事件的資料

把資料塞回去,比如塞到某個暫存器裡,或者塞到某個暫存器所指向的物件的某個欄位等等

如果需要立即回放則呼叫原來被hook的原始方法,如果不是立即回放,則需要把現場資訊儲存起來,並等待合適的時機由播放佇列來播放(呼叫)

//xRegs 表示統一彙編器傳入當前所有的通用暫存器資料,它們地址存在一個數組指標裡

//dRegs 表示統一彙編器傳入當前所有的浮點暫存器資料,它們地址也存在一個數組指標裡

//dRegs 表示統一彙編器傳入當前堆疊指標

//fp  表示呼叫棧幀指標

void
 replay_entry_start(
void
* xRegs, 
void
* dRegs, 
void
* spReg, 
CallBackRetIns
 *retIns,
StackFrame
 *fp, 
void
 *con_stub_lp)

{

    
void
 *objAdr = (((
void
 **)xRegs)[
0
]);
//獲取物件本身self或者block物件本身

    
EngineManager
 *manager = [
EngineManager
 sharedInstance];

    
ReplayEventIns
 *node = [manager getEventInsWithBlock:objAdr];

    id obj = (__bridge id)objAdr;

    
void
 *xrArg = ((
void
 **)xRegs)+
2
;

    
if
(
nil
 == node)

    {

        SEL selecter = (SEL)(((
void
 **)xRegs)[
1
]); 
//對應的物件呼叫的方法

        
Class
 tclass = [obj 
class
];
//object_getClass(obj);object_getClass方法只能通過物件獲取它的類,不能傳入class 返回class本身,

        
do

        {

            node = [manager getEventIns:tclass sel:selecter];
//通過物件和方法獲取對應的事件指令節點

        }
while
(
nil
 == node && (tclass = class_getSuperclass(tclass)));

    }

    
else

    {

        xrArg = ((
void
 **)xRegs)+
1
;

    }

    
assert
(node && 
"node is nil in replay_call_start"
);

    
//回撥通知上層當前回放是否打斷

    
if
(node.
BreakCurReplayExe
 && node.
BreakCurReplayExe
(obj,node,xrArg,dRegs))

    {

        retIns->nodeAddr = NULL;

        retIns->recordOrReplayData = NULL;

        retIns->return_address = NULL;

        
return
;

    }

    
bool
 needReplay = 
true
;

    
//回撥通知上層當前即將回放該事件

    
if
(node.willReplay)

    {

        needReplay = (*(node.willReplay))(obj,node,xrArg,dRegs);

    }

    
if
(needReplay)

    {

        
ReplayEventData
 *replayData = 
nil
;

        
if
(node.getReplayData)

        {

        
//獲取回放該事件對應的資料

            replayData = (*(node.getReplayData))(obj,node,xrArg,dRegs);

        }

        
else
//預設獲取方法

        {

            replayData = [manager getNextReplayEventData:node];

        }

        
//以下就是真正的回放,即是把資料塞回去,並呼叫原來被hook的方法

        
if
(replayData)

        {

            
if
(replay_type_intercept_call == node.replayType)

            {

                sstuffArg(xRegs,dRegs,spReg,node,replayData.orgDic);

                
NSArray
 *arglist = fetchAllArgInReplay(xRegs, dRegs, spReg, node);

                
ReplayInvocation
 *funobj = [[
ReplayInvocation
 alloc] initWithFunPtr:node.callBack ? node.callBack : [node getOrgFun]

                                                                               args:arglist

                                                                            argType:[node getFunTypeStr]

                                                                            retType:rf_return_type_v];

                
if
([[
EngineManager
 sharedInstance] setRepalyEventReady:replayData funObj:funobj])

                {

                    
//放到播放佇列裡播放,返回沒呼叫地址,讓其不往下走

                    retIns->return_address = NULL;

                    
return
 ;

                }

            }

            
else

            {

            
//塞資料

                sstuffArg(xRegs,dRegs,spReg,node,replayData.orgDic);

            }

        }

        retIns->nodeAddr = (__bridge 
void
 *)node;

        retIns->recordOrReplayData = (__bridge 
void
 *)replayData;

        retIns->return_address = node.callBack ? node.callBack : [node getOrgFun];

        replayData.runStatus = relay_event_run_status_runFinish;

    }

    
else

    {

        retIns->nodeAddr = NULL;

        retIns->recordOrReplayData = NULL;

        retIns->return_address = [node getOrgFun];

    }

}

3.怎樣統一hook block
如果你只是想大概理解block的底層技術,你只需google一下即可。
如果你想全面深入的理解block底層技術,那網上的那些資料遠遠滿足不了你的需求。
只能閱讀蘋果編譯器clang原始碼和列出比較有代表性的block例子原始碼,然後轉成c語言和彙編,通過c語言結合彙編研究底層細節。

何謂oc block
block就是閉包,跟回撥函式callback很類似,閉包也是物件

blcok的特點: 1.可有引數列表 2.可有返回值 3.有方法體 4.capture上下文變數 5.有物件引用計數的記憶體管理策略(block生命週期)

block的一般儲存在記憶體中形態有三種 _NSConcretStackBlock(棧)_NSConcretGlobalBlock(全域性)_NSConcretMallocBlock(堆)

系統底層怎樣表達block
我們先來看一下block的例子:

void
 test()

{

    __block 
int
 var1 = 
8
; 
//上下文變數


    
NSString
 *var2 = @
"我是第二個變數”; //上下文變數


    void (^block)(int) = ^(int arg)//引數列表

    {

        var1 = 6;

        NSLog(@"
arg = %d,var1 = %d, var2 = %@
", arg, var1, var2);

    };


    block(1);//呼叫block語法


    dispatch_async(dispatch_get_global_queue(0, 0), ^

    {

        block(2); //非同步呼叫block

    });

}

這段程式碼首先定義兩個變數,接著定義一個block,最後呼叫block。

兩個變數:這兩個變數都是被block引用,第一個變數有關鍵字_block,表示可以在block裡對該變數賦值,第二個變數沒有_block關鍵字,在block裡只能讀,不能寫。

兩個呼叫block的語句:第一個直接在當前方法test()裡呼叫,此時的block記憶體資料在棧上,第二個是非同步呼叫,就是說當執行block(2)時test()可能已經執行完了,test()呼叫棧可能已經被銷燬。那這種情況block的資料肯定不能在棧上,只能在堆上或者在全域性區。

系統底層表達block比較重要的幾種資料結構如下:

注意:雖然底層是用這些結構體來表達block,但是它們並不是原始碼,是二進位制程式碼

enum

{

    BLOCK_REFCOUNT_MASK =     (
0xffff
),

    BLOCK_NEEDS_FREE =        (
1
 << 
24
),

    BLOCK_HAS_COPY_DISPOSE =  (
1
 << 
25
),

    BLOCK_HAS_CTOR =          (
1
 << 
26
),
//todo == BLOCK_HAS_CXX_OBJ?

    BLOCK_IS_GC =             (
1
 << 
27
),

    BLOCK_IS_GLOBAL =         (
1
 << 
28
),

    BLOCK_HAS_DESCRIPTOR =    (
1
 << 
29
),
//todo == BLOCK_USE_STRET?


    BLOCK_HAS_SIGNATURE  =    (
1
 << 
30
),

    OBLOCK_HAS_EXTENDED_LAYOUT = (
1
 << 
31
)

};


enum

{

    BLOCK_FIELD_IS_OBJECT   =  
3
,

    BLOCK_FIELD_IS_BLOCK    =  
7
,

    BLOCK_FIELD_IS_BYREF    =  
8
,

    OBLOCK_FIELD_IS_WEAK     = 
16
,

    OBLOCK_BYREF_CALLER      = 
128

};


typedef
 
struct
 block_descriptor_head

{

    
unsigned
 
long
 
int
 reserved;

    
unsigned
 
long
 
int
 size; 
//表示主體block結構體的記憶體大小


}block_descriptor_head;



typedef
 
struct
 block_descriptor_has_help

{

    
unsigned
 
long
 
int
 reserved;

    
unsigned
 
long
 
int
 size; 
//表示主體block結構體的記憶體大小

    
void
 (*copy)(
void
 *dst, 
void
 *src);
//當block被retain時會執行此函式指標

    
void
 (*dispose)(
void
 *);
//block被銷燬時呼叫

    
struct
 block_arg_var_descriptor *argVar;


}block_descriptor_has_help;


typedef
 
struct
 block_descriptor_has_sig

{

    
unsigned
 
long
 
int
 reserved;

    
unsigned
 
long
 
int
 size;

    
const
 
char
 *signature;
//block的簽名信息

    
struct
 block_arg_var_descriptor *argVar;


}block_descriptor_has_sig;



typedef
 
struct
 block_descriptor_has_all

{

    
unsigned
 
long
 
int
 reserved;

    
unsigned
 
long
 
int
 size;

    
void
 (*copy)(
void
 *dst, 
void
 *src);

    
void
 (*dispose)(
void
 *);

    
const
 
char
 *signature;

    
struct
 block_arg_var_descriptor *argVar;


}block_descriptor_has_all;



typedef
 
struct
 block_info_1

{


    
void
 *isa;
//表示當前blcok是在堆上還是在棧上,或在全域性區_NSConcreteGlobalBlock

    
int
 flags; 
//對應上面的enum值,這些列舉值是我從編譯器原始碼拷貝過來的

    
int
 reserved;

    
void
 (*invoke)(
void
 *, ...);
//block對應的方法體(執行體,就是程式碼段)

    
void
 *descriptor;
//此處指向上面幾個結構體中的一個,具體哪一個根據flags值來定,它用來進一步來描述block資訊


    
//從這個欄位開始起,後面的欄位表示的都是此block對外引用的變數。

    
NSString
 *var2;

    byref_var1_1 var1; 



} block_info_1;

這個例子中的block在底層表達大概如下圖:

首先用block_info_1來表達block本身,然後用block_desc_1來具體描述block相關資訊(比如block_info_1結構體大小,在堆上還是在棧上?copy或dispose時呼叫哪個方法等等),然而block_desc_1具體是哪個結構體是由block_info_1中flags欄位來決定的,block_info_1裡的invoke欄位是指向block方法體,即是程式碼段。block的呼叫就是執行這個函式指標。由於var1是可寫的,所以需要設計一個結構體(byref_var1_1)來表達var1,為什麼var2直接用他原有的型別表達,而var1要用結構體來表達。篇幅有限,這個自己想想吧?

block小結
為了表達block,底層設計三種結構體:block_info_1,block_desc_1,byref_var1_1,三種函式指標: block invoke方法體,copy方法,dispose方法

其實表達block是非常複雜的,還涉及到block的生命週期,記憶體管理問題等等,我在這裡只是簡單的貫穿主流程來介紹的,很多細節都沒介紹。

怎樣統一hook block
通過上面的分析,得知oc裡的block就是一個結構體指標,所以我在原始碼裡可以直接把它轉成結構體指標來處理。
統一hook block原始碼如下

VoidfunBlock
 createNewBlock(
VoidfunBlock
 orgblock, 
ReplayEventIns
 *blockEvent,
bool
 isRecord)

{

    
if
(orgblock && blockEvent)

    {

        
VoidfunBlock
 newBlock = ^(
void
)

        {

            orgblock();

            
if
(
nil
 == blockEvent)

            {

                
assert
(
0
);

            }

        };

        trace_block_layout *blockLayout = (__bridge trace_block_layout *)newBlock;

        blockLayout->invoke = (
void
 (*)(
void
 *, ...))(isRecord?hook_var_block_callBack_record:hook_var_block_callBack_replay);

        
return
 newBlock;

    }

    
return
 
nil
;

}

我們首先新建一個新的block newBlock,然後把原來的block orgblock 和 事件指令blockEvent包到新的blcok中,這樣達到引用的效果。然後把新的block轉成結構體指標,並把結構體指標中的欄位invoke(方法體)指向統一回調方法。你可能詫異新的block是沒有引數型別的,原來block是有引數型別,外面呼叫原來block傳遞引數時會不會引起crash?答案是否定的,因為這裡構造新的block時 我們只用block資料結構,block的回撥方法欄位已經被閹割,回撥方法已經指向統一方法了,這個統一方法可以接受任何型別的引數,包括沒有引數型別。這個統一方法也是彙編實現,程式碼實現跟上面的彙編層程式碼類似,這裡就不附上原始碼了。

那怎樣在新的blcok裡讀取原來的block和事件指令物件呢?
程式碼如下:

void
 var_block_callback_start_record(trace_block_layout * blockLayout)

{

    
VoidfunBlock
 orgBlock = (__bridge 
VoidfunBlock
)(*((
void
 **)((
char
 *)blockLayout + 
sizeof
(trace_block_layout))));

    
ReplayEventIns
 *node = (__bridge 
ReplayEventIns
 *)(*((
void
 **)((
char
 *)blockLayout + 
40
)));


}

原文連結

https://mp.weixin.qq.com/s/xDhR9iZwM8PQehkTIlsOhw

服務推薦