iOS研發助手DoraemonKit技術實現(一)

易翔本尊發表於2018-11-12

一、前言

一個比較成熟的App,經歷了多個版本的迭代之後,為了方便調式和測試,往往會積累一些工具來應付這些場景。最近我們組就開源了一款適用於iOS App線下開發、測試、驗收階段,內建在App中的工具集合。使用DoraemonKit,你無需連線電腦,就可以對於App的資訊進行快速的檢視。一鍵接入、使用方便,提高開發、測試、視覺同學的工作效率,提高我們App上線的完整度和穩定性。

目前DoraemonKit擁有的功能大概分為以下幾點:

  1. 常用工具 : App資訊展示,沙盒瀏覽、MockGPS、H5任意門、子執行緒UI檢查、日誌顯示。
  2. 效能工具 : 幀率監控、CPU監控、記憶體監控、流量監控、自定義監控。
  3. 視覺工具 : 顏色吸管、元件檢查、對齊標尺。
  4. 業務專區 : 支援業務測試元件接入到DoraemonKit皮膚中。

拿我們App接入效果如下:

dorameonKit.png
上面兩行是業務線自定義的工具,接入方可以自定義。除此之外都是內建工具集合。

因為裡面功能比較多,大概會分三篇文章介紹DoraemonKit的使用和技術實現,這是第一篇主要介紹常用工具集中的幾款工具實現。

二、技術實現

2.1:App資訊展示

App基礎資訊展示.png

我們要看一些手機資訊或者App的一些基本資訊的時候,需要到系統設定去找,比較麻煩。特別是許可權資訊,在我們app裝的比較多的時候,我們很難快速找到我們app的許可權資訊。而這些資訊從程式碼角度都是比較容易獲取的。我們把我們感興趣的資訊列表出來直接檢視,避免了去手機設定裡檢視或者檢視原始碼的麻煩。

獲取手機型號

我們從手機設定裡面是找不到我們的手機具體是哪一款的文字表述的,比如我的手機是iphone8 Pro,在手機型號裡面顯示的是MQ8E2CH/A。對於iPhone不熟悉的人很難從外表對iphone進行區分。而手機型號,我們從程式碼角度就很好獲取。

+ (NSString *)iphoneType{
    struct utsname systemInfo;
    uname(&systemInfo);
    NSString *platform = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
    
    //iPhone
    if ([platform isEqualToString:@"iPhone1,1"]) return @"iPhone 1G";
     ...
     //其他對應關係請看下面對應表    
    return platform;
}

複製程式碼

iPhone裝置型別與通用手機型別一一對應關係表

裝置型別 通用型別
iPhone1,1 iPhone 1G
iPhone1,2 iPhone 3G
iPhone2,1 iPhone 3GS
iPhone3,1 iPhone 4
iPhone3,2 iPhone 4
iPhone4,1 iPhone 4S
iPhone5,1 iPhone 5
iPhone5,2 iPhone 5
iPhone5,3 iPhone 5C
iPhone5,4 iPhone 5C
iPhone6,1 iPhone 5S
iPhone6,2 iPhone 5S
iPhone7,1 iPhone 6 Plus
iPhone7,2 iPhone 6
iPhone8,1 iPhone 6S
iPhone8,2 iPhone 6S Plus
iPhone8,4 iPhone SE
iPhone9,1 iPhone 7
iPhone9,3 iPhone 7
iPhone9,2 iPhone 7 Plus
iPhone9,4 iPhone 7 Plus
iPhone10,1 iPhone 8
iPhone10.4 iPhone 8
iPhone10,2 iPhone 8 Plus
iPhone10,5 iPhone 8 Plus
iPhone10,3 iPhone X
iPhone10,6 iPhone X
iPhone11,8 iPhone XR
iPhone11,2 iPhone XS
iPhone11,4 iPhone XS Max
Phone11,6 iPhone XS Max

獲取手機系統版本

//獲取手機系統版本
NSString *phoneVersion = [[UIDevice currentDevice] systemVersion];
複製程式碼

獲取App BundleId

一個app分為測試版本、企業版本、appStore發售版本,每一個app長得都一樣,如何對他們進行區分呢,那就要用到BundleId這個屬性了。

//獲取bundle id
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];

複製程式碼

獲取App 版本號

//獲取App版本號
NSString *bundleVersionCode = [[[NSBundle mainBundle]infoDictionary] objectForKey:@"CFBundleVersion"];

複製程式碼

許可權資訊檢視

當我們發現App執行不正常,比如無法定位,網路一直失敗,無法收到推送資訊等問題的時候,我們第一個反應就是去手機設定裡面去看我們app相關的許可權有沒有開啟。DoraemonKit整合了對於地理位置許可權、網路許可權、推送許可權、相機許可權、麥克風許可權、相簿許可權、通訊錄許可權、日曆許可權、提醒事項許可權的查詢。

由於程式碼比較多,這裡就不一一貼出來了。大家可以去DorameonKit/Core/Plugin/AppInfo中自己去檢視。這裡講一下,許可權查詢結果幾個值的意義。

  • NotDetermined => 使用者還沒有選擇。
  • Restricted => 該許可權受限,比如家長控制。
  • Denied => 使用者拒絕使用該許可權。
  • Authorized => 使用者同意使用該許可權。

2.2:沙盒瀏覽

沙盒瀏覽器.png

以前如果我們要去檢視App快取、日誌資訊,都需要訪問沙盒。由於iOS的封閉性,我們無法直接檢視沙盒中的檔案內容。如果我們要去訪問沙盒,基本上有兩種方式,第一種使用Xcode自帶的工具,從Windows-->Devices進入裝置管理介面,通過Download Container的方式匯出整個app的沙盒。第二種方式,就是自己寫程式碼,訪問沙盒中指定檔案,然後使用NSLog的方式列印出來。這兩種方式都比較麻煩。

DoraemonKit給出的解決方案:就是自己做一個簡單的檔案瀏覽器,通過NSFileManager物件對沙盒檔案進行遍歷,同時支援對於檔案和資料夾的刪除操作。對於檔案支援本地預覽或者通過airdrop的方式或者其他分享方式傳送到PC端進行更加細緻的操作。

怎麼用NSFileManager物件遍歷檔案和刪除檔案這裡就不說了,大家可以參考DorameonKit/Core/Plugin/Sanbox中的程式碼。這裡講一下:如何將手機中的檔案快速上傳到Mac端?剛開始我們還繞了一點路,我們在手機端搭了一個微服務,mac通過瀏覽器去訪問它。後來和同事聊天的時候知道了UIActivityViewController這個類,可以十分便捷地吊起系統分享元件或者是其他註冊到系統分享元件中的分享方式,比如微信、釘釘。實現程式碼非常簡單,如下所示:

- (void)shareFileWithPath:(NSString *)filePath{
    
    NSURL *url = [NSURL fileURLWithPath:filePath];
    NSArray *objectsToShare = @[url];

    UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:objectsToShare applicationActivities:nil];
    NSArray *excludedActivities = @[UIActivityTypePostToTwitter, UIActivityTypePostToFacebook,
                                    UIActivityTypePostToWeibo,
                                    UIActivityTypeMessage, UIActivityTypeMail,
                                    UIActivityTypePrint, UIActivityTypeCopyToPasteboard,
                                    UIActivityTypeAssignToContact, UIActivityTypeSaveToCameraRoll,
                                    UIActivityTypeAddToReadingList, UIActivityTypePostToFlickr,
                                    UIActivityTypePostToVimeo, UIActivityTypePostToTencentWeibo];
    controller.excludedActivityTypes = excludedActivities;

    [self presentViewController:controller animated:YES completion:nil];
}
複製程式碼

2.3:MockGPS

mockGPS.png

我們有些業務會根據地理位置不同,而有不同的業務處理邏輯。而我們開發或者測試,當然不可能去每一個地址都測試一遍。這種情況下,測試同學一般會找到我們讓我們手動改掉系統獲取經緯度的回撥,或者修改GPX檔案,然後再重新打一個包。這樣也非常麻煩。

DoraemonKit給出的解決方案:提供一套地圖介面,支援在地圖中滑動選擇或者手動輸入經緯度,然後自動替換掉我們App中返回的當前經緯度資訊。這裡的難點是如何不需要重新打包自動替換掉系統返回的當前經緯度資訊?

CLLocationManager的delegate中有一個方法如下:

/*
 *  locationManager:didUpdateLocations:
 *
 *  Discussion:
 *    Invoked when new locations are available.  Required for delivery of
 *    deferred locations.  If implemented, updates will
 *    not be delivered to locationManager:didUpdateToLocation:fromLocation:
 *
 *    locations is an array of CLLocation objects in chronological order.
 */
- (void)locationManager:(CLLocationManager *)manager
	 didUpdateLocations:(NSArray<CLLocation *> *)locations API_AVAILABLE(ios(6.0), macos(10.9));

複製程式碼

我們通常是在這個函式中獲取當前系統的經緯度資訊。我們如果想要沒有侵入式的修改這個函式的預設實現方式,想到的第一個方法就是Method Swizzling。但是真正在實現過程中,你會發現Method Swizzling需要當前例項和方法,方法是- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations 我們有了,但是例項,每一個app都有自己的實現,無法做到統一處理。我們就換了一個思路,如何能獲取該實現了該定位方法的例項呢?就是使用Method Swizzling Hook住CLLocationManager的setDelegate方法,就能獲取具體是哪一個例項實現了- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations 方法。

具體方法如下:

第一步: 生成一個CLLocationManager的分類CLLocationManager(Doraemon),在這個分類中,實現- (void)doraemon_swizzleLocationDelegate:(id)delegate這個方法,用來進行方法交換。

- (void)doraemon_swizzleLocationDelegate:(id)delegate {
    if (delegate) {
        //1、讓所有的CLLocationManager的代理都設定為[DoraemonGPSMocker shareInstance],讓他做中間轉發
        [self doraemon_swizzleLocationDelegate:[DoraemonGPSMocker shareInstance]];
        //2、繫結所有CLLocationManager例項與delegate的關係,用於[DoraemonGPSMocker shareInstance]做目標轉發用。
        [[DoraemonGPSMocker shareInstance] addLocationBinder:self delegate:delegate];
        
        //3、處理[DoraemonGPSMocker shareInstance]沒有實現的selector,並且給使用者提示。
        Protocol *proto = objc_getProtocol("CLLocationManagerDelegate");
        unsigned int count;
        struct objc_method_description *methods = protocol_copyMethodDescriptionList(proto, NO, YES, &count);
        NSMutableArray *array = [NSMutableArray array];
        for(unsigned i = 0; i < count; i++)
        {
            SEL sel = methods[i].name;
            if ([delegate respondsToSelector:sel]) {
                if (![[DoraemonGPSMocker shareInstance] respondsToSelector:sel]) {
                    NSAssert(NO, @"你在Delegate %@ 中所使用的SEL %@,暫不支援,請聯絡DoraemonKit開發者",delegate,sel);
                }
            }
        }
        free(methods);
        
    }else{
        [self doraemon_swizzleLocationDelegate:delegate];
    }
}
複製程式碼

在這個函式中主要做了三件事情,1、將所有的定位回撥統一交給[DoraemonGPSMocker shareInstance]處理 2、[DoraemonGPSMocker shareInstance]繫結了所有CLLocationManager與它的delegate的一一對應關係。3、處理[DoraemonGPSMocker shareInstance]沒有實現的selector,並且給使用者提示。

第二步:當有一個定位回撥過來的時候,我們先傳給[DoraemonGPSMocker shareInstance],然後[DoraemonGPSMocker shareInstance]再轉發給它繫結過的所有的delegate。那我們App為例,繫結關係如下:

{
    "0x2800a07a0_binder" = "<CLLocationManager: 0x2800a07a0>";
    "0x2800a07a0_delegate" = "<MAMapLocationManager: 0x2800a04d0>";
    "0x2800b59a0_binder" = "<CLLocationManager: 0x2800b59a0>";
    "0x2800b59a0_delegate" = "<KDDriverLocationManager: 0x2829d3bf0>";
}
複製程式碼

由此可見,我們App的統一定位KDDriverLocationManager和蘋果地圖的定位MAMapLocationManager都是使用都是CLLocationManager提供的。

具體 DoraemonGPSMocker這個類如何實現,請參考DorameonKit/Core/Plugin/GPS中的程式碼。

2.4:H5任意門

H5任意門.png

有的時候Native和H5開發同時開發一個功能,H5依賴native提供入口,而這個時候Native還沒有開發好,這個時候H5開發就沒法在App上看到效果。再比如,有些H5頁面處於的位置比較深入,就像我們代駕司機端,做單流程比較多,有的H5介面需要很繁瑣的操作才能展示到App上,不方便我們檢視和定位問題。 這個時候我們可以為app做一個簡單的瀏覽器,輸入url,使用自帶的容器進行跳轉。因為每一個app的H5容器基本上都是自定義過得,都會有自己的bridge定製化,所以這個H5容器沒有辦法使用系統原生的UIWebView或者WKWebView,就只能交給業務方自己去完成。我們在DorameonKit初始化的時候,提供了一個回撥讓業務方用自己的H5容器去開啟這個Url:

[[DoraemonManager shareInstance] addH5DoorBlock:^(NSString *h5Url) {
              //使用自己的H5容器開啟這個連結
 }];

複製程式碼

這個工具實現比較簡單,就不多說了,程式碼路徑在DorameonKit/Core/Plugin/H5.

2.5:子執行緒UI檢查

子執行緒UI_1.png
子執行緒UI_2.png
子執行緒UI_3.png

在iOS中是不允許在子執行緒中對UI進行操作和渲染的,不然會造成未知的錯誤和問題,甚至會導致crash。我們在最近幾個版本中發現新增了一些crash,調查原因就是在子執行緒中操作UI導致的。為了對於這種情況可以提早被我們發現,我在在DorameonKit中增加了子執行緒UI渲染檢查查詢。

具體事項思路,我們hook住UIView的三個必須在主執行緒中操作的繪製方法。1、setNeedsLayout 2、setNeedsDisplay 3、setNeedsDisplayInRect:。然後判斷他們是不是在子執行緒中進行操作,如果是在子執行緒進行操作的話,列印出當前程式碼呼叫堆疊,提供給開發進行解決。具體程式碼如下:

@implementation UIView (Doraemon)

+ (void)load{
    [[self  class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsLayout) swizzledSel:@selector(doraemon_setNeedsLayout)];
    [[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsDisplay) swizzledSel:@selector(doraemon_setNeedsDisplay)];
    [[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsDisplayInRect:) swizzledSel:@selector(doraemon_setNeedsDisplayInRect:)];
}

- (void)doraemon_setNeedsLayout{
    [self doraemon_setNeedsLayout];
    [self uiCheck];
}

- (void)doraemon_setNeedsDisplay{
    [self doraemon_setNeedsDisplay];
    [self uiCheck];
}

- (void)doraemon_setNeedsDisplayInRect:(CGRect)rect{
    [self doraemon_setNeedsDisplayInRect:rect];
    [self uiCheck];
}

- (void)uiCheck{
    if([[DoraemonCacheManager sharedInstance] subThreadUICheckSwitch]){
        if(![NSThread isMainThread]){
            NSString *report = [BSBacktraceLogger bs_backtraceOfCurrentThread];
            NSDictionary *dic = @{
                                  @"title":[DoraemonUtil dateFormatNow],
                                  @"content":report
                                  };
            [[DoraemonSubThreadUICheckManager sharedInstance].checkArray addObject:dic];
        }
    }
}

@end
複製程式碼

完整程式碼實現請參考DorameonKit/Core/Plugin/SubThreadUICheck

2.6:日誌顯示

日誌顯示.png

這個主要是方便我們檢視本地日誌,以前我們如果要檢視日誌,需要自己寫程式碼,訪問沙盒匯出日誌檔案,然後再檢視。也是比較麻煩的。

DoraemonKit的解決方案是:我們每一次觸發日誌的時候,都把日誌內容顯示到介面上,方便我們檢視。 如何實現的呢?因為我們這個工具並不是一個通用性的工具,只針對於底層日誌庫是CocoaLumberjack的情況。稍微講一下的CocoaLumberjack原理,所有的log都會發給DDLog物件,其執行在自己的一個GCD佇列中,之後,DDLog會將log分發給其下注冊的一個或者多個Logger中,這一步在多核下面是併發的,效率很高。每一個Logger處理收到的log也是在它們自己的GCD佇列下做的,它們詢問其下的Formatter,獲取Log訊息格式,然後根據Logger的邏輯,將log訊息分發到不同的地方。系統自帶三個Logger處理器,DDTTYLogger,主要將日誌傳送到Xcode控制檯;DDASLLogger,主要講日誌傳送到蘋果的日誌系統Console.app; DDFileLogger,主要將日誌傳送到檔案中儲存起來,也是我們開發用到最多的。但是自帶的Logger並不滿足我們的需求,我們的需求是將日誌顯示到UI介面中,所以我們需要新建一個類DoraemonLogger,繼承於DDAbstractLogger,然後重寫logMessage方法,將每一條傳過來的日誌列印到UI介面中。

log實現.png

這個工具參考LumberjackConsole這個開源專案完成,因為剛出iOS11的時候,作者沒有適配,所以我們自己拷貝一份程式碼出來,自己維護了。 完整程式碼實現請參考DorameonKit/WithLogger中.

三、總結

寫這篇文章主要是為了能夠讓大家對於DorameonKit進行快速的瞭解,大家如果有什麼好的想法,或者發現我們的這個專案有bug,歡迎大家去github上提Issues或者直接Pull requests,我們會第一時間處理,也希望我們這個工具集合能在大家的一起努力下,做得更加完善。

如果大家覺得我們這個專案還可以的話,點上一顆star吧。

DoraemonKit專案地址:github.com/didi/Doraem…

四、交流群

https://javer.oss-cn-shanghai.aliyuncs.com/doraemon/github/DoraemonKitQQ.jpeg

相關文章