iOS 开源库源码分析之FLEX
281 |本文基于FLEX 2.4.0
282 |FLEX 是杂志应用公司 Flipboard 出品的一个 App 内调试的工具。
283 |本文主要讨论 FLEX 怎么做到的网络监听,FileBrowser 的设计思路,FLEX 怎么获取到堆上对象并且展示出来,绘制出整个 App 的视图层级,FLEX 关于 runtime 的运用。
284 |Network
285 |Network 部分主要是监听网络请求相关的数据,所以这里要监听请求相关的回调,有两种办法
286 |-
287 |
- 一是使用 NSURLProtocol 代理请求回调 288 |
- 二是如 FLEX 所使用的 hook NSURLConnection 和 NSURLSession 的代理方法。目前来讲普遍都是 hook 某一个类的方法,想要 hook 一个 protocol 的方法,这里是通过寻找工程里面所有实现了 protocol 的类,然后再 hook 这些类的方法,可谓另辟蹊径。 289 |
一个 App 内请求的数据量还是比较大,所以只考虑把请求的数据保存在内存中,这里是一个单例类 FLEXNetworkRecorder 来进行数据的相关处理,每一个数据对应一个 model 类 FLEXNetworkTransaction。为了达到数据与界面分离的效果,这里当数据更新时采用的是 NSNotification 来通知界面。目前请求处理任务是放在子线程,由于通知是同步的并且接收方是用来更新界面的,所以这里会把发送通知事件切换到主线程。
291 |FLEX 提供了抓取到请求之后,提供 copy url 的功能,实现在 FLEXNetworkCurlLogger 中,主要是把 NSURLRequest 拼接处 curl 的字符串,如下
292 |293 | curl -v -X GET 'http://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=LZ2jWpKCEYzR8gf5yrSQCQ' -H 'Accept-Encoding: gzip, deflate' -H 'Accept: */*' -H 'Accept-Language: en-us' -H 'Cookie: 1P_JAR=2018-03-10-08; NID=125=HP2nIQkOz_aVYIzH3zrKicXoGyAQSquxoDjOVBZoPyO2sD8dxbYcHVOHkrYwLLOI9YVGFu66TVHZT77kHzTJXNxCVrO50KtKNv4nuGlszsGweCLvorszOnGZHPtWnArU;' 294 |295 | 296 |
curl是一个支持各种网络协议的数据传输命令行工具。
297 |-v/--verbose 表示获取更多的连接信息
298 |-X GET 用来选定方法
299 |-H 用来添加请求头
300 |-d/--data 是 POST 请求的 application/x-www-url-encoded 方式的 body 数据。
301 |对于处理回调这里也有一个新思路,一个 UITableViewCell 的点击从 didSelectRowAtIndexPath 中移到了 ViewModel 这一层处理,并且是把这个处理使用 block 属性的方式赋予每个 ViewModel 自己处理。
302 |FLEXNetworkDetailRow *requestURLRow = [[FLEXNetworkDetailRow alloc] init];
303 | requestURLRow.title = @"Request URL";
304 | NSURL *url = transaction.request.URL;
305 | requestURLRow.detailText = url.absoluteString;
306 | requestURLRow.selectionFuture = ^{
307 | UIViewController *urlWebViewController = [[FLEXWebViewController alloc] initWithURL:url];
308 | urlWebViewController.title = url.absoluteString;
309 | return urlWebViewController;
310 | };
311 | [rows addObject:requestURLRow];
312 |
313 | FLEX 计算流量是通过把所有请求 response data 的大小加起来,不过 FLEX 只统计 response,没有统计 request,并且只统计 body 没有统计 header,所以数据是不准确的。
314 |NSCache 基本上就是跟 NSMutableDictionary 类似,唯一不同的是它会自动释放内存,FLEX 使用 NSCache 来保存 response data。
315 |FileBrowser
316 |适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作。
317 |里面有 Target,Adapter,Adaptee。
318 |Target 就是目标对象,这里来说就是定义的协议,FLEXFileBrowserFileOperationController。
319 |Adaptee 就是适配者,实际要访问的对象,这里就是 NSFileManager 的删除或移动。
320 |Adapter 就是适配器,就是FLEXFileBrowserFileDeleteOperationController,FLEXFileBrowserFileRenameOperationController,访问FLEXFileBrowserFileOperationController 协议就能访问 NSFileManager 的操作
321 |查询 FileManager 的 path 或者文件因为这是一个耗时操作,所以这里把操作放入NSOperation,自定义一个 Operation,实现 main 方法,然后加入 NSOperationQueue。
323 |DatabaseBrowser
324 |FLEXSQLiteDatabaseManager 对于 SQLite 的操作是一个简化版的 fmdb,去除了 GCD 队列的管理,主要作用就是查询数据库。
325 |FLEXRealmDatabaseManager 是实现能够读取 Realm 数据库,这里的实现并没有引入 Realm 库,首先通过
326 |#if __has_include(<Realm/Realm.h>)
327 | #else
328 | #endif
329 |
330 | 来判断想要引入的文件是否存在,如果不存在就 FLEXRealmDefines 定义了一堆 Realm 的 class,并且不实现,然后就可以保证不引入一个库而编译通过。
331 |Heap Objects
332 |第一个列表 FLEXLiveObjectsTableViewController 是返回所有注册的类,这个通过 objc_copyClassList 可以获取到。
333 |另外通过下面的方法还可以获取到堆上的所有实例
334 |// Inspired by:
335 | // http://llvm.org/svn/llvm-project/lldb/tags/RELEASE_34/final/examples/darwin/heap_find/heap/heap_find.cpp
336 | // https://gist.github.com/samdmarshall/17f4e66b5e2e579fd396
337 |
338 | vm_address_t *zones = NULL;
339 | unsigned int zoneCount = 0;
340 | kern_return_t result = malloc_get_all_zones(TASK_NULL, reader, &zones, &zoneCount);
341 |
342 | if (result == KERN_SUCCESS) {
343 | for (unsigned int i = 0; i < zoneCount; i++) {
344 | malloc_zone_t *zone = (malloc_zone_t *)zones[i];
345 | if (zone->introspect && zone->introspect->enumerator) {
346 | zone->introspect->enumerator(TASK_NULL, (__bridge void *)block, MALLOC_PTR_IN_USE_RANGE_TYPE, (vm_address_t)zone, reader, &range_callback);
347 | }
348 | }
349 | }
350 |
351 | 所以 FLEX 才可以显示所有注册的类并且显示该类所有的实例。
352 |在打开具体某个实例的时候,这里运用了简单工厂模式。
353 |简单工厂模式的参与者
354 |Factory:工厂角色,接收客户端请求,通过请求创建对象的产品对象
355 |Abstract Product:抽象产品角色,工厂模式创建对象的父类
356 |Concrete Product:具体产品角色,工厂模式创建的对象
357 |其实UIButton通过
358 |+ (instancetype)buttonWithType:(UIButtonType)buttonType;
359 |
360 | 创建就是简单工厂模式
361 |这里综合前面说到适配器模式,适配器模式主要是解决接口不兼容的问题,将一个接口转换为另一个接口,工厂方法是定义一个创建对象的接口,根据不同的参数返回不同的实例。
363 |View Hierarchy
364 |关于获取所有的 UIWindow 实例,使用的是 UIWindow 的私有方法,"allWindowsIncludingInternalWindows:onlyVisibleWindows:",这里有一个避开苹果检查的办法就是,方法通过数组组装出来的
365 |NSArray *allWindowsComponents = @[@"al", @"lWindo", @"wsIncl", @"udingInt", @"ernalWin", @"dows:o", @"nlyVisi", @"bleWin", @"dows:"];
366 | SEL allWindowsSelector = NSSelectorFromString([allWindowsComponents componentsJoinedByString:@""]);
367 |
368 | 一个 UIView 的层级则是巧妙地循环其 superView,来获取其层级,最后画出整个 App 的层级图。
369 |"select"功能的实现,会把 tap 上所有的 UIView 找出来,办法是 for 循环所有的 UIView,找出在其 tap 范围内的 UIView。
370 |FLEXWindow
371 |打开 FLEX 界面是定义了一个 UIWindow,然后把它显示出来,不过由于 FLEX 有一个浮窗工具栏,这个时候 keyWindow 还是系统的,只有当点击工具栏进入 FLEX 全屏界面才会设置 keyWindow 而接收键盘事件。
372 |首先重写了触摸响应的方案,以工具栏为例,当点击 FLEX 工具栏时就响应,点击其他位置 FLEX 就会不响应,而传递给下层的 UIWindow。
373 |- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
374 |
375 | preview image
376 |FLEX 有一个预览 UIView 实例画面的功能,实现机制就是获取到 UIView 实例之后,把它转化为 UIImage 来显示。
377 |+ (UIViewController *)imagePreviewViewControllerForView:(UIView *)view
378 | {
379 | UIViewController *imagePreviewViewController = nil;
380 | if (!CGRectIsEmpty(view.bounds)) {
381 | CGSize viewSize = view.bounds.size;
382 | UIGraphicsBeginImageContextWithOptions(viewSize, NO, 0.0);
383 | [view drawViewHierarchyInRect:CGRectMake(0, 0, viewSize.width, viewSize.height) afterScreenUpdates:YES];
384 | UIImage *previewImage = UIGraphicsGetImageFromCurrentImageContext();
385 | UIGraphicsEndImageContext();
386 | imagePreviewViewController = [[FLEXImagePreviewViewController alloc] initWithImage:previewImage];
387 | }
388 | return imagePreviewViewController;
389 | }
390 |
391 | FLEXRuntimeUtility
392 |首先可以看Classes and metaclasses中的一张图
393 |这张图表示的是对象的内存关系,图中实线是 super_class 指针,虚线是 isa 指针,类对象存放实例方法,元类对象存放类方法。
394 |-
395 |
- 实例对象的 isa 指向类对象(类也是对象,面向对象中一切都是对象) 396 |
- 类对象的 isa 指向元类对象 397 |
- Root class (class) 其实就是 NSObject,NSObject 是没有超类的,所以 Root class(class) 的 superclass 指向 nil。 398 |
- Root class(meta) 的 superclass 指向 Root class(class),也就是 NSObject,形成一个回路。 399 |
- 每个 Meta class 的 isa 指针都指向 Root class (meta)。 400 |
这里讲解一下 FLEXRuntimeUtility 的相关应用
402 |属性
403 |获取一个类的属性
404 |objc_property_t *propertyList = class_copyPropertyList(class, &propertyCount);
405 |
406 | 假如
407 |@property (readonly, copy) NSString *debugDescription;
408 |
409 | 获取属性的名字
410 |NSString *name = @(property_getName(property));
411 |
412 | 根据属性可以获取这个属性的相关特性
413 |NSString *attributes = @(property_getAttributes(property));
414 |
415 | 值为
416 |417 | T@"NSString",R,C 418 |419 | 420 |
这个字符串的意义可以查看参考 421 | 比如T就是属性的类型,R就是read-only,C就是copy
422 |另外还可以动态添加属性
423 |class_addProperty(theClass, name, attributes, totalAttributesCount);
424 |
425 | 实例变量
426 |获取实例变量
427 |Ivar *ivarList = class_copyIvarList(class, &ivarCount);
428 |
429 | 获取实例变量的类型
430 |const char *type = ivar_getTypeEncoding(ivar);
431 |
432 | 获取变量的值
433 |value = object_getIvar(object, ivar);
434 |
435 | 方法
436 |获取实例方法
437 |Class class = [self.object class];
438 | Method *methodList = class_copyMethodList(class, &methodCount);
439 |
440 | 获取类方法
441 |const char *className = [NSStringFromClass([self.object class]) UTF8String];
442 |
443 | Class metaClass = objc_getMetaClass(className);
444 | Method *methodList = class_copyMethodList(metaClass, &methodCount);
445 |
446 | 获取 selector 名字
447 |NSString *selectorName = NSStringFromSelector(method_getName(method));
448 |
449 | 获取返回类型
450 |char *returnType = method_copyReturnType(method);
451 |
452 | 获取参数类型
453 |char *argType = method_copyArgumentType(method, argIndex);
454 |
455 | 这里的 Type 是 type encoding 之后的结果,比如如果是 BOOL 类型,这里 argType 将是 "B",所以这里会将 @encode(BOOL) 之后的结果与 B 对比,相同则表示为 BOOL 类型。
456 |最后,文章到这就结束了,FLEX 是 iOS 中一个比较特殊的库,很少有人去做 App 内调试相关的东西,通过这里就可以把 App 的调试框架搭起来了,而且 FLEX 关于 runtime 的应用可谓到了一个极致,学习 runtime 也可以多看这个库。
457 |本文作者coderyi
458 | 459 | 460 |