iOS工具集DoraemonKit技术实现(一)

作者 : 开心源码 本文共10425个字,预计阅读时间需要27分钟 发布时间: 2022-05-12 共224人阅读

一、前言

一个比较成熟的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";    if ([platform isEqualToString:@"iPhone1,2"]) return @"iPhone 3G";    if ([platform isEqualToString:@"iPhone2,1"]) return @"iPhone 3GS";    if ([platform isEqualToString:@"iPhone3,1"]) return @"iPhone 4";    if ([platform isEqualToString:@"iPhone3,2"]) return @"iPhone 4";    if ([platform isEqualToString:@"iPhone4,1"]) return @"iPhone 4S";    if ([platform isEqualToString:@"iPhone5,1"]) return @"iPhone 5";    if ([platform isEqualToString:@"iPhone5,2"]) return @"iPhone 5";    if ([platform isEqualToString:@"iPhone5,3"]) return @"iPhone 5C";    if ([platform isEqualToString:@"iPhone5,4"]) return @"iPhone 5C";    if ([platform isEqualToString:@"iPhone6,1"]) return @"iPhone 5S";    if ([platform isEqualToString:@"iPhone6,2"]) return @"iPhone 5S";    if ([platform isEqualToString:@"iPhone7,1"]) return @"iPhone 6 Plus";    if ([platform isEqualToString:@"iPhone7,2"]) return @"iPhone 6";    if ([platform isEqualToString:@"iPhone8,1"]) return @"iPhone 6S";    if ([platform isEqualToString:@"iPhone8,2"]) return @"iPhone 6S Plus";    if ([platform isEqualToString:@"iPhone8,4"]) return @"iPhone SE";    if ([platform isEqualToString:@"iPhone9,1"]) return @"iPhone 7";    if ([platform isEqualToString:@"iPhone9,3"]) return @"iPhone 7";    if ([platform isEqualToString:@"iPhone9,2"]) return @"iPhone 7 Plus";    if ([platform isEqualToString:@"iPhone9,4"]) return @"iPhone 7 Plus";    if ([platform isEqualToString:@"iPhone10,1"]) return @"iPhone 8";    if ([platform isEqualToString:@"iPhone10.4"]) return @"iPhone 8";    if ([platform isEqualToString:@"iPhone10,2"]) return @"iPhone 8 Plus";    if ([platform isEqualToString:@"iPhone10,5"]) return @"iPhone 8 Plus";    if ([platform isEqualToString:@"iPhone10,3"]) return @"iPhone X";    if ([platform isEqualToString:@"iPhone10,6"]) return @"iPhone X";        return platform;}

获取手机系统版本

//获取手机系统版本NSString *phoneVersion = [[UIDevice currentDevice] systemVersion];

获取App BundleId

一个app分为测试版本、企业版本、appStore发售版本,每一个app长得都一样,如何对他们进行区分呢,那就要用到BundleId这个属性了。

//获取bundle idNSString *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项目地址: didi/DoraemonKit

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » iOS工具集DoraemonKit技术实现(一)

发表回复