深入理解GoruntimeSetFinalizer原理剖析

finalizer是与对象关联的一个函数,通过runtime.SetFinalizer 来设置,它在对象被GC的时候,这个finalizer会被调用,以完成对象生命中最后一程。由于finalizer的存在,导致了对象在三色标记中,不可能被标为白色对象,也就是垃圾,所以,这个对象的生命也会得以延续一个GC周期。正如defer一样,我们也可以通过 Finalizer 完成一些类似于资源释放的操作 1. 结构概览1.1. heaptype mspan struct { // 当前span上所有对象的special串成链表 // special中有个offset,就是数据对象在span上的offset,通过offset,将数据对象和special关联起来 specials *special // linked list of special records sorted by offset.}1.2. specialtype special struct { next *special // linked list in span // 数据对象在span上的offset offset uint16 // span offset of object kind byte // kind of special}1.3. specialfinalizertype specialfinalizer struct { special special fn *funcval // May be a heap pointer. // return的数据的大小 nret uintptr // 第一个参数的类型 fint *_type // May be a heap pointer, but always live. // 与finalizer关联的数据对象的指针类型 ot *ptrtype // May be a heap pointer, but always live.}1.4. finalizertype finalizer struct { fn *funcval // function to call (may be a heap pointer) arg unsafe.Pointer // ptr to object (may be a heap pointer) nret uintptr // bytes of return values from fn fint *_type // type of first argument of fn ot *ptrtype // type of ptr to object (may be a heap pointer)}1.5. 全局变量var finlock mutex // protects the following variables// 运行finalizer的g,只有一个g,不用的时候休眠,需要的时候再唤醒var fing *g // goroutine that runs finalizers// finalizer的全局队列,这里是已经设置的finalizer串成的链表var finq *finblock // list of finalizers that are to be executed// 已经释放的finblock的链表,用finc缓存起来,以后需要使用的时候可以直接取走,避免再走一遍内存分配了var finc *finblock // cache of free blocksvar finptrmask [_FinBlockSize / sys.PtrSize / 8]bytevar fingwait bool // fing的标志位,通过 fingwait和fingwake,来确定是否需要唤醒fingvar fingwake bool// 所有的blocks串成的链表var allfin *finblock // list of all blocks2. 源码分析2.1. 创建finalizer2.1.1. mainfunc main() { // i 就是后面说的 数据对象 var i = 3 // 这里的func 就是后面一直说的 finalizer runtime.SetFinalizer(&i, func(i *int) { fmt.Println(i, *i, "set finalizer") }) time.Sleep(time.Second * 5)}2.1.2. SetFinalizer根据 数据对象 ,生成一个special对象,并绑定到 数据对象 所在的span,串联到span.specials上,并且确保fing的存在 ...

September 8, 2019 · 8 min · jiezi

探秘Runtime-Runtime加载过程

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/4fb2d7014e9e 程序加载过程在iOS程序中会用到很多系统的动态库,这些动态库都是动态加载的。所有iOS程序共用一套系统动态库,在程序开始运行时才会开始链接动态库。 除了在项目设置里显式出现的动态库外,还会有一些隐式存在的动态库。例如objc和Runtime所属的libobjc.dyld和libSystem.dyld,在libSystem中包含常用的libdispatch(GCD)、libsystem_c(C语言基础库)、libsystem_blocks(Block)等。 使用动态库的优点: 防止重复。iOS系统中所有App公用一套系统动态库,防止重复的内存占用。减少包体积。因为系统动态库被内置到iOS系统中,所以打包时不需要把这部分代码打进去,可以减小包体积。动态性。因为系统动态库是动态加载的,所以可以在更新系统后,将动态库换成新的动态库。加载过程在应用程序启动后,由dyld(the dynamic link editor)进行程序的初始化操作。大概流程就像下面列出的步骤,其中第3、4、5步会执行多次,在ImageLoader加载新的image进内存后就会执行一次。 在引用程序启动后,由dyld将应用程序加载到二进制中,并完成一些文件的初始化操作。Runtime向dyld中注册回调函数。通过ImageLoader将所有image加载到内存中。dyld在image发生改变时,主动调用回调函数。Runtime接收到dyld的函数回调,开始执行map_images、load_images等操作,并回调+load方法。调用main()函数,开始执行业务代码。ImageLoader是image的加载器,image可以理解为编译后的二进制。 下面是在Runtime的map_images函数打断点,观察回调情况的汇编代码。可以看出,调用是由dyld发起的,由ImageLoader通知dyld进行调用。 关于dyld我并没有深入研究,有兴趣的同学可以到Github上下载源码研究一下。 动态加载一个OC程序可以在运行过程中动态加载和链接新类或Category,新类或Category会加载到程序中,其处理方式和其他类是相同的。动态加载还可以做许多不同的事,动态加载允许应用程序进行自定义处理。 OC提供了objc_loadModules运行时函数,执行Mach-O中模块的动态加载,在上层NSBundle对象提供了更简单的访问API。 map images在Runtime加载时,会调用_objc_init函数,并在内部注册三个函数指针。其中map_images函数是初始化的关键,内部完成了大量Runtime环境的初始化操作。 在map_images函数中,内部也是做了一个调用中转。然后调用到map_images_nolock函数,内部核心就是_read_images函数。 void _objc_init(void){ // .... 各种init _dyld_objc_notify_register(&map_images, load_images, unmap_image);}void map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]){ rwlock_writer_t lock(runtimeLock); return map_images_nolock(count, paths, mhdrs);}void map_images_nolock(unsigned mhCount, const char * const mhPaths[], const struct mach_header * const mhdrs[]){ if (hCount > 0) { _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses); }}在_read_images函数中完成了大量的初始化操作,函数内部代码量比较大,下面是精简版带注释的源代码。 ...

June 24, 2019 · 6 min · jiezi

iOS自动布局框架-Masonry详解

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/ea74b230c70d 目前iOS开发中大多数页面都已经开始使用Interface Builder的方式进行UI开发了,但是在一些变化比较复杂的页面,还是需要通过代码来进行UI开发的。而且有很多比较老的项目,本身就还在采用纯代码的方式进行开发。而现在iPhone和iPad屏幕尺寸越来越多,虽然开发者只需要根据屏幕点进行开发,而不需要基于像素点进行UI开发。但如果在项目中根据不同屏幕尺寸进行各种判断,写死坐标的话,这样开发起来是很吃力的。 所以一般用纯代码开发UI的话,一般都是配合一些自动化布局的框架进行屏幕适配。苹果为我们提供的适配框架有:VFL、UIViewAutoresizing、Auto Layout、Size Classes等。 其中Auto Layout是使用频率最高的布局框架,但是其也有弊端。就是在使用NSLayoutConstraint 的时候,会发现代码量很多,而且大多都是重复性的代码,以至于好多人都不想用这个框架。 后来Github上的出现了基于NSLayoutConstraint 封装的第三方布局框架Masonry,Masonry使用起来非常方便,本篇文章就详细讲一下Masonry的使用。 Masonry介绍这篇文章只是简单介绍Masonry,以及Masonry的使用,并且会举一些例子出来。但并不会涉及到Masonry的内部实现,以后会专门写篇文章来介绍其内部实现原理,包括顺便讲一下链式语法。 什么是MasonryMasonry是一个对系统NSLayoutConstraint进行封装的第三方自动布局框架,采用链式编程的方式提供给开发者API。系统AutoLayout支持的操作,Masonry都支持,相比系统API功能来说,Masonry是有过之而无不及。 Masonry采取了链式编程的方式,代码理解起来非常清晰易懂,而且写完之后代码量看起来非常少。之前用NSLayoutConstraint写很多代码才能实现的布局,用Masonry最少一行代码就可以搞定。下面看到Masonry的代码就会发现,太简单易懂了。 Masonry是同时支持Mac和iOS两个平台的,在这两个平台上都可以使用Masonry进行自动布局。我们可以从MASUtilities.h文件中,看到下面的定义,这就是Masonry通过宏定义的方式,区分两个平台独有的一些关键字。 #if TARGET_OS_IPHONE #import <UIKit/UIKit.h> #define MAS_VIEW UIView #define MASEdgeInsets UIEdgeInsets#elif TARGET_OS_MAC #import <AppKit/AppKit.h> #define MAS_VIEW NSView #define MASEdgeInsets NSEdgeInsets#endifGithub地址:https://github.com/SnapKit/Masonry 集成方式Masonry支持CocoaPods,可以直接通过podfile文件进行集成,需要在CocoaPods中添加下面代码: pod 'Masonry'Masonry学习建议在UI开发中,纯代码和Interface Builder我都是用过的,在开发过程中也积累了一些经验。对于初学者学习纯代码AutoLayout,我建议还是先学会Interface Builder方式的AutoLayout,领悟苹果对自动布局的规则和思想,然后再把这套思想嵌套在纯代码上。这样学习起来更好入手,也可以避免踩好多坑。 在项目中设置的AutoLayout约束,起到对视图布局的标记作用。设置好约束之后,程序运行过程中创建视图时,会根据设置好的约束计算frame,并渲染到视图上。 所以在纯代码情况下,视图设置的约束是否正确,要以运行之后显示的结果和打印的log为准。 Masonry中的坑在使用Masonry进行约束时,有一些是需要注意的。 在使用Masonry添加约束之前,需要在addSubview之后才能使用,否则会导致崩溃。在添加约束时初学者经常会出现一些错误,约束出现问题的原因一般就是两种:约束冲突和缺少约束。对于这两种问题,可以通过调试和log排查。之前使用Interface Builder添加约束,如果约束有错误直接就可以看出来,并且会以红色或者黄色警告体现出来。而Masonry则不会直观的体现出来,而是以运行过程中崩溃或者打印异常log体现,所以这也是手写代码进行AutoLayout的一个缺点。这个问题只能通过多敲代码,积攒纯代码进行AutoLayout的经验,慢慢就用起来越来越得心应手了。Masonry基础使用Masonry基础APImas_makeConstraints() 添加约束mas_remakeConstraints() 移除之前的约束,重新添加新的约束mas_updateConstraints() 更新约束,写哪条更新哪条,其他约束不变equalTo() 参数是对象类型,一般是视图对象或者mas_width这样的坐标系对象mas_equalTo() 和上面功能相同,参数可以传递基础数据类型对象,可以理解为比上面的API更强大width() 用来表示宽度,例如代表view的宽度mas_width() 用来获取宽度的值。和上面的区别在于,一个代表某个坐标系对象,一个用来获取坐标系对象的值Auto Boxing上面例如equalTo或者width这样的,有时候需要涉及到使用mas_前缀,这在开发中需要注意作区分。如果在当前类引入#import "Masonry.h"之前,用下面两种宏定义声明一下,就不需要区分mas_前缀。 // 定义这个常量,就可以不用在开发过程中使用"mas_"前缀。#define MAS_SHORTHAND// 定义这个常量,就可以让Masonry帮我们自动把基础数据类型的数据,自动装箱为对象类型。#define MAS_SHORTHAND_GLOBALS修饰语句Masonry为了让代码使用和阅读更容易理解,所以直接通过点语法就可以调用,还添加了and和with两个方法。这两个方法内部实际上什么都没干,只是在内部将self直接返回,功能就是为了更加方便阅读,对代码执行没有实际作用。例如下面的例子: make.top.and.bottom.equalTo(self.containerView).with.offset(padding);其内部代码实现,实际上就是直接将self返回。 - (MASConstraint *)with { return self;}更新约束和布局关于更新约束布局相关的API,主要用以下四个API: ...

June 24, 2019 · 3 min · jiezi

iOS黑魔法-Method-Swizzling

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/ff19c04b34d0 公司年底要在新年前发一个版本,最近一直很忙,好久没有更新博客了。正好现在新版本开发的差不多了,抽空总结一下。由于最近开发新版本,就避免不了在开发和调试过程中引起崩溃,以及诱发一些之前的__bug__导致的崩溃。而且项目比较大也很不好排查,正好想起之前研究过的Method Swizzling,考虑是否能用这个苹果的“黑魔法”解决问题,当然用好这个黑魔法并不局限于解决这些问题.... 需求就拿我们公司项目来说吧,我们公司是做导航的,而且项目规模比较大,各个控制器功能都已经实现。突然有一天老大过来,说我们要在所有页面添加统计功能,也就是用户进入这个页面就统计一次。我们会想到下面的一些方法: 手动添加直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴...上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。 继承我们可以使用OOP的特性之一,继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。 然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。 Category我们可以为UIViewController建一个Category,然后在所有控制器中引入这个Category。当然我们也可以添加一个PCH文件,然后将这个Category添加到PCH文件中。 我们创建一个Category来覆盖系统方法,系统会优先调用Category中的代码,然后在调用原类中的代码。 我们可以通过下面的这段伪代码来看一下: #import "UIViewController+EventGather.h"@implementation UIViewController (EventGather)- (void)viewDidLoad { NSLog(@"页面统计:%@", self);}@endMethod Swizzling我们可以使用苹果的“黑魔法”Method Swizzling,Method Swizzling本质上就是对IMP和SEL进行交换。 Method Swizzling原理Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。 而且Method Swizzling也是__iOS__中AOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。 原理分析首先,让我们通过两张图片来了解一下Method Swizzling的实现原理 上面图一中selector2原本对应着IMP2,但是为了更方便的实现特定业务需求,我们在图二中添加了selector3和IMP3,并且让selector2指向了IMP3,而selector3则指向了IMP2,这样就实现了“方法互换”。 在OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用。 在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SEL和IMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP。 Method Swizzling使用在实现Method Swizzling时,核心代码主要就是一个runtime的C语言API: OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);代码示例就拿上面我们说的页面统计的需求来说吧,这个需求在很多公司都很常见,我们下面的Demo就通过Method Swizzling简单的实现这个需求。 我们先给UIViewController添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。 定义Method Swizzling中我们自定义的方法时,需要注意尽量加前缀,以防止和其他地方命名冲突,Method Swizzling的替换方法命名一定要是唯一的,至少在被替换的类中必须是唯一的。 #import "UIViewController+swizzling.h"#import <objc/runtime.h>@implementation UIViewController (swizzling)+ (void)load { // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。 Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad)); Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad)); /** 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。 */ if (!class_addMethod([self class], @selector(swizzlingViewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) { method_exchangeImplementations(fromMethod, toMethod); }}// 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。- (void)swizzlingViewDidLoad { NSString *str = [NSString stringWithFormat:@"%@", self.class]; // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉 if(![str containsString:@"UI"]){ NSLog(@"统计打点 : %@", self.class); } [self swizzlingViewDidLoad];}@end看到上面的代码,肯定有人会问:楼主,你太粗心了,你在swizzlingViewDidLoad方法中又调用了[self swizzlingViewDidLoad];,这难道不会产生递归调用吗?答:然而....并不会????。 ...

June 24, 2019 · 2 min · jiezi

探秘Runtime-Runtime-Message-Forward

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/f313e8e32946 当一个对象的方法被调用时,首先在对象所属的类中查找方法列表,如果当前类中没有则向父类查找,一直找到根类NSObject。如果始终没有找到方法实现,则进入消息转发步骤中。 动态消息解析当一个方法没有实现时,也就是在cache lsit和其继承关系的method list中,没有找到对应的方法。这时会进入消息转发阶段,但是在进入消息转发阶段前,Runtime会给一次机会动态添加方法实现。 可以通过重写resolveInstanceMethod:和resolveClassMethod:方法,动态添加未实现的方法。其中第一个是添加实例方法,第二个是添加类方法。这两个方法都有一个BOOL返回值,返回NO则进入消息转发机制。 void dynamicMethodIMP(id self, SEL _cmd) { // implementation ....}+ (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(resolveThisMethodDynamically)) { class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:sel];}在通过class_addMethod函数动态添加实现时,后面有一个"v@:"来描述SEL对应的函数实现,具体的描述可以参考官方文档。 Message Forwarding 在进行消息转发之前,还可以在forwardingTargetForSelector:方法中将未实现的消息,转发给其他对象。可以在下面方法中,返回响应未实现方法的其他对象。 - (id)forwardingTargetForSelector:(SEL)aSelector { NSString *selectorName = NSStringFromSelector(aSelector); if ([selectorName isEqualToString:@"selector"]) { return object; } return [super forwardingTargetForSelector:aSelector];}当forwardingTargetForSelector:方法未做出任何响应的话,会来到消息转发流程。消息转发时会首先调用methodSignatureForSelector:方法,在方法内部生成NSMethodSignature类型的方法签名对象。在生成签名对象时,可以指定target和SEL,可以将这两个参数换成其他参数,将消息转发给其他对象。 [otherObject methodSignatureForSelector:otherSelector];生成NSMethodSignature签名对象后,就会调用forwardInvocation:方法,这是消息转发中最后一步了,如果在这步还没有对消息进行处理,则会导致崩溃。 这个方法中会传入一个NSInvocation对象,这个对象就是通过刚才生成的签名对象创建的,可以通过invocation调用其他对象的方法,调用其invokeWithTarget:即可。 - (void)forwardInvocation:(NSInvocation *)anInvocation { if ([object respondsToSelector:[anInvocation selector]]) { [anInvocation invokeWithTarget:object]; } else { [super forwardInvocation:anInvocation]; }}消息转发将一条消息发送给一个不能处理的对象会引起崩溃,但是在崩溃之前,系统给响应对象一次处理异常的机会。 ...

June 24, 2019 · 2 min · jiezi

探秘Runtime-深入剖析Category

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/0dc2513e117b Category有了之前Runtime的基础,一些内部实现就很好理解了。在OC中可以通过Category添加属性、方法、协议,在Runtime中Class和Category都是通过结构体实现的。 和Category语法很相似的还有Extension,二者的区别在于,Extension在编译期就直接和原类编译在一起,而Category是在运行时动态添加到原类中的。 基于之前的源码分析,我们来分析一下Category的实现原理。在_read_images函数中会执行一个循环嵌套,外部循环遍历所有类,并取出当前类对应Category数组。内部循环会遍历取出的Category数组,将每个category_t对象取出,最终执行addUnattachedCategoryForClass函数添加到Category哈希表中。 // 将category_t添加到list中,并通过NXMapInsert函数,更新所属类的Category列表static void addUnattachedCategoryForClass(category_t *cat, Class cls, header_info *catHeader){ // 获取到未添加的Category哈希表 NXMapTable *cats = unattachedCategories(); category_list *list; // 获取到buckets中的value,并向value对应的数组中添加category_t list = (category_list *)NXMapGet(cats, cls); if (!list) { list = (category_list *) calloc(sizeof(*list) + sizeof(list->list[0]), 1); } else { list = (category_list *) realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1)); } // 替换之前的list字段 list->list[list->count++] = (locstamped_category_t){cat, catHeader}; NXMapInsert(cats, cls, list);}Category维护了一个名为category_map的哈希表,哈希表存储所有category_t对象。 ...

June 24, 2019 · 3 min · jiezi

组件化架构漫谈

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/67a6004f6930 前段时间公司项目打算重构,准确来说应该是按之前的产品逻辑重写一个项目????。在重构项目之前涉及到架构选型的问题,我和组里小伙伴一起研究了一下组件化架构,打算将项目重构为组件化架构。当然不是直接拿来照搬,还是要根据公司具体的业务需求设计架构。在学习组件化架构的过程中,从很多高质量的博客中学到不少东西,例如蘑菇街李忠、casatwy、bang的博客。在学习过程中也遇到一些问题,在微博和QQ上和一些做iOS的朋友进行了交流,非常感谢这些朋友的帮助。 本篇文章主要针对于之前蘑菇街提出的组件化方案,以及casatwy提出的组件化方案进行分析,后面还会简单提到滴滴、淘宝、微信的组件化架构,最后会简单说一下我公司设计的组件化架构。 组件化架构的由来随着移动互联网的不断发展,很多程序代码量和业务越来越多,现有架构已经不适合公司业务的发展速度了,很多都面临着重构的问题。 在公司项目开发中,如果项目比较小,普通的单工程+MVC架构就可以满足大多数需求了。但是像淘宝、蘑菇街、微信这样的大型项目,原有的单工程架构就不足以满足架构需求了。 就拿淘宝来说,淘宝在13年开启的“All in 无线”战略中,就将阿里系大多数业务都加入到手机淘宝中,使客户端出现了业务的爆发。在这种情况下,单工程架构则已经远远不能满足现有业务需求了。所以在这种情况下,淘宝在13年开启了插件化架构的重构,后来在14年迎来了手机淘宝有史以来最大规模的重构,将项目重构为组件化架构。 蘑菇街的组件化架构原因在一个项目越来越大,开发人员越来越多的情况下,项目会遇到很多问题。 业务模块间划分不清晰,模块之间耦合度很大,非常难维护。所有模块代码都编写在一个项目中,测试某个模块或功能,需要编译运行整个项目。 为了解决上面的问题,可以考虑加一个中间层来协调各个模块间的调用,所有的模块间的调用都会经过中间层中转。 但是发现增加这个中间层后,耦合还是存在的。中间层对被调用模块存在耦合,其他模块也需要耦合中间层才能发起调用。这样还是存在之前的相互耦合的问题,而且本质上比之前更麻烦了。 架构改进所以应该做的是,只让其他模块对中间层产生耦合关系,中间层不对其他模块发生耦合。对于这个问题,可以采用组件化的架构,将每个模块作为一个组件。并且建立一个主项目,这个主项目负责集成所有组件。这样带来的好处是很多的: 业务划分更佳清晰,新人接手更佳容易,可以按组件分配开发任务。项目可维护性更强,提高开发效率。更好排查问题,某个组件出现问题,直接对组件进行处理。开发测试过程中,可以只编译自己那部分代码,不需要编译整个项目代码。方便集成,项目需要哪个模块直接通过CocoaPods集成即可。 进行组件化开发后,可以把每个组件当做一个独立的app,每个组件甚至可以采取不同的架构,例如分别使用MVVM、MVC、MVCS等架构,根据自己的编程习惯做选择。 MGJRouter方案蘑菇街通过MGJRouter实现中间层,由MGJRouter进行组件间的消息转发,从名字上来说更像是“路由器”。实现方式大致是,在提供服务的组件中提前注册block,然后在调用方组件中通过URL调用block,下面是调用方式。 架构设计 MGJRouter是一个单例对象,在其内部维护着一个“URL -> block”格式的注册表,通过这个注册表来保存服务方注册的block,以及使调用方可以通过URL映射出block,并通过MGJRouter对服务方发起调用。 MGJRouter是所有组件的调度中心,负责所有组件的调用、切换、特殊处理等操作,可以用来处理一切组件间发生的关系。除了原生页面的解析外,还可以根据URL跳转H5页面。 在服务方组件中都对外提供一个PublicHeader,在PublicHeader中声明当前组件所提供的所有功能,这样其他组件想知道当前组件有什么功能,直接看PublicHeader即可。每一个block都对应着一个URL,调用方可以通过URL对block发起调用。 #ifndef UserCenterPublicHeader_h#define UserCenterPublicHeader_h/** 跳转用户登录界面 */static const NSString * CTBUCUserLogin = @"CTB://UserCenter/UserLogin";/** 跳转用户注册界面 */static const NSString * CTBUCUserRegister = @"CTB://UserCenter/UserRegister";/** 获取用户状态 */static const NSString * CTBUCUserStatus = @"CTB://UserCenter/UserStatus";#endif在组件内部实现block的注册工作,以及block对外提供服务的代码实现。在注册的时候需要注意注册时机,应该保证调用时URL对应的block已经注册。 蘑菇街项目使用git作为版本控制工具,将每个组件都当做一个独立工程,并建立主项目来集成所有组件。集成方式是在主项目中通过CocoaPods来集成,将所有组件当做二方库集成到项目中。详细的集成技术点在下面“标准组件化架构设计”章节中会讲到。 MGJRouter调用下面代码模拟对详情页的注册、调用,在调用过程中传递id参数。参数传递可以有两种方式,类似于Get请求在URL后面拼接参数,以及通过字典传递参数。下面是注册的示例代码: [MGJRouter registerURLPattern:@"mgj://detail" toHandler:^(NSDictionary *routerParameters) { // 下面可以在拿到参数后,为其他组件提供对应的服务 NSString uid = routerParameters[@"id"];}];通过openURL:方法传入的URL参数,对详情页已经注册的block方法发起调用。调用方式类似于GET请求,URL地址后面拼接参数。 ...

June 24, 2019 · 3 min · jiezi

探秘Runtime-Runtime的应用

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/4a22a39b69c5 attribute__attribute__是一套编译器指令,被GNU和LLVM编译器所支持,允许对于__attribute__增加一些参数,做一些高级检查和优化。 __attribute__的语法是,在后面加两个括号,然后写属性列表,属性列表以逗号分隔。在iOS中,很多例如NS_CLASS_AVAILABLE_IOS的宏定义,内部也是通过__attribute__实现的。 __attribute__((attribute1, attribute2));下面是一些__attribute__的常用属性,更完整的属性列表可以到llvm的官网查看。 objc_subclassing_restrictedobjc_subclassing_restricted属性表示被修饰的类不能被其他类继承,否则会报下面的错误。 __attribute__((objc_subclassing_restricted))@interface TestObject : NSObject@property (nonatomic, strong) NSObject *object;@property (nonatomic, assign) NSInteger age;@end@interface Child : TestObject@end错误信息:Cannot subclass a class that was declared with the 'objc_subclassing_restricted' attributeobjc_requires_superobjc_requires_super属性表示子类必须调用被修饰的方法super,否则报黄色警告。 @interface TestObject : NSObject- (void)testMethod __attribute__((objc_requires_super));@end@interface Child : TestObject@end警告信息:(不报错)Method possibly missing a [super testMethod] callconstructor / destructorconstructor属性表示在main函数执行之前,可以执行一些操作。destructor属性表示在main函数执行之后做一些操作。constructor的执行时机是在所有load方法都执行完之后,才会执行所有constructor属性修饰的函数。 __attribute__((constructor)) static void beforeMain() { NSLog(@"before main");}__attribute__((destructor)) static void afterMain() { NSLog(@"after main");}int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"execute main"); } return 0;}执行结果:debug-objc[23391:1143291] before maindebug-objc[23391:1143291] execute maindebug-objc[23391:1143291] after main在有多个constructor或destructor属性修饰的函数时,可以通过设置优先级来指定执行顺序。格式是__attribute__((constructor(101)))的方式,在属性后面直接跟优先级。 ...

June 24, 2019 · 3 min · jiezi

探秘Runtime-Runtime消息发送机制

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/014af0de67cd 方法调用在OC中方法调用是通过Runtime实现的,Runtime进行方法调用本质上是发送消息,通过objc_msgSend()函数进行消息发送。 例如下面的OC代码会被转换为Runtime代码。 原方法:[object testMethod]转换后的调用:objc_msgSend(object, @selector(testMethod));发送消息的第二个参数是一个SEL类型的参数,在项目里经常会出现,不同的类定义了相同的方法,这样就会有相同的SEL。那么问题就来了,也是很多人博客里都问过的一个问题,不同类的SEL是同一个吗? 然而,事实是通过我们的验证,创建两个不同的类,并定义两个相同的方法,通过@selector()获取SEL并打印。我们发现SEL都是同一个对象,地址都是相同的。由此证明,不同类的相同SEL是同一个对象。 @interface TestObject : NSObject- (void)testMethod;@end@interface TestObject2 : NSObject- (void)testMethod;@end// TestObject2实现文件也一样@implementation TestObject- (void)testMethod { NSLog(@"TestObject testMethod %p", @selector(testMethod));}@end// 结果:TestObject testMethod 0x100000f81TestObject2 testMethod 0x100000f81在Runtime中维护了一个SEL的表,这个表存储SEL不按照类来存储,只要相同的SEL就会被看做一个,并存储到表中。在项目加载时,会将所有方法都加载到这个表中,而动态生成的方法也会被加载到表中。 隐藏参数我们在方法内部可以通过self获取到当前对象,但是self又是从哪来的呢?方法实现的本质也是C函数,C函数除了方法传入的参数外,还会有两个默认参数,这两个参数在通过objc_msgSend()调用时也会传入。这两个参数在Runtime中并没有声明,而是在编译时自动生成的。 从objc_msgSend的声明中可以看出这两个隐藏参数的存在。 objc_msgSend(void /* id self, SEL op, ... */ )self,调用当前方法的对象。_cmd,当前被调用方法的SEL。虽然这两个参数在调用和实现方法中都没有明确声明,但是我们仍然可以使用它。响应对象就是self,被调用方法的selector是_cmd。 - (void)method { id target = getTheReceiver(); SEL method = getTheMethod(); if ( target == self || method == _cmd ) return nil; return [target performSelector:method];}函数调用一个对象被创建后,自身的类及其父类一直到NSObject类的部分,都会包含在对象的内存中,例如其父类的实例变量。当通过[super class]的方式调用其父类的方法时,会创建一个结构体。 ...

June 24, 2019 · 4 min · jiezi

探秘Runtime-Runtime源码分析

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/3019605a4fc9 本文基于objc-723版本,在Apple Github和Apple OpenSource上有源码,但是需要自己编译。 重点来了~,可以到我的Github上下载编译好的源码,源码中已经写了大量的注释,方便读者研究。(如果觉得还不错,各位大佬麻烦点个Star????)Runtime Analyze 对象的初始化流程在对象初始化的时候,一般都会调用alloc+init方法实例化,或者通过new方法进行实例化。下面将会分析通过alloc+init的方式实例化的过程,以下代码都是关键代码。 前面两步很简单,都是直接进行函数调用。 + (id)alloc { return _objc_rootAlloc(self);}id _objc_rootAlloc(Class cls){ return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);}在创建对象的地方有两种方式,一种是通过calloc开辟内存,然后通过initInstanceIsa函数初始化这块内存。第二种是直接调用class_createInstance函数,由内部实现初始化逻辑。 static ALWAYS_INLINE idcallAlloc(Class cls, bool checkNil, bool allocWithZone=false){ if (fastpath(cls->canAllocFast())) { bool dtor = cls->hasCxxDtor(); id obj = (id)calloc(1, cls->bits.fastInstanceSize()); if (slowpath(!obj)) return callBadAllocHandler(cls); obj->initInstanceIsa(cls, dtor); return obj; } else { id obj = class_createInstance(cls, 0); if (slowpath(!obj)) return callBadAllocHandler(cls); return obj; }}但是在最新版的objc-723中,调用canAllocFast函数直接返回false,所以只会执行上面第二个else代码块。 ...

June 24, 2019 · 7 min · jiezi

探秘Runtime-剖析Runtime结构体

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/5b7e7c8075ef NSObject之前的定义在OC1.0中,Runtime很多定义都写在NSObject.h文件中,如果之前研究过Runtime的同学可以应该见过下面的定义,定义了一些基础的信息。 // 声明Class和idtypedef struct objc_class *Class;typedef struct objc_object *id;// 声明常用变量typedef struct objc_method *Method;typedef struct objc_ivar *Ivar;typedef struct objc_category *Category;typedef struct objc_property *objc_property_t;// objc_object和objc_classstruct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY;};struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;#endif } OBJC2_UNAVAILABLE;之前的Runtime结构也比较简单,都是一些很直接的结构体定义,现在新版的Runtime在操作的时候,各种地址偏移操作和位运算。 ...

June 24, 2019 · 4 min · jiezi

探秘Runtime-Runtime介绍

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/ce97c66027cd Runtime是iOS系统中重要的组成部分,面试也是必问的问题,所以Runtime是一个iOS工程师必须掌握的知识点。现在市面上有很多关于Runtime的学习资料,也有不少高质量的,但是大多数质量都不是很高,而且都只介绍某个点,并不全面。 这段时间正好公司内部组织技术分享,我分享的主题就是Runtime,我把分享的资料发到博客,大家一起学习交流。 文章都是我的一些笔记,和平时的技术积累。个人水平有限,文章有什么问题还请各位大神指导,谢谢!???? 描述OC语言是一门动态语言,会将程序的一些决定工作从编译期推迟到运行期。由于OC语言运行时的特性,所以其不只需要依赖编译器,还需要依赖运行时环境。 OC语言在编译期都会被编译为C语言的Runtime代码,二进制执行过程中执行的都是C语言代码。而OC的类本质上都是结构体,在编译时都会以结构体的形式被编译到二进制中。Runtime是一套由C、C++、汇编实现的API,所有的方法调用都叫做发送消息。 根据Apple官方文档的描述,目前OC运行时分为两个版本,Modern和Legacy。二者的区别在于Legacy在实例变量发生改变后,需要重新编译其子类。Modern在实例变量发生改变后,不需要重新编译其子类。 Runtime不只是一些C语言的API,其由Class、Meta Class、Instance、Class Instance组成,是一套完整的面向对象的数据结构。所以研究Runtime整体的对象模型,比研究API是怎么实现的更有意义。 使用RuntimeRuntime是一个共享动态库,其目录位于/usr/include/objc,由一系列的C函数和结构体构成。和Runtime系统发生交互的方式有三种,一般都是用前两种: 使用OC源码直接使用上层OC源码,底层会通过Runtime为其提供运行支持,上层不需要关心Runtime运行。NSObject在OC代码中绝大多数的类都是继承自NSObject的,NSProxy类例外。Runtime在NSObject中定义了一些基础操作,NSObject的子类也具备这些特性。Runtime动态库上层的OC源码都是通过Runtime实现的,我们一般不直接使用Runtime,直接和OC代码打交道就可以。使用Runtime需要引入下面两个头文件,一些基础方法都定义在这两个文件中。 #import <objc/runtime.h>#import <objc/message.h>对象模型下面图中表示了对象间isa的关系,以及类的继承关系。 从Runtime源码可以看出,每个对象都是一个objc_object的结构体,在结构体中有一个isa指针,该指针指向自己所属的类,由Runtime负责创建对象。 类被定义为objc_class结构体,objc_class结构体继承自objc_object,所以类也是对象。在应用程序中,类对象只会被创建一份。在objc_class结构体中定义了对象的method list、protocol、ivar list等,表示对象的行为。 既然类是对象,那类对象也是其他类的实例。所以Runtime中设计出了meta class,通过meta class来创建类对象,所以类对象的isa指向对应的meta class。而meta class也是一个对象,所有元类的isa都指向其根元类,根原类的isa指针指向自己。通过这种设计,isa的整体结构形成了一个闭环。 // 精简版定义typedef struct objc_class *Class;struct objc_class : objc_object { // Class ISA; Class superclass;}struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY;};在对象的继承体系中,类和元类都有各自的继承体系,但它们都有共同的根父类NSObject,而NSObject的父类指向nil。需要注意的是,上图中Root Class(Class)是NSObject类对象,而Root Class(Meta)是NSObject的元类对象。 基础定义在objc-private.h文件中,有一些项目中常用的基础定义,这是最新的objc-723中的定义,可以来看一下。 typedef struct objc_class *Class;typedef struct objc_object *id;typedef struct method_t *Method;typedef struct ivar_t *Ivar;typedef struct category_t *Category;typedef struct property_t *objc_property_t;IMP在Runtime中IMP本质上就是一个函数指针,其定义如下。在IMP中有两个默认的参数id和SEL,id也就是方法中的self,这和objc_msgSend()函数传递的参数一样。 ...

June 21, 2019 · 2 min · jiezi

开发函数计算的正确姿势-Fun-validate-语法校验排错指南

1. 前言首先介绍下在本文出现的几个比较重要的概念: 函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息 参考。Fun: Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档 参考。 template.yml: template.yml 用于定义 serverless 应用的模型。无论是使用 fun local 还是 fun deploy 等功能,都是通过解析 tempalte.yml 的内容,构建出用户定义的云资源模型,进而实现本地云资源的运行调试以及发布等功能。template.yml 支持的规范文档可以参考。 template.yml 所描述的 Serverless 模型,是 Fun 所有功能的基石。template.yml 的正确性对后续能够顺利使用 Fun 的各项功能无疑是非常关键的。为了帮助用户更快速的修正 template.yml 中错误的描述,我们在 Fun 2.14.0 优化了语法校验的错误信息,可以达到更精准定位报错并修复的目的。 下面我们就通过一个示例,学习如何根据报错信息纠正 template.yml 中的错误语法描述。 备注:请确保 Fun 工具版本在 2.14.0+ 2. 错误的 template.yml 示例ROSTemplateFormatVersion: '2015-09-01'Transform: 'Aliyun::Serverless-2018-04-03'Resources: local-http-demo: Type: 'Aliyun::Serverless::InvalidService' Properties: Description: 'local invoke demo' nodejs8: Type: 'Aliyun::Serverless::InvalidFunction' Properties: Handler: index.handler CodeUri: nodejs8/ Description: 'http trigger demo with nodejs8!' Events: http-test: Type: HTTP Properties: AuthType: ANONYMOUS Method: ['GET', 'POST', 'PUT']在上面的示例中,我们原意是想要描述一个叫做 local-http-demo 的服务,并在服务下定义了一个名为 nodejs8 的函数,同时为该函数配置一个匿名的 HTTP 触发器,支持 GET、POST、PUT 的 HTTP 请求。 ...

May 22, 2019 · 3 min · jiezi

ObjectiveC-Method-Swizzling

Method Swizzling已经被聊烂了,都知道这是Objective-C的黑魔法,可以交换两个方法的实现。今天我也来聊一下Method Swizzling。 使用方法我们先贴上这一大把代码吧 @interface UIViewController (Swizzling)@end@implementation UIViewController (Swizzling)+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(viewWillAppear:); SEL swizzledSelector = @selector(swizzling_viewWillAppear:); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (success) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } });}#pragma mark - Method Swizzling- (void)swizzling_viewWillAppear:(BOOL)animated { [self swizzling_viewWillAppear:animated]; NSLog(@"==%@",NSStringFromClass([self class]));}@end好的,上面就是Method Swizzling的使用方法,将方法- (void)swizzling_viewWillAppear:(BOOL)animated和系统级方法- (void)viewWillAppear:(BOOL)animated交换。常用的场景就是埋点,这个咱就不细说了。 ...

May 16, 2019 · 2 min · jiezi

看完就懂的无痕埋点

无痕埋点的设计与实现在移动互联网时代,对于每个公司、企业来说,用户的数据非常重要。重要到什么程度,用户在这个页面停留多久、点击了什么按钮、浏览了什么内容、什么手机、什么网络环境、App什么版本等都需要清清楚楚。甚至一些大厂的蛮多业务成果都是靠基于用户操作行为和记录的推荐转换二次。那么有了上述的诉求,那么技术人员如何满足这些需求?引出来了一个技术点-“埋点”埋点手段业界中对于代码埋点主要有3种主流的方案:代码手动埋点、可视化埋点、无痕埋点。简单说说这几种埋点方案。代码手动埋点:根据业务需求(运营、产品、开发多个角度出发)在需要埋点地方手动调用埋点接口,上传埋点数据。可视化埋点:通过可视化配置工具完成采集节点,在前端自动解析配置并上报埋点数据,从而实现可视化“无痕埋点”无痕埋点:通过技术手段,完成对用户行为数据无差别的统计上传的工作。后期数据分析处理的时候通过技术手段筛选出合适的数据进行统计分析。技术选型代码手动埋点该方案情况下,如果需要埋点,则需要在工程代码中,写埋点相关代码。因为侵入了业务代码,对业务代码产生了污染,显而易见的缺点是埋点的成本较高、且违背了单一原则。例1:假如你需要知道用户在点击“购买按钮”时的相关信息(手机型号、App版本、页面路径、停留时间、动作等等),那么就需要在按钮的点击事件里面去写埋点统计的代码。这样明显的弊端就是在之前业务逻辑的代码上面又多出了埋点的代码。由于埋点代码分散、埋点的工作量很大、代码维护成本较高、后期重构很头痛。例2:假如 App 采用了 Hybrid 架构,当 App 的第一版本发布的时候 H5 的关键业务逻辑统计是由 Native 定义好关键逻辑(比如H5调起了Native的分享功能,那么存在一个分享的埋点事件)的桥接。假如某天增加了一个扫一扫功能,未定义扫一扫的埋点桥接,那么 H5 页面变动的时候,Native 埋点代码不去更新的话,变动的 H5 的业务就未被精确统计。优点:产品、运营工作量少,对照业务映射表就可以还原出相关业务场景、数据精细无须大量的加工和处理缺点:开发工作量大、前期需要和运营、产品指定的好业务标识,以便产品和运营进行数据统计分析可视化埋点可视化埋点的出现,是为解决代码埋点流程复杂、成本高、新开发的页面(H5、或者服务端下发的 json 去生成相应页面)不能及时拥有埋点能力前端在「埋点编辑模式」下,以“可视化”的方式去配置、绑定关键业务模块的路径到前端可以唯一确定到view的xpath过程。用户每次操作的控件,都生成一个 xpath 字符串,然后通过接口将 xpath 字符串(view在前端系统中的唯一定位。以 iOS 为例,App名称、控制器名称、一层层view、同类型view的序号:“GoodCell.21.RetailTableView.GoodsViewController.*baoApp”)到真正的业务模块(“宝App-商城控制器-分销商品列表-第21个商品被点击了”)的映射关系上传到服务端。xpath 具体是什么在下文会有介绍。之后操作 App 就生成对应的 xpath 和埋点数据(开发者通过技术手段将从服务端获取的关键数据塞到前端的 UI 控件上。 iOS 端为例, UIView 的 accessibilityIdentifier 属性可以设置我们从服务端获取的埋点数据)上传到服务端。优点:数据量相对准确、后期数据分析成本低缺点:前期控件的唯一识别、定位都需要额外开发;可视化平台的开发成本较高;对于额外需求的分析可能会比较困难无痕埋点通过技术手段无差别地记录用户在前端页面上的行为。可以正确的获取 PV、UV、IP、Action、Time 等信息。缺点:前期开发统计基础信息的技术产品成本较高、后期数据分析数据量很大、分析成本较高(大量数据传统的关系型数据库压力大)优点:开发人员工作量小、数据全面、无遗漏、产品和运营按需分析、支持动态页面的统计分析如何选择结合上述优缺点,我们选择了无痕埋点+可视化埋点结合的技术方案。怎么说呢?对于关键的业务开发结束上线后、通过可视化方案(类似于一个界面,想想看 Dreamwaver,你在界面上拖拖控件,简单编辑下就可以生成对应的 HTML 代码)点击一下绑定对应关系到服务端。那么这个对应关系是什么?我们需要唯一定位一个前端元素,那么想到的办法就是不管 Native 和 Web 前端,控件或者元素来说就是一个树形层级,DOM tree 或者 UI tree,所以我们通过技术手段定位到这个元素,以 Native iOS 为例子假如我点击商品详情页的加入购物车按钮会根据 UI 层级结构生成一个唯一标识 “addCartButton.GoodsViewController.GoodsView.*BaoApp” 。但是用户在使用 App 的时候,上传的是这串东西的 MD5到服务端。这么做有2个原因:服务端数据库存储这串很长的东西不是很好;埋点数据被劫持的话直接看到明文不太好。所以 MD5 再上传。操刀就干数据的收集实现方案由以下几个关键指标:现有代码改动少、尽量不要侵入业务代码去实现拦截系统事件全量收集如何唯一标识一个控件元素不侵入业务代码拦截系统事件以 iOS 为例。我们会想到 AOP(Aspect Oriented Programming)面向切面编程思想。动态地在函数调用前后插入相应的代码,在 Objective-C 中我们可以利用 Runtime 特性,用 Method Swizzling 来 hook 相应的函数为了给所有类方便地 hook,我们可以给 NSObject 添加个 Category,名字叫做 NSObject+MethodSwizzling+ (void)swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{ Class class = [self class]; //原有方法 Method originalMethod = class_getInstanceMethod(class, originalSelector); //替换原有方法的新方法 Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); //先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况 BOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) {//添加成功:表明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else {//添加失败:表明源SEL已经有IMP,直接将两个SEL的IMP交换即可 method_exchangeImplementations(originalMethod, swizzledMethod); }}+ (void)swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{ Class class = [self class]; //原有方法 Method originalMethod = class_getClassMethod(class, originalSelector); //替换原有方法的新方法 Method swizzledMethod = class_getClassMethod(class, swizzledSelector); //先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况 BOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) {//添加成功:表明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP method_exchangeImplementations(originalMethod, swizzledMethod); } else {//添加失败:表明SEL已经有IMP,直接将两个SEL的IMP交换即可 method_exchangeImplementations(originalMethod, swizzledMethod); }}全量收集我们会想到 hook AppDelegate 代理方法、UIViewController 生命周期方法、按钮点击事件、手势事件、各种系统控件的点击回调方法、应用状态切换等等。动作事件App 状态的切换给 Appdelegate 添加分类,hook 生命周期UIViewController 生命周期函数给 UIViewController 添加分类,hook 生命周期UIButton 等的点击UIButton 添加分类,hook 点击事件UICollectionView、UITableView 等的在对应的 Cell 添加分类,hook 点击事件手势事件 UITapGestureRecognizer、UIControl、UIResponder相应系统事件以统计页面的打开时间和统计页面的打开、关闭的需求为例,我们对 UIViewController 进行 hookstatic char *viewController_open_time = “viewController_open_time”;static char *viewController_close_time = “viewController_close_time”;// load 方法里面添加 dispatch_once 是为了防止手动调用 load 方法。+ (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @autoreleasepool { [[self class] swizzleMethod:@selector(viewWillAppear:) swizzledSelector:@selector(viewWillAppear:)]; [[self class] swizzleMethod:@selector(viewWillDisappear:) swizzledSelector:@selector(viewWillDisappear:)]; } });}#pragma mark - add prop- (void)setOpenTime:(NSDate *)openTime{ objc_setAssociatedObject(self,&viewController_open_time, openTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (NSDate *)getOpenTime{ return objc_getAssociatedObject(self, &viewController_open_time);}- (void)setCloseTime:(NSDate *)closeTime{ objc_setAssociatedObject(self,&viewController_close_time, closeTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (NSDate *)getCloseTime{ return objc_getAssociatedObject(self, &viewController_close_time);}- (void)viewWillAppear:(BOOL)animated{ NSString *className = NSStringFromClass([self class]); NSString *refer = [NSString string]; if ([self getPageUrl:className]) { //设置打开时间 [self setOpenTime:[NSDate dateWithTimeIntervalSinceNow:0]]; if (self.navigationController) { if (self.navigationController.viewControllers.count >=2) { //获取当前vc 栈中 上一个VC UIViewController referVC = self.navigationController.viewControllers[self.navigationController.viewControllers.count-2]; refer = [self getPageUrl:NSStringFromClass([referVC class])]; } } if (!refer || refer.length == 0) { refer = @“unknown”; } [SDGDataCenter openPage:[self getPageUrl:className] fromPage:refer]; } [self viewWillAppear:animated];}- (void)viewWillDisappear:(BOOL)animated{ NSString className = NSStringFromClass([self class]); if ([self getPageUrl:className]) { [self setCloseTime:[NSDate dateWithTimeIntervalSinceNow:0]]; [SDGDataCenter leavePage:[self getPageUrl:className] spendTime:[self p_calculationTimeSpend]]; } [self viewWillDisappear:animated];}#pragma mark - private method- (NSString )p_calculationTimeSpend{ if (![self getOpenTime] || ![self getCloseTime]) { return @“unknown”; } NSTimeInterval aTimer = [[self getCloseTime] timeIntervalSinceDate:[self getOpenTime]]; int hour = (int)(aTimer/3600); int minute = (int)(aTimer - hour3600)/60; int second = aTimer - hour3600 - minute60; return [NSString stringWithFormat:@"%d",second];}@end如何唯一标识一个控件元素xpath 是移动端定义可操作区域的唯一标识。既然想通过一个字符串标识前端系统中可操作的控件,那么 xpath 需要2个指标:唯一性:在同一系统中不存在不同控件有着相同的 xpath稳定性:不同版本的系统中,在页面结构没有变动的情况下,不同版本的相同页面,相同的控件的 xpath 需要保持一致。我们想到 Naive、H5 页面等系统渲染的时候都是以树形结构去绘制和渲染,所以我们以当前的 View 到系统的根元素之间的所有关键点(UIViewController、UIView、UIView容器(UITableView、UICollectionView等)、UIButton…)串联起来这样就唯一定位了控件元素。为了精确定位元素节点,参看下图假设一个 UIView 中有三个子 view,先后顺序是:label、button1、button2,那么深度依次为: 0、1、2。假如用户做了某些操作将 label1 从父 view 中被移除了。此时 UIView 只有 2 个子view:button1、button2,而且深度变为了:0、1。可以看出仅仅由于其中某个子 view 的改变,却导致其它子 view 的深度都发生了变化。因此,在设计的时候需要注意,在新增/移除某一 view 时,尽量减少对已有 view 的深度的影响,调整了对节点的深度的计算方式:采用当前 view 位于其父 view 中的所有 与当前 view 同类型 子view 中的索引值。我们再看一下上面的这个例子,最初 label、button1、button2 的深度依次是:0、0、1。在 label 被移除后,button1、button2 的深度依次为:0、1。可以看出,在这个例子中,label 的移除并未对 button1、button2 的深度造成影响,这种调整后的计算方式在一定程度上增强了 xpath 的抗干扰性。另外,调整后的深度的计算方式是依赖于各节点的类型的,因此,此时必须要将各节点的名称放到viewPath中,而不再是仅仅为了增加可读性。在标识控件元素的层级时,需要知道「当前 view 位于其父 view 中的所有 与当前 view 同类型 子view 中的索引值」。参看上图,如果不是同类型的话,则唯一性得不到保证。有个问题,比如我们点击的元素是 UITableViewCell,那么它虽然可以定位到类似于这个标示 xxApp.GoodsViewController.GoodsTableView.GoodsCell,同类型的 Cell 有多个,所以单凭借这个字符串是没有办法定位具体的那个 Cell 被点击了。有2个解决方案利用系统提供的 accessibilityIdentifier 官方给出的解释是标识用户界面元素的字符串找出当前元素在父层同类型元素中的索引。根据当前的元素遍历当前元素的父级元素的子元素,如果出现相同的元素,则需要判断当前元素是所在层级的第几个元素/A string that identifies the user interface element.default == nil/@property(nullable, nonatomic, copy) NSString *accessibilityIdentifier NS_AVAILABLE_IOS(5_0);服务端下发唯一标识接口获取的数据,里面有当前元素的唯一标识。比如在 UITableView 的界面去请求接口拿到数据,那么在在获取到的数据源里面会有一个字段,专门用来存储动态化的经常变动的数据。cell.accessibilityIdentifier = [[[SDGGoodsCategoryServices sharedInstance].categories[indexPath.section] children][indexPath.row].spmContent yy_modelToJSONString];判断在同层级、同类型的控件元素里面的序号对当前的控件元素的父视图的全部子视图进行遍历,如果存在和当前的控件元素同类型的控件,那么需要判断当前控件元素在同类型控件元素中的所处的位置,那么则可以唯一定位。举例:GoodsCell-3.GoodsTableView.GoodsViewController.xxApp//UIResponder分类{ // if (self.xq_identifier_ka == nil) { if ([self isKindOfClass:[UIView class]]) { UIView *view = (id)self; NSString *sameViewTreeNode = [view obtainSameSuperViewSameClassViewTreeIndexPath]; NSMutableString *str = [NSMutableString string]; //特殊的 加减购 因为带有spm但是要区分加减 需要带TreeNode NSString *className = [NSString stringWithUTF8String:object_getClassName(view)]; if (!view.accessibilityIdentifier || [className isEqualToString:@“XQButton”]) { [str appendString:sameViewTreeNode]; [str appendString:@","]; } while (view.nextResponder) { [str appendFormat:@"%@,", NSStringFromClass(view.class)]; if ([view.class isSubclassOfClass:[UIViewController class]]) { break; } view = (id)view.nextResponder; } self.xq_identifier_ka = [self md5String:[NSString stringWithFormat:@"%@",str]]; // self.xq_identifier_ka = [NSString stringWithFormat:@"%@",str]; }// } return self.xq_identifier_ka;}// UIView 分类(NSString *)obtainSameSuperViewSameClassViewTreeIndexPat{ NSString *classStr = NSStringFromClass([self class]); //cell的子view //UITableView 特殊的superview (UITableViewContentView) //UICollectionViewCell BOOL shouldUseSuperView = ([classStr isEqualToString:@“UITableViewCellContentView”]) || ([[self.superview class] isKindOfClass:[UITableViewCell class]])|| ([[self.superview class] isKindOfClass:[UICollectionViewCell class]]); if (shouldUseSuperView) { return [self obtainIndexPathByView:self.superview]; }else { return [self obtainIndexPathByView:self]; }}(NSString )obtainIndexPathByView:(UIView )view{ NSInteger viewTreeNodeDepth = NSIntegerMin; NSInteger sameViewTreeNodeDepth = NSIntegerMin; NSString *classStr = NSStringFromClass([view class]); NSMutableArray *sameClassArr = [[NSMutableArray alloc]init]; //所处父view的全部subviews根节点深度 for (NSInteger index =0; index < view.superview.subviews.count; index ++) { //同类型 if ([classStr isEqualToString:NSStringFromClass([view.superview.subviews[index] class])]){ [sameClassArr addObject:view.superview.subviews[index]]; } if (view == view.superview.subviews[index]) { viewTreeNodeDepth = index; break; } } //所处父view的同类型subviews根节点深度 for (NSInteger index =0; index < sameClassArr.count; index ++) { if (view == sameClassArr[index]) { sameViewTreeNodeDepth = index; break; } } return [NSString stringWithFormat:@"%ld",sameViewTreeNodeDepth]; }## 数据的上传数据通过上面的办法收集完了,那么如何及时、高效的上传到后端,给运营分析、处理呢?App 运行期间用户会点击非常多的数据,如果实时上传的话对于网络的利用率较低,所以需要考虑一个机制去控制用户产生的埋点数据的上传。思路是这样的。对外部暴露出一个接口,用来将产生的数据往数据中心存储。用户产生的数据会先保存到 AppMonitor 的内存中去,设置一个临界值(memoryEventMax = 50),如果存储的值达到设置的临界值 memoryEventMax,那么将内存中的数据写入文件系统,以 zip 的形式保存下来,然后上传到埋点系统。如果没有达到临界值但是存在一些 App 状态切换的情况,这时候需要及时保存数据到持久化。当下次打开 App 就去从本地持久化的地方读取是否有未上传的数据,如果有就上传日志信息,成功后删除本地的日志压缩包。App 应用状态的切换策略如下:- didFinishLaunchWithOptions:内存日志信息写入硬盘- didBecomeActive:上传- willTerimate:内存日志信息写入硬盘- didEnterBackground:内存日志信息写入硬盘// 将App日志信息写入到内存中。当内存中的数量到达一定规模(超过设置的内存中存储的数量)的时候就将内存中的日志存储到文件信息中(void)joinEvent:(NSDictionary *)dictionary{if (dictionary) { NSDictionary *tmp = [self createDicWithEvent:dictionary]; if (!s_memoryArray) { s_memoryArray = [NSMutableArray array]; } [s_memoryArray addObject:tmp]; if ([s_memoryArray count] >= s_flushNum) { [self writeEventLogsInFilesCompletion:^{ [self startUploadLogFile]; }]; }}}// 外界调用的数据传递入口(App埋点统计)(void)traceEvent:(AMStatisticEvent *)event{// 线程锁,防止多处调用产生并发问题@synchronized (self) { if (event && event.userInfo) { [self joinEvent:event.userInfo]; }}}// 将内存中的数据写入到文件中,持久化存储(void)writeEventLogsInFilesCompletion:(void(^)(void))completionBlock{NSArray *tmp = nil;@synchronized (self) { tmp = s_memoryArray; s_memoryArray = nil;}if (tmp) { __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSString *jsonFilePath = [weakSelf createTraceJsonFile]; if ([weakSelf writeArr:tmp toFilePath:jsonFilePath]) { NSString *zipedFilePath = [weakSelf zipJsonFile:jsonFilePath]; if (zipedFilePath) { [AppMonotior clearCacheFile:jsonFilePath]; if (completionBlock) { completionBlock(); } } } });}}// 从App埋点统计压缩包文件夹中的每个压缩包文件上传服务端,成功后就删除本地的日志压缩包(void)startUploadLogFile{NSArray *fList = [self listFilesAtPath:[self eventJsonPath]];if (!fList || [fList count] == 0) { return;}[fList enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { if (![obj hasSuffix:@".zip"]) { return; } NSString *zipedPath = obj; unsigned long long fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:zipedPath error:nil] fileSize]; if (!fileSize || fileSize < 1) { return; } [self uploadZipFileWithPath:zipedPath completion:^(NSString *completionResult) { if ([completionResult isEqual:@“OK”]) { [AppMonotior clearCacheFile:zipedPath]; } }];}];}总结下来关键步骤:1. hook 系统的各种事件(UIResponder、UITableView、UICollectionView代理事件、UIControl事件、UITapGestureRecognizers)、hook 应用程序、控制器生命周期。在做本来的逻辑之前添加额外的监控代码2. 对于点击的元素按照视图树生成对应的唯一标识(addCartButton.GoodsView.GoodsViewController) 的 md5 值3. 在业务开发完毕,进入埋点的编辑模式,将 md5 和关键的页面的关键事件(运营、产品想统计的关键模块:App层级、业务模块、关键页面、关键操作)给绑定起来。比如 addCartButton.GoodsView.GoodsViewController.tbApp 对应了 tbApp-商城模块-商品详情页-加入购物车功能。4. 将所需要的数据存储下来5. 设计机制等到合适的时机去上传数据 ...

April 2, 2019 · 4 min · jiezi

Objective-C 中的消息与消息转发

大家都知道OC是一门动态语言,其动态性由底层的runtime库来支撑实现。OC所有的方法都是通过runtime来发送消息,当我们探讨消息发送,其实也就是在探讨OC方法的调用过程。[receiver message];这是我们很熟悉的一个OC方法的调用,大家都知道这个方法最终会被编译器转换为消息发送函数objc_msgSend(receiver, @selector(message));首先声明咱们这篇文章不去讲解具体的class的数据结构一类的细节问题,我们主要关注的是这个过程。很遗憾,objc_msgSend的实现是用汇编写的,我并不能看懂。但是从runtime的源码中我发现了一个关键的方法://查找方法的实现extern IMP lookUpImpOrForward(Class, SEL, id obj, bool initialize, bool cache, bool resolver);我们要执行一个方法,其实最重要的就是找到这个方法的实现。下面我们来看一下这个lookUpImpOrForward函数的源码。lookUpImpOrForward通过源码中的注释可以看出来,主要的流程分为以下几个阶段:无锁状态下从缓存中查找方法的实现runtimeLock.assertUnlocked();// Optimistic cache lookupif (cache) { imp = cache_getImp(cls, sel); if (imp) return imp;}类的实现和初始化// 判断类是否已经实现if (!cls->isRealized()) { realizeClass(cls);} // 是否初始化if (initialize && !cls->isInitialized()) { runtimeLock.unlock(); _class_initialize (_class_getNonMetaClass(cls, inst)); runtimeLock.lock();在执行这两段代码之前还有两行代码,一个是runtimeLock.lock();,注释里面解释是通过加锁在遇到并发实现类的时候保护实现的过程。另一个是checkIsKnownClass(cls);。看名字是检测这个类是不是我们已知的类,什么情况下可能出现我们未知的类呢?我猜测是通过NSClassFromString()来获取类的时候,如果class的string写错了,就会生成一个未知的类,这时候肯定找不到方法的实现,直接就crash掉行了。我们的Class在底层其实是一个名为objc_class的结构体,大家可以去源码中看一下,这里我们不细说。执行realizeClass方法其实等于对类的第一次初始化,包括配置类的读写空间(class_rw_t)并且返回类的正确的结构体,就相当于搭好了这个类的框架。_class_initialize则是让类执行我们熟悉的+initialize方法。加锁,查找加锁下面就要开始真正的IMP查找了,查找之前有一个runtimeLock.assertLocked();加锁。注释中给出了解释,runtimeLock在查找方法的时候加锁,是为了保持method-lookup(查找方法)和 cache-fill(缓存填充)这两种方法的原子性,// Otherwise, a category could be added but ignored indefinitely because// the cache was re-filled with the old value after the cache flush on// behalf of the category.这几行注释我没读明白啥意思,感觉是说如果不加锁,有添加category的情况时,会导致缓存被冲洗掉。在类中查找通过代码可以看出,首先依然是从缓存中查找,然后在当前的类中查找,最后通过一个循环来从各级父类中查找。这部分代码我们就不展示出来了,大家自己可以看源码,如果找到了这个方法的实现,我们看到会调用log_and_fill_cache这样一个方法,其实就是把这次查找缓存起来,方便下次使用。如果没有找到就进入到下一个环节。动态决议这个名称大家应该是耳熟的,我们都知道消息转发机制,也就是说在我们调用了一个未实现的方法时,并不会直接crash掉,然后报unrecognized selector sent to instance错误。我们的系统会给我们两次机会,第一次就是动态决议。if (resolver && !triedResolver) { runtimeLock.unlock(); _class_resolveMethod(cls, sel, inst); runtimeLock.lock(); // Don’t cache the result; we don’t hold the lock so it may have // changed already. Re-do the search from scratch instead. triedResolver = YES; goto retry;}在OC层面最直接的表现形式就是看我们是否实现了+ (BOOL)resolveClassMethod:(SEL)sel或者+ (BOOL)resolveInstanceMethod:(SEL)sel方法(对应类方法和实例方法)。void _class_resolveMethod(Class cls, SEL sel, id inst){ if (! cls->isMetaClass()) {//不是元类 // try [cls resolveInstanceMethod:sel] _class_resolveInstanceMethod(cls, sel, inst); } else { // try [nonMetaClass resolveClassMethod:sel] // and [cls resolveInstanceMethod:sel] _class_resolveClassMethod(cls, sel, inst); if (!lookUpImpOrNil(cls, sel, inst, NO/initialize/, YES/cache/, NO/resolver/)) { _class_resolveInstanceMethod(cls, sel, inst); } }}根据cls是否为元类来调用_class_resolveInstanceMethod或_class_resolveClassMethod。/************************************************************************ lookUpImpOrNil.* Like lookUpImpOrForward, but returns nil instead of _objc_msgForward_impcache**********************************************************************/IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver){ IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver); // 这个imp或者是一个正经的IMP指针,或者是一个汇编的入口 //_objc_msgForward_impcache 是一个汇编的入口 (看名字应该是消息转发相关的) // 本方法是不进行消息转发的 ~ 所以如果获取到的IMP是这个入口,就直接return nil if (imp == _objc_msgForward_impcache) return nil; else return imp;}通过lookUpImpOrNil的源码我们知道,其实它封装了lookUpImpOrForward,不进行lookUpImpOrForward中最后的消息转发这一步,而且在这个if中,参数resolver传入了NO,也就是动态决议这一步也不进行,只进行了方法在本类和各级父类中的查找,如果找不到,则跟非元类一样执行_class_resolveInstanceMethod。else中为什么要再执行这一段if代码,我不是很理解。具体的动态决议实现的代码,我们看其中一个就行来看,用_class_resolveInstanceMethod当例子吧static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst){ if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, NO/initialize/, YES/cache/, NO/resolver/)) { // Resolver not implemented. // 没有找到SEL_resolveInstanceMethod(resolveInstanceMethod)方法 return; } // 下面应该是类型转换的代码而已吧? BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; // 执行实现的+resolveInstanceMethod方法 bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); // Cache the result (good or bad) so the resolver doesn’t fire next time. // +resolveInstanceMethod adds to self a.k.a. cls //通过+resolveInstanceMethod动态添加了方法,在进行一次查找 IMP imp = lookUpImpOrNil(cls, sel, inst, NO/initialize/, YES/cache/, NO/resolver/); if (resolved && PrintResolving) { if (imp) { _objc_inform(“RESOLVE: method %c[%s %s] " “dynamically resolved to %p”, cls->isMetaClass() ? ‘+’ : ‘-’, cls->nameForLogging(), sel_getName(sel), imp); } else { // Method resolver didn’t add anything? _objc_inform(“RESOLVE: +[%s resolveInstanceMethod:%s] returned YES” “, but no new implementation of %c[%s %s] was found”, cls->nameForLogging(), sel_getName(sel), cls->isMetaClass() ? ‘+’ : ‘-’, cls->nameForLogging(), sel_getName(sel)); } }}一开始先通过调用lookUpImpOrNil来查找是否已经实现了+resolveInstanceMethod,没有实现就直接返回了。如果实现了,通过objc_msgSend来执行实现的+resolveInstanceMethod方法。我们在+resolveInstanceMethod方法中都是动态的添加方法,所以在执行完之后在进行一次查找。消息转发如果在动态决议之后,依然没有找到方法,那我们还有最后一次机会,那就是消息转发。imp = (IMP)_objc_msgForward_impcache;cache_fill(cls, sel, imp, inst);很遗憾_objc_msgForward_impcache汇编实现看不懂。不过我们还是可以从另一个方向来研究消息转发到底做了什么。先补充一个骚操作,这也是我这两天刚学到的,就是打印runtime的代码执行日志,这么说可能不太贴切,反正就是能看到我们执行一个方法的过程中调用了哪些方法。想要开始打印的地方加上下面代码extern void instrumentObjcMessageSends(BOOL);instrumentObjcMessageSends(YES);想要关闭的地方加上下面代码extern void instrumentObjcMessageSends(BOOL);instrumentObjcMessageSends(NO);当然还有别的方式,大家可以去查一下。首先我们新建一个工程,类型就选择macOS->Common Line Tool。因为我用iOS的工程不管用。main函数中这样写#import <Foundation/Foundation.h>#import <objc/runtime.h>#import “Sark.h"int main(int argc, const char * argv[]) { @autoreleasepool { extern void instrumentObjcMessageSends(BOOL); instrumentObjcMessageSends(YES); Sark * test = [[Sark alloc] init]; [test performSelector:@selector(xxx)]; instrumentObjcMessageSends(NO); } return 0;}创建SEL选择子的时候我们故意穿进去一个xxx,就是为了让类找不到方法,然后走消息转发的的这个过程。执行一下代码,运行时发送的所有消息都会打印到/private/tmp/msgSend-xxxx文件里了。(这是电脑系统的路径)如果找起来不方便可以直接使用下面的命令行打开该文件。open /private/tmp/从该路径的msgSend-xxx文件中我们找到了这么一部分代码+ Sark NSObject initialize+ Sark NSObject alloc- Sark NSObject init- Sark NSObject performSelector:+ Sark NSObject resolveInstanceMethod:+ Sark NSObject resolveInstanceMethod:- Sark NSObject forwardingTargetForSelector:- Sark NSObject forwardingTargetForSelector:- Sark NSObject methodSignatureForSelector:- Sark NSObject methodSignatureForSelector:- Sark NSObject class- Sark NSObject doesNotRecognizeSelector:- Sark NSObject doesNotRecognizeSelector:- Sark NSObject class我们可以看到在执行完了resolveInstanceMethod之后又执行forwardingTargetForSelector:和methodSignatureForSelector:,最后才因为找不到方法执行doesNotRecognizeSelector:。现在我们可以大概了解在消息转发的过程中执行了哪些方法了,经过查阅资料我们得出:全部的消息转发过程做了如下几件事:(包括动态决议+消息转发)调用resolveInstanceMethod:方法,允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回。如果仍没实现,继续下面的动作。调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。调用forwardInvocation:方法,将地3步获取到的方法签名包装成Invocation传入,如何处理就在这里面了。上面这4个方法均是模板方法,开发者可以override,由runtime来调用。具体如何重写使用这几个方法,大家可以自己查一下。参考资料https://blog.ibireme.com/2013…https://github.com/draveness/… ...

March 20, 2019 · 2 min · jiezi

Objective-C runtime学习 - 上 - 瞎逼逼

前言入坑iOS开发已经一年半,对所学所用的知识却一直没有好好做梳理,这里梳理一下我对Objective-C runtime的学习和理解。runtime是OC这个语言的核心,想要弄明白runtime,就不得不从语言设计层面开始说起,本篇主要讲讲这些宏观的东西,下篇再关注其实现细节与应用。正文20世纪80年代,大家逐渐意识到在复杂系统开发和GUI编程中,面向对象编程有巨大的价值。由于当时C语言的广泛应用,一个比较直接的想法就是,在C语言的基础上扩展出面向对象能力,能够最大程度上减少开发者的迁移成本。于是Objective-C和C++都在这段时间里诞生了。不过这两种语言在实现上都有着巨大的区别。Objective-C选择使用C语言实现一套面向对象的库(我们称之为Objective-C的runtime),并给Objective-C添加了一些面向对象的语法。编译时,Objective-C的语法被处理为C的语法,然后和runtime库放到一起编译。因此,Objective-C可以基于runtime实现一些动态特性,我的程序里打包了一个运行时系统嘛。而C++选择完全在编译期实现其面向对象特性。这两种截然不同的实现,反映了设计者对语言动态特性的不同考量。其中最重要的一点是,对“面向对象”这一编程范式的理念不同。面向对象思想意味着把程序抽象为各种独立而又互相调用的对象。在语言设计上,这里需要面对一个具体的问题,对象间的通信,应当是种什么形式?直觉上,对象间的通信,直接用方法调用的形式就好了。世界上第一个面向对象的语言Simula就是这么实现的。但实际上会面临一些问题。具体来讲,当你调用一个对象的一个方法时,你怎么知道它有这个方法呢?这意味着你要确定这个对象的类型,也就是说,你必须先知道一个对象的类型才能对它进行调用。在很多场景下,这种限制是不必要的。于是,世界上第二个面向对象的语言Smalltalk进行了一些改进,它实现了一套与目标对象无关的消息传递机制。消息的发送方可以不关注接收方的类型,也不关注接收方是否可以正常处理这条消息。而消息的接收方需要对收到的消息做处理,有对应的方法实现就去调用,没有就走异常处理流程。C++传承了Simula的“静态消息机制”,而Objective-C传承了Smalltalk的“动态消息机制”。这两者的区别看似没那么大,却在很大程度上影响了之后面向对象体系的发展。回到我们的Objective-C,为了实现这种“动态消息机制”,通常需要一个运行时系统来处理。Objective-C的发明者选择了最简单有效的方式,实现一套简单的运行时系统跟程序打包到一起。比起后来Java的虚拟机或JavaScript的执行引擎,Objective-C的实现充分利用了原有的C编译器,只要给Objective-C做个预处理器,把Objective-C代码转成C代码然后和runtime库放一起用C的编译器编译就可以了,实现难度和工作量都是比较低的。此外,这样的实现保证了Objective-C跟C的完全兼容,可以说,Objective-C代码在编译时就是C代码,因此可以和C或C++混编。结语总体而言,Objective-C的语言设计至少在当时还是比较优越的。“动态消息机制”现在看来还是带来了一些好处的。而在C的基础上添加一个很薄的runtime层来为C提供面向对象机制,也是个很合适的做法。后记想要从宏观上理解清楚Objective-C runtime的来龙去脉,其实需要对整个面向对象的发展史有足够的理解,对面向对象的各种实现的优缺点有一定认知,显然我的能力是远远不够的。本文也只能简单梳理我个人的一点理解,可能还有颇多错漏。另外,Objective-C的语言设计在当时有着不少优点的,但是Objective-C的语言发展已经停滞不前多年,比起一些热门的语言它在一定程度上是落后不少的,而苹果已经拥抱swift,因此这种停滞可能是永久性的。扩展阅读function/bind的救赎(上)——孟岩C++多继承有什么坏处,Java的接口为什么可以摈弃这些坏处? - invalid s的回答 - 知乎

March 10, 2019 · 1 min · jiezi

深入学习runtime

本文的切入点是2014年的一场线下分享会,也就是sunnyxx分享的objc runtime。很惭愧,这么多年了才完整的看了一下这个分享会视频。当时他出了一份试题,并戏称精神病院objc runtime入院考试。我们今天的这篇文章就是从这个试题中的题目入手,来深入的学习runtime。源码版本objc4-750第一题@implementation Son : Father- (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self;}@end第一行的[self class]应该是没有疑问的,肯定是Son,问题就出在这个[super class]。大家都知道,我们OC的方法在底层会编译为一个objc_msgSend的方法(消息发送),[self class]符合这个情况,因为self是类的一个隐藏参数。但是super并不是一个参数,它是一个关键字,实际上是一个“编译器标示符”,所以这就有点不一样了,经查阅资料,在调用[super class]的时候,runtime调用的是objc_msgSendSuper方法,而不是objc_msgSend。首先要做的是验证一下是否是调用了objc_msgSendSuper。这里用到了clang这个工具,我们可以把OC的代码转成C/C++。@implementation Son- (void)test { [super class];}@end在终端运行clang -rewrite-objc Son.m生成一个Son.cpp文件。在这个.cpp文件的底部我们可以找到这么一部分代码// @implementation Sonstatic void _I_Son_test(Son * self, SEL _cmd) { ((Class (*)(__rw_objc_super *, SEL))(void )objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass(“Son”))}, sel_registerName(“class”));}// @end看起来乱七八糟,有很多强制类型转换的代码,不用理它,我们只要看到了我们想要的objc_msgSendSuper就好。去源码中看一下这个方法(具体实现好像是汇编,看不懂)OBJC_EXPORT voidobjc_msgSendSuper(void / struct objc_super *super, SEL op, … / ) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);可以看出来这个方法第一个参数是一个objc_super类型的结构体,第二个是一个我们常见的SEL,后面的…代表还有扩展参数。再看一下这个objc_super结构体。/// Specifies the superclass of an instance. struct objc_super { /// Specifies an instance of a class. __unsafe_unretained _Nonnull id receiver; /// Specifies the particular superclass of the instance to message. #if !defined(__cplusplus) && !OBJC2 / For compatibility with old objc-runtime.h header 为了兼容老的 / __unsafe_unretained _Nonnull Class class;#else __unsafe_unretained _Nonnull Class super_class;#endif / super_class is the first class to search */};第一个参数是接收消息的receiver,第二个是super_class(见名知意~ ????)。我们和上面提到的.cpp中的代码对应一下就会发现重点了,receiver是self。所以,这个[super class]的工作原理是,从objc_super结构体的super_class指向类的方法列表开始查找class方法,找到这个方法之后使用receiver来调用。所以,调用class方法的其实还是self,结果也就是打印Son。第二题下面代码的结果?BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];对于这个问题我们就要从OC类的结构开始说起了。我们都应该有所了解,每一个Objective-c的对象底层都是一个C语言的结构体,在之前老的源码中体现出,所有对象都包含一个isa类型的指针,在新的源码中已经不是这样了,用一个结构体isa_t代替了isa。这个isa_t结构体包含了当前对象指向的类的信息。我们来看看当前的类的结构,首先从我们的祖宗类NSObject开始吧。@interface NSObject <NSObject> { Class isa OBJC_ISA_AVAILABILITY;}我们的NSObject类有一个Class类型的变量isa,通过源码我们可以了解到这个Class到底是什么typedef struct objc_class *Class;typedef struct objc_object *id;struct objc_object {private: isa_t isa;}struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags}上面的代码是我从源码中复制拼到一起来的。可以看出来,Class就是是一个objc_class结构体,objc_class中有四个成员变量Class superclass,cache_t cache,class_data_bits_t bits,和从objc_object中继承过来的isa_t isa。当Objc为一个对象分配内存,初始化实例变量后,在这些实例变量的结构体中第一个就是isa。而且从上面的objc_class的结构可以看出来,不仅仅是实例会包含一个isa结构体,所有的类也会有这个isa。所以说,我们可以得出这样一个结论:Objective-c中的类也是一个对象。那现在就有了一个新的问题,类的isa结构体中储存的是什么?这里就要引入一个元类的概念。知识补充:在Objective-c中,每个对象能执行的方法并没有存在这个对象中,因为如果每一个对象都单独储存可执行的方法,那对内存来说是一个很大的浪费,所以说每个对象可执行的方法,也就是我们说的一个类的实例方法,都储存在这个类的objc_class结构体中的class_data_bits_t结构体里面。在执行方法是,对象通过自己的isa找到对应的类,然后在class_data_bits_t中查找方法实现。关于方法的结构,可以看这篇博客来理解一些。(跳转链接)引入元类就是来保证了实例方法和类方法查找调用机制的一致性。所以让一个类的isa指向他的元类,这样的话,对象调用实例方法可以通过isa找到对应的类,然后查找方法的实现并调用,在调用类方法的时候,通过类的isa找到对应的元类,在元类里完成类方法的查找和调用。下面这种图也是在网上很常见的了,不需要过多解释,大家看一下记住就行了。看到这里我们就要回到我们的题目上了。首先呢,还是要去看一下这个源码中isKindOfClass:和isMemberOfClass:的实现了。isKindOfClass先看isKindOfClass吧,源码中提供了一个类方法一个实例方法。+ (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO;}- (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO;}总体的逻辑都是一样的,都是先声明一个Class类型的tcls,然后把这个tcls跟cls比较,看是否相等,如果不相等则循环tcls的各级superclass来进行比较,直到为tcls为nil停止循环。不同的地方就是类方法初始的tcls是object_getClass((id)self),实例方法的是[self class]。object_getClass((id)self)其实是返回了这个self的isa对应的结构,因为这个方法是在类方法中调用的,self则代表这个类,那object_getClass((id)self)返回的也应该是这个类的元类了。其实在-isKindOfClass这个实例方法中,调用方法的是一个对象,tcls初始等于[self class],也就是对相对应的类。我们可以看出来,在实例方法中这个tcls初始的值也是方法调用者的isa对应的结构,跟类方法中逻辑是一致的。回到我们的题目中,BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];[NSObject class]也就是NSObject类调用这个isKindOfClass:方法(类方法),方法的参数也是NSObject的类。在第一次循环中,tcls对应的应该是NSObject的isa指向的,也就是NSObject的元类,它跟NSObject类不相等。第二次循环,tcls取自己的superclass继续比较,我们上面的那个图,大家可以看一下,NSObject的元类的父类就是NSObject这个类本身,在与NSObject比较结果是相等。所以res1为YES。BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];跟上面一样来分析,在第一次循环中,tcls对应的应该是Sark的isa指向的,也就是Sark的元类,跟Sark的类相比,肯定是不相等。第二次循环,tcls取superclass,从图中可以看出,Sark元类的父类是NSObject的元类,跟Sark的类相比,肯定也是不相等。第三次循环,NSObject元类的父类是NSObject类,也不相等。再取superclass,NSObject的superclass为nil,循环结束,返回NO,所以res3是NO。isMemberOfClass+ (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls;}- (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls;}有了上面isKindOfClass逻辑分析的基础,isMemberOfClass的逻辑我们应该很清楚,就是使用方法调用者的isa对应的结构和传入的cls参数比较。BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];NSObject类的isa对应的是NSObject的元类,和NSObject类相比不相等,所以res2为NO。Sark类的isa对应的是Sark的元类,和Sark类相比也是不相等,所以,res4也是NO。第三题下面的代码会?Compile Error / Runtime Crash / NSLog…?@interface NSObject (Sark)+ (void)foo;@end@implementation NSObject (Sark)- (void)foo { NSLog(@“IMP: -[NSObject (Sark) foo]”);}@end// 测试代码[NSObject foo];[[NSObject new] foo];[[NSObject new] foo];这一个代码应该是毫无疑问会调用到-foo方法。问题就在这个[NSObject foo],因为在我们的认识中[NSObject foo]是调用的类方法,实现的是实例方法,应该不能调用到。其实这个题的考点跟第二个题差不多,我们已经知道了,一个类的实例方法储存在类中,类方法储存在这个类的元类。所以NSObject在调用foo这个方法是,会先去NSObject的元类中找这个方法,没有找到,那就要去父类中继续查找。上面图已经给出了,NSObject的元类的父类是NSObject类,所以在NSObject中查找方法,找到方法之后执行打印。第四题下面的代码会?Compile Error / Runtime Crash / NSLog…?@interface Sark : NSObject@property (nonatomic, copy) NSString *name;@end@implementation Sark- (void)speak { NSLog(@“my name’s %@”, self.name);}@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; id cls = [Sark class]; void *obj = &cls; [(__bridge id)obj speak];}@end这里我们先上结果:my name’s <ViewController: 0x7f9454c1c680>不管地址是多少,打印的总是ViewController。我们先想一下为什么可以成功的调用speak?id cls = [Sark class];创建了一个Sark的class。void *obj = &cls;创建一个obj指针指向了cls的地址。最后使用(__bridge id)obj把这个obj指针转成一个oc的对象,用对象来调用speak,所以可以调用成功。我们在方法中输出的是self.name,为什么会打印出来ViewController?经过查阅资料得知,在调用self.name的时候,本质上是self指针在内存向高位地址偏移一个指针。(这个还得以后深入研究)为了验证一下查到的这个结论,我改写了一下speak方法中的代码如下。- (void)speak { unsigned int count = 0; Ivar * ivars = class_copyIvarList([self class], &count); for (int i = 0; i < count; i ++) { Ivar ivar = ivars[i]; ptrdiff_t offSet = ivar_getOffset(ivar); const char * n = ivar_getName(ivar); NSLog(@"%@—–%ld",[NSString stringWithUTF8String:n],offSet); } NSLog(@“my name’s %@”, self.name);}取到类的各个变量,然后打印出他的偏移。输出结构如下:_name—–8偏移了一个指针。那为什么打印出来了ViewController的地址,我们就要研究各个变量的内存地址位置关系了。在iewDidLoad中变量的压栈顺序如下所示:第一个参数self和第二个参数_cmd是隐藏参数,第三和第四个参数是执行[super viewDidLoad]之后进栈的,之前第一题的时候我们有了解过,super调用的方法在底层编译之后会有一个objc_super类型的结构体。在结构体中有receiver和super_class两个变量,receiver就是self。我在网上查过很多的资料,都是super_class比receiver(self)先入栈,不太懂为什么是super_class先入。最后是生成的obj进栈。所以在打印self.name的时候,是obj的指针向高位偏移了一个指针,也就是self,所以打印出来的是ViewController的指针。参考https://github.com/draveness/…http://blog.sunnyxx.com/2014/...https://www.jianshu.com/p/743...https://github.com/ming1016/s… ...

March 7, 2019 · 2 min · jiezi

函数运行环境系统动态链接库版本太低?函数计算 fun 神助力分忧解难

背景最近在处理线上工单的时候,遇到一个用户使用 nodejs runtime 时因为函数计算运行环境的 gcc 版本过低导致无法运行的问题,觉得非常有意思,所以深入的帮用户寻找了解决方案。觉得这个场景应该具有一定的通用性,所以在这篇文章里面重点的介绍一下如何使用函数计算的周边工具 fun 解决因为 runtime 中系统版本导致的各种兼容性问题。场景介绍用户问题简要描述一下用户当时遇到的问题:用户使用函数计算的 nodejs8 runtime,在本地自己的开发环境使用 npm install couchbase 安装了 couchbase 这个第三方库。couchbase 封装了 C 库,依赖系统底层动态链接库 libstdc++.so.6。因为用户自己的开发环境的操作系统内核比较新,所以本地安装、编译和调试都比较顺利。所以,最后按照函数计算的打包方式成功创建了 Function,但是执行 InvokeFunction 时,遇到了这样的错误:“errorMessage”: “/usr/lib/x86_64-linux-gnu/libstdc++.so.6: version CXXABI_1.3.9' not found (required by /code/node_modules/couchbase/build/Release/couchbase_impl.node)", "errorType": "Error", "stackTrace": [ "Error: /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version CXXABI_1.3.9’ not found (required by /code/node_modules/couchbase/build/Release/couchbase_impl.node)”,…错误发生的原因如堆栈描述,即没有 CXXABI_1.3.9 这个版本,可以看到函数计算 nodejs 环境中的支持情况:root@1fe79eb58dbd:/code# strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 |grep CXXABI_ CXXABI_1.3CXXABI_1.3.1CXXABI_1.3.2CXXABI_1.3.3CXXABI_1.3.4CXXABI_1.3.5CXXABI_1.3.6CXXABI_1.3.7CXXABI_1.3.8CXXABI_TM_1升级底层系统版本的代价比较大,需要长时间的稳定性、兼容性测试和观察,所以,为了支持这类使用场景,我们希望能够有比较简单的方式绕行。场景复现和问题解决前提:先按照 fun 的安装步骤安装 fun工具,并进行 fun config 配置。在本地很快搭建了一个项目目录:- test_code/ - index.js - template.yml其中 index.js 和 template.yml 的 内容分别为# index.jsconst couchbase = require(‘couchbase’).Mock;module.exports.handler = function(event, context, callback) { var cluster = new couchbase.Cluster(); var bucket = cluster.openBucket(); bucket.upsert(’testdoc’, {name:‘Frank’}, function(err, result) { if (err) throw err; bucket.get(’testdoc’, function(err, result) { if (err) throw err; console.log(result.value); // {name: Frank} }); }); callback(null, { hello: ‘world’ })}# template.yml ROSTemplateFormatVersion: ‘2015-09-01’Transform: ‘Aliyun::Serverless-2018-04-03’Resources: fc: # service name Type: ‘Aliyun::Serverless::Service’ Properties: Description: ‘fc test’ helloworld: # function name Type: ‘Aliyun::Serverless::Function’ Properties: Handler: index.handler Runtime: nodejs8 CodeUri: ‘./’ Timeout: 60为了能够在本地模拟函数计算的真实环境进行依赖包安装和调试,这里生成一个 fun.yml 文件用于 fun install 安装使用,内容如下:runtime: nodejs8tasks: - shell: |- if [ ! -f /code/.fun/root/usr/lib/x86_64-linux-gnu/libstdc++.so.6 ]; then mkdir -p /code/.fun/tmp/archives/ curl http://mirrors.ustc.edu.cn/debian/pool/main/g/gcc-6/libstdc++6_6.3.0-18+deb9u1_amd64.deb -o /code/.fun/tmp/archives/libstdc++6_6.3.0-18+deb9u1_amd64.deb bash -c ‘for f in $(ls /code/.fun/tmp/archives/*.deb); do dpkg -x $f /code/.fun/root; done;’ rm -rf /code/.fun/tmp/archives fi - name: install couchbase shell: npm install couchbasefun.yml中参数说明:前面的分析已经了解到函数计算 nodejs8 runtime 的 libstdc++.so.6 的版本偏低,所以,我们找到一个更新的版本来支持,见新版本的 libstdc++.so.6 的 CXXABI_ 参数:$strings .fun/root/usr/lib/x86_64-linux-gnu/libstdc++.so.6|grep CXXABI_CXXABI_1.3CXXABI_1.3.1CXXABI_1.3.2CXXABI_1.3.3CXXABI_1.3.4CXXABI_1.3.5CXXABI_1.3.6CXXABI_1.3.7CXXABI_1.3.8CXXABI_1.3.9CXXABI_1.3.10CXXABI_TM_1CXXABI_FLOAT128执行 fun install 命令安装各种第三方依赖,显示如下:本地执行情况执行 fun local invoke helloworld,可以看到执行成功的效果:$fun local invoke helloworld begin pullling image aliyunfc/runtime-nodejs8:1.4.0………………………………………………………pull image finishedpull image finishedFC Invoke Start RequestId: 78e20963-b314-4d69-843a-35a3f465796cload code for handler:index.handlerFC Invoke End RequestId: 78e20963-b314-4d69-843a-35a3f465796c{“hello”:“world”}2019-02-19T08:16:45.073Z 78e20963-b314-4d69-843a-35a3f465796c [verbose] { name: ‘Frank’ }发布上线使用 fun deploy 发布上线,然后到控制台执行一下线上实际的运行效果:总结fun install 功能能够将代码和依赖文件分离开,独立安装系统依赖文件,而且 fun local 和 fun deply 都能够自动帮你设置第三方库的依赖引用路径,让您无需关心环境变量问题。本文的解法只是提供了一个对于系统版本偏低无法满足用户一些高级库使用需求时的简单绕行方案,仅供参考,对于一些复杂的环境依赖问题,可能还需要具体情况具体分析。更多参考:函数计算 nodejs runtimefun local99dXnVk4I)fun install本文作者:清宵阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 21, 2019 · 2 min · jiezi

Objective-C中的associated object释放时机问题

如果对象A持有对象B,B作为A的associated object,并且表面上B没有其他被强引用的地方,那么对象A被释放时,对象B一定会同时释放吗?大部分情况下是,但真有不是的时候。最近实现代码的时候不小心就碰到了这样的特殊情况。需求需要监听对象A释放(dealloc)并执行对象A的a方法。此时引入对象B,并作为对象A的associated object。A释放时触发B释放,在B的dealloc方法中执行A的a方法。对象B需要一个指向对象A的属性,并声明为unsafe_unretained(或assign),因为weak指针此时已经失效了。示例代码@interface MyObject1 : NSObject@end@implementation MyObject1- (void)foo { NSLog(@“success”);}@end@interface MyObject2 : NSObject@property (nonatomic, unsafe_unretained) MyObject1 *obj1;@end@implementation MyObject2- (void)dealloc { [self.obj1 foo];}+ (instancetype)create { return [[self class] new];}@end@implementation ViewController+ (void)load { [self fun1];}+ (void)fun1 { MyObject1 *mo1 = [MyObject1 new]; @synchronized (self) { MyObject2 *mo2 = [MyObject2 create]; mo2.obj1 = mo1; objc_setAssociatedObject(mo1, @selector(viewDidLoad), mo2, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }}@end问题运行时出现崩溃,unsafe_unretained指针已经野了,和预期的不一样。堆栈是这样的:观察崩溃的堆栈,发现mo2对象是被自动释放池释放了。因为mo1对象是在函数退出时就立即释放,这样导致mo1比mo2先被销毁,mo2访问了无效指针导致了崩溃。这个问题和@synchronized有关系,但目前我还不知道它和arc之间有什么联系。下面给出另一个case,修改一行代码就不会崩溃了:+ (void)fun2 { MyObject1 *mo1 = [MyObject1 new]; MyObject2 *mo2 = [MyObject2 create]; @synchronized (self) { mo2.obj1 = mo1; objc_setAssociatedObject(mo1, @selector(viewDidLoad), mo2, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }}实际上只是把mo2的声明移动到了@synchronized外面,堆栈变成了这样:这时,mo2的释放发生在调用方法的结束时。分析使用Hooper查看汇编代码,观察fun1和fun2的不同。节选出关键部分:fun1:fun2:核心在于:fun1中,创建mo2后调用了retain,fun2中,调用的则是objc_retainAutoreleasedReturnValue。我们再来看看create方法:关键的一行在最后,调用了objc_autoreleaseReturnValue。关于objc_retainAutoreleasedReturnValue和objc_autoreleaseReturnValue,请移步 https://www.jianshu.com/p/2f05060fa377 。大意是,这两个方法成对出现时,可以优化掉[[obj autorelease] retain]这种骚操作。结论在fun1中,由于没有objc_retainAutoreleasedReturnValue,取而代之的是retain,导致对象被放入自动释放池。对于@synchronized为什么会造成不同,我还没有那么深入。因为全局自动释放池会延迟对象的释放,如果代码非常依赖对象的释放时机则会比较危险。我认为这样做是最保险的,创建一个局部自动释放池,保证局部变量在函数结束时立即释放:+ (void)fun3 { MyObject1 *mo1 = [MyObject1 new]; @autoreleasepool { @synchronized (self) { MyObject2 *mo2 = [MyObject2 create]; mo2.obj1 = mo1; objc_setAssociatedObject(mo1, @selector(viewDidLoad), mo2, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } }}参考资料objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue函数对ARC的优化 https://www.jianshu.com/p/2f05060fa377本文作者:三豊阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 19, 2019 · 1 min · jiezi

Golang - 调度剖析【第三部分】

本篇是调度剖析的第三部分,将重点关注并发特性。回顾:第一部分第二部分简介首先,在我平时遇到问题的时候,特别是如果它是一个新问题,我一开始并不会考虑使用并发的设计去解决它。我会先实现顺序执行的逻辑,并确保它能正常工作。然后在可读性和技术关键点都 Review 之后,我才会开始思考并发执行的实用性和可行性。有的时候,并发执行是一个很好的选择,有时则不一定。在本系列的第一部分中,我解释了系统调度的机制和语义,如果你打算编写多线程代码,我认为这些机制和语义对于实现正确的逻辑是很重要的。在第二部分中,我解释了Go 调度的语义,我认为它能帮助你理解如何在 Go 中编写高质量的并发程序。在这篇文章中,我会把系统调度和Go 调度的机制和语义结合在一起,以便更深入地理解什么才是并发以及它的本质。什么是并发并发意味着乱序执行。拿一组原来是顺序执行的指令,而后找到一种方法,使这些指令乱序执行,但仍然产生相同的结果。那么,顺序执行还是乱序执行?根本在于,针对我们目前考虑的问题,使用并发必须是有收益的!确切来说,是并发带来的性能提升要大于它带来的复杂性成本。当然有些场景,代码逻辑就已经约束了我们不能执行乱序,这样使用并发也就没有了意义。并发与并行理解并发与并行的不同也非常重要。并行意味着同时执行两个或更多指令,简单来说,只有多个CPU核心之间才叫并行。在 Go 中,至少要有两个操作系统硬件线程并至少有两个 Goroutine 时才能实现并行,每个 Goroutine 在一个单独的系统线程上执行指令。如图:我们看到有两个逻辑处理器P,每个逻辑处理器都挂载在一个系统线程M上,而每个M适配到计算机上的一个CPU处理器Core。其中,有两个 Goroutine G1 和 G2 在并行执行,因为它们同时在各自的系统硬件线程上执行指令。再看,在每一个逻辑处理器中,都有三个 Goroutine G2 G3 G5 或 G1 G4 G6 轮流共享各自的系统线程。看起来就像这三个 Goroutine 在同时运行着,没有特定顺序地执行它们的指令,并在系统线程上共享时间。那么这就会发生竞争,有时候如果只在一个物理核心上实现并发则实际上会降低吞吐量。还有有意思的是,有时候即便利用上了并行的并发,也不会给你带来想象中更大的性能提升。工作负载我们怎么判断在什么时候并发会更有意义呢?我们就从了解当前执行逻辑的工作负载类型开始。在考虑并发时,有两种类型的工作负载是很重要的。两种类型CPU-Bound:这是一种不会导致 Goroutine 主动切换上下文到等待状态的类型。它会一直不停地进行计算。比如说,计算 到第 N 位的 Goroutine 就是 CPU-Bound 的。IO-Bound:与上面相反,这种类型会导致 Goroutine 自然地进入到等待状态。它包括请求通过网络访问资源,或使用系统调用进入操作系统,或等待事件的发生。比如说,需要读取文件的 Goroutine 就是 IO-Bound。我把同步事件(互斥,原子),会导致 Goroutine 等待的情况也包含在此类。在 CPU-Bound 中,我们需要利用并行。因为单个系统线程处理多个 Goroutine 的效率不高。而使用比系统线程更多的 Goroutine 也会拖慢执行速度,因为在系统线程上切换 Goroutine 是有时间成本的。上下文切换会导致发生STW(Stop The World),意思是在切换期间当前工作指令都不会被执行。在 IO-Bound 中,并行则不是必须的了。单个系统线程可以高效地处理多个 Goroutine,是因为Goroutine 在执行这类指令时会自然地进入和退出等待状态。使用比系统线程更多的 Goroutine 可以加快执行速度,因为此时在系统线程上切换 Goroutine 的延迟成本并不会产生STW事件。进入到IO阻塞时,CPU就闲下来了,那么我们可以使不同的 Goroutine 有效地复用相同的线程,不让系统线程闲置。我们如何评估一个系统线程匹配多少 Gorountine 是最合适的呢?如果 Goroutine 少了,则会无法充分利用硬件;如果 Goroutine 多了,则会导致上下文切换延迟。这是一个值得考虑的问题,但此时暂不深究。现在,更重要的是要通过仔细推敲代码来帮助我们准确识别什么情况需要并发,什么情况不能用并发,以及是否需要并行。加法我们不需要复杂的代码来展示和理解这些语义。先来看看下面这个名为add的函数:1 func add(numbers []int) int {2 var v int3 for _, n := range numbers {4 v += n5 }6 return v7 }在第 1 行,声明了一个名为add的函数,它接收一个整型切片并返回切片中所有元素的和。它从第 2 行开始,声明了一个v变量来保存总和。然后第 3 行,线性地遍历切片,并且每个数字被加到v中。最后在第 6 行,函数将最终的总和返回给调用者。问题:add函数是否适合并发执行?从大体上来说答案是适合的。可以将输入切片分解,然后同时处理它们。最后将每个小切片的执行结果相加,就可以得到和顺序执行相同的最终结果。与此同时,引申出另外一个问题:应该分成多少个小切片来处理是性能最佳的呢?要回答此问题,我们必须知道它的工作负载类型。add函数正在执行 CPU-Bound 工作负载,因为实现算法正在执行纯数学运算,并且它不会导致 Goroutine 进入等待状态。这意味着每个系统线程使用一个 Goroutine 就可以获得不错的吞吐量。并发版本下面来看一下并发版本如何实现,声明一个 addConcurrent 函数。代码量相比顺序版本增加了很多。1 func addConcurrent(goroutines int, numbers []int) int {2 var v int643 totalNumbers := len(numbers)4 lastGoroutine := goroutines - 15 stride := totalNumbers / goroutines67 var wg sync.WaitGroup8 wg.Add(goroutines)910 for g := 0; g < goroutines; g++ {11 go func(g int) {12 start := g * stride13 end := start + stride14 if g == lastGoroutine {15 end = totalNumbers16 }1718 var lv int19 for _, n := range numbers[start:end] {20 lv += n21 }2223 atomic.AddInt64(&v, int64(lv))24 wg.Done()25 }(g)26 }2728 wg.Wait()2930 return int(v)31 }第 5 行:计算每个 Goroutine 的子切片大小。使用输入切片总数除以 Goroutine 的数量得到。第 10 行:创建一定数量的 Goroutine 执行子任务第 14-16 行:子切片剩下的所有元素都放到最后一个 Goroutine 执行,可能比前几个 Goroutine 处理的数据要多。第 23 行:将子结果追加到最终结果中。然而,并发版本肯定比顺序版本更复杂,但和增加的复杂性相比,性能有提升吗?值得这么做吗?让我们用事实来说话,下面运行基准测试。基准测试下面的基准测试,我使用了1000万个数字的切片,并关闭了GC。分别有顺序版本add函数和并发版本addConcurrent函数。func BenchmarkSequential(b *testing.B) { for i := 0; i < b.N; i++ { add(numbers) }}func BenchmarkConcurrent(b *testing.B) { for i := 0; i < b.N; i++ { addConcurrent(runtime.NumCPU(), numbers) }}无并行以下是所有 Goroutine 只有一个硬件线程可用的结果。顺序版本使用 1 Goroutine,并发版本在我的机器上使用runtime.NumCPU或 8 Goroutines。在这种情况下,并发版本实际正跑在没有并行的机制上。10 Million Numbers using 8 goroutines with 1 core2.9 GHz Intel 4 Core i7Concurrency WITHOUT Parallelism—————————————————————————–$ GOGC=off go test -cpu 1 -run none -bench . -benchtime 3sgoos: darwingoarch: amd64pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/cpu-boundBenchmarkSequential 1000 5720764 ns/op : ~10% FasterBenchmarkConcurrent 1000 6387344 ns/opBenchmarkSequentialAgain 1000 5614666 ns/op : ~13% FasterBenchmarkConcurrentAgain 1000 6482612 ns/op结果表明:当只有一个系统线程可用于所有 Goroutine 时,顺序版本比并发快约10%到13%。这和我们之前的理论预期相符,主要就是因为并发版本在单核上的上下文切换和 Goroutine 管理调度的开销。有并行以下是每个 Goroutine 都有单独可用的系统线程的结果。顺序版本使用 1 Goroutine,并发版本在我的机器上使用runtime.NumCPU或 8 Goroutines。在这种情况下,并发版本利用上了并行机制。10 Million Numbers using 8 goroutines with 8 cores2.9 GHz Intel 4 Core i7Concurrency WITH Parallelism—————————————————————————–$ GOGC=off go test -cpu 8 -run none -bench . -benchtime 3sgoos: darwingoarch: amd64pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/cpu-boundBenchmarkSequential-8 1000 5910799 ns/opBenchmarkConcurrent-8 2000 3362643 ns/op : ~43% FasterBenchmarkSequentialAgain-8 1000 5933444 ns/opBenchmarkConcurrentAgain-8 2000 3477253 ns/op : ~41% Faster结果表明:当为每个 Goroutine 提供单独的系统线程时,并发版本比顺序版本快大约41%到43%。这才也和预期一致,所有 Goroutine 现都在并行运行着,意味着他们真的在同时执行。排序另外,我们也要知道并非所有的 CPU-Bound 都适合并发。当切分输入或合并结果的代价非常高时,就不太合适。下面展示一个冒泡排序算法来说明此场景。顺序版本01 package main0203 import “fmt"0405 func bubbleSort(numbers []int) {06 n := len(numbers)07 for i := 0; i < n; i++ {08 if !sweep(numbers, i) {09 return10 }11 }12 }1314 func sweep(numbers []int, currentPass int) bool {15 var idx int16 idxNext := idx + 117 n := len(numbers)18 var swap bool1920 for idxNext < (n - currentPass) {21 a := numbers[idx]22 b := numbers[idxNext]23 if a > b {24 numbers[idx] = b25 numbers[idxNext] = a26 swap = true27 }28 idx++29 idxNext = idx + 130 }31 return swap32 }3334 func main() {35 org := []int{1, 3, 2, 4, 8, 6, 7, 2, 3, 0}36 fmt.Println(org)3738 bubbleSort(org)39 fmt.Println(org)40 }这种排序算法会扫描每次在交换值时传递的切片。在对所有内容进行排序之前,可能需要多次遍历切片。那么问题:bubbleSort函数是否适用并发?我相信答案是否定的。原始切片可以分解为较小的,并且可以同时对它们排序。但是!在并发执行完之后,没有一个有效的手段将子结果的切片排序合并。下面我们来看并发版本是如何实现的。并发版本01 func bubbleSortConcurrent(goroutines int, numbers []int) {02 totalNumbers := len(numbers)03 lastGoroutine := goroutines - 104 stride := totalNumbers / goroutines0506 var wg sync.WaitGroup07 wg.Add(goroutines)0809 for g := 0; g < goroutines; g++ {10 go func(g int) {11 start := g * stride12 end := start + stride13 if g == lastGoroutine {14 end = totalNumbers15 }1617 bubbleSort(numbers[start:end])18 wg.Done()19 }(g)20 }2122 wg.Wait()2324 // Ugh, we have to sort the entire list again.25 bubbleSort(numbers)26 }bubbleSortConcurrent它使用多个 Goroutine 同时对输入的一部分进行排序。我们直接来看结果:Before: 25 51 15 57 87 10 10 85 90 32 98 53 91 82 84 97 67 37 71 94 26 2 81 79 66 70 93 86 19 81 52 75 85 10 87 49After: 10 10 15 25 32 51 53 57 85 87 90 98 2 26 37 67 71 79 81 82 84 91 94 97 10 19 49 52 66 70 75 81 85 86 87 93由于冒泡排序的本质是依次扫描,第 25 行对 bubbleSort 的调用将掩盖使用并发解决问题带来的潜在收益。结论是:在冒泡排序中,使用并发不会带来性能提升。读取文件前面已经举了两个 CPU-Bound 的例子,下面我们来看 IO-Bound。顺序版本01 func find(topic string, docs []string) int {02 var found int03 for _, doc := range docs {04 items, err := read(doc)05 if err != nil {06 continue07 }08 for _, item := range items {09 if strings.Contains(item.Description, topic) {10 found++11 }12 }13 }14 return found15 }第 2 行:声明了一个名为 found 的变量,用于保存在给定文档中找到指定主题的次数。第 3-4 行:迭代文档,并使用read函数读取每个文档。第 8-11 行:使用 strings.Contains 函数检查文档中是否包含指定主题。如果包含,则found加1。然后来看一下read是如何实现的。01 func read(doc string) ([]item, error) {02 time.Sleep(time.Millisecond) // 模拟阻塞的读03 var d document04 if err := xml.Unmarshal([]byte(file), &d); err != nil {05 return nil, err06 }07 return d.Channel.Items, nil08 }此功能以 time.Sleep 开始,持续1毫秒。此调用用于模拟在我们执行实际系统调用以从磁盘读取文档时可能产生的延迟。这种延迟的一致性对于准确测量find顺序版本和并发版本的性能差距非常重要。然后在第 03-07 行,将存储在全局变量文件中的模拟 xml 文档反序列化为struct值。最后,将Items返回给调用者。并发版本01 func findConcurrent(goroutines int, topic string, docs []string) int {02 var found int640304 ch := make(chan string, len(docs))05 for _, doc := range docs {06 ch <- doc07 }08 close(ch)0910 var wg sync.WaitGroup11 wg.Add(goroutines)1213 for g := 0; g < goroutines; g++ {14 go func() {15 var lFound int6416 for doc := range ch {17 items, err := read(doc)18 if err != nil {19 continue20 }21 for _, item := range items {22 if strings.Contains(item.Description, topic) {23 lFound++24 }25 }26 }27 atomic.AddInt64(&found, lFound)28 wg.Done()29 }()30 }3132 wg.Wait()3334 return int(found)35 }第 4-7 行:创建一个channel并写入所有要处理的文档。第 8 行:关闭这个channel,这样当读取完所有文档后就会直接退出循环。第 16-26 行:每个 Goroutine 都从同一个channel接收文档,read 并 strings.Contains 逻辑和顺序的版本一致。第 27 行:将各个 Goroutine 计数加在一起作为最终计数。基准测试同样的,我们再次运行基准测试来验证我们的结论。func BenchmarkSequential(b *testing.B) { for i := 0; i < b.N; i++ { find(“test”, docs) }}func BenchmarkConcurrent(b *testing.B) { for i := 0; i < b.N; i++ { findConcurrent(runtime.NumCPU(), “test”, docs) }}无并行10 Thousand Documents using 8 goroutines with 1 core2.9 GHz Intel 4 Core i7Concurrency WITHOUT Parallelism—————————————————————————–$ GOGC=off go test -cpu 1 -run none -bench . -benchtime 3sgoos: darwingoarch: amd64pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/io-boundBenchmarkSequential 3 1483458120 ns/opBenchmarkConcurrent 20 188941855 ns/op : ~87% FasterBenchmarkSequentialAgain 2 1502682536 ns/opBenchmarkConcurrentAgain 20 184037843 ns/op : ~88% Faster当只有一个系统线程时,并发版本比顺序版本快大约87%到88%。与预期一致,因为所有 Goroutine 都有效地共享单个系统线程。有并行10 Thousand Documents using 8 goroutines with 8 core2.9 GHz Intel 4 Core i7Concurrency WITH Parallelism—————————————————————————–$ GOGC=off go test -run none -bench . -benchtime 3sgoos: darwingoarch: amd64pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/io-boundBenchmarkSequential-8 3 1490947198 ns/opBenchmarkConcurrent-8 20 187382200 ns/op : ~88% FasterBenchmarkSequentialAgain-8 3 1416126029 ns/opBenchmarkConcurrentAgain-8 20 185965460 ns/op : ~87% Faster有意思的来了,使用额外的系统线程提供并行能力,实际代码性能却没有提升。也印证了开头的说法。结语我们可以清楚地看到,使用 IO-Bound 并不需要并行来获得性能上的巨大提升。这与我们在 CPU-Bound 中看到的结果相反。当涉及像冒泡排序这样的算法时,并发的使用会增加复杂性而没有任何实际的性能优势。所以,我们在考虑解决方案时,首先要确定它是否适合并发,而不是盲目认为使用更多的 Goroutine 就一定会提升性能。 ...

December 11, 2018 · 5 min · jiezi