一个比较成熟的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…