优化 Autolayout

优化 Autolayout

Florian Kugler 最近发表了一篇关于iOS上 Autolayout 性能的文章. 它研究了 Autolayout 添加视图需要多长时间, 以及所需时间的变化随着视图的数量的增加. 该帖子虽然提供了非常有用的信息, 但似乎并不能最好地代表 Autolayout 的真实表现, 而是展示了一组最坏情况.

在这片报告中,我想更多地了解为什么 Florian 得到了他所做的结果. 我想强调一下 Autolayout 可能会遭遇一些不良的用法, 另外一个我想强调的是,到底是什么让”Autolayout 需要几秒钟才能布局几百个视图”和”Autolayout 可以快速布局几百个视图”这两种说法都是真实的,尽管它们看似矛盾的本性.

测量方法

首先,我想详细说明我如何测量 我认为 Florian 报告中有问题的数字。 我认为这与Florian测量它们的方式略有不同,但更接近地展示了 Autolayout 的真实情况。 我从最初的源项目开始,并进行了修改以测试一些变化。 你可以在GitHub上找到我的项目版本。

为了测量时间,我使用Time Profiler模板在Instruments中运行应用程序。 我不觉得每次都需要重新启动应用程序,因为几乎没有缓存。 我连续3次运行每个测试,清除每个测试之间的视图。 之后,在仪器中,我专注于每个测试运行的样本区域。 为了获得布局所花费的时间,我使用了仪器所说的布局方法所花费的时间。 我计算了3次运行的平均值,并用它来提供这篇文章的数据。

重新创建初步结果

由于我的方法略有不同,而且我使用的是与弗洛里安(第三代iPad)不同的设备,我首先开始测试他所做的相同事情。 他的项目测试了3种布局方式:

  • 视图的平面层次结构,绝对位于根视图中
  • 平面的视图层次结构,彼此相对定位
  • 嵌套的视图层次结构,彼此相对定位

他还通过简单设置 Frame 来完成平面和嵌套层次结构. 下面的图表显示了我为平面层次结构所获得的内容。

如果你比较 Florian 安的报告,你会发现这看起来很不一样。 在 Florian 的图中,绿线比橙线差,但它们都相当接近。 在我的图表中,橙色线条更糟糕(例如,对于600个视图,Florian 得到5秒,而我接近7.5秒,尽管有更快的iPad),但绿线更好(对于 600次观看我得到2.5秒对比弗洛里安的6-7秒)。

我把这个区别归结为测量的差异。 如前所述,我使用了创建约束的方法的时间。 为此,我在每个方法结尾的根视图上调用了-layoutIfNeeded方法。 这会强制Autolayout立即运行,而不是推迟到运行循环结束,这意味着Instruments会计算创建约束的方法的性能,而不是系统方法。

我怀疑Florian正在测量CPU工作的总时间,但这并不一定都归功于Autolayout。 我相信我的方式更能说明Autolayout正在做什么,但Florian’s更能说明应用程序可能无响应的时间。 无论如何,实际值与曲线无关,以及我们可以找到的任何相对改进。

嵌套布局图与 Florian 测试的差异较小,曲线非常相似. 唯一的区别是我的时间稍微快一些,这是在更快的设备上运行时的预期。

Locality 的力量

我注意到在源码中所有的约束都是被添加到根视图上的. 在某些情况下,这是必需的,因为约束引用了根视图。 约束引用的所有视图必须位于要添加到的视图的子树中。 因此,您可以将UI中的每个约束都放入应用程序的根视图中。

出于几个原因,你不想这样做。 最明显的是,在本地添加约束时,理解代码要简单得多。 另一个是它极大地影响了性能。

让我们来看看我们的平面布局。 虽然位置约束需要在根视图上,但大小限制却不是。 我更改了代码,以便将大小约束添加到子视图中,并获得以下结果:

紫色线是相对布局,大小约束尽可能是局部的,红色是绝对布局的等效线。 如您所见,我们正在获得一些性能改进。 我不是百分百肯定,但我有根据的猜测是,这是因为我们正在缩小根视图上的计算大小。 我们让Autolayout执行布局的一部分作为很多小的计算,而不是在一个大blob中计算整个事物。

但这些收益相对较小。 更复杂的计算仍然聚集在一起并且尽可能地本地化。 让我们看一下嵌套布局,因为与视图相关的所有约束都可以放在直接的超视图中,从而大大增加了局部性。 下图显示了这一改进的显着程度。

为了给出实际数字,当将所有约束放在根视图中时,200视图布局花费22.75秒,但是当将它们放在直接超视图上时仅花费2.00秒。 对根视图施加相同的约束会导致代码运行速度慢11倍。 这个教训应该是显而易见的。 使用Autolayout时,尽可能将所有约束放在本地。

更改已存在视图的层级

Florian提到约束满足问题具有多项式复杂性。我们可以在上图的曲线中看到这一点。但是,上述测试在很大程度上无法代表Autolayout的实际使用。了解Autolayout将1000个视图投射到父视图中的速度有多快,这一点非常有用,因为了解NSArray在添加数百万个对象时的速度有多快。但是,创建的大多数NSArray很少能容纳超过100个项目,其中许多项目少于10个。同样,单个视图很少能容纳超过40-50个子视图,或者视图层次结构超过20个 – 30个观点深(我怀疑这些价值被高估)。

更现实的情况是拥有一个视图层次结构,我们想要移动一些视图,或添加一些额外的视图。我基于上面的那些进行了一些测试。同时采用上面使用的尺寸的平面(绝对,非相对)和嵌套布局,我计算了移动所有视图和添加10个额外视图所需的时间。

正如我们从下面的图表中看到的那样,即使在最多1000个视图中,向平面添加额外的10个视图,绝对定位的布局在很大程度上是线性的。这是因为我们只引用根视图,因此不需要重新计算所有其他视图。如果我们将视图插入到相对定位视图链的中间,那么它可能不会那么快。

类似地,移动在很大程度上是线性的,尽管它在1000个视图中确实有峰值。同样,这是因为一个视图的约束不依赖于任何其他同级视图。

如果我们看一下嵌套布局,我们发现移动似乎也是线性的。 虽然它看起来比平面层次更浅,但它只是图表的一个技巧,它们大部分都在同一条线上。 在添加时,我们确实看到了一条曲线,但是我们在这里添加了另外10层视图层次结构,每个层次都依赖于前一个层次。

所有这些都需要注意的是,它们与之前的测试相比有多快。要添加1000个绝对定位的视图需要6.6秒,但要添加额外的10个仅花费0.055秒。这归结为Cassowary Constraint Solver的聪明才智。

它不是每次都试图从头开始重新解决整个问题,而是一个增量系统。它可以重复使用以前的所有计算,只需在添加,编辑或删除约束时修改结果。这就是为什么一次性添加几百个视图可能需要几秒钟,但您可以快速调整该窗口的大小,并重新计算所有约束并设置帧。

Autolayout比手动设置帧慢。它推广了整个UI中解决相当复杂的布局问题。拥有专注于单个视图的专用算法总是会更快运行。 Autolayout的优势不在于在运行时使布局更快,而在于使我们在编码时更快更容易地定义布局。

与我们使用的许多工具一样,Autolayout充分利用了我们拥有丰富处理能力的优势,以便我们更轻松地编写应用程序。对于绝大多数用例而言,如果使用得当,Autolayout的速度已经足够快了。这可能听起来像是一个借口,但它是我们用来证明用更高级别的编程语言而不是汇编语言编写的理由。

参考资料

`runtime`的实际应用

runtime的实际应用

你在项目中用过 runtime 吗?举个例子

runtime的概念

runtime 是 OC底层的一套C语言的API(引入 <objc/runtime.h> 或<objc/message.h>), 编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件).

runtime的实际应用

动态交换两个方法的实现 – method swizzling

我们想要在一款 iOS app 中追踪每一个视图控制器被用户呈现了几次: 这可以通过在每个视图控制器的 viewDidAppear: 方法中添加追踪代码来实现,但这样会大量重复的样板代码。继承是另一种可行的方式,但是这要求所有被继承的视图控制器如 UIViewController, UITableViewController, UINavigationController 都在 viewDidAppear:实现追踪代码,这同样会造成很多重复代码。 幸运的是,这里有另外一种可行的方式:从 category 实现 method swizzling.

核心API

// 获得某个类的类方法
Method class_getClassMethod(Class cls , SEL name);

// 获取某个类的实例方法
Method class_getInstanceMethod(Class cls , SEL name);

// 交换两个方法的实现
void method_exchangeImplementations(Method m1, Method m2);

实例代码

#import &lt;objc/runtime.h&gt;

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(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(@&quot;viewWillAppear: %@&quot;, self);
}

@end

为分类添加属性

众所周知,分类中是无法设置属性的,如果在分类的声明中写@property 只能为其生成get 和 set 方法的声明,但无法生成成员变量,就是虽然点语法能调用出来,但程序执行后会crash.

核心API

// 设置动态关联对象
void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy);
// 获取动态关联对象
id objc_getAssociatedObject(id object , const void *key);

实例代码

@interface UIView (SQIAdditions)

@property(nonatomic, strong) NSArray &lt;void (^)(void)&gt;*sqi_handleBlockArr;

@end

@implementation UIView (SQIAdditions)

static char *HandleBlockArrKey = &quot;HandleBlockArrKey&quot;;


- (void)setSqi_handleBlockArr:(NSArray&lt;void (^)(void)&gt; *)handleBlockArr {
    objc_setAssociatedObject(self, HandleBlockArrKey, handleBlockArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSArray&lt;void (^)(void)&gt; *)sqi_handleBlockArr {
    return objc_getAssociatedObject(self, HandleBlockArrKey);
}

@end

自定义对象的归档和解档

自定义对象如果想实现归档和解档, 必须遵守NSCoding协议, 必须实现encodeWithCoder,initWithCoder的两个方法告诉系统,你的对象里面到底有那些数据.

核心API

// 获取某个类的所有成员变量
Ivar  _Nonnull * class_copyIvarList(Class cls, unsigned int *outCount);
// 获取成员变量的名字
const char *ivar_getName(Ivar v);
// 获取成员变量的类型
const char *ivar_getTypeEndcoding(Ivar v);

实例代码

  • 获取Person类中所有成员变量的名字和类型
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &amp;outCount);

// 遍历所有成员变量
for (int i = 0; i &lt; outCount; i++) {
    // 取出i位置对应的成员变量
    Ivar ivar = ivars[i];
    const char *name = ivar_getName(ivar);
    const char *type = ivar_getTypeEncoding(ivar);
    NSLog(@&quot;成员变量名:%s 成员变量类型:%s&quot;,name,type);
}
// 注意释放内存!
free(ivars);
  • 利用runtime 获取所有属性来重写归档解档方法
// 设置不需要归解档的属性
- (NSArray *)ignoredNames {
    return @[@&quot;_aaa&quot;,@&quot;_bbb&quot;,@&quot;_ccc&quot;];
}

// 解档方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super initWithCoder:aDecoder]) {
        // 获取所有成员变量
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &amp;outCount);

        for (int i = 0; i &lt; outCount; i++) {
            Ivar ivar = ivars[i];
            // 将每个成员变量名转换为NSString对象类型
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];

            // 忽略不需要解档的属性
            if ([[self ignoredNames] containsObject:key]) {
                continue;
            }

            // 根据变量名解档取值,无论是什么类型
            id value = [aDecoder decodeObjectForKey:key];
            // 取出的值再设置给属性
            [self setValue:value forKey:key];
            // 这两步就相当于以前的 self.age = [aDecoder decodeObjectForKey:@&quot;_age&quot;];
        }
        free(ivars);
    }
    return self;
}

// 归档调用方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
     // 获取所有成员变量
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList([self class], &amp;outCount);
    for (int i = 0; i &lt; outCount; i++) {
        Ivar ivar = ivars[i];
        // 将每个成员变量名转换为NSString对象类型
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 忽略不需要归档的属性
        if ([[self ignoredNames] containsObject:key]) {
            continue;
        }

        // 通过成员变量名,取出成员变量的值
        id value = [self valueForKeyPath:key];
        // 再将值归档
        [aCoder encodeObject:value forKey:key];
        // 这两步就相当于 [aCoder encodeObject:@(self.age) forKey:@&quot;_age&quot;];
    }
    free(ivars);
}

依据上面的原理我们就可以给NSObject做一个分类,让我们不需要每次都写这么一长串代码,只要实现一小段代码就可以让一个对象具有归解档的能力。

完整的分类代码

NSObject+SQIExtension.h

#import &lt;Foundation/Foundation.h&gt;

@interface NSObject (SQIExtension)

- (NSArray *)ignoredNames;
- (void)encode:(NSCoder *)aCoder;
- (void)decode:(NSCoder *)aDecoder;

@end

NSObject+SQIExtension.m

#import &quot;NSObject+SQIExtension.h&quot;
#import &lt;objc/runtime.h&gt;

@implementation NSObject (Extension)

- (void)decode:(NSCoder *)aDecoder {
    // 一层层父类往上查找,对父类的属性执行归解档方法
    Class c = self.class;
    while (c &amp;&amp;c != [NSObject class]) {

        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(c, &amp;outCount);
        for (int i = 0; i &lt; outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];

            // 如果有实现该方法再去调用
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) continue;
            }

            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivars);
        c = ;
    }

}

- (void)encode:(NSCoder *)aCoder {
    // 一层层父类往上查找,对父类的属性执行归解档方法
    Class c = self.class;
    while (c &amp;&amp;c != [NSObject class]) {

        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &amp;outCount);
        for (int i = 0; i &lt; outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];

            // 如果有实现该方法再去调用
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) continue;
            }

            id value = [self valueForKeyPath:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
        c = ;
    }
}
@end

分类的使用方法

在需要归解档的对象中实现下面方法即可:

// 设置需要忽略的属性
- (NSArray *)ignoredNames {
    return @[@&quot;bone&quot;];
}

// 在系统方法内来调用我们的方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        [self decode:aDecoder];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [self encode:aCoder];
}

这样看来,我们每次又要写同样的代码,我们可以将归解档两个方法封装为宏,在需要的地方一句宏搞定,如果有不需要归解档的属性就实现ignoredNames 方法,具体可以看我的demo,这个也是MJExtension中那个一句宏就可以解决归解档的实现原理。

关键字`__weak`和`__block`

关键字__weak__block

1. 都属于变量修饰符, 用于修饰变量(variable).
2. __weak 和 __block 的使用场景几乎与 block 息息相关. 而所谓 block, 就是 Objective-C 对于闭包的实现. 闭包就是没有名字的函数, 或者理解为指向函数的指针.

__weak关键字

  1. 主要用于防止 block 造成循环引用, 特别是 block 对 self 的引用.
    __weak typeof(self) weakSelf = self;
    
  2. 代表弱引用.

  3. 只能在 ARC 模式下使用, 只能修饰对象类型, 不能修饰基本数据类型.

__block关键字

  1. 主要用于在 block 内部, 修改外部变量的值 & 对外部变量进行重新赋值.

    block可以访问外部变量(局部变量)(实际上访问的不是同一个变量), 但是不能修改. 如果想要在`block`内部修改外部变量, 需要使用`__block`修饰外部变量, 所以说:`__block`的功能是让`block`能够修改外部变量的值.
    
  2. block对外部变量的处理
    • 如果block内部存在外部变量的引用, block 默认是将其复制到其数据结构中来实现访问的.

    const_non_local_variables

    • 如果外部变量有__block修饰, block 对其访问是通过复制其引用地址来实现的.

    mutable-non-local-variables

  3. __block对象在block中不会被block强引用一次, 所以不会造成循环引用.

  4. __block不管是 ARC 还是 MRC 模式下都可以使用, 可以修饰对象, 还可以修饰基本数据类型。

Others

  1. 在一篇博客中, 看到了下面这样一句话, 但我感觉不准确.
_____block对象可以在block中被重新赋值,_____weak不可以。
  1. 在一篇大神的博客中, 看到了这样一句话, 到现在还是一脸懵逼, 感觉没什么逻辑性可言(吐槽一下, 哈哈).

__block的大神解释

  1. 如果想要深入了解block 对外部变量的处理的细节, 可以自己写一个简单的demo, 然后通过clang进行手动编译, 处理细节一览无余. 这里推荐一篇写的不错的博客:你真的理解__block修饰符的原理么?

变量限定符

变量限定符

ARC为变量提供了四种生命周期限定符.

  • __strong

    默认限定符(可省略,无需显式引入),表示对指定(双重含义:①特定的;②指针变量指向的)的对象有强引用.

  • __weak

    • 表示对指定的对象具有弱引用,弱引用没有保持引用对象存活的功能,当没有强引用指向对象时,对象会被回收,弱引用会自动被置为nil(__weak所具有的安全性).
    • 主要用于防止Block中的循环引用.
    __weak typeof(self) weakSelf = self;
    
  • __unsafe_unretained
    • 极少使用,为兼容iOS5以下版本的产物,可以理解成MRC下的assign)
    • __weak类似,只是当没有强引用指向对象时,__unsafe_unretained修饰的指针不会被置为nil.
  • __autoreleasing
    • 极少使用
    • 常用于修饰使用id *传递的消息参数,它表示 autorelease方法会在传递参数的方法中被调用.

变量限定符的使用格式(使用语法)

// 类名 * 变量限定符 变量名
TypeName * qualifier variable

变量限定符的使用示例

// 对象创建后引用计数为1,对象在p1的引用期间不会被回收.
Person * __strong p1 = [Person alloc] init];
// 对象创建后引用计数为0,对象会被立即释放,且p2将被设置为nil.
Person * __weak p2 = [Person alloc] init];
// 对象创建后引用计数为1(别问我,我也很懵逼,如果你有想法,记得告诉我),对象会被立即释放,但p3将被设置为nil.
Person * __strong p3 = [Person alloc] init];
// 对象创建后引用计数为1,当方法返回时对象会被立即释放.
Person * __strong p4 = [Person alloc] init];

Others

  1. 将一个对象作为参数传递给某个方法,相当于在这个方法中,把这个对象赋值给了一个名为形参的指针变量.

Category的”方法覆盖规则”

Category的”方法覆盖规则”

问题描述

有一个类MyClass.

// MyClass.h文件
@interface MyClass : NSObject

+ (void)sqi_print;

@end

// MyClass.m文件
@implementation MyClass

+ (void)sqi_print {
    NSLog(@&quot;-&gt; MyClass&quot;);
}

@end

新建一个 MyClass 的分类SQI_CAT_ONE

// MyClass+SQI_CAT_ONE.h文件
@interface MyClass (SQI_CAT_ONE)

+ (void)sqi_print;

@end

// MyClass+SQI_CAT_ONE.m文件
@implementation MyClass (SQI_CAT_ONE)

+ (void)sqi_print {
    NSLog(@&quot;-&gt; MyClass (SQI_CAT_ONE)&quot;);
}

@end

再新建另一个 MyClass 的分类SQI_CAT_TWO

// MyClass+SQI_CAT_TWO.h文件
@interface MyClass (SQI_CAT_TWO)

+ (void)sqi_print;

@end

// MyClass+SQI_CAT_TWO.m文件
@implementation MyClass (SQI_CAT_TWO)

+ (void)sqi_print {
    NSLog(@&quot;-&gt; MyClass (SQI_CAT_TWO)&quot;);
}

@end

在 ViewController 中引入两个分类

#import &quot;ViewController.h&quot;
#import &quot;MyClass+SQI_CAT_TWO.h&quot;
#import &quot;MyClass+SQI_CAT_ONE.h&quot;

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // Do any additional setup after loading the view.
    [MyClass sqi_print];
}

@end

控制台打印结果

情况1 -> MyClass+SQI_CAT_TWO.m文件后编译

情况1 -> 控制台打印

情况2 -> MyClass+SQI_CAT_ONE.m文件后编译

情况2 -> 控制台打印

分类覆盖规则分析

  1. App的启动的时候(load方法之前), 会把分类中的类方法列表和原类中的类方法列表拼接成一个新的类方法列表, 而且分类中的方法会放在新列表的前面.
  2. 具体是哪一个分类中的方法处于更前面, 这取决于编译顺序.
  3. 运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休^_^,殊不知后面可能还有一样名字的方法。

iOS开发应该了解的HTTP状态码

Category (分类)

状态码 类别 含义
1XX Informational (信息性状态码) 接收的请求正在处理
2XX Success (成功状态码) 请求正常处理完毕
3XX Redirection (重定向状态码) 需要进行附加操作以完成请求
4XX Client Error (客户端错误状态码) 服务器无法处理请求
5XX Server Error (服务器错误状态码) 服务器处理请求出错

Detail (详解)

2XX 成功

200 OK

服务端成功处理了客户端的请求, 没有发现异常.

204 No Content

服务端成功处理了客户端的请求, 但是没有资源可以返回.

206 Partial Content

服务器成功处理了客户端的范围请求.

3XX 重定向

301 Moved Permanently

永久性重定向. 表示请求的资源已被分配了新的URI, 以后应使用资源现有的URI.

302 Found

临时性重定向. 表示请求的资源已被分配了新的URI, 希望用户(本次)能使用新的URI访问.

303 See Other

303状态码和302状态码有着相同的功能, 但303状态码明确表示客户端应当采用GET方法获取资源.

304 Not Modified

  1. 客户端发送GET请求获取资源时, 报文中包含If-Match, If-Modified-Since, If-None-Match, If-Range, If-Unmodified-Since中任一首部.
  2. 服务端允许资源访问, 但是由于请求报文中的首部条件未满足, 直接返回304 Not Modified (服务端资源未改变, 可直接只用客户端未过期的缓存).
  3. 虽然被划分为3XX, 但是和重定向没有关系.

307 Temporary Redirect

临时重定向.

4XX 客户端错误

表示客户端是发生错误的原因所在.

400 Bad Request

请求报文中存在语法错误.

401 Unauthorized

表示:发送的请求需要有通过HTTP认证的认证信息; 若之前已经进行过1次请求, 则表示用户认证失败.

403 Forbidden

资源请求被服务器拒绝了.

404 Not Found

无法找到请求的资源.

5XX 服务器错误

500 Internal Server Error

服务器在执行请求时发生了错误.

503 Service Unavailable

表明服务器暂时处于超负荷或者正在进行停机维护, 现在无法处理请求.

`Objective-C`的`Singleton`模式

Objective-CSingleton模式

Overview

Singleton 模式的定义

// 确保一个类有且仅有唯一一个单例, 并且提供一个全局的访问点.
Ensures a class has only one instance, and provide a global point of access.

Objective-C 开发

Singleton 模式是 iOS 开发中非常有用的设计模式, 可用于对象之间方便地共享(或传递)数据. 相对于对象之间手动传递数据, Singleton 模式更加的方便直接, 不需要对象之间有任何的联系.


Implementation

技术要点

  1. 仅有一个对象 -> 用一个全局的东西保存这个对象.
  2. 全局的访问点 -> 工厂方法创建对象.
  3. 线程安全 -> dispatch_once

Show code

&lt;br /&gt;+ (SQISingleton *)shareInstance {

    static SQISingleton * s_instance_sqi_singleton = nil ;
    
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
        if (s_instance_sqi_manager == nil) {
            s_instance_sqi_manager = [[SQISingleton alloc] init];
        }
    });
    return (SQISingleton *)s_instance_sqi_singleton;
}

关于static关键字 & extern关键字

  1. static关键字修饰局部变量:①. 当static关键字修饰局部变量时,该局部变量只会初始化一次,在系统中只有一份内存; ②. static关键字不可以改变局部变量的作用域,但是可延长局部变量的生命周期,该变量直到整个项目结束的时候才会被销毁

  2. static修饰的全局变量:作用域仅限于当前文件,外部类不可以访问到该变量

  3. extern-引用关键字,当某一个全局变量,没有用static修饰时,其作用域为整个项目文件,若是在其他类想引用该变量,则用extern关键字,例如,想引用其他类的全局变量,int age = 10;则在当前类中实现,extern int age;也可以在外部修改该变量,extern int age = 40;,若某个文件中的全局变量不想被外界修改,则用static修饰该变量,则其作用域只限于该文件.


代码鲁棒性

看起来很完美了。可是Objective-C毕竟是Objective-C。别的语言,诸如C++,java,构造方法可以隐藏。Objective-C中的方法,实际上都是公开的,虽然我们提供了一个方便的工厂方法的访问入口,但是里面的alloc方法依旧是可见的,可以调用到的。也就是说,虽然你给了我一个工厂方法,调皮的小伙伴可能依旧会使用alloc的方式创建对象。这样会导致外面使用的时候,依旧可能创建多个实例。

关于这个事情的处理,可以分为两派。一个是冷酷派,技术上实现无论你怎么调用,我都给你同一个单例对象;一个是温柔派,是从编译器上给调皮的小伙伴提示,你不能这么造对象,温柔的指出有问题,但不强制约束。

冷酷派的实现

冷酷派的实现从OC的对象创建角度出发,就是把创建对象的各种入口给封死了。alloc,copy等等,无论是采用哪种方式创建,我都保证给出的对象是同一个。

由 Objective-C 的一些特性可以知道,在对象创建的时候,无论是 alloc 还是 new ,都会调用到 allocWithZone:方法。在通过拷贝的时候创建对象时,会调用到-(id)copyWithZone:(NSZone *)zone-(id)mutableCopyWithZone:(NSZone *)zone方法。因此,可以重写这些方法,让创建的对象唯一.

+(id)allocWithZone:(NSZone *)zone{
    return [SQISingleton sharedInstance];
}
+(SQISingleton *) sharedInstance{
    static SQISingleton * s_instance_sqi_singleton = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
        s_instance_sqi_singleton = [[super allocWithZone:nil] init];
    });
    return s_instance_sqi_singleton;
}
-(id)copyWithZone:(NSZone *)zone{
    return [SQISingleton sharedInstance];
}
-(id)mutableCopyWithZone:(NSZone *)zone{
    return [SQISingleton sharedInstance];
}

温柔派的实现

温柔派就直接告诉外面,alloc,new,copy,mutableCopy方法不可以直接调用。否则编译不过。

+(instancetype) alloc __attribute__((unavailable(&quot;call sharedInstance instead&quot;)));
+(instancetype) new __attribute__((unavailable(&quot;call sharedInstance instead&quot;)));
-(instancetype) copy __attribute__((unavailable(&quot;call sharedInstance instead&quot;)));
-(instancetype) mutableCopy __attribute__((unavailable(&quot;call sharedInstance instead&quot;)));

我个人的话比较喜欢采用温柔派的实现。不需要这么多复杂的实现。也让使用方有比较明确的概念这个是个单例,不要调皮。对于一般的业务场景是足够了的。

一劳永逸的方法

大神们把单例模式的各种套路封装成了宏。这样使用的时候,就不需要每个类都手动写一遍里面的重复代码了。省去了敲代码的时间。

以温柔派的为例,大概是这样子的。

// 定义
#define SQI_SINGLETON_DEF(_type_) + (_type_ *)sharedInstance;\
+(instancetype) alloc __attribute__((unavailable(&quot;call sharedInstance instead&quot;)));\
+(instancetype) new __attribute__((unavailable(&quot;call sharedInstance instead&quot;)));\
-(instancetype) copy __attribute__((unavailable(&quot;call sharedInstance instead&quot;)));\
-(instancetype) mutableCopy __attribute__((unavailable(&quot;call sharedInstance instead&quot;)));\

// 实现
#define SQI_SINGLETON_IMP(_type_) + (_type_ *)sharedInstance{\
static _type_ *theSharedInstance = nil;\
static dispatch_once_t onceToken;\
dispatch_once(&amp;onceToken, ^{\
theSharedInstance = [[super alloc] init];\
});\
return theSharedInstance;\
}

那么,在定义和实现的时候就很简单了:

@interface SQISingleton : NSObject
    SQI_SINGLETON_DEF(SQISingleton);
@end
@implementation SQISingleton
    SQI_SINGLETON_IMP(SQISingleton);
@end

单例模式潜在的问题

Singleton 模式延长了对象的生命周期, 若单例对象本身比较大, 或者强引用了比较大的对象, 持久的内存占用是一个不得不考虑的问题.

属性修饰符 atomic & nonatomic

目前为止, 在苹果的官方文档上, 已经找不到对这两个属性修饰符的介绍, 网络上对其细节说明也是千奇百怪, 各执一词.

atomic(原子性)

  1. 顾名思义, 表示原子性, 这个概念其实并不新鲜, 早在linux系统下编程本身也是有这个东西的, 所谓原子, 就是不可再化分, 已经是最小的操作单位(所谓操作指的是对内存的读写), 网上很多地方都在讨论 Objective-C 下的 atomic 不安全, 不能保证数据的并发性, 实际上有一点误导了大家, 认为 atomic 本身是不安全的. 实际上, 并非 atomic 不安全, 而是网上一些说法有问题.

  2. atomic关键字会保证属性生成的成员变量在同一时间只能有一条线程对其进行读/写操作, 本质上是在 setter 方法和 getter 方法中添加了一个.

// @property(retain) UITextField *userName;
// 系统生成的代码如下:

- (UILabel *) titleLabel {
    UITextField *label = nil;
    @synchronized(self) {
        label = [[_titleLabel retain] autorelease];
    }
    return label;
}

- (void) setTitleLabel:(UILabel *)titleLabel {
    @synchronized(self) {
      [_titleLabel release];
      _titleLabel = [titleLabel retain];
    }
}
  1. 这里需要说明的是 atomic 仅仅是保证了 setter 方法和 getter 方法的完整性, 确保任何线程在获取数据的时候, 都会得到一个完整的数据.

nonatomic(非原子性)

  1. 没有保证 getter 和 setter 存取方法的线程安全.
- (void)setCurrentImage:(UIImage *)currentImage
{
    if (_currentImage != currentImage) {
        [_currentImage release];
        _currentImage = [currentImage retain];
    }
}

- (UIImage *)currentImage
{
    return _currentImage;
}

Others

  1. atomic要比nonatomic慢大约20倍. 一般如果条件允许, 我们可以让服务器来进行加锁操作.
  2. 虽然 atomic 保证了 setter & getter 的读写安全, 但不能保证其它方法造成的线程安全问题, 比如NSArry对象调用addObject:方法在 atomic 下是没有线程安全保证的.

使用UIView实现视图的转场动画

1. 声明动画块(animation Block)

[UIView beginAnimations:@"View Flip" content:NULL];

beginAnimations: content:方法接受两个参数,第一个是动画块标题,只有在更直接地使用Core Animation(即用于实现动画的框架)时才会用到这个标题,对于上面的实例,可以使用nil替换。第二个参数是一个指针,指向关联到这个动画快的对象(或者任何C语言数据类型)。这里使用空数据(NULL或者nil),因为没有必要指定对象。

2. 指明动画的持续时间

[UIView setAnimationDuration:0.25];

类方法setAnimationDuration:设定了动画的持续时间,它告诉了 UIView 动画应该运行多长时间(以秒为单位)。

3. 设置动画曲线(animation curve)

[UIView setAnimationCure:UIViewAnimationCureEaseInOut];

这决定了动画的运行速度。默认情况下,动画曲线是一条线性曲线,使动画匀速运行。示例中的UIViewAnimationCureEaseInOut指定了“慢速开始,中间加速,慢慢停止”。这样动画看起来会更加自然,不那么呆板。

4. 指定要使用的转场动画类型

[UIView setAnimationTransition:UIViewAnimationFlipFromRight forView:xxxView cache:YES];

在写此篇博客时,iOS提供了5种视图转场类型:

typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
    UIViewAnimationTransitionNone,  //一个视图突然变成另一个视图(若需要此种效果,根本无需创建一个动画快)
    UIViewAnimationTransitionFlipFromLeft,  // 左侧翻入
    UIViewAnimationTransitionFlipFromRight,  // 右侧翻入
    UIViewAnimationTransitionCurlUp,  // 向上卷起
    UIViewAnimationTransitionCurlDown,  // 向下卷起
};

cache会在动画开始生成一个快照,并在动画执行过程的每一步使用这个快照(对这个快照做动画变换),而不是重新绘制视图。应该始终对动画进行缓存(也就是说此参数应该始终传 YES),除非视图外观在动画执行过程中需要改变。

5. 调用 commitAnimations 方法

typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
    [UIView commitAnimations];
};

在第一步创建动画块开始一直到调用commitAnimations,中间所有的动作都会被制成动画。
由于 Cocoa Touch 在后台使用了 Core Animation(说人话其实就是底层封装了 Core Animation),所以我们能够使用极少的代码制作出非常复杂而精美的动画。