iOS 开源库源码分析之FLEX

本文基于FLEX 2.4.0

FLEX 是杂志应用公司 Flipboard 出品的一个 App 内调试的工具。

本文主要讨论 FLEX 怎么做到的网络监听,FileBrowser 的设计思路,FLEX 怎么获取到堆上对象并且展示出来,绘制出整个 App 的视图层级,FLEX 关于 runtime 的运用。

Network

Network 部分主要是监听网络请求相关的数据,所以这里要监听请求相关的回调,有两种办法

  • 一是使用 NSURLProtocol 代理请求回调
  • 二是如 FLEX 所使用的 hook NSURLConnection 和 NSURLSession 的代理方法。目前来讲普遍都是 hook 某一个类的方法,想要 hook 一个 protocol 的方法,这里是通过寻找工程里面所有实现了 protocol 的类,然后再 hook 这些类的方法,可谓另辟蹊径。

一个 App 内请求的数据量还是比较大,所以只考虑把请求的数据保存在内存中,这里是一个单例类 FLEXNetworkRecorder 来进行数据的相关处理,每一个数据对应一个 model 类 FLEXNetworkTransaction。为了达到数据与界面分离的效果,这里当数据更新时采用的是 NSNotification 来通知界面。目前请求处理任务是放在子线程,由于通知是同步的并且接收方是用来更新界面的,所以这里会把发送通知事件切换到主线程。

FLEX 提供了抓取到请求之后,提供 copy url 的功能,实现在 FLEXNetworkCurlLogger 中,主要是把 NSURLRequest 拼接处 curl 的字符串,如下

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;'

curl是一个支持各种网络协议的数据传输命令行工具。

-v/--verbose 表示获取更多的连接信息

-X GET 用来选定方法

-H 用来添加请求头

-d/--data 是 POST 请求的 application/x-www-url-encoded 方式的 body 数据。

对于处理回调这里也有一个新思路,一个 UITableViewCell 的点击从 didSelectRowAtIndexPath 中移到了 ViewModel 这一层处理,并且是把这个处理使用 block 属性的方式赋予每个 ViewModel 自己处理。

FLEXNetworkDetailRow *requestURLRow = [[FLEXNetworkDetailRow alloc] init];
requestURLRow.title = @"Request URL";
NSURL *url = transaction.request.URL;
requestURLRow.detailText = url.absoluteString;
requestURLRow.selectionFuture = ^{
UIViewController *urlWebViewController = [[FLEXWebViewController alloc] initWithURL:url];
urlWebViewController.title = url.absoluteString;
return urlWebViewController;
};
[rows addObject:requestURLRow];

FLEX 计算流量是通过把所有请求 response data 的大小加起来,不过 FLEX 只统计 response,没有统计 request,并且只统计 body 没有统计 header,所以数据是不准确的。

NSCache 基本上就是跟 NSMutableDictionary 类似,唯一不同的是它会自动释放内存,FLEX 使用 NSCache 来保存 response data。

FileBrowser

适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作。

里面有 Target,Adapter,Adaptee。

Target 就是目标对象,这里来说就是定义的协议,FLEXFileBrowserFileOperationController。

Adaptee 就是适配者,实际要访问的对象,这里就是 NSFileManager 的删除或移动。

Adapter 就是适配器,就是FLEXFileBrowserFileDeleteOperationController,FLEXFileBrowserFileRenameOperationController,访问FLEXFileBrowserFileOperationController 协议就能访问 NSFileManager 的操作

FLEXFileBrowserFileOperationController

查询 FileManager 的 path 或者文件因为这是一个耗时操作,所以这里把操作放入NSOperation,自定义一个 Operation,实现 main 方法,然后加入 NSOperationQueue。

DatabaseBrowser

FLEXSQLiteDatabaseManager 对于 SQLite 的操作是一个简化版的 fmdb,去除了 GCD 队列的管理,主要作用就是查询数据库。

FLEXRealmDatabaseManager 是实现能够读取 Realm 数据库,这里的实现并没有引入 Realm 库,首先通过

#if __has_include(<Realm/Realm.h>)
#else
#endif

来判断想要引入的文件是否存在,如果不存在就 FLEXRealmDefines 定义了一堆 Realm 的 class,并且不实现,然后就可以保证不引入一个库而编译通过。

Heap Objects

第一个列表 FLEXLiveObjectsTableViewController 是返回所有注册的类,这个通过 objc_copyClassList 可以获取到。

另外通过下面的方法还可以获取到堆上的所有实例

// Inspired by:
// http://llvm.org/svn/llvm-project/lldb/tags/RELEASE_34/final/examples/darwin/heap_find/heap/heap_find.cpp
// https://gist.github.com/samdmarshall/17f4e66b5e2e579fd396

vm_address_t *zones = NULL;
unsigned int zoneCount = 0;
kern_return_t result = malloc_get_all_zones(TASK_NULL, reader, &zones, &zoneCount);

if (result == KERN_SUCCESS) {
    for (unsigned int i = 0; i < zoneCount; i++) {
        malloc_zone_t *zone = (malloc_zone_t *)zones[i];
        if (zone->introspect && zone->introspect->enumerator) {
            zone->introspect->enumerator(TASK_NULL, (__bridge void *)block, MALLOC_PTR_IN_USE_RANGE_TYPE, (vm_address_t)zone, reader, &range_callback);
        }
    }
}

所以 FLEX 才可以显示所有注册的类并且显示该类所有的实例。

在打开具体某个实例的时候,这里运用了简单工厂模式。

简单工厂模式的参与者

Factory:工厂角色,接收客户端请求,通过请求创建对象的产品对象

Abstract Product:抽象产品角色,工厂模式创建对象的父类

Concrete Product:具体产品角色,工厂模式创建的对象

其实UIButton通过

+ (instancetype)buttonWithType:(UIButtonType)buttonType;

创建就是简单工厂模式

这里综合前面说到适配器模式,适配器模式主要是解决接口不兼容的问题,将一个接口转换为另一个接口,工厂方法是定义一个创建对象的接口,根据不同的参数返回不同的实例。

View Hierarchy

关于获取所有的 UIWindow 实例,使用的是 UIWindow 的私有方法,"allWindowsIncludingInternalWindows:onlyVisibleWindows:",这里有一个避开苹果检查的办法就是,方法通过数组组装出来的

NSArray *allWindowsComponents = @[@"al", @"lWindo", @"wsIncl", @"udingInt", @"ernalWin", @"dows:o", @"nlyVisi", @"bleWin", @"dows:"];
SEL allWindowsSelector = NSSelectorFromString([allWindowsComponents componentsJoinedByString:@""]);

一个 UIView 的层级则是巧妙地循环其 superView,来获取其层级,最后画出整个 App 的层级图。

"select"功能的实现,会把 tap 上所有的 UIView 找出来,办法是 for 循环所有的 UIView,找出在其 tap 范围内的 UIView。

FLEXWindow

打开 FLEX 界面是定义了一个 UIWindow,然后把它显示出来,不过由于 FLEX 有一个浮窗工具栏,这个时候 keyWindow 还是系统的,只有当点击工具栏进入 FLEX 全屏界面才会设置 keyWindow 而接收键盘事件。

首先重写了触摸响应的方案,以工具栏为例,当点击 FLEX 工具栏时就响应,点击其他位置 FLEX 就会不响应,而传递给下层的 UIWindow。

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

preview image

FLEX 有一个预览 UIView 实例画面的功能,实现机制就是获取到 UIView 实例之后,把它转化为 UIImage 来显示。

+ (UIViewController *)imagePreviewViewControllerForView:(UIView *)view
{
    UIViewController *imagePreviewViewController = nil;
    if (!CGRectIsEmpty(view.bounds)) {
        CGSize viewSize = view.bounds.size;
        UIGraphicsBeginImageContextWithOptions(viewSize, NO, 0.0);
        [view drawViewHierarchyInRect:CGRectMake(0, 0, viewSize.width, viewSize.height) afterScreenUpdates:YES];
        UIImage *previewImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        imagePreviewViewController = [[FLEXImagePreviewViewController alloc] initWithImage:previewImage];
    }
    return imagePreviewViewController;
}

FLEXRuntimeUtility

首先可以看Classes and metaclasses中的一张

这张图表示的是对象的内存关系,图中实线是 super_class 指针,虚线是 isa 指针,类对象存放实例方法,元类对象存放类方法。

  • 实例对象的 isa 指向类对象(类也是对象,面向对象中一切都是对象)
  • 类对象的 isa 指向元类对象
  • Root class (class) 其实就是 NSObject,NSObject 是没有超类的,所以 Root class(class) 的 superclass 指向 nil。
  • Root class(meta) 的 superclass 指向 Root class(class),也就是 NSObject,形成一个回路。
  • 每个 Meta class 的 isa 指针都指向 Root class (meta)。

这里讲解一下 FLEXRuntimeUtility 的相关应用

属性

获取一个类的属性

objc_property_t *propertyList = class_copyPropertyList(class, &propertyCount);

假如

@property (readonly, copy) NSString *debugDescription;

获取属性的名字

NSString *name = @(property_getName(property));

根据属性可以获取这个属性的相关特性

NSString *attributes = @(property_getAttributes(property));

值为

T@"NSString",R,C

这个字符串的意义可以查看参考 比如T就是属性的类型,R就是read-only,C就是copy

另外还可以动态添加属性

class_addProperty(theClass, name, attributes, totalAttributesCount);

实例变量

获取实例变量

Ivar *ivarList = class_copyIvarList(class, &ivarCount);

获取实例变量的类型

const char *type = ivar_getTypeEncoding(ivar);

获取变量的值

value = object_getIvar(object, ivar);

方法

获取实例方法

Class class = [self.object class];
Method *methodList = class_copyMethodList(class, &methodCount);

获取类方法

const char *className = [NSStringFromClass([self.object class]) UTF8String];

Class metaClass = objc_getMetaClass(className);
Method *methodList = class_copyMethodList(metaClass, &methodCount);

获取 selector 名字

NSString *selectorName = NSStringFromSelector(method_getName(method));

获取返回类型

char *returnType = method_copyReturnType(method);

获取参数类型

char *argType = method_copyArgumentType(method, argIndex);

这里的 Type 是 type encoding 之后的结果,比如如果是 BOOL 类型,这里 argType 将是 "B",所以这里会将 @encode(BOOL) 之后的结果与 B 对比,相同则表示为 BOOL 类型。

最后,文章到这就结束了,FLEX 是 iOS 中一个比较特殊的库,很少有人去做 App 内调试相关的东西,通过这里就可以把 App 的调试框架搭起来了,而且 FLEX 关于 runtime 的应用可谓到了一个极致,学习 runtime 也可以多看这个库。

本文作者coderyi

results matching ""

    No results matching ""