侧滑手势问题

因为发现自己的项目里面的FD 侧滑,有bug 看到这个文章觉得很有道理记录下

From: https://juejin.im/post/5adeda3051882567336a5dc9

序言

   在ios7以后,苹果推出了手势滑动返回功能,也就是从屏幕左侧向右滑动可返回上一个界面。大大提高了APP在大屏手机和iPad上的操作体验,场景切换更加流畅。做右滑返回手势配置时,可能会遇到的问题:

   1. 右滑返回手势为什么失效?

   2. 右滑返回手势如何全局开启及怎么避免页面卡死?

   3. 特定页面停用右滑手势后如何再次开启?

   4. 右滑返回手势与滚动视图手势冲突怎么解决?

   5. 全屏右滑返回怎么设置?

问题分析

右滑返回手势为什么失效?

   右滑返回手势失效主要是因为自定义了页面中navigationItem的leftBarButtonItem或leftBarButtonItems,或是self.navigationItem.hidesBackButton = YES;隐藏了返回按钮,亦或是self.navigationItem.leftItemsSupplementBackButton = NO;,让我们来梳理下。    UINavigationItem(Apple文档)是一个常见的类,然而还有不少开发者对该类了解甚少,这里注重说明下backBarButtonItemleftBarButtonItemrightBarButtonItemleftItemsSupplementBackButton四个属性。leftBarButtonItem、rightBarButtonItem是在当前页面设置,并展示在当前页面的navigationItem上。backBarButtonItem若是在当前页面设置,却展示在次级页面navigationItem上。

   比如在AViewController push BViewController时,在A设置了self.navigationItem.backBarButtonItem的title和image,经过试验发现,这个backBarButtonItem为BViewController的self.navigationController.navigationBar.backItem.backBarButtonItem。虽然self.navigationController.navigationBar.backItem.backBarButtonItem 是读写属性,但是self.navigationController、self.navigationController.navigationBar、 self.navigationController.navigationBar.backItem,都是readonly属性,因此backBarButtonItem,只能在AViewController中定义并在Push:BViewController之前进行设置。leftBarButtonItem、rightBarButtonItem可以在BViewController的ViewDidLoad后设置。

   注意backBarButtonItem只能自定义image和title,不能重写target 或 action,系统会忽略其他的相关设置项。如果硬是需要重写action做一些其他的工作,则需要自定义一个leftBarButtonItem。    系统默认情况下leftBarButtonItem的优先级是要高于backBarButtonItem的,当存在leftBarButtonItem时,自动忽略backBarButtonItem,达到重写backBarButtonItem的目的,但会造成右滑返回手势的响应代理从当前页面被覆盖性移除。同时,系统也提供了leftItemsSupplementBackButton属性来控制backBarButtonItem 是否被 leftBarButtonItem “覆盖”,默认值是NO,若配置leftBarButtonItem,还需要有返回按钮和右滑手势,需要在leftBarButtonItem或leftBarButtonItems后,把leftItemsSupplementBackButton,设置为YES。

特定页面停用右滑手势?

   如左右分页浏览、看视频、看音频、支付等特定页面场景,是“不希望”用户便捷离开的,或有弹窗提示的需求,也有避免用户误操作的考虑。同时,可能存在右滑返回手势冲突,或右滑返回后可能有音频焦点不能及时释放的问题。怎么做呢?我们可以通过代码设置停用右滑返回手势,或改用presentViewController方式加载页面。

恢复右滑手势的解决方案

方案一 手势代理替换

   系统的自带的有返回箭头和上级页面title的返回按钮,我们无需设置,系统自动生成,默认tintColor为蓝色。然而,这样的样式并不是我们想要的。我们通常做法是去,设置该页面的leftBarButtonItem或leftBarButtonItems,来自定义返回按钮的样式。通过上面的问题分析,我们可以知道,leftBarButtonItem或leftBarButtonItems 直接覆盖了self.navigationController.navigationBar.backItem.backBarButtonItem,造成右滑返回手势的响应代理从当前页面被覆盖性移除,造成右滑返回手势失效。我们可以通过在上个页面设置self.navigationItem.backBarButtonItem,并在下个页面设置self.navigationItem.leftItemsSupplementBackButton = YES。没有做基类管理的项目可能到处都是自定义leftBarButtonItem或leftBarButtonItem,工作量较大。快上车,让老司机带你一程!

保留系统的右滑返回手势

   既然设置backBarButtonItem较为繁杂,我们可以换个思路,手势已被覆盖性移除,我们需要给页面添加上右滑返回手势。若项目有全局的UINavigationController基类,实现下列参考代码:

@implementation YGNavigationController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //设置右滑返回手势的代理为自身
    __weak typeof(self) weakself = self;
    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
        self.interactivePopGestureRecognizer.delegate = (id)weakself;
    }
}


//这个方法是在手势将要激活前调用:返回YES允许右滑手势的激活,返回NO不允许右滑手势的激活
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if (gestureRecognizer == self.interactivePopGestureRecognizer) {
        //屏蔽调用rootViewController的滑动返回手势,避免右滑返回手势引起死机问题
        if (self.viewControllers.count < 2 ||
 self.visibleViewController == [self.viewControllers objectAtIndex:0]) {
            return NO;
        }
    }
    //这里就是非右滑手势调用的方法啦,统一允许激活
    return YES;
}
复制代码

   将项目中的使用UINavigationController 替换为UINavigationController基类,自定义返回按钮设置不变,恢复了右滑返回手势。注意:导航栏的左侧也是支持右滑返回手势,若有UIViewController基类也可以参照上面设置代码调整设置,来消除导航栏的左侧小区域的右滑返回。

   一定要实现UIGestureRecognizerDelegate 并做rootViewController 判断,否则,在rootViewController页面会存在右滑返回死机的问题。

特定页面停用右滑手势

   我们查看UINavigationController 文档,可以找到

@property(nullable, nonatomic, readonly) UIGestureRecognizer *interactivePopGestureRecognizer NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;
复制代码

   可以通过设置页面的VC.navigationController.interactivePopGestureRecognizer.enabled 来控制当前页面的右滑返回手势是否可用。我们可以创建一个UIViewController 的分类创建两个类方法。

+ (void)popGestureClose:(UIViewController *)VC
{
    // 禁用侧滑返回手势
    if ([VC.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
        //这里对添加到右滑视图上的所有手势禁用
        for (UIGestureRecognizer *popGesture in VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers) {
            popGesture.enabled = NO;
        }
        //若开启全屏右滑,不能再使用下面方法,请对数组进行处理
        //VC.navigationController.interactivePopGestureRecognizer.enabled = NO;
    }
}

+ (void)popGestureOpen:(UIViewController *)VC
{
    // 启用侧滑返回手势
    if ([VC.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
    //这里对添加到右滑视图上的所有手势启用
        for (UIGestureRecognizer *popGesture in VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers) {
            popGesture.enabled = YES;
        }
        //若开启全屏右滑,不能再使用下面方法,请对数组进行处理
        //VC.navigationController.interactivePopGestureRecognizer.enabled = YES;
    }
}
复制代码

   具体怎么使用呢?我们需要在停用右滑返回手势的页面实现以下两个方法,经过多次调试验证,必须是以下两个方法。停用当前页面后,不影响上级页面和下级页面的右滑返回。

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [UIViewController popGestureClose:self];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [UIViewController popGestureOpen:self];
}

复制代码

方案二 原生态:自定义backBarButtonItem

   网上的思路大多是基于方案一,这是我在研究方案一中回溯思路得出的一个方案,直接利用系统的backBarButtonItem和右滑返回手势特性,相对更稳定,更高效,我想iOS系统APP的右滑返回设计应是这个“官方思路”。

保留系统的右滑返回手势

   这里需要对每个页面设置自己的backBarButtonItem,就像设置每个页面的leftBarButtonItem的思路一样。但是backBarButtonItem是一个特殊的按钮,可以说只响应页面的返回和销毁,表现为只能自定义image和title,不能重写target 或 action。来让我们自定义以下backBarButtonItem。参照问题分析的思路,须在AViewController中实现下列参考代码:

    UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil];
    //自定义返回按钮的视图,如细化返回图标。
     [self.navigationController.navigationBar setBackIndicatorImage:[UIImage imageNamed:@"navi_back_icon"]];
     [self.navigationController.navigationBar setBackIndicatorTransitionMaskImage:[UIImage imageNamed:@"navi_back_icon"]];
     //设置tintColor 改变自定图片颜色
     self.navigationController.navigationBar.tintColor = [UIColor whiteColor];
     //设置自定义的返回按钮
     self.navigationItem.backBarButtonItem = backItem;
复制代码

   按照上面的创建思路,已经完成页面自定义返回按钮,并保留了右滑返回手势(注意:导航栏的左侧是不只支持右滑返回手势,这里和方案一有一点区别)。在AViewController push BViewController 或 CViewController 都不需要在再重定义leftBarButtonItem,来实返回按钮了。依次实现各个控制器的backBarButtonItem,即可完成整个APP的右滑返回手势功能,当然以上代码我们可以封装到一个UIViewController基类并在ViewDidLoad方法中来统一设置,或者封装一个工具方法统一调用,当新的页面页面需要不同的返回样式时,在push页面CViewController之前,重新创建backBarButtonItem覆盖即可。    注意:因系统backBarButtonItem中封装的UIButton使用的左图右标题的布局样式和通常的UIButton上图下标题的布局样式有一定的差别,造成即使标题为空,返回按钮的图标的位置依然偏左,我们可以通过UIBarButtonItem的UIBarButtonSystemItemFixedSpace来调图标位置或者设置占位符标题增大手势响应区域。

特定页面停用右滑手势或左侧新添按钮

   怎么做呢?自定义leftBarButtonItem或leftBarButtonItems,并设置leftItemsSupplementBackButton = YES。参考代码:

 //自定义返回按钮
     UIButton *studySearch = [UIButton buttonWithType:UIButtonTypeCustom];
     [studySearch setImage:[UIImage imageNamed:@"study_search"] forState:UIControlStateNormal];
     [studySearch sizeToFit];
     [studySearch addTarget:self action:@selector(studySearchAction) forControlEvents:UIControlEventTouchUpInside];
    UIBarButtonItem *studySearchItem = [[UIBarButtonItem alloc] initWithCustomView:studySearch];
     self.navigationItem.leftBarButtonItems = @[studySearchItem];
     //是否支持显示左滑返回按钮,NO不显示:leftBarButtonItems覆盖backBarButtonItem,
     //YES显示:backBarButtonItem 显示在leftBarButtonItems左侧
     self.navigationItem.leftItemsSupplementBackButton = YES;
复制代码

   leftItemsSupplementBackButton必须在自定义leftBarButtonItem或leftBarButtonItems后才有效。

方案三 完全自定义导航栏

   有些项目中的导航栏或导航控制器是完全自定义的,具体的实现的可以参照方案一实施,这里不再做深入探究。

右滑返回引起手势的冲突

   方案二不会存在方案一中的卡死现象。iOS系统中,滑动返回手势其实是一个UIPanGestureRecognizer,UIScrollView的滑动手势也是UIPanGestureRecognizer,UIPanGestureRecognizer接收顺序和UIView的层次结构是一致的。

UINavigationController.view —>  UIViewController.view —>  UIScrollView —>  Screen and User's finger
复制代码

   原理:UIScrollView(包括子类UITextView、UITableView、UICollectionView)的panGestureRecognizer先接收到手势事件,直接处理后不在往下传递。实际上这就是两个panGestureRecognizer共存的问题。scrollView的pan手势会让系统的pan手势失效,当UIScrollView(UICollectionView)有多页的时候也会出现滑动返回失效的情况,我们需要在scrollView的位置在初始位置的时候,让两个手势同时启用。 可以创建UIScrollView的类别category,然后在此类别中实现以下方法即可:

@implementation UIScrollView (PopGesture)

//此方法返回YES时,手势事件会一直往下传递,不论当前层次是否对该事件进行响应。
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if ([self panBack:gestureRecognizer]) {
        return YES;
    }
    return NO;
}

//location_X可自己定义,其代表的是滑动返回距左边的有效长度
- (BOOL)panBack:(UIGestureRecognizer *)gestureRecognizer
{
    //是滑动返回距左边的有效长度
    int location_X = 40;
    if (gestureRecognizer == self.panGestureRecognizer) {
        UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;
        CGPoint point = [pan translationInView:self];
        UIGestureRecognizerState state = gestureRecognizer.state;
        if (UIGestureRecognizerStateBegan == state || UIGestureRecognizerStatePossible == state) {
            CGPoint location = [gestureRecognizer locationInView:self];
            //下面的是只允许在第一张时滑动返回生效
            if (point.x > 0 && location.x < location_X && self.contentOffset.x <= 0) {
                return YES;
            }
         //   这是允许每张图片都可实现滑动返回
         //   int temp1 = location.x;
         //   int temp2 = SCREEN_WIDTH;
         //   NSInteger XX = temp1 % temp2;
         //   if (point.x > 0 && XX < location_X) {
         //      return YES;
         //   }
        }
    }
    return NO;
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if ([self panBack:gestureRecognizer]) {
        return NO;
    }
    return YES;
}

@end
复制代码

右滑返回的全屏幕设置

   随着手机屏幕的变大,原来右滑返回略显不够人性化,尤其是对手小的朋友,如何能愉快的单手玩手机呢。对于app要全屏右滑或保持原生边缘触发,各有说辞,这里不讨论其好坏,根据产品需要而定。我们在方案一的基础上,创建一个屏幕手势,添加到原来的self.interactivePopGestureRecognizer.view 右滑返回手势的视图上,即是讲手势添加到VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers数组中,添加手势必须在设置代理之前完成

- (void)viewDidLoad
{
    [super viewDidLoad];
    //设全屏启动右滑返回手势,此处可以优化为iPad 上支持全屏
    if ((UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)) {
        id target = self.interactivePopGestureRecognizer.delegate;
        SEL handler = NSSelectorFromString(@"handleNavigationTransition:");
        // 获取添加系统边缘触发手势的View
        UIView *targetView = self.interactivePopGestureRecognizer.view;
        // 创建pan手势 作用范围是全屏
        UIPanGestureRecognizer *fullScreenGes = [[UIPanGestureRecognizer alloc]initWithTarget:target action:handler];
        fullScreenGes.delegate = self;
        [targetView addGestureRecognizer:fullScreenGes];
        // 关闭边缘触发手势 防止和原有边缘手势冲突(也可不用关闭)
        [self.interactivePopGestureRecognizer setEnabled:NO];
    }
    //设置右滑返回手势的代理为自身
    __weak typeof(self) weakself = self;
    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
        self.interactivePopGestureRecognizer.delegate = (id)weakself;
    }
}
复制代码

   注意: 系统在self.interactivePopGestureRecognizer.view上已经添加有VC.navigationController.interactivePopGestureRecognizer手势,也可以在VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers数组中取出,此时数组中,有两个响应手势。因此对方案一中的手势控制就要使用数组形式的处理方式。

for (UIGestureRecognizer *popGesture in VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers) {
            popGesture.enabled = NO;
        }
复制代码

总结

   iOS开发都是基于苹果系统的开发,设置系统级全局性的功能时,最好选择系统或在系统的基础上自定义,尽量少些自以为是的完全自定义,少些奇葩设计,好的内容才是一个产品的核心,好的产品体验也是用户留存的粘合剂!

原文

Objective-C Runtime使用之全局字体替换为第三方字体

From: https://www.cnblogs.com/n1ckyxu/p/6084544.html

前言:

  iOS开发里头,常用的设置字体方式是使用UIFont的systemFontOfSize这个Class Method,在一半情况下都算够用。

最近有设计师朋友问能不能在客户端中使用特定的字体,答案是可以的,我们可以通过手动给工程添加配置字体的ttf文件(字体库)

然后通过fontWithName:name size:size这个 Class Method即可选用,然而在一个已经经过长时间开发的客户端,会有历史遗漏问题

导致整个工程的字体配置可能存在修改工作量大,改漏改错等情况,针对这种情况我们也可以通过runtime来解决。

1、导入第三方字体

首先需要下载一个.ttf为后缀的文件,也就是字体库。下载后将文件导入工程,如图

接着需要在工程配置info.plist中添加这个字体

在info.plist中添加一行,key是Fonts provided by application,中文意思即 字体由应用程序提供

这是个array对象,那么我们把它展开

往里面添加一个item,内容即我们刚刚添加的那个文件名

然后在Build Phases里添加资源文件 如图

接下来可以在工程中,通过UIFont 这个类 遍历我们现在可以用的字体集和字体名字

遍历代码如下

NSArray *fontFamilys = [UIFont familyNames];
for (NSString *familyName in fontFamilys) {
    NSLog(@"family name : %@",familyName);
    NSArray *fontNames = [UIFont fontNamesForFamilyName:familyName];
    for (NSString *fontName in fontNames) {
        NSLog(@"font name : %@",fontName);
    }
}

注意 ,不同的iOS大版本之间,可使用的字体库会有差异,但是我们这里只需要取到我们手动添加的字体

遍历出来的内容很多,不翻页也不好找到我们添加的字体。

我这里添加的字体是微软雅黑,那么我搜一下

也是可以找到的,这里我们需要取font name,即图上的2016-11-21 09:49:45.780 FontDemo[17853:921926] font name : MicrosoftYaHei

取到字体名字,我们就可以通过

[UIFont fontWithName:@"MicrosoftYaHei" size:16];

fontWithName: size: 这个类方法去得到我们需要的UIFont对象,也就是雅黑字体

——————————不华丽的分割线————————–

好了,单个字体的更换这里是实现了,但是我这里需要的是全局的字体修改

接下来的内容又要接触到objc runtime 的method exchange了,也就是method swizzling

在Objective-c中,hook方案能解决很多问题,这里的问题是其中之一

但是这种全局设置的方法交换也有一定的局限性,比如 我需要再换其他字体呢? 这个问题后面再探讨

开始设置method swizzling

首先 建立一个UIFont的categroy

在.m文件中 实现load方法,并调用父类load

+ (void)load{
    [super load];
}

接着 做method swizzling的过程 只需要调用一次,

那么可以用gcd的once 执行,

+ (void)load{
    [super load];
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oldMethod = class_getClassMethod([self class], @selector(systemFontOfSize:));
        Method newMethod = class_getClassMethod([self class], @selector(__nickyfontchanger_YaheiFontOfSize:));
        method_exchangeImplementations(oldMethod, newMethod);
    });
}

别忘了#import

解析一下上面这几句代码

首先Method即方法,class_getClassMethod这是获取类方法,因为我们原来使用的systemFontOfSize是个类方法。

如果要交换的是实例方法,那么就要用class_getInstanceMethod 获取

先获取旧的方法,再获取新的方法,新的方法是写在这个category里的

像我这里:

+ (UIFont *)__nickyfontchanger_YaheiFontOfSize:(CGFloat)fontSize{
    UIFont *font = [UIFont fontWithName:@"MicrosoftYaHei" size:fontSize];
    if (!font)return [self __nickyfontchanger_YaheiFontOfSize:fontSize];
    return font;
}

再来解析一下这个方法的执行:

首先获取我们的第三方字体,若字体不存在,则返回系统默认字体

但是为什么我返回系统默认字体的时候,调用的是 [self __nickyfontchanger_YaheiFontOfSize:fontSize]呢?

因为方法已经交换了,实际上这个方法的pointer指向的是系统的systemFontOfSize这个方法

具体的实现

那么再运行一下工程看看?

ps:问题来了

我要单独给某个字体设置成系统字体怎么办?

事实上我们这里只是把两个方法交换了而已,所以我们只要把+ (UIFont *)__nickyfontchanger_YaheiFontOfSize:(CGFloat)fontSize;这个方法写到.h的声明里面即可,它实际就是系统字体

如有错误欢迎更正

XCode 9.3 新的编译选项,优化 Swift 编译生成代码的尺寸

From: https://swiftcafe.io/post/swift-optimization

XCode 9.3 新的编译选项,优化 Swift 编译生成代码的尺寸

swift 发布于 2018年04月08日


在 Swift 4.1 的编译器中,提供了一个新的优化选项。可以减少代码生成尺寸。 对生成的代码进行优化,是很多编译器都提供的功能,Swift 之前版本的编译器其实也提供了优化功能,如果你打开 XCode 项目的 Build Settings, 就可以找到一个叫做 Optimization Level 的选项,默认情况下,XCode 会对 Release 下的项目默认开启编译器优化:

这个选项在实际编译的时候,会给编译器传递一个 -O 参数,它就代表进行性能优化。

新增的编译选项

XCode 9.3,除了上述的性能优化之外,还提供了另外一个新的选项 -Osize, 对代码尺寸进行优化:

从上图中可以看到 -O-Osize 两个参数是互斥的,只能选一个。 -O 前面我们说过了,是对代码的执行速度进行优化,但执行速度提升了,就会牺牲一部分代码空间。

反之 -Osize 是专门为节省代码空间这个目的而来的优化选项。 那么他们分别能带来多少收益和损失呢? 这点在 swift.org 的官方 Blog 也有介绍:

-Osize 根据项目不同,大致可以优化掉 5% - 30% 的代码空间占用。 相比 -0 来说,会损失大概 5% 的运行时性能。 如果你的项目对运行速度不是特别敏感,并且可以接受轻微的性能损失,那么 -Osize 就值得一用。

Single File 和 Whole Module

除了 -O-Osize, 还有另外一个概念也值得说一下。 就是 Single FileWhole Module 。 在之前的 XCode 版本,这两个选项和 -O 是连在一起设置的,Xcode 9.3 中,将他们分离出来,可以独立设置:

Single FileWhole Module 这两个模式分别对应编译器以什么方式处理优化操作。

Single File 是逐个文件进行优化,它的好处是对于增量编译的项目来说,它可以减少编译时间,对没有更改的源文件,不用每次都重新编译。并且可以充分利用多核 CPU,并行优化多个文件,提高编译速度。

但它的缺点就是对于一些需要跨文件的优化操作,它没办法处理。如果某个文件被多次引用,那么对这些引用方文件进行优化的时候,会反复的重新处理这个被引用的文件,如果你项目中类似的交叉引用比较多,就会影响性能。

Whole Module 则是将项目所有的文件看做一个整体,不会产生 Single File 模式对同一个文件反复处理的问题,并且可以进行最大限度的优化,包括跨文件的优化操作。

缺点是,不能充分利用多核处理器的性能,并且对于增量编译,每次也都需要重新编译整个项目。

XCode 的默认设置使用的是 Whole Module 模式。 我特意搜索了一下,在 Stack Overflow 上面找到了一个比较好的总结,也给大家贴出来参考一下:

这篇帖子的地址是 https://stackoverflow.com/questions/47998583/whats-the-difference-between-single-file-optimization-vs-whole-module-optimizat

对于这个选项,我的理解是,如果没有特殊情况,使用默认的 Whole Module 优化即可。 它会牺牲部分编译性能,但的优化结果是最好的。

何为编译优化

关于编译优化,这话话题其实不小。 简单来说,现在我们用高级语言写出的代码,更大程度上是基于我们人的思维逻辑。 然后通过编译器,变成机器的逻辑。比如现在如果大家开发一个项目,更关注业务逻辑的实现,比如点击购买按钮,能不能正常调用下订单的函数。或者当用户完成某个功能,能不能按照预定的要求弹出评价提示等。

我们现在越来越少的会为怎么写一行代码能够减少电量消耗,或者如何提高多核 CPU 的利用率这类的问题花费精力。编译优化就是在一定程度上帮助我们处理这类问题的功能。

用 swift.org 中一篇 Blog 来举个例子:

struct X {
var x: Int { return 27 }
}

比如上面这个代码,定义了一个属性 x, 它通过一个函数返回一个整数 27。 如果你开启了编译优化,编译器就有可能将这个属性优化为 inline 函数(因为这个函数体相对简单)。所谓 inline 函数,就是在调用它的地方直接把它展开成代码。比如这样:

let ins = X()
print(ins.x)

上述代码实际在编译优化后,就成了这样:

let ins = X()
print(27)

那么这样替换能带来什么好处呢,因为在程序真正执行的时候,函数调用的开销要比直接执行某段代码大很多,所以将一些比较小的函数直接优化成 inline 的,就肯定会提高程序运行的效率了。

这就是编译优化的一个例子,上面说的 inline 替换是对性能进行优化,可想而知如果你的代码中多次调用了 ins.x 这个属性,那么他们就都会被替换,我们这个例子中这个函数体还比较简单,如果函数体稍微复杂一些,你的代码总量必然会被编译优化增大。 过多的 inline 虽然会对性能提升有帮助,但无疑会增大代码的尺寸。

这也是程序设计中守恒的一个定律,同样的条件下,空间和性能不可能兼得,需要取舍。

相信通过这个解释,大家应该更能理解 -O-Osize 的区别了。 以官方 Blog 的解释,-O 更着重于优化性能,同时会带来代码空间的增大。 -Osize 着重于代码尺寸,比如官方 Blog 上面就有一点明确的说明,-Osize 对于 inline 函数的优化标准就比 -O 谨慎很多。 从这个角度看, inline 优化少了,代码尺寸自然会变小,同样的运行性能就会稍微降低。

当然,上面只是通过 inline 优化给大家举个例子,目的是帮助大家更好的理解编译优化的运作原理。 实际的编译优化操作,要远比我们这里描述的复杂。

总结

XCode 9.3 新增的 -Osize 编译选项,给大家提供了一个新的选择。 这篇文章也通过对它的介绍,给大家分享了关于编译优化知识的一些基本概念,也许会帮你在讨论问题的时候多一些谈资。同样,现在其实有不少项目对空间尺寸的优化需求在增多,我想这也是 -Osize 这个新的编译选项提供出来的原因之一吧。 如果你的项目恰好也有这方面的需求,不妨可以试一下它。

如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。


使用WKWebView进行性能调优

From: http://xibhe.com/2018/02/03/WKWebView-disabuse/
使用WKWebView进行性能调优
最近一周,用户频繁反应一个问题:切换到某个功能页面后,加载H5页面相应时间过长,当H5页面未展示出来时,此时,再切换到其他页面,App会卡死。我们试着在公司的网络环境下复现这个问题,但并未复现。

错误的尝试

最开始时并没有意识到是webView的原因,反而因为前几天刚解决了一个UI线程的bug,将这个卡顿问题主观上当做线程问题去解决。基于此做了以下操作:

  1. 增加webView加载失败的代理方法;
  2. 在加载完成和加载失败时,取消加载进度动画的展示;
  3. 在将项目中的页面替换为 WKWebView 后,发现在访问下个H5页面时,无法共享 Cookie 的问题(下面会详细说下这个问题是如何解决的),导致无法获取到已经验证成功的用户登录信息。

先期采用方法1和方法2,但测试时还是会造成卡顿。后期替换为 WKWebView 后,亟待解决 Cookie 无法共享的问题,想着能不能在每次加载H5页面时,都在请求链接后面拼上用户信息的各种参数,经测试,这样做仍然无法解决页面跳转后读取用户信息的bug。而且还因每次访问页面频繁与服务器进行验证,给服务器带来了性能压力。

问题的复现

这时考虑到用户应该是在弱网环境下进行操作,遇到的问题。于是,使用网络封包分析工具Charles模拟慢速网络。选择Throttle present:56 kbps Modem。此时,再切换页面,先切换到那个加载H5的页面,然后再来回切换其他几个页面,就会出现APP卡死的情况。(这里需要说明的是其他切换的页面有4个同样是加载H5页面,一共有8个主界面)。

现在问题基本可以明确了,每次加载H5页面时都要初始化webView导致了程序内存消耗过大,造成APP卡死。

控制台报错

调试时,在程序频繁切换刷新页面直至卡死阶段,控制台一直报错,主要报错如下:

1. Domain=NSURLErrorDomain Code=-999

<LZoutsourceViewController.m : 226> -[LZoutsourceViewController webView:didFailProvisionalNavigation:withError:]
2018-01-31 21:02:22.084257+0800 CloudOfficeTest[9230:4603782] error:Error Domain=NSURLErrorDomain Code=-999 "(null)" UserInfo={NSErrorFailingURLStringKey=http://test.net/h5/zskt/spkt.html? _WKRecoveryAttempterErrorKey=<WKReloadFrameErrorRecoveryAttempter: 0x1c0822d20>}

2. NSURLConnection finished with error - code -1002

2018-01-31 21:35:56.144596+0800 CloudOfficeTest[9301:4618465] NSURLConnection finished with error - code -1002
2018-01-31 21:36:02.742996+0800 CloudOfficeTest[9301:4618815] TIC TCP Conn Failed [14:0x1c41702c0]: 3:-9802 Err(-9802)

3. failed to return after waiting 10 seconds. main run loop mode: kCFRunLoopDefaultMode

2018-02-01 11:09:01.952689+0800 CloudOfficeTest[614:68129] void SendDelegateMessage(NSInvocation *): delegate (webView:decidePolicyForNavigationAction:request:frame:decisionListener:) failed to return after waiting 10 seconds. main run loop mode: kCFRunLoopDefaultMode

其中,前两个错误都有错误码,分别对应

Code=-999,NSURLErrorCancelled

code -1002,NSURLErrorUnsupportedURL

-999的错误,是因为webView在之前的请求还没有加载完成,就发起了下一个请求,此时webView会取消之前的请求,因此会回调的请求失败这里。

这里使用的是WKWebView,因此,需要在WKWebView加载失败的代理方法里拦截掉被取消的请求。

- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error
{
    // code = -999,被取消什么也不干
    if ([error code] == NSURLErrorCancelled) {
        return;
    }
    NSLog(@"error:%@",error);

    // 失败后的后续处理.....
}

第3个错误中看到了main run loop的字样,感觉很有可能是造成卡顿的元凶了。又在项目中全局搜了一下报错的这个方法,发现是使用的js与oc交互框架—WebViewJavaScriptBridge中的方法。

// WebViewJavascriptBridge.m

- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener

这个方法是框架WebViewJavascriptBridge中的方法,主要用于处理UIWebView与JS交互。到目前为止,仍然不能定位到究竟是UIWebView与JS交互时发生了什么?才导致报这个错误。只是隐隐的感觉到可能和初始化UIWebView时的内存消耗有关,毕竟WKWebView的内存消耗相比UIWebView低了一个数量级。于是,将加载会卡顿的页面替换为WKWebView来加载H5页面,通过降低频繁初始化消耗的内存,减少页面卡死的概率。但在替换后遇到一些比较棘手的问题。

具体替换步骤

  • 引入WKWebView的代理,生成WKWebViewJavascriptBridge桥接对象

    #import “WKWebViewJavascriptBridge.h”
    #import “LZWKWebKitSupport.h”

    @interface LZPartnerMainViewController ()

    @property WKWebViewJavascriptBridge jsBridge;
    /WKWebView/
    @property (nonatomic, strong) WKWebView
    wkWebView;

  • 初始化WKWebView

    • (void)viewDidLoad
      {
      _wkWebView = [LZWKWebKitSupport createSharableWKWebView:YES isShowNav:YES];
      [_wkWebView addObserver:self forKeyPath:@”estimatedProgress” options:NSKeyValueObservingOptionNew context:nil];
      [self.view addSubview:_wkWebView];

    self.jsBridge = [WKWebViewJavascriptBridge bridgeForWebView:_wkWebView];
    [self.jsBridge setWebViewDelegate:self];

    // 使用WKWebViewJavascriptBridge进行桥接,OC端注册方法,由js端进行调用

    [_jsBridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"data:%@",data);
        NSString *urlStr = nil;
        NSString *processIsTop = nil;
        if ([data isKindOfClass:[NSString class]]) {
            urlStr = data;
        }else{
            NSDictionary *dic = data;
            urlStr = dic[@"url"];
            processIsTop = dic[@"processIsTop"];
        }
        responseCallback(@"Response from testObjcCallback");
    }];
    

    }

注意,这里通过LZWKWebKitSupport来初始化一个WkWebView是为了同步Cookie,后面会具体说到为什么要同步Cookie及如何同步。

  • 设置WkWebView的代理方法

    #pragma mark - WKNavigationDelegate
    // 开始加载

    • (void)webView:(WKWebView )webView didCommitNavigation:(WKNavigation )navigation
      {
      self.loadingView.hidden = NO;
      NSLog(@”didCommitNavigation”);
      }

    // 加载完成

    • (void)webView:(WKWebView )webView didFinishNavigation:(WKNavigation )navigation
      {
      self.loadingView.hidden = YES;
      NSLog(@”didFinishNavigation”);
      }

    // 加载失败

    • (void)webView:(WKWebView )webView didFailProvisionalNavigation:(WKNavigation )navigation withError:(NSError *)error
      {
      // code = -999
      if ([error code] == NSURLErrorCancelled) {
      return;
      
      }
      NSLog(@”didFailProvisionalNavigation error.code = %ld”,error.code);
      }

    #pragma mark - wkwebviewDelegate

    • (void)webView:(WKWebView )webView decidePolicyForNavigationAction:(WKNavigationAction )navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
      {
      decisionHandler(WKNavigationActionPolicyAllow);
      }

    //接收到服务器响应 后决定是否允许跳转,主要用来处理请求失败的情况。

    • (void)webView:(WKWebView )webView decidePolicyForNavigationResponse:(WKNavigationResponse )navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
      {
      decisionHandler(WKNavigationResponsePolicyAllow);
      NSHTTPURLResponse response = (NSHTTPURLResponse )navigationResponse.response;

      // 读取cookies
      NSArray cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
      for (NSHTTPCookie
      cookie in cookies) {

      [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
      

      }

      if (response.statusCode && response.statusCode != 200) {

      LZErrorHintType type = LZErrorHintType404;
      if (![[Singleton shareInstance] hasNet]) {
          type = LZErrorHintTypeNet;
      }
      __weak typeof(self) weakSelf = self;
      if (!_errorView) {
          //弹出错误界面,点击刷新按钮刷新界面
          LZErrorHintView *errorView = [[LZErrorHintView alloc] initWithFrame:self.view.bounds type:type refreshBlock:^{
              [weakSelf loadHTMLPage];
          }];
          weakSelf.errorView = errorView;
          [self.view addSubview:errorView];
      }
      return;
      

      }
      if ([[Singleton shareInstance] hasNet]) {

      if (_errorView) {
          [_errorView removeFromSuperview];
          _errorView = nil;
      }
      

      }else{

      if(!_errorView){
          __weak typeof(self) weakSelf = self;
          LZErrorHintView *errorView = [[LZErrorHintView alloc] initWithFrame:self.view.bounds type:LZErrorHintTypeNet refreshBlock:^{
              [weakSelf viewWillAppear:YES];
          }];
          _errorView = errorView;
          errorView.tag = 2200;
          [self.view addSubview:errorView];
      }
      

      }
      }

替换UIWebView为WKWebView后遇到的问题及解决方法

1. 使用WKWebViewJavascriptBridge进行桥接时,加载H5页面闪退。

这里需要更新WebViewJavaScriptBridge桥接框架中WKWebView的桥接方法,

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;

    if ([_base isWebViewJavascriptBridgeURL:url]) {
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            [self WKFlushMessageQueue];
        } else {
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return; 
        // 对比之前的方法,这个地方多了一个return
    }

    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

2. WKWebView加载完网页后,点击里面的按钮,不跳转的问题。

设置WKWebView的另一个代理WKUIDelegate,从名称能看出它是webView在user interface上的代理,

// 创建新的webView
// 可以指定配置对象、导航动作对象、window特性。如果没用实现这个方法,不会加载链接,如果返回的是原webview会崩溃。
-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{
if (!navigationAction.targetFrame.isMainFrame) {
    [webView loadRequest:navigationAction.request];
}
    return nil;
}

要调用下面的方法是有条件的,WKNavigationDelegate中的该方法是用户点击网页上的链接,需打开新页面时,将先调,是否允许跳转到链接。

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
 WKFrameInfo *sFrame = navigationAction.sourceFrame;//navigationAction的出处
 WKFrameInfo *tFrame = navigationAction.targetFrame;//navigationAction的目标
//只有当  tFrame.mainFrame == NO;时,表明这个 WKNavigationAction 将会新开一个页面。
// 才会调用createWebViewWithConfiguration这个代理方法。
}

这样就新开一个webView,如果我们只是显示网页,这样会消耗性能,没有必要。

3. 如何同步WKWebView的Cookie

在将UIWebView替换为WKWebView后加载速度提高了,页面卡死的问题基本没有再出现过。但遇到了一个更加棘手的问题,之前使用的是UIWebView,它会对首次加载H5页面后的用户登录信息进行同步,这样我由当前的H5页面跳转到一个新的UIWebView进行请求时,会自动找到上个页面同步的用户信息,从而加载当前用户对应的内容。

WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。

因此,如何实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie)数据。是决定能否继续使用WKWebView的关键。如果不能解决这个问题,就只能再继续使用之前的 UIWebView 了,之前所做的一切都没有用处了。

解决多个 WKWebView 之间共享 Cookie 的问题,首先要弄明白三个问题?

  1. WKWebViewwebViewCookie 设置,读取上有什么不同?
  2. WKWebView 会将对应的 Cookie 存在什么地方?
  3. 如何取到 WKWebViewCookie 并将其注入到要访问的下一个 WKWebView 中?

结合以上三个问题,在网上搜索很多关于 WKWebViewCookie 存储在什么地方? 这些资料普遍认为 WKWebView 拥有自己的私有存储,不会将 Cookie 存入到标准的 Cookie 容器 NSHTTPCookieStorage 中。但在实际项目中,却发现 WKWebView 实例可以读取到存储于 NSHTTPCookieStorage 中的 Cookie。最后,看到了腾讯Bugly的一篇技术文章 —- WKWebView 那些坑,也印证了我的观点。

实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中。

看来以后搜索技术文章,不能太片面了,一定要结合一些大厂的权威技术文章来具体分析。

下一步,就是如何在发起请求时注入 通过 NSHTTPCookieStorage 获取的Cookie。网上关于 WKWebViewCookie 注入方法有以下几种:

  1. JS注入 —- 在初始化 WKWebView 的时候,通过 WKUserScript 设置,使用javascript 注入 Cookie,一开始发送 NSMutableURLRequest 请求的时候也要加上 Cookie,并且保证两个地方的设置的cookie一致。参考 — Can I set the cookies to be used by a WKWebView?
  2. WKHTTPCookieStore —- 利用 iOS11 API WKHTTPCookieStore 解决 WKWebView 首次请求不携带 Cookie 的问题。参考 — iOS WebView 中的 Cookie 处理业务场景“IP直连”方案说明
  3. 利用 iOS11 之前的 API 解决 WKWebView 首次请求不携带 Cookie 的问题。参考 — iOS WebView 中的 Cookie 处理业务场景“IP直连”方案说明
  4. 通过让所有 WKWebView 共享同一个 WKProcessPool 实例,可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie) 数据。不过 WKWebView WKProcessPool 实例在 app 杀进程重启后会被重置,导致 WKProcessPool 中的 Cookiesession Cookie 数据丢失,目前也无法实现 WKProcessPool 实例本地化保存。

方法1,经过测试行不通,可能是后台读取 Cookie 的方式有问题;方法2,是 iOS 11API ,不具有普适性;方法3,在测试时无法通过 url 匹配到 Cookie;最后,只剩下方法4了,需要注意在特殊场景下 Cookie 丢失的情况:

app 杀进程重启后会被重置,导致 WKProcessPool 中的 Cookiesession Cookie 数据丢失,目前也无法实现 WKProcessPool 实例本地化保存。

但以我们的应用为例,哪怕是主动杀进程,重新打开应用;还是应用突然闪退,重新打开应用。首次加载某个含有用户登录验证的H5页面时,需要在发起请求的地方拼上用户特定信息的参数,因此,即使之前存储的 Cookie 数据丢失了,也会在首次加载时重新获取。如下:

Singleton *sin = [Singleton shareInstance];
NSString *baseIpPort = [LZUserDefaults objectForKey:PreferenceKey_SystemInit_ZyPartnerIPPort];
NSString *urlString = [[NSString stringWithFormat:@"%@/test1/test2?Id=%@&Name=%@&Pid=%@",baseIpPort,sin.clinicId,[LZUserDefaults objectForKey:PreferenceKey_Name],[LZUserDefaults objectForKey:PreferenceKey_Pid]] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
request.HTTPMethod = @"POST";
request.timeoutInterval = 15.0f;
[_wkWebView loadRequest:request];

因此,对于 APP 重启后 Cookie 数据可能丢失的情况,难道不可以在首次加载H5页面时,重新获取一下用户登录信息的 Cookie 吗?对我而言,现在的项目就是这样做的。

1. 新建一个名为 LZWKWebKitSupport 的类,用于生成一个统一的,全局使用同一个 WKProcessPoolWKWebView 对象。

// LZWKWebKitSupport.h

#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>

@interface LZWKWebKitSupport : NSObject
@property (nonatomic, strong,readonly) WKProcessPool *processPool;
+ (instancetype)sharedSupport;
+ (WKWebView *)createSharableWKWebView:(BOOL)isFullScreen isShowNav:(BOOL)showNav;
@end


// LZWKWebKitSupport.m
#import "LZWKWebKitSupport.h"
@interface LZWKWebKitSupport()
@end

@implementation LZWKWebKitSupport
+ (instancetype)sharedSupport {
    static LZWKWebKitSupport *_instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [LZWKWebKitSupport new];
    });
    return  _instance;
}

- (instancetype)init {
    if (self = [super init]) {
        self.processPool = [WKProcessPool new];
    }
    return self;
}

+ (WKWebView *)createSharableWKWebView:(BOOL)isFullScreen isShowNav:(BOOL)showNav
{
    WKUserContentController* userContentController = [WKUserContentController new];
    NSMutableString *cookies = [NSMutableString string];
    WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[cookies copy]                                                        injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    [userContentController addUserScript:cookieScript];    
    WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
    // 一下两个属性是允许H5视频自动播放,并且全屏,可忽略
    configuration.allowsInlineMediaPlayback = YES;
    configuration.mediaPlaybackRequiresUserAction = NO;
    // 全局使用同一个processPool
    configuration.processPool = [[LZWKWebKitSupport sharedSupport] processPool];
    configuration.userContentController = userContentController;
    // 考虑到左侧菜单栏,需要设置webView的不同frame
    WKWebView *wk_webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, y, width, height) configuration:configuration];

    return wk_webView;
}
@end

2. 在加载H5的地方初始化 LZWKWebKitSupport,并在 WKNavigationDelegate 中获取 cookie,并设置到本地。

// 初始化LZWKWebKitSupport
- (void)viewDidLoad{
_wkWebView = [LZWKWebKitSupport createSharableWKWebView:YES isShowNav:YES];
[self.view addSubview:_wkWebView];
}


#pragma mark - wkwebviewDelegate
//接收到服务器响应 后决定是否允许跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
{
    decisionHandler(WKNavigationResponsePolicyAllow);
    NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
    // 读取cookie,并设置到本地
    NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
    for (NSHTTPCookie *cookie in cookies) {
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
    }
}

3. 在从第一个H5页面跳转至第二个H5页面时,在发起请求时注入Cookie。

这里以跳转到 LZDetailViewController 页面为例,先是通过LZWKWebKitSupport 初始化一个 WKWebView

// LZDetailViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    //初始化视图
    [self setUpSubViews];   
}

- (void)setUpSubViews{
    _wkWebView = [LZWKWebKitSupport createSharableWKWebView:YES isShowNav:NO];
    _wkWebView.UIDelegate = self;
    [self.view addSubview:_wkWebView];
    _jsBridge = [WKWebViewJavascriptBridge bridgeForWebView:_wkWebView];
    [_jsBridge setWebViewDelegate:self];
}

然后在加载请求时,注入之前设置的 Cookie

- (void)loadUrl{
    if (!_urlStr) {
        return;
    }

    NSURL *url = [NSURL URLWithString:_urlStr];
    NSMutableString *cookies = [NSMutableString string];
    NSMutableURLRequest *requestObj = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0];
    // 一般都只需要同步BJSESSIONID,可视不同需求自己做更改
    NSString * BJSESSIONID;
    // 获取本地所有的Cookie
    NSArray *tmp = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    for (NSHTTPCookie * cookie in tmp) {
        if ([cookie.name isEqualToString:@"BJSESSIONID"]) {
            BJSESSIONID = cookie.value;
            break;
        }
    }
    if (BJSESSIONID.length) {
        // 格式化Cookie
        [cookies appendFormat:@"BJSESSIONID=%@;",BJSESSIONID];
    }
    // 注入Cookie
    [requestObj setValue:cookies forHTTPHeaderField:@"Cookie"];
    // 加载请求
    [self.wkWebView loadRequest:requestObj];
}

通过以上三步就可以达到同步 Cookie 的目的,现在看来之前通过 JS脚本 注入 Cookie 失败,可能是由于后台需要同步 BJSESSIONID,而BJSESSIONIDHtppOnly,不允许通过js脚本修改。

最后,需要特别注意的一点是:考虑在加载H5页前,是否需要清除某些H5页面的 Cookie ?

这里对于我们的项目而言,加载的需要验证用户身份信息的H5页面,是需要清除 Cookie 的,因为用户的权限不同,所看到的界面就不同,在同一台设备下切换不同的用户时,如果不清除之前的 Cookie,所展示的就是上一个用户的信息。

- (void)deleteWKCookies
{
    // 清除WKWebView缓存的cookie(根据ip)
    if ([[UIDevice currentDevice].systemVersion floatValue] >= 9.0){

        NSString *iPPort = [LZUserDefaults objectForKey:PreferenceKey_SystemInit_ZyIPPort];
        NSArray *iPPortArray = [iPPort componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/"]];
        NSString *recordIP;
        if ([iPPortArray count] > 2) {
            recordIP = partnerIPPortArray[2];
        }

        WKWebsiteDataStore *dateStore = [WKWebsiteDataStore defaultDataStore];
        [dateStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records) {
            for (WKWebsiteDataRecord *record  in records)
            {
                // 以www.baidu.com为例,是否包含baidu.com
                if ([recordIP containsString:record.displayName])
                {
                    [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes forDataRecords:@[record] completionHandler:^{
                        NSLog(@"Cookies for %@ deleted successfully",record.displayName);
                    }];
                }
            }
        }];
    }
}

WebView性能优化总结

一个加载网页的过程中,native、网络、后端处理、CPU都会参与,各自都有必要的工作和依赖关系;让他们相互并行处理而不是相互阻塞才可以让网页加载更快:

  • WebView初始化慢,可以在初始化同时先请求数据,让后端和网络不要闲着。
  • 后端处理慢,可以让服务器分trunk输出,在后端计算的同时前端也加载网络静态资源。
  • 脚本执行慢,就让脚本在最后运行,不阻塞页面解析。
  • 同时,合理的预加载、预缓存可以让加载速度的瓶颈更小。
  • WebView初始化慢,就随时初始化好一个WebView待用。
  • DNS和链接慢,想办法复用客户端使用的域名和链接。
  • 脚本执行慢,可以把框架代码拆分出来,在请求页面之前就执行好。

上面是美团点评技术团队关于WebView性能优化的总结。

对比我们项目中有哪些页面用到了 UIWebView,哪些用到了 WKWebView,发现当前程序中一共有8个主要模块,其中,一共有4个主要模块是通过加载H5页面展示的,还有一个模块中部分嵌套了H5页面。这些页面中,有三个页面使用 WkWebView 加载,剩下的使用的是 UIWebView 加载页面,发生卡顿的页面多是频繁初始化 UIWebView 加载H5时发生的。

这里我们的项目中使用UIWebViewWKWebView 的地方有很多,没有一个管理类去居中调控的话,后期维护起来会很耗时,而且很容易出现bug。下一步的优化就是要构建这样一种集构建,配置,分发,操控为一身的通用类。

参考资料

WebView性能、体验分析与优化

webView:decidePolicyForNavigationAction:decisionHandler:

WKWebView 那些坑

iOS WebView 中的 Cookie 处理业务场景“IP直连”方案说明

Developer wknavigationdelegate documentation

WKWebView and UIWebView Cookie

WKWebView 那些坑

From: https://zhuanlan.zhihu.com/p/24990222

本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:WKWebView 那些坑

导语

WKWebView 是苹果在 WWDC 2014 上推出的新一代 webView 组件,用以替代 UIKit 中笨重难用、内存泄漏的 UIWebView, 拥有60fps滚动刷新率、和 safari 相同的 JavaScript 引擎。简单的适配方法就不细说了,本文主要讲述适配 WKWebView 过程中填过的坑以及善待解决的技术难题。

1、WKWebView 白屏问题

WKWebView 自诩拥有更快的加载速度,更低的内存占用,但实际上 WKWebView 是一个多进程组件,Network Loading 以及 UI Rendering 在其它进程中执行。初次适配 WKWebView 的时候,我们也惊讶于打开 WKWebView 后,App 进程内存消耗反而大幅下降,但是仔细观察会发现,Other Process 的内存占用会增加。在一些用 webGL 渲染的复杂页面,使用 WKWebView 总体的内存占用(App Process Memory + Other Process Memory)不见得比 UIWebView 少很多。

在 UIWebView 上当内存占用太大的时候,App Process 会 crash;而在 WKWebView 上当总体的内存占用比较大的时候,WebContent Process 会 crash,从而出现白屏现象。在 WKWebView 中加载下面的测试链接可以稳定重现白屏现象:

<Memory test>

这个时候 WKWebView.URL 会变为 nil, 简单的 reload 刷新操作已经失效,对于一些长驻的H5页面影响比较大。

我们最后的解决方案是:

A、借助 WKNavigtionDelegate

iOS 9以后 WKNavigtionDelegate 新增了一个回调函数:

- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));

当 WKWebView 总体内存占用过大,页面即将白屏的时候,系统会调用上面的回调函数,我们在该函数里执行[webView reload](这个时候 webView.URL 取值尚不为 nil)解决白屏问题。在一些高内存消耗的页面可能会频繁刷新当前页面,H5侧也要做相应的适配操作。

B、检测 webView.title 是否为空

并不是所有H5页面白屏的时候都会调用上面的回调函数,比如,最近遇到在一个高内存消耗的H5页面上 present 系统相机,拍照完毕后返回原来页面的时候出现白屏现象(拍照过程消耗了大量内存,导致内存紧张,WebContent Process 被系统挂起),但上面的回调函数并没有被调用。在WKWebView白屏的时候,另一种现象是 webView.titile 会被置空, 因此,可以在 viewWillAppear 的时候检测 webView.title 是否为空来 reload 页面。

综合以上两种方法可以解决绝大多数的白屏问题。

Cookie 问题是目前 WKWebView 的一大短板

2.1、WKWebView Cookie存储

业界普遍认为 WKWebView 拥有自己的私有存储,不会将 Cookie 存入到标准的 Cookie 容器 NSHTTPCookieStorage 中。

实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中,FireFox 工程师曾建议通过 reset WKProcessPool 来触发 Cookie 同步到 NSHTTPCookieStorage 中,实践发现不起作用,并可能会引发当前页面 session cookie 丢失等问题。

WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie

比如,NSHTTPCookieStorage 中存储了一个 Cookie:

name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT;

通过 UIWebView 发起请求http://y.qq.com/, 则请求头会自动带上 cookie: Nicholas=test;
而通过 WKWebView发起请求http://y.qq.com/, 请求头不会自动带上 cookie: Nicholas=test。

2.2、WKProcessPool

苹果开发者文档对 WKProcessPool 的定义是:A WKProcessPool object represents a pool of Web Content process. 通过让所有 WKWebView 共享同一个 WKProcessPool 实例,可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie)数据。不过 WKWebView WKProcessPool 实例在 app 杀进程重启后会被重置,导致 WKProcessPool 中的 Cookie、session Cookie 数据丢失,目前也无法实现 WKProcessPool 实例本地化保存。

2.3、Workaround

由于许多 H5 业务都依赖于 Cookie 作登录态校验,而 WKWebView 上请求不会自动携带 Cookie, 目前的主要解决方案是:

a、WKWebView loadRequest 前,在 request header 中设置 Cookie, 解决首个请求 Cookie 带不上的问题;(注: 如果页面直接读取浏览器缓存,其实并没有写入浏览器,也有可能时机不对, 我自己现有操作是后台java 读取请求中的cookies 并设置,然后代理跳转的时候也手动加上cookies)

WKWebView * webView = [WKWebView new]; 
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"手机统一登录"]]; 
[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"]; 
[webView loadRequest:request];

b、通过 document.cookie 设置 Cookie 解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题;

注意:document.cookie()无法跨域设置 cookie

WKUserContentController* userContentController = [WKUserContentController new]; 
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; 
[userContentController addUserScript:cookieScript];

这种方案无法解决302请求的 Cookie 问题,比如,第一个请求是 [http://www.a.com][5],我们通过在 request header 里带上 Cookie 解决该请求的 Cookie 问题,接着页面302跳转到 http://www.b.com,这个时候 http://www.b.com 这个请求就可能因为没有携带 cookie 而无法访问。当然,由于每一次页面跳转前都会调用回调函数:

[5]: https://link.zhihu.com/?target=http%3A//www.a.com
  • (void)webView:(WKWebView )webView decidePolicyForNavigationAction:(WKNavigationAction )navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

可以在该回调函数里拦截302请求,copy request,在 request header 中带上 cookie 并重新 loadRequest。不过这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题,毕竟-[WKWebView loadRequest:]只适合加载 mainFrame 请求。

3、WKWebView NSURLProtocol问题

WKWebView 在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。苹果开源的 webKit2 源码暴露了[私有API][7]:

[7]: https://link.zhihu.com/?target=https%3A//github.com/WebKit/webkit/blob/master/Tools/TestWebKitAPI/cocoa/TestProtocol.mm%23L56-L63
  • [WKBrowsingContextController registerSchemeForCustomProtocol:]

通过注册 http(s) scheme 后 WKWebView 将可以使用 NSURLProtocol 拦截 http(s) 请求:

Class cls = NSClassFromString(@"WKBrowsingContextController”); 
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); 
if ([(id)cls respondsToSelector:sel]) { 
           // 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理 
           [(id)cls performSelector:sel withObject:@"http"]; 
           [(id)cls performSelector:sel withObject:@"https"]; 
}

但是这种方案目前存在两个严重缺陷:

a、post 请求 body 数据被清空

由于 WKWebView 在独立进程里执行网络请求。一旦注册 http(s) scheme 后,网络请求将从 Network Process 发送到 App Process,这样 NSURLProtocol 才能拦截网络请求。在 webkit2 的设计里使用 MessageQueue 进行进程之间的通信,Network Process 会将请求 encode 成一个 Message,然后通过 IPC 发送给 App Process。出于性能的原因,encode 的时候 HTTPBody 和 HTTPBodyStream 这两个字段被丢弃掉了(参考苹果源码:
https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88 及bug report: <WKWebView does not fully support custom NSURLProtocol>)。

因此,如果通过 registerSchemeForCustomProtocol 注册了 http(s) scheme, 那么由 WKWebView 发起的所有 http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理,导致 post 请求 body 被清空

b、对ATS支持不足

测试发现一旦打开ATS开关:Allow Arbitrary Loads 选项设置为NO,同时通过 registerSchemeForCustomProtocol 注册了 http(s) scheme,WKWebView 发起的所有 http 网络请求将被阻塞(即便将Allow Arbitrary Loads in Web Content 选项设置为YES);

WKWebView 可以注册 customScheme, 比如 dynamic://, 因此希望使用离线功能又不使用 post 方式的请求可以通过 customScheme 发起请求,比如 dynamic://http://www.dynamicalbumlocalimage.com/,然后在 app 进程 NSURLProtocol 拦截这个请求并加载离线数据。不足:使用 post 方式的请求该方案依然不适用,同时需要 H5 侧修改请求 scheme 以及 CSP 规则;

4、WKWebView loadRequest 问题

在 WKWebView 上通过 loadRequest 发起的 post 请求 body 数据会丢失:

//同样是由于进程间通信性能问题,HTTPBody字段被丢弃
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
[wkwebview loadRequest: request];

workaround:

假如想通过-[WKWebView loadRequest:]加载 post 请求 request1: http://h5.qzone.qq.com/mqzone/index,可以通过以下步骤实现:

  1. 替换请求 scheme,生成新的 post 请求 request2: post://http://h5.qzone.qq.com/mqzone/index, 同时将 request1 的 body 字段复制到 request2 的 header 中(WebKit 不会丢弃 header 字段);
  2. 通过-[WKWebView loadRequest:]加载新的 post 请求 request2;
  3. 通过 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注册 scheme: post://;
  4. 注册 NSURLProtocol 拦截请求post://http://h5.qzone.qq.com/mqzone/index ,替换请求 scheme, 生成新的请求 request3: http://h5.qzone.qq.com/mqzone/index,将 request2 header的body 字段复制到 request3 的 body 中,并使用 NSURLConnection 加载 request3,最后通过 NSURLProtocolClient 将加载结果返回 WKWebView;

5、WKWebView 页面样式问题

在 WKWebView 适配过程中,我们发现部分H5页面元素位置向下偏移被拉伸变形,追踪后发现主要是H5页面高度值异常导致:

a. 空间H5页面有透明导航、透明导航下拉刷新、全屏等需求,因此之前 webView 整个是从(0, 0)开始布局,通过调整webView.scrollView.contentInset 来适配特殊导航栏需求。而在 WKWebView 上对 contentInset 的调整会反馈到webView.scrollView.contentSize.height的变化上,比如设置 webView.scrollView.contentInset.top = a,那么contentSize.height的值会增加a,导致H5页面长度增加,页面元素位置向下偏移;

解决方案是:调整WKWebView布局方式,避免调整webView.scrollView.contentInset。实际上,即便在 UIWebView 上也不建议直接调整webView.scrollView.contentInset的值,这确实会带来一些奇怪的问题。如果某些特殊情况下非得调整 contentInset 不可的话,可以通过下面方式让H5页面恢复正常显示:

/**设置contentInset值后通过调整webView.frame让页面恢复正常显示 
 *参考:http://km.oa.com/articles/show/277372
 */ 
webView.scrollView.contentInset = UIEdgeInsetsMake(a, 0, 0, 0); 
webView.frame = CGRectMake(webView.frame.origin.x, webView.frame.origin.y, webView.frame.size.width, webView.frame.size.height - a);

b. 在接入 now 直播的时候,我们发现在 iOS 9 上 WKWebView 会出现页面被拉伸变形的情况,最后发现是window.innerHeight值不准确导致(在WKWebView上返回了一个非常大的值),而H5同学通过获取window.innerHeight来设置页面高度,导致页面整体被拉伸。通过查阅相关资料发现,这个bug只在 iOS 9 的几个系统版本上出现,苹果后来fix了这个bug。我们最后的解决方案是:延迟调用window.innerHeight

setTimeout(function(){height = window.innerHeight},0);

or

Use shrink-to-fit meta-tag 
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1, shrink-to-fit=no">

6、WKWebView 截屏问题

空间玩吧H5小游戏有截屏分享的功能,WKWebView 下通过 -[CALayer renderInContext:]实现截屏的方式失效,需要通过以下方式实现截屏功能:

@implementation UIView (ImageSnapshot) 
- (UIImage*)imageSnapshot { 
    UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES,self.contentScaleFactor); 
    [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES]; 
    UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext(); 
    UIGraphicsEndImageContext(); 
    return newImage; 
} 
@end

然而这种方式依然解决不了 webGL 页面的截屏问题,笔者已经翻遍苹果文档,研究过 webKit2 源码里的截屏私有API,依然没有找到合适的解决方案,同时发现 Safari 以及 Chrome 这两个全量切换到 WKWebView 的浏览器也存在同样的问题:对webGL 页面的截屏结果不是空白就是纯黑图片。无奈之下,我们只能约定一个JS接口,让游戏开发商实现该接口,具体是通过 canvas getImageData()方法取得图片数据后返回 base64 格式的数据,客户端在需要截图的时候,调用这个JS接口获取 base64 String 并转换成 UIImage。

7、WKWebView crash问题

WKWebView 放量后,外网新增了一些 crash, 其中一类 crash 的主要堆栈如下:

... 
28 UIKit 0x0000000190513360 UIApplicationMain + 208 
29 Qzone 0x0000000101380570 main (main.m:181) 
30 libdyld.dylib 0x00000001895205b8 _dyld_process_info_notify_release + 36 
Completion handler passed to -[QZWebController webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:] was not called

主要是JS调用window.alert()函数引起的,从 crash 堆栈可以看出是 WKWebView 回调函数:

+ (void) presentAlertOnController:(nonnull UIViewController*)parentController title:(nullable NSString*)title message:(nullable NSString *)message handler:(nonnull void (^)())completionHandler;

completionHandler 没有被调用导致的。在适配 WKWebView 的时候,我们需要自己实现该回调函数,window.alert()才能调起 alert 框,我们最初的实现是这样的:

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler 
{ 
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; 
    [alertController addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]]; 
    [self presentViewController:alertController animated:YES completion:^{}]; 
}

如果 WKWebView 退出的时候,JS刚好执行了window.alert(), alert 框可能弹不出来,completionHandler 最后没有被执行,导致 crash;另一种情况是在 WKWebView 一打开,JS就执行window.alert(),这个时候由于 WKWebView 所在的 UIViewController 出现(push或present)的动画尚未结束,alert 框可能弹不出来,completionHandler 最后没有被执行,导致 crash。我们最终的实现大致是这样的:

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler 
{ 
    if (/*UIViewController of WKWebView has finish push or present animation*/) { 
        completionHandler(); 
        return; 
    } 
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; 
    [alertController addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]]; 
    if (/*UIViewController of WKWebView is visible*/) 
        [self presentViewController:alertController animated:YES completion:^{}]; 
    else 
        completionHandler(); 
}

确保上面两种情况下 completionHandler 都能被执行,消除了 WKWebView 下弹 alert 框的 crash,WKWebView 下弹 confirm 框的 crash 的原因与解决方式与 alert 类似。

另一个 crash 发生在 WKWebView 退出前调用:

-[WKWebView evaluateJavaScript: completionHandler:]

执行JS代码的情况下。WKWebView 退出并被释放后导致completionHandler变成野指针,而此时 javaScript Core 还在执行JS代码,待 javaScript Core 执行完毕后会调用completionHandler(),导致 crash。这个 crash 只发生在 iOS 8 系统上,参考Apple Open Source,在iOS9及以后系统苹果已经修复了这个bug,主要是对completionHandler block做了copy(refer: <[Changeset 179160][14]>);对于iOS 8系统,可以通过在 completionHandler 里 retain WKWebView 防止 completionHandler 被过早释放。我们最后用 methodSwizzle hook 了这个系统方法:

[14]: https://link.zhihu.com/?target=https%3A//trac.webkit.org/changeset/179160
  • (void) load
    {
    [self jr_swizzleMethod:NSSelectorFromString(@"evaluateJavaScript:completionHandler:") withMethod:@selector(altEvaluateJavaScript:completionHandler:) error:nil]; 
    
    }
    /*
    • fix: WKWebView crashes on deallocation if it has pending JavaScript evaluation
      */
      • (void)altEvaluateJavaScript:(NSString )javaScriptString completionHandler:(void (^)(id, NSError ))completionHandler
        {
        id strongSelf = self;
        [self altEvaluateJavaScript:javaScriptString completionHandler:^(id r, NSError *e) {
        [strongSelf title];
        if (completionHandler) {
        completionHandler(r, e); 
        
        }
        }];
        }

8、其它问题

8.1、视频自动播放

WKWebView 需要通过WKWebViewConfiguration.mediaPlaybackRequiresUserAction设置是否允许自动播放,但一定要在 WKWebView 初始化之前设置,在 WKWebView 初始化之后设置无效。

8.2、goBack API问题

WKWebView 上调用 -[WKWebView goBack], 回退到上一个页面后不会触发window.onload()函数、不会执行JS。

8.3、页面滚动速率

WKWebView 需要通过scrollView delegate调整滚动速率:

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
     scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
}

9、结语

本文总结了在 WKWebView 上踩过的一些坑。虽然 WKWebView 坑比较多,但是相对 UIWebView 在内存消耗、稳定性方面还是有很大的优势。尽管苹果对 WKWebView 的开发进度过于缓慢,但相信 WKWebView 才是未来。

更多精彩内容欢迎关注腾讯 Bugly的微信公众账号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!

CocoaPods 项目添加错误记录

From: https://www.jianshu.com/p/283584683b0b

上篇文章[Cocoapods]项目添加Cocoapods支持主要介绍了添加Cocoapods支持的大致过程, 当然文章看上去是一帆风顺的. 但是, 事实并不是这样. 上篇文章篇幅过长, 就把之间遇到的一些问题, 单独写了出来.

1. ERROR | spec: The specification defined in LZTool.podspec could not be loaded.
MacBook:PodTest Artron_LQQ$ pod lib lint LZTool.podspec

 -> LZTool.podspec
    - ERROR | spec: The specification defined in `LZTool.podspec` could not be loaded.


[!] Invalid `LZTool.podspec` file: syntax error, unexpected tIDENTIFIER, expecting keyword_end
...<-DESC 这是一个测试文档, 这里主要是描述类库...
...                               ^
LZTool.podspec:28: syntax error, unexpected '\n', expecting '='.

 #  from LZTool.podspec:27
 #  -------------------------------------------
 #    #   * Finally, don't worry about the indent, CocoaPods strips it!
 >    s.description  = <<-DESC 这是一个测试文档, 这里主要是描述类库的功能设计初衷介绍
 #                     DESC
 #  -------------------------------------------


[!] LZTool.podspec did not pass validation, due to 1 error.
[!] The validator for Swift projects uses Swift 3.0 by default, if you are using a different version of swift you can use a `.swift-version` file to set the version for your Pod. For example to use Swift 2.3, run: 
    `echo "2.3" > .swift-version`.
You can use the `--no-clean` option to inspect any issue.
MacBook:PodTest Artron_LQQ$

这个错误是使用指令pod lib lint LZTool.podspec 检查文件是否合法时发生的;

可以看出是在设置s.description 字段时发生的错误, 当时的写法是:

s.description  = <<-DESC LZTool 是一个用于保存一些常用工具类的工具 DESC

以为可以吧他们并到一行, 导致一直报这个错, 后来写成两行, 还是报错, 最后才试出这样写才对:

s.description  = <<-DESC 
                          LZTool 是一个用于保存一些常用工具类的工具
                   DESC
2. [iOS] file patterns: The source_files pattern did not match any file.
MacBook:PodTest Artron_LQQ$ pod lib lint LZTool.podspec

 -> LZTool (0.0.1)
    - WARN  | github_sources: Github repositories should end in `.git`.
    - ERROR | [iOS] file patterns: The `source_files` pattern did not match any file.

[!] LZTool did not pass validation, due to 1 error and 1 warning.
[!] The validator for Swift projects uses Swift 3.0 by default, if you are using a different version of swift you can use a `.swift-version` file to set the version for your Pod. For example to use Swift 2.3, run: 
    `echo "2.3" > .swift-version`.
You can use the `--no-clean` option to inspect any issue.
MacBook:PodTest Artron_LQQ$

这个错误也是使用指令pod lib lint LZTool.podspec 检查文件是否合法时发生的;

这个是在指定共享的类库时, 文件路径不对, 也就是设置s.source_files 字段时, 发生了错误, 这里的路径是相对于LZTool.podspec文件的, 如果是与LZTool.podspec同级的文件夹, 直接写文件夹名称即可, 如:

s.source_files = "LZTool"

如果有多级目录, 一定要逐级添加. 这里也可以这么写:

s.source_files = "LZTool/*.{h,m}"
3. fatal: Remote branch 0.0.1 not found in upstream origin
MacBook:PodTest Artron_LQQ$ pod spec lint

 -> LZTool (0.0.1)
    - ERROR | [iOS] unknown: Encountered an unknown error ([!] /usr/bin/git clone https://github.com/LQQZYY/PodTest.git /var/folders/14/95vmx0495_s5292ltvwpsc8h0000gn/T/d20170111-11240-1l3iq9n --template= --single-branch --depth 1 --branch 0.0.1

Cloning into '/var/folders/14/95vmx0495_s5292ltvwpsc8h0000gn/T/d20170111-11240-1l3iq9n'...
warning: Could not find remote branch 0.0.1 to clone.
fatal: Remote branch 0.0.1 not found in upstream origin
) during validation.

Analyzed 1 podspec.

[!] The spec did not pass validation, due to 1 error.
[!] The validator for Swift projects uses Swift 3.0 by default, if you are using a different version of swift you can use a `.swift-version` file to set the version for your Pod. For example to use Swift 2.3, run: 
    `echo "2.3" > .swift-version`.
MacBook:PodTest Artron_LQQ$

这个是使用指令pod spec lint来检查文件是否可用时发生的错误;

这是因为在你托管代码的库里(这里是指github)找不到这个分支, 也就是在编辑 LZTool.podspec 时, 里面的字段 s.verson, s.source 中的 taggithub创建的release版本号不一致导致, 修改为一样即可!

这里还有另外一个解决方法方法二

4. 加载xib问题

PS: 这个不是我个人遇到的问题, 是在[][3]文章中看到, 这里记录一下;
如果通过cocoapods下载的类库中含有Xib文件, 使用原来的方式初始化就不起作用了:

[3]: https://link.jianshu.com?t=http://www.cocoachina.com/ios/20160415/15939.html

[[[NSBundle mainBundle] loadNibNamed:@”xibName” owner:self options:nil] lastObject];
[self.collectionView registerNib:[UINib nibWithNibName:@”xibName” bundle:nil] forCellWithReuseIdentifier:@”ZLCollectionCell”];

应该使用下面这种方式初始化:

#define kZLPhotoBrowserBundle [NSBundle bundleForClass:[self class]]
[[kZLPhotoBrowserBundle loadNibNamed:@"ZLPhotoActionSheet" owner:self options:nil] lastObject];
[self.collectionView registerNib:[UINib nibWithNibName:@"ZLCollectionCell" bundle:kZLPhotoBrowserBundle] forCellWithReuseIdentifier:@"ZLCollectionCell"];

这样就能正常使用了;

5.加载图片资源问题

PS: 这个问题我尝试了一个demo, 直接设置了s.resources = “LZScaner/images/*.png” , 图片也能正常显示( 可参考我设置的podspec文件LZScaner.podspec )没有遇到这个问题, 但是还是把这个方式记录一下吧…

如果通过代码“[UIImage imageNamed:@”picName”]” 去设置图片的话,则图片资源有可能无法正常显示, 可通过以下方式解决:

  • 创建bundle资源目录

    command+N -> Resource -> Settings Bundle

删除bundle携带的无用文件,把图片资源添加到bundle资源内

  • 改变代码图片路径

    // 图片路径
    #define kZLPhotoBrowserSrcName(file) [@”ZLPhotoBrowser.bundle” stringByAppendingPathComponent:file]
    #define kZLPhotoBrowserFrameworkSrcName(file) [@”Frameworks/ZLPhotoBrowser.framework/ZLPhotoBrowser.bundle” stringByAppendingPathComponent:file]
    kZLPhotoBrowserSrcName(file) 为通过copy文件夹方式获取图片路径的宏
    kZLPhotoBrowserFrameworkSrcName(file) 为通过cocoapods下载安装获取图片路径的宏

  • 然后修改代码中设置图片的方式如下

    UIImage *img = [UIImage imageNamed:kZLPhotoBrowserSrcName(@”img.png”)]?:[UIImage imageNamed:kZLPhotoBrowserFrameworkSrcName(@”img.png”)];

podspec地址: ZLPhotoBrowser.podspec 可以参考学习!

6. trunk: getaddrinfo: nodename nor servname provided, or not known
MacBook:LZScaner Artron_LQQ$ pod trunk register 302934443@qq.com --description= 'LZScaner'
[!] There was an error registering with trunk: getaddrinfo: nodename nor servname provided, or not known
MacBook:LZScaner Artron_LQQ$

出现这个原因是, 我开了APN, 把APN关了, 重新启动一下网络就好了…

7. xcodebuild: Returned an unsuccessful exit code. You can use --verbose for more information
MacBook:LZSortTool Artron_LQQ$ pod spec lint

 -> LZSortTool (0.0.1)
    - WARN  | [iOS] license: Unable to find a license file
    - ERROR | [iOS] xcodebuild: Returned an unsuccessful exit code. You can use `--verbose` for more information.
    - NOTE  | [iOS] xcodebuild:  LZSortTool/LZSortToolDemo/LZSortToolDemo/LZSortClass/LZSortTool.m:14:9: fatal error: 'ChineseToPinyin.h' file not found

Analyzed 1 podspec.

[!] The spec did not pass validation, due to 1 error and 1 warning.
[!] The validator for Swift projects uses Swift 3.0 by default, if you are using a different version of swift you can use a `.swift-version` file to set the version for your Pod. For example to use Swift 2.3, run: 
    `echo "2.3" > .swift-version`.
MacBook:LZSortTool Artron_LQQ$

这个是执行 pod spec lint指令的时候产生的错误:

这个错误从- NOTE可以看出, 是项目中的这个文件ChineseToPinyin.h找不到, 回到项目, 编译一下也会报这个错. 是因为我修改了本地文件中的这个文件的路径, 项目中引用的还是原先的路径, 打开左侧文件列表, 可以发现这个文件是红色的, 删除引用, 重新添加, 然后到你的github上面, 重新添加一个release版本, 再重新执行指令即可验证通过.

8. 验证成功后搜索不到新加的支持库

如果在制作支持库的过程中没有错误, 或者最好添加库验证通过, 即出现下面这个界面:

上传成功

在使用

pod search LZTool

的时候, 搜索不到这个库, 或者在使用时找不到这个库, 可以使用下面的指令来清理一下缓存:

rm ~/Library/Caches/CocoaPods/search_index.json

然后再去使用, 基本就可以了.

CocoaPods 私有仓库的创建(超详细)

CocoaPods 私有仓库的创建(超详细)

From: https://www.jianshu.com/p/0c640821b36f

本文发布于 BY Blog简书 转载请保留链接

最近参照了网上一大堆 CocoaPods私有仓库 的教程,按教程操作得到的pod仓库里面是这样的~

代码和版本描述居然混在了一起,简直太糟糕~

虽然也能用,但是和 CocoaPods 本身的理念就不相符。

在上一篇《CocoaPods公有仓库的创建》中我们了解到,master 目录中只存放 代码库 的描述文件,而不是存放代码。就像这样

代码我们另外存放在代码仓库中

很多人不了解CocoaPods的工作原理就复制粘贴别人的教程来做教程~

吐槽结束,进入正文

创建版本库(repo)

首先,创建一个像 master 一样的存放版本描述文件的git仓库,因为是私人git仓库,我们选择 oschina 创建远程私有仓库(因为是免费的)或者也可以在GitHub上创建($7/month)。

下面以 oschina 为例

创建版本描述仓库

回到终端,将这个远程的私有版本仓库添加到本地,repo 就是 repository 储存库的缩写。

$ pod repo add MyRepo https://git.oschina.net/baiyingqiu/MyRepo.git

查看在 Finder 目录 ~/.cocoapods/repos, 可以发现增加了一个 MyRepo 的储存库

创建代码库

回到 oschina 创建私人代码库

创建时添加 MIT LicenseREADME

将仓库克隆到本地,添加你的代码文件仓库名.podspec 描述文件,还有.swift-version.

如下

.swift-version文件用来知道swift版本,用命令行创建

$ echo "3.0" > .swift-version

.podspec 文件是你这个代码库的pod描述文件,可以通过pod指令创建空白模板:

$ pod spec create MyAdditions

或者 强烈建议 直接拷贝下面的模板进行修改

Pod::Spec.new do |s|
  s.name         = "MyAdditions" # 项目名称
  s.version      = "0.0.1"        # 版本号 与 你仓库的 标签号 对应
  s.license      = "MIT"          # 开源证书
  s.summary      = "私人pod代码" # 项目简介

  s.homepage     = "https://git.oschina.net/baiyingqiu/MyAdditions" # 仓库的主页
  s.source       = { :git => "https://git.oschina.net/baiyingqiu/MyAdditions.git", :tag => "#{s.version}" }#你的仓库地址,不能用SSH地址
  s.source_files = "MyAdditions/*.{h,m}" # 你代码的位置, BYPhoneNumTF/*.{h,m} 表示 BYPhoneNumTF 文件夹下所有的.h和.m文件
  s.requires_arc = true # 是否启用ARC
  s.platform     = :ios, "7.0" #平台及支持的最低版本
  # s.frameworks   = "UIKit", "Foundation" #支持的框架
  # s.dependency   = "AFNetworking" # 依赖库

  # User
  s.author             = { "BY" => "qiubaiyingios@163.com" } # 作者信息
  s.social_media_url   = "http://qiubaiying.github.io" # 个人主页

end

这里我要说一下一个坑,用 [oschina][8] 创建私人仓库时, 在验证时可能会找不到 MIT LICENSE证书,将其中的

[8]: https://link.jianshu.com?t=http://git.oschina.net/

s.license = “MIT”
修改为,指定文件
s.license = { :type => “MIT”, :file => “LICENSE” }

然后开始验证我们的仓库配置是否正确,并按照要求进行修改

$ pod lib lint

一般出现错误警告,需要添加 --private 或者 --allow-warnings,就可以通过验证

$ pod lib lint --private

验证成功后出现

 -> MyAdditions (0.0.1)

MyAdditions passed validation.

将描述文件推送到版本库

将项目打上标签推到远程仓库,标签号 和 版本号对应 都是0.0.1

最后将我们的代码仓库的描述信息,push 到我们的版本仓库中

$ pod repo push MyRepo MyAdditions.podspec

这时会对远程仓库进行验证,成功的话就会在 ~/.cocoapods/repos/MyRep中发现新增的仓库描述信息了

若是出现错误信息

[!] The repo `MyRepo` at `../.cocoapods/repos/MyRepo` is not clean

更新下我们的版本库,

$ pod repo update MyRepo

再继续上传即可。

pod repo push MyRepo MyAdditions.podspec 的过程就是

  1. 验证 MyAdditions.podspec 文件
  • 拉取远程版本库 MyRepo
  • 添加 MyAdditions.podspec 到版本库中
  • push 到远程

添加完成后我们就可以在pod中搜索

$ pod search MyAdditions

-> MyAdditions (0.0.1)
   Some category of the framework and UIKit
   pod 'MyAdditions', '~> 0.0.1'
   - Homepage: https://git.oschina.net/baiyingqiu/MyAdditions
   - Source:   https://git.oschina.net/baiyingqiu/MyAdditions.git
   - Versions: 0.0.1 [MyRepo repo]
(END)

私人pod库的使用

使用私人pod库的需要在Podflie中添加这句话,指明你的版本库地址。

source ‘https://git.oschina.net/baiyingqiu/MyRepo.git’

注意是版本库的地址,而不是代码库的地址,很多教程都把我搞晕了~

若有还使用了公有的pod库,需要把公有库地址也带上

source 'https://github.com/CocoaPods/Specs.git'

最后的Podflie文件变成这个样子

source ‘https://github.com/CocoaPods/Specs.git’
source ‘https://git.oschina.net/baiyingqiu/MyRepo.git’

platform :ios, '8.0'

target ‘MyPodTest’ do
use_frameworks!

pod “BYPhoneNumTF” #公有库
pod ‘MyAdditions’ #我们的私有库
pod ‘BYAdditions’ #这是我又添加到版本库中的另一个代码库

end

测试:

$ pod install

加载完成可以看到代码已经整合到我们的项目中了

perfect!

回到Fender中 ~/.cocoapods/repos,会发现 repos 中增加了一个pod版本库。

执行 pod install 命令时

  • 会拉取远程 Podfliesource 标记 版本库 到本地的 repos 文件夹中

  • 在 版本库 中搜索我们pod ‘MyAdditions’MyAdditions.podspec 文件。

  • 根据 MyAdditions.podspec 文件中描述的源码地址下载并整合到项目中

通过 《CocoaPods私有仓库的创建》《CocoaPods公有仓库的创建》这两篇文章,相信大家对CocoaPods的工作原理都有了更深层次的了解。

在写博客和和创建的过程中,踩了不少的坑(😀前人教程留下的),很多的东西只有自己操作完才能真正的领会。

最后,如果本文有什么错误或者有什么不同的观点欢迎提出交流。😉


Objective-C Runtime 的一些基本使用

Objective-C Runtime 的一些基本使用
From: https://www.jianshu.com/p/ff114e69cc0a

在上一篇文章《Objective-C Runtime详解》中我们探讨了Runtime的基本原理,这篇文章我们将总结一下Runtime的一些基本使用

目录

  • 查询方法
  • 给分类添加属性
  • 更换代码的实现方法
  • 动态添加方法
  • 字典转属性

先创建两个类

ClassA.h

#import <Foundation/Foundation.h>

@interface ClassA : NSObject {
    // 公有变量
    NSString *_publicVar1;
    NSString *_publicVar2;

}
// 公有属性
@property(nonatomic,copy) NSString *publicProperty1;
@property(nonatomic,copy) NSString *publicProperty2;

/* 公有方法 */
-(void)methodAOfClassAWithArg:(NSString *)arg;

@end

ClassA.m

#import "ClassA.h"

@interface ClassA()
// 私有属性
@property(nonatomic,copy) NSString *privateProperty1;
@property(nonatomic,copy) NSString *privateProperty2;

@end

@implementation ClassA {
    // 私有变量
    NSString *_privateVar1;
    NSString *_privateVar2;
}

/* 公有方法 */
-(void)methodAOfClassAWithArg:(NSString *)arg {
    NSLog(@" methodAOfClassA arg = %@", arg);
}

/* 私有方法 */
-(void)MethodBOfClassAWithArg:(NSString *)arg {
    NSLog(@" methodBOfClassA arg = %@", arg);
}
@end

ClassB.h

#import <Foundation/Foundation.h>

@interface ClassB : NSObject

/* 公有方法 */
-(void)methodAOfClassBWithArg:(NSString *)arg;

@end

ClassB.m

#import "ClassB.h"

@implementation ClassB
- (void)methodAOfClassBWithArg:(NSString *)arg {
    NSLog(@" methodAOfClassB arg = %@", arg);
}

-(void)methodBOfClassBWithArg:(NSString *)arg {
    NSLog(@" methodBOfClassB arg = %@", arg);
}

@end

查询方法


在Objective-C Runtime下没有真正意义上的私有变量和方法,因为这些私有变量和方法都可以通过Runtime方法获取,这当然包括系统的私有API。接下来我们来一一介绍获取类中属性和方法的方法。当然不要忘了#import <objc/runtime.h>.

获取类的名称

方法:const char *object_getClassName(id obj),使用比较简单,传入对象即可得到对应分类名。

ClassA *classA = [[ClassA alloc] init];
const char *className = object_getClassName(classA);
NSLog(@"className = %@", [NSString stringWithUTF8String:className]);

//输出
className = ClassA
获取类中的方法

方法:Method *class_copyMethodList(Class cls, unsigned int *outCount)

上代码:

UInt32 count;
char dst;
Method *methods = class_copyMethodList([classA class], &count);//获取方法列表
for (int i = 0; i < count; i++) {
    Method method = methods[i];// 获取方法
    SEL methodName = method_getName(method);// 获取方法名
    method_getReturnType(method, &dst, sizeof(char));// 获取方法返回类型
    const char *methodType = method_getTypeEncoding(method);// 获取方法参数类型和返回类型
    NSLog(@"methodName = %@",NSStringFromSelector(methodName));
    NSLog(@"dst = %c", dst);
}

 // 输出
 methodName = methodAOfClassAWithArg:
 dst = v
 methodType = v24@0:8@16
 methodName = MethodBOfClassAWithArg:
 dst = v
 methodType = v24@0:8@16
 methodName = publicProperty1
 dst = @
 methodType = @16@0:8
 methodName = setPublicProperty1:
 dst = v
 methodType = v24@0:8@16
 methodName = publicProperty2
 dst = @
 methodType = @16@0:8
 methodName = setPublicProperty2:
 dst = v
 methodType = v24@0:8@16
 methodName = privateProperty1
 dst = @
 methodType = @16@0:8
 methodName = setPrivateProperty1:
 dst = v
 methodType = v24@0:8@16
 methodName = privateProperty2
 dst = @
 methodType = @16@0:8
 methodName = setPrivateProperty2:
 dst = v
 methodType = v24@0:8@16
 methodName = .cxx_destruct
 dst = v
 methodType = v16@0:8

class_copyMethodList([classA class], &count) 传入元类和计数器地址,返回方法列表。这里注意,返回的是Method结构体类型的C数组,Method类型我们在[上篇文章][2]中已经详细说明,

[2]: https://www.jianshu.com/p/a36bfc976b8e

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
} 

但要区分Method *methodsMethod method的区别,这是比较基础C语言知识。还有Uint32是OC定义的unsigned int类型typedef unsigned int UInt32;

这里我们来看看 method_getReturnType(method, &dst, sizeof(char)) 方法简单输出返回值类型,输出为 v@ ,参考Apple文档可知道返回类型为 voidid

A void v
A method selector (SEL)  :
An object (whether statically typed or typed id) @ 

method_getTypeEncoding(method)方法可以输出返回值,参数类型以及接收器类型。我们看输出的v24@0:8@16,分析上面的说明就可以知道: v24返回类型为viod,@0接收器类型为id,@16参数类型为id

至于类型后面的值观察可以发现都是相差8,我认为是在method中的位置,分别以8bit存储不同类型的数据。

若有两个参数返回值为 v32@0:8@16@24 ,对比可以猜测,在method中各个成员的排列是这样的: 接收器|SEl标识|参数1|参数2|...|返回值,然后由 method_getTypeEncoding(method) 输出的顺序为: 返回值类型|接收器类型|SEL标识|参数1|参数2|... 此处为个人见解,如有错误或不同意见欢迎提出探讨。

最后发现了一个奇怪的方法 .cxx_destruct ,在中这篇文章中:

ARC actually creates a -.cxx_destruct method to handle freeing instance variables. This method was originally created for calling C++ destructors automatically when an object was destroyed.

和《Effective Objective-C 2.0》中提到的:

When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.

可以了解到,.cxx_destruct 方法原本是为了C++对象析构的,ARC借用了这个方法插入代码实现了自动内存释放的工作

关于 .cxx_destruct 可以参考这篇文章:ARC下dealloc过程及.cxx_destruct的探究

获取类中的属性

在 [上篇文章][5] 的 Property 中我们也提到了获取类中的属性的方法,如下:

[5]: https://www.jianshu.com/p/a36bfc976b8e

id LenderClass = objc_getClass(“ClassA”);//获取classA 的元类,不同于[ClassA class]返回本身
unsigned int outCount;//属性数量
// 获取属性列表
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

// 遍历
for (int i = 0; i < outCount; i++) {

    objc_property_t property = properties[i];

    const char *propertyName = property_getName(property);// 获取属性名
    const char *propertyAttributes = property_getAttributes(property);// 获取属性描述

    printf("propertyName:%s \n", propertyName);
    printf("propertyAttributes:%s\n--------\n", propertyAttributes);//属性名及描述
}


// 输出
propertyName:privateProperty1 
propertyAttributes:T@"NSString",C,N,V_privateProperty1
--------
propertyName:privateProperty2 
propertyAttributes:T@"NSString",C,N,V_privateProperty2
--------
propertyName:publicProperty1 
propertyAttributes:T@"NSString",C,N,V_publicProperty1
--------
propertyName:publicProperty2 
propertyAttributes:T@"NSString",C,N,V_publicProperty2
--------

发现会输出公有属性以及私有属性。

获取类中的成员变量

我们可以发现获取类中的方法,属性过程基本一致:通过元类获取方法列表或属性列表,然后在进行遍历。获取成员变量也一样:

id selfClass = [Class class];
unsigned int numIvars = 0;
Ivar *ivars = class_copyIvarList(selfClass, &numIvars);
for(int i = 0; i < numIvars; i++) {
    Ivar ivar = ivars[i];
    const char *ivarName = ivar_getName(ivar);
    const char *ivarType = ivar_getTypeEncoding(ivar);// 获取类型

    printf("ivarName:%s\n", ivarName);
    printf("ivarType:%s\n------\n", ivarType);
}


// 输出
ivarName:_publicVar1
ivarType:@"NSString"
------
ivarName:_publicVar2
ivarType:@"NSString"
------
ivarName:_privateVar1
ivarType:@"NSString"
------
ivarName:_privateVar2
ivarType:@"NSString"
------
ivarName:_publicProperty1
ivarType:@"NSString"
------
ivarName:_publicProperty2
ivarType:@"NSString"
------
ivarName:_privateProperty1
ivarType:@"NSString"
------
ivarName:_privateProperty2
ivarType:@

可以发现输出了所有的成员变量,包括属性声明的 _+属性名 变量。

给分类添加属性


众所周知,分类中是不能声明属性的。

我们创建一个 ClassA 的分类 ClassA+CategoryA ,在 ClassA+CategoryA 中添加一个属性 name

#import "ClassA.h"

@interface ClassA (CategoryA)

@property (nonatomic, strong) NSString *name;

@end

若在我们调用ClassA分类的name 将会crash,原因是分类中使用 @property 声明属性并不会生成settergetter方法,但是我们会想,我们可以自己实现呀,没错,看下面的代码

#import "ClassA+CategoryA.h"
#import <objc/runtime.h>

@implementation ClassA (CategoryA)

- (NSString *)name {
    return name;
}

- (void)setName:(NSString *)name {
    _name = name;
}

@end

这里会报编译错误,因为分类中使用 @property 声明属性也不会生成成员变量 _name,并且手动声明也不行

编译错误,提示实例变量无法添加到分类中,用正常的方法确实无法在分类中添加属性。

但是可以通过Runtim机制进行“添加”。其本质是给这个类添加属性关联,而非把这个属性添加到类中。

#import "ClassA+CategoryA.h"
#import <objc/runtime.h>


@implementation ClassA (CategoryA)

- (NSString *)name {
    // _cmd -> @selector(name)
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

调用:

classA.name = @"邱帅";
NSLog(@"%@",classA.name);

// 输出
2016-11-21 16:18:48.084 UseRuntime[4392:1325037] 邱帅

可以看出添加属性成功!

我们来看看关联属性的这几个方法:

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

OBJC_EXPORT void objc_removeAssociatedObjects(id object)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

objc_setAssociatedObject() 方法为关联属性,参数如下:

  • object:属性关联的源对象,这里使用了self,代表关联本类的对象
  • key:区分属性的唯一标识,因为关联的属性可能不止一个,我们使用了- (NSString *)name方法的SEL @selector(name)作为唯一标示,当然也可以用下面的方法来生成Key :

    //利用静态变量地址唯一不变的特性
    1、static void *strKey = &strKey;

    2、static NSString *strKey = @”strKey”;

    3、static char strKey;

  • value:关联的属性值
  • policy:设置关联对象的copystorynonatomic等参数:

这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,              
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,                                                  
    OBJC_ASSOCIATION_RETAIN = 01401,       
    OBJC_ASSOCIATION_COPY = 01403                                               
};

objc_getAssociatedObject(id object, const void *key) 方法通过 objectKey 直接获取关联的属性值

上面代码中的第二个参数写的是 _cmd,等价于@selector(name)

Objective-C的编译器在编译后会在每个方法中加两个隐藏的参数:
一个是_cmd,当前方法的一个SEL指针。
另一个就是用的比较多的self,指向当前对象的一个指针。

objc_removeAssociatedObjects() 移除关联

我们使用上面的获取类中属性和成员变量的方法,发现输出:

// 有属性输出
propertyName:name 
propertyAttributes:T@"NSString",&,N

没有成员变量 _name,进一步说明分类中不能添加成员变量!其本质是添加属性与分类之间关联。

更换代码实现方法(Method Swizzling)


在[上篇][6]中详细介绍了Method Swizzling的原理,其本质是更换了 selectorIMP

[6]: https://www.jianshu.com/p/a36bfc976b8e

#import “ViewController.h”

#import <objc/runtime.h>
#import "ClassA.h"
#import "ClassB.h"

@interface ViewController ()

@end

@implementation ViewController

+ (void)load {
    Method classA_method = class_getInstanceMethod([ClassA class], @selector(methodAOfClassAWithArg:));
    Method classB_method = class_getInstanceMethod([ClassB class], @selector(methodAOfClassBWithArg:));
    method_exchangeImplementations(classA_method, classB_method);
}
- (void)viewDidLoad {
    [super viewDidLoad];

    [classA methodAOfClassAWithArg:@"classA 发出的 A方法"];
    [classB methodAOfClassBWithArg:@"classB 发出的 A方法"];
}    

// 输出

2016-11-22 13:07:15.151 UseRuntime[1015:533335]  methodAOfClassB arg = classA 发出的 A方法
2016-11-22 13:07:15.151 UseRuntime[1015:533335]  methodAOfClassA arg = classB 发出的 A方法

首先交换方法写在 +(void)load,在程序的一开始就调用执行,你将不会碰到并发问题。

我们可以发现两个方法的实现过程以及对换。

当然,平时使用我们并不会这么做,当我们要在系统提供的方法上再扩充功能时(不能重写系统方法),就可以使用Method Swizzling.

我们给NSArray添加一个分类AddLog,给 arrayByAddingObject:方法添加一个输出方法:

#import "NSArray+AddLog.h"
#import <objc/runtime.h>

@implementation NSArray (AddLog)

+ (void)load {

    SEL ori_selector = @selector(arrayByAddingObject:);
    SEL my_selector = @selector(my_arrayByAddingObject:);

    Method ori_method = class_getInstanceMethod([NSArray class], ori_selector);
    Method my_method  = class_getInstanceMethod([NSArray class], my_selector);

    if (([NSArray class], ori_selector, method_getImplementation(my_method), method_getTypeEncoding(my_method))) {

        class_replaceMethod([NSArray class], my_selector, method_getImplementation(ori_method), method_getTypeEncoding(ori_method));

    } else {
        method_exchangeImplementations(ori_method, my_method);
    }

}

- (NSArray *)my_arrayByAddingObject:(id)anObject {

    NSArray *array = [self my_arrayByAddingObject:anObject];
    NSLog(@"添加了一个元素 %@", anObject);
    return array;
}

@end

我们来看看这三个方法:

  • class_addMethod():给一个方法添加新的方法和实现
  • class_replaceMethod():取代了对于一个给定的类的实现方法
  • method_exchangeImplementations():交换两个类的实现方法

这里我们先使用 class_addMethod() 在类中添加方法,若返回Yes说明类中没有该方法,然后再使用 class_replaceMethod() 方法进行取代;若返回NO,说明类中有该方法,使用method_exchangeImplementations()直接交换两者的 IMP.

其实在这里直接使用method_exchangeImplementations()进行交换就可以了。因为类中必定有arrayByAddingObject:方法。

我给我们自己的方法命名为my_arrayByAddingObject:,在原来的方法名上加上前缀,既可以防止命名冲突,又方便阅读,在我们my_arrayByAddingObject:方法中调用本身

NSArray *array = [self my_arrayByAddingObject:anObject];

看似会陷入递归调用,其实则不会,因为我们已经在+ (void)load方法中更换了IMP,他会调用arrayByAddingObject:方法,然后在后面添加我们需要添加的功能。

arrayByAddingObject:方法的调用不变;

NSArray *arr1 = @[@"one", @"two"];
NSArray *arr2 = [arr1 arrayByAddingObject:@"three"];
NSLog(@"arr2 = %@", arr2);


// 输出
2016-11-22 13:57:00.021 UseRuntime[1147:743449] 添加了一个元素 three
2016-11-22 13:57:00.021 UseRuntime[1147:743449] arr2 = (
    one,
    two,
    three
)

动态添加方法

动态添加方法就是在消息转发前在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中使用class_addMethod() 添加方法。

下面我面添加一个名为resolveThisMethodDynamically的方法:

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
    printf("执行了dynamicMethodIMP!!!!");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    if (sel == @selector(resolveThisMethodDynamically)) {
        class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

调用:

performSelector:@selector(resolveThisMethodDynamically)];

// 输出
执行了dynamicMethodIMP!!!!

对于上面添加的的方法 resolveThisMethodDynamically ,使用 [self performSelector:@selector(resolveThisMethodDynamically)] 进行调用,不能使用[self resolveThisMethodDynamically],因为压根就没有声明 -(void)resolveThisMethodDynamically,会报编译错误。

整个过程就是,performSelector:调用resolveThisMethodDynamically方法,然后在列表中找不到(因为类中根本就没有注册该方法),然后跳入 + (BOOL)resolveInstanceMethod: 中,我们再为resolveThisMethodDynamically方法添加具体实现。

字典转属性

将字典转化为模型,是在我们iOS开发中最为常用的技能。iOS的模型框架如JSONModel,MJExtension,MJExtension等皆是利用了runtime,将字典转为模型,不过兼顾的细节更多。下面我们来实现一个简易的字典转模型框架。

先上代码:

#import "NSObject+BYModel.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation NSObject (BYModel)

- (void)by_modelSetDictionary:(NSDictionary *)dic {

    Class cls = [self class];

    // 遍历本类和父类的变量
    while (cls) {
        //获取所有成员变量
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(cls, &outCount);

        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];

            // 获取变量名
            NSMutableString *ivar_Name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];

            [ivar_Name replaceCharactersInRange:NSMakeRange(0, 1) withString:@""];// _ivar -> ivar

            //
            NSString *key = [ivar_Name copy];
            if ([key isEqualToString:@"dece"]) {
                key = @"description";
            }
            if ([key isEqualToString:@"ID"]) {
                key = @"id";
            }

            id value = dic[key];
            if (!value) continue;

            // 拼接SEL    ivar -> setIvar:

            NSString *cap = [ivar_Name substringToIndex:1];
            cap = cap.uppercaseString; // a->A
            [ivar_Name replaceCharactersInRange:NSMakeRange(0, 1) withString:cap];
            [ivar_Name insertString:@"set" atIndex:0];
            [ivar_Name appendString:@":"];

            SEL selector = NSSelectorFromString(ivar_Name);

            // 判断类型并发送消息
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

            if ([type hasPrefix:@"@"]) { // 对象类型
                objc_msgSend(self, selector, value);
            } else { // 非对象类型
                if ([type isEqualToString:@"d"]) {
                    objc_msgSend(self, selector, [value doubleValue]);
                } else if ([type isEqualToString:@"f"]) {
                    objc_msgSend(self, selector, [value floatValue]);
                } else if ([type isEqualToString:@"i"]) {
                    objc_msgSend(self, selector, [value intValue]);
                } else {
                    objc_msgSend(self, selector, [value longLongValue]);
                }
            }


        }
        // 获取父类进行遍历变量
        cls = class_getSuperclass(cls);
    }

}

这个这个段代码可能出现编译错误:

解决办法很简单:

将项目 Project -> Build Settings -> Enable strct checking of objc_msgSend Calls 设置为 NO 即可

接下来我们创建一个模型类Student

#import <Foundation/Foundation.h>

@interface Student : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int idNumber;

@end

使用我们的转模型方法:

NSDictionary *dic = @{ @"name":@"邱帅", @"age": @(23), @"idNumber":@(1234567)};

Student *stu = [Student new];
[stu by_modelSetDictionary:dic];

NSLog(@"%@", [NSString stringWithFormat:@"%@, %d, %d", stu.name, stu.age, stu.idNumber]);

// 输出
2016-11-24 15:32:46.351 Demo_字典转模型(Runtime)[2131:884627] 邱帅, 23, 1234567

该方法先利用我们上面介绍的class_copyIvarList()获取类中的成员变量列表,然后进行遍历,拼接字符串setIvar:,最后调用objc_msgSend()直接发送设置变量的消息,完成属性的赋值。

while (cls) {

    //code..

 cls = class_getSuperclass(cls);
}

这个循环是则获取父类中的属性:当前类的属性遍历结束之后,指向父类,若父类存在则在继续遍历属性,否则就退出循环。

当然,这个方法只是介绍了利用runtime进行字典转模型的原理,实际中还有很多需要考虑的细节,项目中我还是推荐使用像YYModel这些比较成熟而且安全的模型框架。

关于快速字典转模型可以参考我写的一篇《快速完成JSON\字典转模型 For YYModel》

Objective-C Runtime详解

From: https://www.jianshu.com/p/a36bfc976b8e

最近在学习Runtime的知识,恰巧发现了这篇博客《Objective-C Runtime》,在此基础上,进行了些许补充说明,如有错误或其他想法,欢迎提出交流。

目录

  • 引言
  • 简介
  • 与Runtime交互
  • RunTime术语
  • 消息
  • 动态方法解析
  • 消息转发
  • 健壮的实例变量
  • 动态添加属性(Object-C Associated Objects)
  • 方法调剂(Method Swizzling)
  • 总结

引言

Objective-C的方法调用实则为“发送消息”,我们来看[dog eat]实际会被编译器转化为

objc_msgSend(dog, SEL)//SEL为eat方法的标识符@selector(@"eat")

若方法中函数参数,则为:

objc_msgSend(dog, SEL, arg1, arg2, ...)

如果消息的接收者能够找到对应的方法,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个方法对应的实现内容,要么就干脆就crash掉。

现在可以看出[dog eat]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送eat这条消息,而dog将要如何响应这条消息,那就要看运行时发生的情况来决定了。

Objective-C 的 Runtime 铸就了它动态语言的特性,这些深层次的知识虽然平时写代码用的少一些,但是却是每个 Objc 程序员需要了解的。

简介

因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。

Runtime其实有两个版本:“modern”和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行(Modern)版的Runtime系统,只能运行在 iOS 和 OS X 10.5 之后的64位程序中。而OS X较老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。

与Runtime交互

Objc 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。

Objective-C源代码

大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(比如objc_msgSend函数及其参数列表中的idSEL都是啥)

NSObject的方法

Cocoa 中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。

有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

Runtime的函数

Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。

Runtime术语

还记得引言中的objc_msgSend:方法吧,它的真身是这样的

id objc_msgSend ( id self, SEL op, ... );

下面将会逐渐展开介绍一些术语,其实它们都对应着数据结构。

SEL

objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的标识,而这个标识的数据结构是SEL:

typedef struct objc_selector *SEL;

本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。这个查找过程我们将在下面讨论。

我们可以在运行时添加新的selector,也可以在运行时获取已存在的selector,我们可以通过下面三种方法来获取SEL:

  1. sel_registerName函数

  2. Objective-C编译器提供的@selector()

  3. NSSelectorFromString()方法

id

objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:

typedef struct objc_object *id;

objc_object又是啥呢:

struct objc_object { Class isa; };

objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档的这句段说明

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

Class

之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:

typedef struct objc_class *Class;

objc_class就是我们摸到的那个瓜,里面的东西多着呢:

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;

可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议

PS:OBJC2_UNAVAILABLE之类的宏定义是苹果在 Objc 中对系统运行版本进行约束的黑魔法,为的是兼容非Objective-C 2.0的遗留逻辑,但我们仍能从中获得一些有价值的信息,有兴趣的可以查看源代码

Objective-C 2.0 的头文件虽然没暴露出objc_class结构体更详细的设计,我们依然可以从Objective-C 1.0 的定义中小窥端倪

objc_class结构体中:ivarsobjc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。而最新版的 Runtime 源码对这一块的描述已经有很大变化,可以参考下美团技术团队的深入理解Objective-C:Category.

PS:任性的话可以在Category中添加@dynamic的属性,并利用运行期动态提供存取方法或干脆动态转发;或者干脆使用关联度对象(AssociatedObject)

其中objc_ivar_listobjc_method_list分别是成员变量列表和方法列表:

struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}

如果你C语言不是特别好,可以理解为objc_ivar_list结构体存储着objc_ivar数组列表,而objc_ivar结构体存储了类的单个成员变量的信息;同理objc_method_list结构体存储着objc_method数组列表,而objc_method结构体存储了类的某个方法的信息。

最后要提到的还有一个objc_cache,顾名思义它是缓存,它在objc_class的作用很重要,在后面会讲到。

不知道你是否注意到了objc_class中也有一个isa对象,这是因为一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。

实际上,类对象是元类对象的一个实例!!元类描述了 一个类对象,就像类对象描述了普通对象一样。不同的是元类的方法列表是类方法的集合,由类对象的选择器来响应。当向一个类发送消息时,objc_msgSend 会通过类对象的isa指针定位到元类,并检查元类的方法列表(包括父类)来决定调用哪个方法。元类代替了类对象描述了类方法,就像类对象代替了实例对象描述了实例化方法。
很显然,元类也是对象,也应该是其他类的实例,实际上元类是根元类(root class’s metaclass)的实例,而根元类是其自身的实例,即根元类的isa指针指向自身。
类的super_class指向其父类,而元类的super_class则指向父类的元类。元类的super class链与类的super class链平行,所以类方法的继承与实例方法的继承也是并行的。而根元类(root class’s metaclass)的super_class指向根类(root class),这样,整个指针链就链接起来了!!

记住,当一个消息发送给任何一个对象, 方法的检查 从对象的 isa 指针开始,然后是父类。实例方法在类中定义, 类方法 在元类和根类中定义。(根类的元类就是根类自己)。在一些计算机语言的原理中,一个类和元类层次结构可以更自由的组成,更深元类链和从单一的元类继承的更多的实例化的类。Objective-C 的类方法 是使用元类的根本原因,在其他方面试图在隐藏元类。例如[NSObject class] 完全相等于 [NSObject self],所以,在形式上他还是返回的 NSObject->isa 指向的元类。 Objective-C语言是一组实用的折中方案。

上图实线是 super_class 指针,虚线是isa指针。 有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类

Method

Method是一种代表类中的某个方法的类型。

typedef struct objc_method *Method;

objc_method在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
  • 方法名 method_name 类型为 SEL, 相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
  • 方法类型method_types是个char指针,存储着方法的 参数类型 和 返回值 类型。
  • method_imp指向了方法的实现,本质上是一个函数指针,后面会详细讲到。

Ivar

Ivar是一种代表类中实例变量的类型。定义如下:

typedef struct objc_ivar *Ivar;

它是一个指向objc_ivar结构体的指针,结构体有如下定义:

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}                                                            OBJC2_UNAVAILABLE;

这里我们注意第三个成员 ivar_offset。它表示基地址偏移字节。

在编译我们的类时,编译器生成了一个 ivar 布局,显示了在类中从哪可以访问我们的 ivars

我们对 ivar 的访问就可以通过 对象地址ivar偏移字节的方法。

但是当我们增加了父类的ivar,这个时候布局就出错了,我们就不得不重新编译子类来恢复兼容性。

而Objective-C Runtime中使用了Non Fragile ivars来避免这个问题

使用Non Fragile ivars时,Runtime会进行检测来调整类中新增的ivar的偏移量。 这样我们就可以通过 对象地址 + 基类大小 + ivar偏移字节的方法来计算出ivar相应的地址,并访问到相应的ivar

可以根据实例查找其在类中的名字,也就是“反射”:

-(NSString *)nameWithInstance:(id)instance {
    unsigned int numIvars = 0;
    NSString *key=nil;
    Ivar * ivars = class_copyIvarList([self class], &numIvars);
    for(int i = 0; i < numIvars; i++) {
        Ivar thisIvar = ivars[i];
        const char *type = ivar_getTypeEncoding(thisIvar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        if (![stringType hasPrefix:@"@"]) {
            continue;
        }
        if ((object_getIvar(self, thisIvar) == instance)) {//此处若 crash 不要慌!
            key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
            break;
        }
    }
    free(ivars);
    return key;
}

class_copyIvarList 函数获取的不仅有实例变量,还有属性。但会在原本的属性名前加上一个下划线。(属性的本质就是 _属性名+set+get方法)

IMP

IMPobjc.h中的定义是:

typedef id (*IMP)(id, SEL, ...);

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。

我们再来看看objc_msgSend()的定义:id objc_msgSend(id self, SEL op, ...)

你会发现IMP指向的方法与objc_msgSend函数类型相同,参数都包含id和SEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组idSEL参数就能确定唯一的方法实现地址。

Cache

runtime.h中Cache的定义如下:

typedef struct objc_cache *Cache

还记得之前 objc_class 结构体中有一个 struct objc_cache *cache 吧,它到底是缓存啥的呢,先看看 objc_cache 的实现:

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

objc_cache 的定义看起来很简单,它包含了下面三个变量:

  • mask:可以认为是当前能达到的最大index(从0开始的),所以缓存的size(total)是mask+1
  • occupied:被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而occupied表示当前被占用的数目
  • buckets:用数组表示的hash表,cache_entry类型,每一个cache_entry代表一个方法缓存

(buckets定义在objc_cache的最后,说明这是一个可变长度的数组)

Cache为方法调用的性能进行优化,下面我们来看看objc_msgSend具体又是如何分发的呢? 我们来看下runtime层objc_msgSend的源码。

objc-msg-arm.s中,objc_msgSend的代码如下:

ps:Apple为了高度优化objc_msgSend的性能,这个文件是汇编写成的,不过即使我们不懂汇编,详尽的注释也可以让我们一窥其真面目

ENTRY objc_msgSend
# check whether receiver is nil
teq     a1, #0
    beq     LMsgSendNilReceiver
# save registers and load receiver's class for CacheLookup
stmfd   sp!, {a4,v1}
ldr     v1, [a1, #ISA]
# receiver is non-nil: search the cache
CacheLookup a2, v1, LMsgSendCacheMiss
# cache hit (imp in ip) and CacheLookup returns with nonstret (eq) set, restore registers and call
ldmfd   sp!, {a4,v1}
bx      ip
# cache miss: go search the method lists
LMsgSendCacheMiss:
ldmfd sp!, {a4,v1}
b _objc_msgSend_uncached
LMsgSendNilReceiver:
    mov     a2, #0
    bx      lr
LMsgSendExit:
END_ENTRY objc_msgSend
STATIC_ENTRY objc_msgSend_uncached
# Push stack frame
stmfd sp!, {a1-a4,r7,lr}
add     r7, sp, #16
# Load class and selector
ldr a3, [a1, #ISA] /* class = receiver->isa  */
/* selector already in a2 */
/* receiver already in a1 */
# Do the lookup
MI_CALL_EXTERNAL(__class_lookupMethodAndLoadCache3)
MOVE    ip, a1
# Prep for forwarding, Pop stack frame and call imp
teq v1, v1 /* set nonstret (eq) */
ldmfd sp!, {a1-a4,r7,lr}
bx ip

如果向更深入了解 objc_cache ,可以看看这篇博文深入理解Objective-C:方法缓存

从上述代码中可以看到,objc_msgSend(就ARM平台而言)的消息分发分为以下几个步骤:

  1. 判断receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象
  2. 从缓存里寻找,找到了则分发,否则
  3. 利用objc-class.mm中_class_lookupMethodAndLoadCache3(为什么有个这么奇怪的方法。本文末尾会解释)方法去寻找selector
  4. 如果支持GC,忽略掉非GC环境的方法(retain等)
  5. 从本class的method list寻找selector,如果找到,填充到缓存中,并返回selector,否则
  6. 寻找父类的method list,并依次往上寻找,直到找到selector,填充到缓存中,并返回selector,否则
  7. 调用_class_resolveMethod,如果可以动态resolve为一个selector,不缓存,方法返回,否则
  8. 转发这个selector,否则
  9. 报错,抛出异常

从上面的分析中我们可以看到,当一个方法在比较“上层”的类中,用比较“下层”(继承关系上的上下层)对象去调用的时候,如果没有缓存,那么整个查找链是相当长的。就算方法是在这个类里面,当方法比较多的时候,每次都查找也是费事费力的一件事情。

当我们需要去调用一个方法数十万次甚至更多地时候,查找方法的消耗会变的非常显著。就算我们平常的非大规模调用,除非一个方法只会调用一次,否则缓存都是有用的。在运行时,那么多对象,那么多方法调用,节省下来的时间也是非常可观的。可见缓存的重要性。

方法缓存存在什么地方?

让我们再去去翻看 objc_class 的定义,

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;

我们看到在类的定义里就有cache字段,没错,类的所有缓存都存在metaclass上,所以每个类都只有一份方法缓存,而不是每一个类的object都保存一份。

子类类即便是从父类取到的方法,也会存在类本身的方法缓存里。而当用一个父类对象去调用那个方法的时候,也会在父类的metaclass里缓存一份。

Property

@property标记了类中的属性,这个不必多说大家都很熟悉,它是一个指向objc_property结构体的指针:

typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用

现在在类中声明声明属性和成员变量:

@interface ViewController ()
{
    int age;
    NSString *name;
}
@property (nonatomic, strong) NSString *property1;
@property (nonatomic, strong) NSString *property2;
@property (nonatomic, assign) int age;//这里的age为属性,对应变量:_age
@property (nonatomic, assign) long ID;

@end

然后用下面的方法来获取类中属性列表:

id LenderClass = objc_getClass("ViewController");//获取calss
//id LenderClass = [MyViewController class];//同上
unsigned int outCount;//属性数量
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);//获取属性列表
for (int i = 0; i < outCount; i++) {// 遍历
    objc_property_t property = properties[i];
    const char *propertyName = property_getName(property);
    const char *propertyAttributes = property_getAttributes(property);
    printf("propertyName:%s \n", propertyName);
    printf("propertyAttributes:%s\n--------\n", propertyAttributes);//属性名及描述
}

控制台输出:

propertyName:property1 
propertyAttributes:T@"NSString",&,N,V_property1
--------
propertyName:property2 
propertyAttributes:T@"NSString",&,N,V_property2
--------
propertyName:age 
propertyAttributes:Ti,N,V_age
--------
propertyName:ID 
propertyAttributes:Tq,N,V_ID

我们再来来看看获取成员变量的方法:

id selfClass = [self class];
unsigned int numIvars = 0;
Ivar *ivars = class_copyIvarList(selfClass, &numIvars);
for(int i = 0; i < numIvars; i++) {
    Ivar ivar = ivars[i];
    const char *ivarType = ivar_getTypeEncoding(ivar);// 获取类型
    const char *ivarName = ivar_getName(ivar);
    printf("ivarName:%s\n", ivarName);
    printf("ivarType:%s\n------\n", ivarType);
}

控制台输出:

ivarName:age
ivarType:i
------
ivarName:name
ivarType:@"NSString"
------
ivarName:_age
ivarType:i
------
ivarName:_property1
ivarType:@"NSString"
------
ivarName:_property2
ivarType:@"NSString"
------
ivarName:_ID
ivarType:q

我们会发现与 class_copyIvarList 函数不同,使用 class_copyPropertyList 函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的,得到属性或者变量名后我们就可以使用KVC去修改访问类中的私有属性或变量。所以OC中没有真正意义上的私有变量,私有方法也是。

消息

前面做了这么多铺垫,现在终于说到了消息了。Objc 中发送消息是用中括号 [] 把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。

有关消息发送和消息转发机制的原理,可以查看这篇文章

objc_msgSend函数

在引言中已经对 objc_msgSend 进行了一点介绍,看起来像是 objc_msgSend 返回了数据,其实 objc_msgSend 从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:

  1. 检测这个 消息 是不是要忽略的。比如 Mac OS X 开发,在ARC中有了垃圾回收就不理会MRC的 retain, release 这些函数了。
  2. 检测这个 目标对象 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
  3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
  4. 如果 cache 找不到就找一下方法分发表。
  5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
  6. 如果还找不到就要开始进入动态方法解析了,后面会提到。

PS:这里说的分发表其实就是 Class 中的方法列表,它将方法选择器和方法实现地址联系起来。

其实编译器会根据情况在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法

PS:有木有发现这些函数的命名规律哦?带 “Super” 的是消息传递给超类;“stret”可分为“st”+“ret”两部分,分别代表 “struct”“return”“fpret”就是 “fp” + “ret”,分别代表“floating-point”“return”

方法中的隐藏参数

我们经常在方法中使用 self 关键字来引用实例本身,但从没有想过为什么 self 就能取到调用当前方法的对象吧。其实 self 的内容是在方法运行时被偷偷的动态传入的

objc_msgSend 找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:

  • 接收消息的对象(也就是self指向的内容)
  • 方法选择器(_cmd指向的内容)

之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在下面的例子中,self引用了接收者对象,而_cmd引用了方法本身的选择器:

- strange
{
    id  target = getTheReceiver();
    SEL method = getTheMethod();

    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}

在这两个参数中,self 更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径

而当方法中的 super 关键字接收到消息时,编译器会创建一个 objc_super 结构体:

struct objc_super { id receiver; Class class; };

这个结构体指明了消息应该被传递给特定父类的定义。但receiver仍然是self本身,这点需要注意,因为当我们想通过[super class]获取超类时,编译器只是将指向selfid指针和classSEL传递给了objc_msgSendSuper函数,因为只有在NSObject类才能找到class方法,然后class方法调用object_getClass(),接着调用objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向selfid指针,与调用[self class]相同,所以我们得到的永远都是self的类型。

获取方法地址

IMP 那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。
NSObject类中有个methodForSelector:实例方法,你可以用它来获取某个方法选择器对应的 IMP ,举个栗子:

void (*imp)(id, SEL, BOOL);//定义一个函数指针
imp = (void (*)(id, SEL, BOOL))[self methodForSelector:@selector(setFilled:)];//获取setFilled:函数的IMP

动态方法解析

你可以动态地提供一个方法的实现。例如我们可以用 @dynamic 关键字在类的实现文件中修饰一个属性:

@dynamic propertyName;

这表明我们会为这个属性提供存取方法,也就是说编译器不会默认为我们生成 setPropertyName:prepertyName 方法,而需要我们自己提供动态方法。我们可以通过分别重载 resolveIntanceMethod:resolvrClassMethod: 方法分别添加实例方法实现和类方法实现。因为当 Runtime 系统在 Cache 和方法分发表中(包括父类)找不到要执行的方法时,Runtime会调用 resolveIntanceMethod:resolvrClassMethod: 来给我们一次动态添加实现的机会。我们需要 class_addMethod函数完成向特定类添加特定方法实现的操作:

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

调用必须使用performSelector:方法,

[self performSelector:@selector(resolveThisMethodDynamically)];

因为performSelector:方法在编译阶段不做检查,若使用[self resolveThisMethodDynamically]则编译无法通过,因为[receiver message]会在编译阶段检查resolveThisMethodDynamically方法是否存在。
上面的例子为resolveThisMethodDynamically方法添加了实现内容,也就是dynamicMethodIMP方法中的代码。其中 “v@:” 表示返回值和参数,这个符号涉及 Type Encoding

PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector:instancesRespondToSelector: 方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的 IMP 的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod: 返回 NO

消息转发

重定向

在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector 方法替换消息的接受者为其他对象:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择 。如果此方法返回 nilself,则会进入消息转发机制(forwardInvocation:);否则将向返回的对象重新发送消息。

转发

当动态方法解析不作处理返回NO时,消息转发机制会被触发。在这时forwardInvocation:方法会被执行,我们可以重写这个方法来定义我们的转发逻辑:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

该消息的唯一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。

当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过 forwardInvocation: 消息通知该对象。每个对象都从NSObject类中继承了 forwardInvocation: 方法。然而,NSObject中的方法实现只是简单地调用了 doesNotRecognizeSelector: 。通过实现我们自己的 forwardInvocation: 方法,我们可以在该方法实现中将消息转发给其它对象。

forwardInvocation: 方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。

注意: forwardInvocation: 方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。否则,forwardInvocation:将不可能会被调用。

转发和多继承

转发和继承相似,可以用于为Objc编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。

这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中 WarriorDiplomat 没有继承关系,但是 Warriornegotiate 消息转发给了 Diplomat 后,就好似 DiplomatWarrior 的超类一样。
消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的

替代者对象(Surrogate Objects)

转发不仅能模拟多继承,也能使轻量级对象代表重量级对象。弱小的女人背后是强大的男人,毕竟女人遇到难题都把它们转发给男人来做了。这里有一些适用案例,可以参看官方文档

转发于继承

尽管转发很像继承,但是NSObject类不会将两者混淆。像 respondsToSelector:isKindOfClass: 这类方法只会考虑继承体系,不会考虑转发链。比如上图中一个 Warrior 对象如果被问到是否能响应 negotiate 消息:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )
    ...

结果是 NO ,尽管它能够接受 negotiate 消息而不报错,因为它靠转发消息给 Diplomat 类来响应消息。

如果你为了某些意图偏要“弄虚作假”让别人以为Warrior 继承到了 Diplomatnegotiate 方法,你得重新实现 respondsToSelector:isKindOfClass: 来加入你的转发算法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;
}

除了respondsToSelector:isKindOfClass:之外,instancesRespondToSelector:中也应该写一份转发算法。如果使用了协议,conformsToProtocol:同样也要加入到这一行列中。类似地,如果一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:来返回准确的方法描述,这个方法会最终响应被转发的消息。比如一个对象能给它的替代者对象转发消息,它需要像下面这样实现methodSignatureForSelector::

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

健壮的实例变量(Non Fragile ivars)

在 Runtime 的现行版本中,最大的特点就是健壮的实例变量。当一个类被编译时,实例变量的布局也就形成了,它表明访问类的实例变量的位置。从对象头部地址开始,实例变量依次根据自己所占空间而产生位移:

再翻出Ivar的定义:

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}                                                            OBJC2_UNAVAILABLE;

ivar 的访问可以通过 对象地址ivar偏移字节(ivar_offset)的方法。

当我们增加了父类的ivar,这个时候布局就出错了,我们就不得不重新编译子类来恢复兼容性。

在健壮的实例变量下编译器生成的实例变量布局跟以前一样,但是当 runtime 系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了

需要注意的是在健壮的实例变量下,不要使用 sizeof(SomeClass),而是用 class_getInstanceSize([SomeClass class]) 代替;也不要使用 offsetof(SomeClass, SomeIvar) ,而要用 ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar")) 来代替。

/* 定义一个Student类 */
@interface Student : NSObject
{
@private
    int age;
}
@end

@implementation Student
// 重写%@输出方法
- (NSString *)description
{
    NSLog(@"current pointer = %p", self);
    NSLog(@"age pointer = %p", &age);
    return [NSString stringWithFormat:@"age = %d", age];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...

        Student *student = [[Student alloc] init];
        Ivar age_ivar = class_getInstanceVariable(object_getClass(student), "age");//获取"age"的ivar
        int *age_pointer = (int *)((__bridge void *)(student) + ivar_getOffset(age_ivar));//定义一个指向age_ivar的指针:指向地址为 student对象地址 + age_ivar的偏移量(ivar_offset)
        NSLog(@"age ivar offset = %td", ivar_getOffset(age_ivar));//输出offset偏移量
        *age_pointer = 10;//对指针age_pointer指向的变量(age_ivar)赋值
        NSLog(@"%@", student);//输出重写的description方法

    }
    return 0;
}

观察控制台输出:

2016-11-11 16:22:56.364 Ivar_offset[1501:928608] age ivar offset = 8
2016-11-11 16:22:56.365 Ivar_offset[1501:928608] current pointer = 0x100400170
2016-11-11 16:22:56.365 Ivar_offset[1501:928608] age pointer = 0x100400178
2016-11-11 16:22:56.366 Ivar_offset[1501:928608] age = 10

我们发现age pointer = current pointer + age ivar offset

Objective-C Associated Objects

在 OS X 10.6 之后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有以下三个:

void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
id objc_getAssociatedObject ( id object, const void *key );
void objc_removeAssociatedObjects ( id object );

这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:

enum {
   OBJC_ASSOCIATION_ASSIGN  = 0,
   OBJC_ASSOCIATION_RETAIN_NONATOMIC  = 1,
   OBJC_ASSOCIATION_COPY_NONATOMIC  = 3,
   OBJC_ASSOCIATION_RETAIN  = 01401,
   OBJC_ASSOCIATION_COPY  = 01403
};

这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。

Method Swizzling

之前所说的消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是 Method Swizzling ,它通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。

这里摘抄一个 NSHipster 的例子

#import <objc/runtime.h> 

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        // When swizzling a class method, use the following: 
        // Class class = object_getClass((id)self); 
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
#pragma mark - Method Swizzling 
- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}
@end

上面的代码通过添加一个 Tracking 类别到 UIViewController 类中,将 UIViewController 类的 viewWillAppear: 方法和 Tracking 类别中 xxx_viewWillAppear: 方法的实现相互调换。Swizzling 应该在 +load 方法中实现,因为 +load 是在一个类最开始加载时调用。dispatch_once 是GCD中的一次性方法,它保证了代码块只执行一次,并让其为一个原子操作,线程安全是很重要的。

先用 class_addMethodclass_replaceMethod 函数将两个方法的实现进行调换,如果类中已经有了 viewWillAppear: 方法的实现,那么就调用 method_exchangeImplementations 函数交换了两个方法的 IMP ,这是苹果提供给我们用于实现 Method Swizzling 的便捷方法。
最后 xxx_viewWillAppear: 方法的定义看似是递归调用引发死循环,其实不会的。因为 [self xxx_viewWillAppear:animated] 消息会动态找到 xxx_viewWillAppear: 方法的实现,而它的实现已经被我们与 viewWillAppear:方法实现进行了互换,所以这段代码不仅不会死循环,如果你把 [self xxx_viewWillAppear:animated] 换成 [self viewWillAppear:animated] 反而会引发死循环。
看到有人说 +load方法本身就是线程安全的,因为它在程序刚开始就被调用,很少会碰到并发问题,于是 stackoverflow 上也有大神给出了另一个 Method Swizzling 的实现:

- (void)replacementReceiveMessage:(const struct BInstantMessage *)arg1 {
    NSLog(@"arg1 is %@", arg1);
    [self replacementReceiveMessage:arg1];
}
+ (void)load {
    SEL originalSelector = @selector(ReceiveMessage:);
    SEL overrideSelector = @selector(replacementReceiveMessage:);
    Method originalMethod = class_getInstanceMethod(self, originalSelector);
    Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
    if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
            class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
            method_exchangeImplementations(originalMethod, overrideMethod);
    }
}

其实也就是去掉了dispatch_once的部分罢了。

Method Swizzling 的确是一个值得深入研究的话题,Method Swizzling 的最佳实现是什么呢?小弟才疏学浅理解的不深刻,找了几篇不错的资源推荐给大家:

总结

我们之所以让自己的类继承 NSObject 不仅仅因为苹果帮我们完成了复杂的内存分配问题,更是因为这使得我们能够用上 Runtime 系统带来的便利。深入理解 Runtime 系统的细节更有利于我们利用消息机制写出功能更强大的代码,比如 Method Swizzling 等。

参考链接

最后,附上Runtime的基本使用:[《Objective-C Runtime 的基本使用》][23]

[23]:

NetworkExtension

From: https://www.jianshu.com/p/5072a8485ceb

前言: [iOS][1] 9 发布之后,推出NetworkExtension, 它可给系统WiFi列表列表里边的WiFi设置密码 、标签(副标题)。 还可获取整个WiFi列表。
首先你得向苹果申请一个权限,人家允许你使用了,你再在工程里面配置一下,这样你才可以使用.苹果会给你发个问卷调查,根据你自己的情况填写.这儿谢谢我初中学霸,专业的英语翻译果然6.
1-1.框架申请链接:https://developer.apple.com/contact/network-extension

问卷调查表

根据自己的实际情况填写.里面有个产品介绍,最好找个英文好的…

1-2 调查表填写完成后,大概过了2 ,3小时,苹果会回复给你一封邮件,并且返回给你一个fllowup.(注意,这并不是代表你已经申请成功了,邮件里面只是确认你填写的问卷信息!)

并不是成功的邮件

1-3 接下来,就只能等了.逛苹果论坛,据说要等三星期,可是,我TM等了5星期是什么鬼.所以当超过三星期的时候我也没闲着,打电话 :4006701855 , 虽然得到的回复还是等…
而且,值得一提的是:有人遇见过这种情况,一直没有收到苹果拒绝或者同意的邮件.但是可以使用这个类.所以当超过3星期以后,我是每天都登录到开发者账号 配置描述文件,如果发现这儿多了一个选项.如下图,这也说明你申请成功了


配置描述文件

1-5 所以呢,打了两次电话,重发了5 ,6 次邮件之后,大概苹果也觉得不好意思了吧,终于通过了.此时你会收到这也一封邮件:

恭喜您,通过了.但是不得不说,这只成功了一小半.后面你可能遇见更痛苦的事情.

2下面就来来说更痛苦的事情.
2-1 配置工程
a .新建一个App ID.

新建 App ID

b. 添加iCloud 和Wireless Accessory

c.新建iCloud Containers

新建iCloud Containers.png

d.打开刚刚创建的App ID 发现这个是黄色的,下面就要编辑这个App ID

编辑刚刚创建的AppID.png

e

f

g 配置App ID完成

2-2 配置描述文件 .

注意选择新建的App ID

注意这个值要加上.png

配置好这个之后,可以到苹果提供的检测环境检测一下配置文件的正确性
附上网址 :https://forums.developer.apple.com/message/75928#75928

这一步很重要:就是检测你的工程配置的描述文件和这个账号使用权限是否对等.后面有小伙伴遇到一个bug就是ruternType一直返回NO,获取不到wifi列表,然后通过上面验证发现,证书的权限和申请使用的权限不相同.
所以这儿有个建议,如果发现获取列表时返回值是NO,把测试证书删掉,重新创建.然后再走一遍上面的流程.(我遇到的BUG奇怪的很,对着英文文档走了好几遍,确定文件没什么问题,就是返回值一直都是NO,后来,在创建iCoud的时候,把那个id改成和App ID不一样,就是按照他下面的要求创建,就奇怪的好了.)

注意 :创建完描述文件别忘了安装到Xcode,直接下载,完了双击就ok了.

3.配置Xcode工程了;
3-1 配置plist文件 (允许后台运行)
通过xml添加

UIBackgroundModes


network-authentication

通过xml方式添加.png

添加完了会有这

3-2 targets->Capabilities->iCloud 和Wireless-Accessory-Configuration

,打开并配置icould

打开Wireless-Accessory-Configuration.png


配置完上面两个你就会发现工程左边会多了一个.entitlements结尾的文件.
然后还要向这个文件里面添加一个BOOL值为YES的字段 com.apple.developer.networking.HotspotHelper

配置.entitlements文件.png

3-3 Tagarts–>Build Settings -> code Signing

配置Build Settings

3-4 上代码 (也是蛮辛苦的)

Register a Hotspot Helper

+ (BOOL)registerWithOptions:(NSDictionary*)options queue:(dispatch_queue_t)queue handler:(NEHotspotHelperHandler)handler

@param options 

 kNEHotspotHelperOptionDisplayName :WIFI的注释tag字符串// 此处设置的内容会在WiFi列表中每个WiFi下边展示出来 

@param queue dispatch_queue_t 用来调用handle的block 

@param handler NEHotspotHelperHandler block 用于执行处理 helper commands.

 @return 注册成功YES, 否则NO. 

@discussion 一旦这个API调用成功,应用程序有资格在后台启动,并参与各种热点相关的功能。 当应用程序启动此方法应该调用一次。再次调用它不会产生影响,并返回NO。

这个方法是主要的.


+ (BOOL)logoff:(NEHotspotNetwork *)network

@param network 对应当前关联的WiFi网络NEHotspotNetwork

 @return 注销命令已成功进入队列YES, 否则NO. 

@discussion 调用此方法使kNEHotspotHelperCommandTypeLogoff型的NEHotspotHelperCommand向应用程序发出的“handler”模块 网络参数必须符合当前关联的WiFi网络,即它必须来自对NEHotspotHelperCommand网络属性或方法supportedInterfaces

+ (NSArray *)supportedNetworkInterfaces

@return 如果没有网络接口被管理,返回nil。否则,返回NEHotspotNetwork对象数组。 

@discussion 每个网络接口由NEHotspotNetwork对象表示。当前返回的数组包含一个NEHotspotNetwork对象代表Wi-Fi接口。

 这种方法的主要目的是当没有得到一个命令来处理它时,让一个热点助手偶尔提供在UI里其准确的状态。 此方法加上NEHotspotNetwork的isChosenHelper方法允许应用程序知道它是否是当前处理的网络。

//最后奉上我的实现代码,获取wifi列表,并给指定ssid做标记;

+(void)getWifiList{

    NSMutableDictionary* options = [[NSMutableDictionary alloc] init];
    [options setObject:@"🔑😀新网程-点我上网😀🔑" forKey:kNEHotspotHelperOptionDisplayName];

    dispatch_queue_t queue = dispatch_queue_create("com.pronetwayXY", NULL);
    BOOL returnType = [NEHotspotHelper registerWithOptions:options queue:queue handler: ^(NEHotspotHelperCommand * cmd) {
        NEHotspotNetwork* network;
        NSLog(@"COMMAND TYPE:   %ld", (long)cmd.commandType);
        [cmd createResponse:kNEHotspotHelperResultAuthenticationRequired];
        if (cmd.commandType == kNEHotspotHelperCommandTypeEvaluate || cmd.commandType ==kNEHotspotHelperCommandTypeFilterScanList) {
            NSLog(@"WIFILIST:   %@", cmd.networkList);
            for (network  in cmd.networkList) {
                // NSLog(@"COMMAND TYPE After:   %ld", (long)cmd.commandType);
                if ([network.SSID isEqualToString:@"ssid"]|| [network.SSID isEqualToString:@"proict_test"]) {

                    double signalStrength = network.signalStrength;
                    NSLog(@"Signal Strength: %f", signalStrength);
                    [network setConfidence:kNEHotspotHelperConfidenceHigh];
                    [network setPassword:@"password"];

                    NEHotspotHelperResponse *response = [cmd createResponse:kNEHotspotHelperResultSuccess];
                    NSLog(@"Response CMD %@", response);

                    [response setNetworkList:@[network]];
                    [response setNetwork:network];
                    [response deliver];
                }
            }
        }
    }];
    NSLog(@"result :%d", returnType);
    NSArray *array = [NEHotspotHelper supportedNetworkInterfaces];
    NSLog(@"wifiArray:%@", array);
    NEHotspotNetwork *connectedNetwork = [array lastObject];
    NSLog(@"supported Network Interface: %@", connectedNetwork);

}

注意 :运行一遍,然后需要打开系统设置连wifi界面,才能在控制台查看打印信息的.

下面附上两张效果图:

系统的wifi列表.png

修改系统wifi的标签

最后附上一个Network Extension 使用的难兄(福)难(利)弟群(群里很多都已经实现了这个功能)—-585640621

demo已上传到gitHub上 :https://github.com/chengkunlun/FirstRePository

参考文档:http://blog.csdn.net/qinxianjun163/article/details/51583057