一個比較成熟的App,經歷了多個版本的迭代以後,爲了方便調式和測試,每每會積累一些工具來應付這些場景。最近咱們組就開源了一款適用於iOS App線下開發、測試、驗收階段,內置在App中的工具集合。使用DoraemonKit,你無需鏈接電腦,就能夠對於App的信息進行快速的查看。一鍵接入、使用方便,提升開發、測試、視覺同窗的工做效率,提升咱們App上線的完整度和穩定性。ios
目前DoraemonKit擁有的功能大概分爲如下幾點:git
拿咱們App接入效果以下: github
上面兩行是業務線自定義的工具,接入方能夠自定義。除此以外都是內置工具集合。由於裏面功能比較多,大概會分三篇文章介紹DoraemonKit的使用和技術實現,這是第一篇主要介紹經常使用工具集中的幾款工具實現。macos
咱們要看一些手機信息或者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設備類型與通用手機類型一一對應關係表bash
設備類型 | 通用類型 |
---|---|
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分爲測試版本、企業版本、appStore發售版本,每個app長得都同樣,如何對他們進行區分呢,那就要用到BundleId這個屬性了。微信
//獲取bundle id
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];
複製代碼
//獲取App版本號
NSString *bundleVersionCode = [[[NSBundle mainBundle]infoDictionary] objectForKey:@"CFBundleVersion"];
複製代碼
當咱們發現App運行不正常,好比沒法定位,網絡一直失敗,沒法收到推送信息等問題的時候,咱們第一個反應就是去手機設置裏面去看咱們app相關的權限有沒有打開。DoraemonKit集成了對於地理位置權限、網絡權限、推送權限、相機權限、麥克風權限、相冊權限、通信錄權限、日曆權限、提醒事項權限的查詢。網絡
因爲代碼比較多,這裏就不一一貼出來了。你們能夠去DorameonKit/Core/Plugin/AppInfo中本身去查看。這裏講一下,權限查詢結果幾個值的意義。併發
之前若是咱們要去查看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];
}
複製代碼
咱們有些業務會根據地理位置不一樣,而有不一樣的業務處理邏輯。而咱們開發或者測試,固然不可能去每個地址都測試一遍。這種狀況下,測試同窗通常會找到咱們讓咱們手動改掉系統獲取經緯度的回調,或者修改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) {
//一、讓全部的CLLocationManager的代理都設置爲[DoraemonGPSMocker shareInstance],讓他作中間轉發
[self doraemon_swizzleLocationDelegate:[DoraemonGPSMocker shareInstance]];
//二、綁定全部CLLocationManager實例與delegate的關係,用於[DoraemonGPSMocker shareInstance]作目標轉發用。
[[DoraemonGPSMocker shareInstance] addLocationBinder:self delegate:delegate];
//三、處理[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];
}
}
複製代碼
在這個函數中主要作了三件事情,一、將全部的定位回調統一交給[DoraemonGPSMocker shareInstance]處理 二、[DoraemonGPSMocker shareInstance]綁定了全部CLLocationManager與它的delegate的一一對應關係。三、處理[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中的代碼。
有的時候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.
在iOS中是不容許在子線程中對UI進行操做和渲染的,否則會形成未知的錯誤和問題,甚至會致使crash。咱們在最近幾個版本中發現新增了一些crash,調查緣由就是在子線程中操做UI致使的。爲了對於這種狀況能夠提前被咱們發現,我在在DorameonKit中增長了子線程UI渲染檢查查詢。
具體事項思路,咱們hook住UIView的三個必須在主線程中操做的繪製方法。一、setNeedsLayout 二、setNeedsDisplay 三、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
這個主要是方便咱們查看本地日誌,之前咱們若是要查看日誌,須要本身寫代碼,訪問沙盒導出日誌文件,而後再查看。也是比較麻煩的。
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界面中。
這個工具參考LumberjackConsole這個開源項目完成,由於剛出iOS11的時候,做者沒有適配,因此咱們本身拷貝一份代碼出來,本身維護了。 完整代碼實現請參考DorameonKit/WithLogger中.
寫這篇文章主要是爲了可以讓你們對於DorameonKit進行快速的瞭解,你們若是有什麼好的想法,或者發現咱們的這個項目有bug,歡迎你們去github上提Issues或者直接Pull requests,咱們會第一時間處理,也但願咱們這個工具集合能在你們的一塊兒努力下,作得更加完善。
若是你們以爲咱們這個項目還能夠的話,點上一顆star吧。
DoraemonKit項目地址:github.com/didi/Doraem…