零、参考文档

一、布局过程

在AutoLayout下,视图的布局过程:

  • updating constraints: 这个过程是我们根据一些条件更新、新增、删除视图的约束的地方。这个过程之后所有的视图约束就都创建好了,后续会根据现有的约束计算布局。
  • update layout: 这个过程真正计算布局的地方,系统根据约束计算好之后我们还可以在layoutSubviews内做一些自定义的布局。

    重要事项

    • setNeedsUpdateConstraints:是标记视图为需要更新约束,保证调用updateConstraintsIfNeeded的时候系统调用updateConstraints,或者系统在下一个运行循环(Runloop)的时候调用updateConstraints
    • setNeedsLayout:是标记视图为需要重新布局,保证调用setNeedLayout的时候系统调用layoutSubviews,或者系统在下一个运行循环管(Runloop)的时候调用layoutSubviews
    • 任何对视图约束的操作(更新、新增、删除)之后都会自动调用setNeedLayout,就会在下一个运行循环(Runloop)或者我们自己调用layoutIfNeed的时候调用layoutSubviews

二、固有内容尺寸(Intrinsic Content Size )

固有内容尺寸是一个视图期望为其显示特定内容得到的大小。UILabel会根据字体和内容自己计算一个固有大小,多行文本的时候可以设置preferredMaxLayoutWidth来计算多行文本的固有大小。UIView没有固有大小,但是我们可以重写intrinsicContentSize,根据视图的内容自己计算视图的固有大小。

三、压缩阻力(Compression Resistance) 吸附阻力( Content Hugging)

  • contentCompression:阻止自己被压缩的优先级,如果两个两个视图出现相互挤压的时候,优先级高的不会被压缩,优先级底的被压缩。例如:当两个UILabel水平相互压缩的时候,我们可以指定不想压缩的UILabel的阻止压缩的优先级高一点。
  • contentHugging:阻止自己被吸附的优先级,当一个视图的Frame改变的时候,可以导致另外一个依赖此视图的视图布局发生改变,设置阻止吸附优先级高一点,可以阻止这个改变。

    优先级(我们可以自己设定值 eg:500)

    • UILayoutPriorityRequired = 1000
    • UILayoutPriorityDefaultHigh = 750
    • UILayoutPriorityDefaultLow = 250
    • UILayoutPriorityFittingSizeLevel = 50

四、Alignment Rect

AutoLayout中的约束都是基于Alignment Rect进行后面的布局计算的。一般情况下我们不做任何处理Alignment Rect是和Frame一样的。Alignment Rect是要我们指定视图的要基于的核心元素的大小。 比如,一个自定义 icon 类型的按钮比我们期望点击目标还要小的时候,这将会很难布局。当插图显示在一个更大的 frame 中时,我们将不得不了解它显示的大小,并且相应调整按钮的 frame,这样 icon 才会和其他界面元素排列好。当我们想要在内容的周围绘制像 badges、阴影、倒影的装饰时,也会发生同样的情况。通过重写以下方法可以指定视图的Alignment Rect

  • alignmentRectInsets:
  • alignmentRectForFrame:
  • frameForAlignmentRect:(要和alignmentRectForFrame可逆

五、AutoLayout动画

只有在动画过程中触发视图的重新布局layoutSubviews才会有动画效果。一般我们先修改视图的约束,系统会自动调用setNeedLayout,然后我们要做的就是在动画过程中调用layoutIfNeed触发系统调用layoutSubviews.

1
2
3
4
self.constraint.offset = 80;
[UIView animateWithDuration:1.0f animations:^{
    [self setNeedsLayout];
}];

六、AutoLayout调试

1. 不可满足的约束条件

当因为有不满足约束条件而抛出异常的时候,我们可以打上断点,如果不是很明确是哪个视图导致的问题,你就需要通过内存地址来辨认视图:

1
2
3
4
(lldb) po [[0x7731880 superview] recursiveDescription]
$3 = 0x07117ac0 <UIView: 0x7730fe0; frame = (32 128; 259 604); layer = <CALayer: 0x7731150>>
   | <UIView: 0x7731880; frame = (90 -50; 80 100); layer = <CALayer: 0x7731450>>
   | <UIView: 0x7731aa0; frame = (90 101; 80 100); layer = <CALayer: 0x7731c60>>

一个更直观的方法是在控制台修改有问题的视图,这样你可以在屏幕上标注出来。比如,你可以改变它的背景颜色:

1
(lldb) expr ((UIView *)0x7731880).backgroundColor = [UIColor purpleColor]

然后从断点处继续执行,就可以看到效果。 你也可以通过改进错误信息本身,来更容易地在 iOS 中弄懂不可满足的约束条件错误到底在哪里。我们可以在一个 category 中重写 NSLayoutConstraint 的描述,并且将视图的 tags 包含进去:

1
2
3
4
5
6
7
8
9
10
11
@implementation NSLayoutConstraint (AutoLayoutDebugging)
#ifdef DEBUG
- (NSString *)description
{
    NSString *description = super.description;
    NSString *asciiArtDescription = self.asciiArtDescription;
    return [description stringByAppendingFormat:@" %@ (%@, %@)", 
        asciiArtDescription, [self.firstItem tag], [self.secondItem tag]];
}
#endif
@end

如果整数的 tag 属性信息不够的话,我们还可以得到更多新奇的东西,并且在视图类中增加我们自己命名的属性,然后可以打印到错误消息中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface UIView (AutoLayoutDebugging)
- (void)setAbc_NameTag:(NSString *)nameTag;
- (NSString *)abc_nameTag;
@end
@implementation UIView (AutoLayoutDebugging)
- (void)setAbc_NameTag:(NSString *)nameTag
{
    objc_setAssociatedObject(self, "abc_nameTag", nameTag, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)abc_nameTag
{
    return objc_getAssociatedObject(self, "abc_nameTag");
}
@end
@implementation NSLayoutConstraint (AutoLayoutDebugging)
#ifdef DEBUG
- (NSString *)description
{
    NSString *description = super.description;
    NSString *asciiArtDescription = self.asciiArtDescription;
    return [description stringByAppendingFormat:@" %@ (%@, %@)", asciiArtDescription, [self.firstItem abc_nameTag], [self.secondItem abc_nameTag]];
}
#endif
@end

另外还有一个打印调试信息的方法: issue-3-auto-layout-debugging

2. 有歧义的布局

UIView提供了三种方式来查明有歧义的布局:hasAmbiguousLayoutexerciseAmbiguityInLayout,和私有方法 _autolayoutTrace。如果我们不想自己遍历视图层并记录这个值,可以使用私有方法 _autolayoutTrace。这将返回一个描述整个视图树的字符串:类似于 recursiveDescription 的输出(当视图存在有歧义的布局时,这个方法会告诉你)。

1
2
3
4
5
6
7
@implementation UIView (AutoLayoutDebugging)
- (void)printAutoLayoutTrace {
    #ifdef DEBUG
    NSLog(@"%@", [self performSelector:@selector(_autolayoutTrace)]);
    #endif
}
@end

另一个标识出有歧义布局更直观的方法就是使用 exerciseAmbiguityInLayout。这将会在有效值之间随机改变视图的 frame。然而,每次调用这个方法只会改变 frame 一次。所以当你启动程序的时候,你根本不会看到改变。创建一个遍历所有视图层级的辅助方法是一个不错的主意,并且让所有包含歧义布局的视图晃动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation UIView (AutoLayoutDebugging)
- (void)exerciseAmiguityInLayoutRepeatedly:(BOOL)recursive {
    #ifdef DEBUG
    if (self.hasAmbiguousLayout) {
        [NSTimer scheduledTimerWithTimeInterval:.5
                                     target:self
                                   selector:@selector(exerciseAmbiguityInLayout)
                                   userInfo:nil
                                    repeats:YES];
    }
    if (recursive) {
        for (UIView *subview in self.subviews) {
            [subview exerciseAmbiguityInLayoutRepeatedly:YES];
        }
    }
    #endif
} 
@end
3. NSUserDefault选项

有几个有用的 NSUserDefault 选项可以帮助我们调试、测试自动布局: – UIViewShowAlignmentRects: 设置视图可见的alignment rects。 – NSDoubleLocalizedStrings: 简单的获取并复制每个本地化的字符串。这是一个测试更长语言布局的好方法。 – AppleTextDirectionNSForceRightToLeftWritingDirection: 模拟从右到左的语言

我们可以通过代码scheme editor设置。

1
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NSDoubleLocalizedStrings"];

七、AutoLayout性能

自动布局是布局过程中额外的一个步骤。它需要一组约束条件,并把这些约束条件转换成 frame。因此这自然会产生一些性能的影响。如果我们对一些复杂的视图的性能要求比较高,就避免通过约束来布局,直接通过计算视图的frame来布局。

八、总结

自动布局是一个帮助我们灵活布局的工具,我们熟悉了以上这些自动布局的知识后,在UI布局层面会给我们很多乐趣。

几个重要的概念

1. 非对称加密

非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。(私钥是要保密的,公钥可以公开) RSA是目前最有影响力的公钥加密算法,它能够抵抗到目前为止已知的绝大多数密码攻击,已被ISO推荐为公钥数据加密标准。RSA是以三个发明者的姓氏首字母组成的。

2. 摘要算法

数据摘要算法是密码学算法中非常重要的一个分支,它通过对所有数据提取指纹信息以实现数据签名、数据完整性校验等功能,由于其不可逆性,有时候会被用做敏感信息的加密。数据摘要算法也被称为哈希(Hash)算法、散列算法。 摘要算法也可以理解为将任意长度的数据,通过一个算法,得到一个固定长度的数据。典型的摘要算法,比如大名鼎鼎的MD5SHA

3. 数字签名

数字签名就是利用非对称加密摘要算法来传输数据,保证数据的完整性合法性。验证过程如下: 1. 发送方使用给一个摘要算法(MD5)得到要发送数据的摘要,然后用自己的私钥和一个非对称加密算法(RSA)对得到的摘要加密,得到加密后的数据,然后将要发送的数据加密后的数据摘要算法加密算法一同发送给接收方。 2. 接收方接收到数据后,根据指定的摘要算法(MD5)得到实际要传输的数据的摘要,然后在根据指定的加密算法(RSA)和已有的公钥解密得到加密数据解密后的数据,最后比较解密后的数据和得到的摘要是否相同,如果相同就说明实际要传输的数据是完成的合法的。

4. 数字证书

数字证书就是通过数字签名方式来传输的一段数据,iOS开发中的数字证书是Apple Worldwide Developer Relations Certification Authority(WWDR)证书认证中心数字签名过的数据,表面上我们看到的就是钥匙串中的证书,实际WWDR数字签名后的证书包含以下内容:

  • 用户的公钥
  • 用户信息
  • 证书机构名称
  • 证书有效期
  • 苹果数字签名 : 用于验证以上信息

iOS签名验证流程

整个过程的前提是已经购买了苹果的开发者账号(\$99或\$299)。并且安装了Xcode,因为安装Xcode的过程中会自动安装苹果的开发者根证书(Apple Worldwide Developer Relations Certification Authority)。这证书包含了苹果CA的公钥。有了这个公钥,我们和Apple就可以进行互信的信息传递。整个过程大致如下:

一. 证书申请
  1. 用我们自己的机器生成CertificateSigningRequest.certSigningRequest文件,在生成的过程中会产生一对公钥和私钥,私钥已经保存在我们的机器上,这个文件包含了我们的公钥,具体内容如下:

    • 申请者信息,此信息是用申请者的私钥加密的。
    • 申请者公钥,此信息是申请者使用的私钥对应的公钥。
    • 摘要算法和公钥加密算法
  2. 上传CertificateSigningRequest.certSigningRequestMemberCenterMemberCenter根据获取到的公钥和我们的用户信息,通过Apple自己的私钥进行数字签名生成证书,这个证书可以通过安装Xcode过程中安装的根证书进行验证。具体证书包含内容如下:

    • 用户的公钥
    • 用户信息
    • 证书机构名称
    • 证书有效期
    • 苹果数字签名 : 通过根证书验证以上信息
  3. 下载生成的证书,双击安装就会出现在钥匙串中,钥匙串会根据证书中的公钥对应上本机器上的私钥。

二. 打包签名
  1. MemberCenter上生成mobileprovision下载安装,mobileprovision包含如下信息:

    • appid:每个appMemberCenter创建的对应的id
    • 包含哪些证书:不同证书对应不同功能。
    • 功能授权列表
    • 可安装的设备列表:iOS设备的UDID列表,发布证书应该是通配。
    • 苹果数字签名:苹果用来验证以上的信息。
  2. 通过Xcode指定要使用的证书,其实是 指定了签名过程中要使用的私钥,这个私钥是和证书中的公钥相对应的。然后指定对应的mobileprovision,由于mobileprovision文件中包含了证书,实际上本地证书就是Xcode用来指定对应私钥用的。

  3. 最后通过指定的私钥对需要签名的数据进行数字签名(编译过程在签名之前,这里省略了编译过程,编译后的二进制文件也是要签名的内容),最终将ipa包的形式输出,ipa的文件结构如下:

  • 资源文件:例如图片、html、等等。
  • _CodeSignature/CodeResources:plist文件,内容是包内所有数据的数字签名。
  • 可执行文件:编译后的二进制文件。
  • mobileprovision:我们之前通过Xcode指定的包含了证书的文件。
  • Frameworks:程序引用的非系统自带的Frameworks。每个Framework的结构跟app其实差不多
三. 验证安装
  1. 解压ipa包,获取embedded.mobileprovision,通过设备上的Apple公钥验证该文件的完整性和安全性。
  2. embedded.mobileprovision文件验证通过,获取该文件内的用户证书,再通过设备上的Apple公钥验证该证书的完整性和安全性。
  3. 证书验证通过后,获取证书内的我们开发者的公钥。然后通过开发者的公钥验证应用程序包内的数据的完整性和安全性。通过后即可安装。

参考资料

  1. 漫谈iOS程序的证书和签名机制
  2. iOS Code Signing 学习笔记
  3. Inside Code Signing
  4. 代码签名探析
  5. iReSign

译自:Hit-Testing in iOS

Hit-Testing是判定与一个点(touch-point)相交互的绘制在屏幕上的图像对象(UIView)的过程。iOS使用Hit-Testing来决定那个UIView是位于手指下面最前面的视图,该视图应该来接收触摸事件。Hit-Testing是通过反向的深度优先搜索算法实现的。

在解释Hit-Testing是如何工作之前,理解Hit-Testing何时执行是很重要的。下面的图片解释了一个简单的触摸事件的过程,从手指触摸到屏幕的一刻起到手指离开屏幕。 正如上图解释的一样,Hit-Testing是在每次手指触摸时执行的。并且是在任何视图或者手势收到UIEvent(代表触摸属于的事件)之前。

1
注意:不知道什么原因,Hit-Testing会执行多次,但是确定的`hit-test`视图是一样的

在Hit-Testing完成和在触摸点下最前端的视图确认下来之后,hit-test会被关联所有触摸事件各个阶段(begin,moved,ended,canceled)的UITouch对象。除了hit-test视图,绑定到该视图的任何手势识别器和他的祖先视图都会关联到UITouch对象。然后,hit-test视图开始接收触摸事件的序列。

一个需要注意的重要的事情是即使手指移动出了hit-test视图的边界到了另外一个视图,hit-test视图任然继续接收所有的触摸事件直到触摸事件结束。

“触摸对象在整个生命周期内都关联他的hit-test视图,即使触摸移动到了这个视图的外面” Event Handling Guide for iOS, iOS Developer Library

正如前面提到的Hit-Testing采用深度优先的反序访问迭代算法(先访问根节点然后从高到低访问低节点)。这种遍历方法可以减少遍历迭代的次数,一旦找到最深的包含触摸点的后裔视图就停止遍历。这是可能的因为子视图总是渲染在父视图的前面和兄弟节点中在数组中靠后的视图渲染在靠前的视图前面。所以当多个视图包含指定的点的时候,最右边子树的最深视图就是最前面的视图。

可见的是子视图的内容模糊了所有父视图的内容。每一个父视图存储他的子视图于一个有序的数组中,在数组中的顺序会影响子视图的显示。如果两个兄弟视图相互覆盖,后加入的视图(存储在子视图数组的后面)出现在另一个的上面。 View Programming Guide for iOS, iOS Developer Library

下图显示了一个视图层次树的例子和对应的绘制在屏幕上的UI。树的叶节点从左到右反映出子视图数组的排序。

正如看到的,“View A”和“View B”和他们的子视图,“View A.2”和“View B.1”是重叠的。由于“View B”比“View A”有一个较高的子视图索引,所以“View B”和他的子视图被渲染在“View A”和他的子视图上面。因此,“View B.1”应该被hit-testing返回当用户的手指触摸在”View B.1”和“View A.2”的重叠区域。

通过深度优先的反向遍历允许一旦找到第一个最深的后裔包含触摸点的视图就停止遍历:

遍历算法以向UIWindow(视图层次结构的根视图)发送hitTest:withEvent:消息开始。这个方法返回的值就是包含触摸点的最前面的视图。

下面流程图说明了hit-test逻辑。

下面的代码显示了原生的hitTest:withEvent:方法的可能实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

hitTest:withEvent:方法首先检查视图是否允许接收触摸事件。视图允许接收触摸事件的条件是:

  • 视图不是隐藏的: self.hidden == NO
  • 视图是允许交互的: self.userInteractionEnabled == YES
  • 视图透明度大于0.01: self.alpha > 0.01
  • 视图包含这个点: pointInside:withEvent: == YES

然后,如果视图允许接收触摸事件,这个方法通过从后往前发送hitTest:withEvent:消息给每一个子视图来穿过接收者的子树,直到子视图中的一个返回nil。这些子视图中的第一个返回的非nil就是在触摸点下面的最前面的视图,被接收者返回。如果所有的子视图都返回nil或者接收者没有子视图返回接收者自己。

否则,如果视图不允许接收触摸事件,这个方法返回nil而根本不会传递到接收者的子树。因此,hit-test可能不会访问所有的视图体系结构中的视图。

覆盖hitTest:withEvent:的一些用途

hitTest:withEvent:可以被覆盖,当所有触摸事件阶段的所有阶段的触摸事件想要被一个视图处理重定向到另外一个视图。

因为hit-test仅仅在触摸事件顺序的第一次触摸事件发送给他的接收者之前(有UITouchPhaseBegan阶段的触摸事件),覆盖hitTest:withEvent:来重定向事件将要重定向所有的触摸事件。

1.增加视图的触摸区域

覆盖hitTest:withEvent:方法的一个用途就是,当一个视图的触摸区域应该大于他的边界的时候。例如下面的插图显示了一个大小为20*20的视图。这个大小对于处理附近的触摸来说太小了。因此,他的触摸区域可以通过覆盖hitTest:withEvent:在每个方向增加10。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    CGRect touchRect = CGRectInset(self.bounds, -10, -10);
    if (CGRectContainsPoint(touchRect, point)) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}
1
注意:为了能够正确的调用`hit-test`,父视图的边界应该包含子视图希望触摸的区域,或者他的`hitTest:withEvent:`方法也应该被覆盖来包含期望的触摸区域。

2.传递触摸事件给下面的视图

有的时候对于一个视图忽略触摸事件并传递给下面的视图是很重要的。例如,假设一个透明的视图覆盖在应用内所有视图的最上面。覆盖层有子视图应该相应触摸事件的一些控件和按钮。但是触摸覆盖层的其他区域应该传递给覆盖层下面的视图。为了完成这个行为,覆盖层需要覆盖hitTest:withEvent:方法来返回包含触摸点的子视图中的一个,然后其他情况返回nil,包括覆盖层包含触摸点的情况:

1
2
3
4
5
6
7
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *hitTestView = [super hitTest:point withEvent:event];
    if (hitTestView == self) {
        hitTestView = nil;
    }
    return hitTestView;
}

2.传递触摸事件给子视图

一个不同的使用场景可能需要父视图重定向所有的触摸事件给他唯一的子视图。这个行为是有必要的当子视图部分占据他的父视图,但是子视图应该响应所有的触摸事件包括发生在父视图上的。例如,假设一个由一个父视图和一个pagingEnabled设置为YESclipsToBounds设置为NO(为了实现传动带的效果)的UIScrollView组成的图片浏览器:

为了使UIScrollView响应不发生在自己边界内但是在父视图的边界内的触摸事件,父视图的hitTest:withEvent:方法应该像下面这样重写:

1
2
3
4
5
6
7
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *hitTestView = [super hitTest:point withEvent:event];
    if (hitTestView) {
        hitTestView = self.scrollView;
    }
    return hitTestView;
}

译自:Don’t Message self in Objective-C init (or dealloc)

在Objective-C的init和dealloc方法中向自己发送消息是有危险的,如下两个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (id)initWithFoo:(id)foo
{
    self = [super init];
    if (self)
        self.something = foo;
    return self;
}

- (void)dealloc
{
    self.something = nil;
    [super dealloc];
}

什么时候不应该向自己发送消息

通常情况下向自己发送消息都是OK的。但是在以下两个地方应该避免:

  • 在对象的创建过程中
  • 在对象的销毁过程中

在这两个阶段,对象处于一个中间状态。缺乏完整性。在这两个阶段调用属性方法是不合适的。因为每个方法维护着和对象相关的变量。下面是一个对象构建过程经过的流程:

  1. 开始:假设对象是一致的。
  2. 过程中:对象状态是变动的。
  3. 结束:重新存储变量,对象是一致的。

苹果已经有文档Practical Memory Management有一张标题 “不要在初始化方法和dealloc中调用访问方法”

Objective-C init/dealloc:解决方法

解决方案非常简单:在init/dealloc方法内,通过直接访问实例变量来代替通过属性方法。在非ARC下,根据属性的attributesretain还是assign来写匹配代码。例如如果something是一个retain属性默认有一个_something实例变量,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (id)initWithFoo:(id)foo
{
    self = [super init];
    if (self)
        _something = [foo retain];
    return self;
}

- (void)dealloc
{
    [_something release];
    [super dealloc];
}

在ARC下:

1
2
3
4
5
6
7
8
9
10
11
12
- (id)initWithFoo:(id)foo
{
    self = [super init];
    if (self)
        _something = foo;
    return self;
}

- (void)dealloc
{
    [_something release];
}

什么时候在init/dealloc中向自己发送消息是OK的

虽然已经建议“避免在init/dealloc向自己发送消息”,但是有两个地方史OK的:

  • init结束的地方
  • dealloc开始的地方

因为在这两个地方对象是一致的。在init里所有的变量已经初始化,在dealloc还没有变量被销毁。

但是我们一定谨慎和知道对象处于生命周期的什么阶段。简单的创建一个对象不应该以繁重的任务开始。保持构建和销毁方法的简洁。

1. iTerm

iTerm2官方网址 http://iterm2.com/index.html

2. Zsh

Mac系统默认安装了Zsh,路径是/bin/zsh,但是系统默认的Shell还是Bash。通过以下命令可以看到Mac系统下的所有Shell:

1
cat /ect/shells

显示如下:

1
2
3
4
5
6
/bin/bash
/bin/csh
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh

把Zsh设置为当前用户的默认Shell:

1
chsh -s /bin/zsh

2.1 安装配置工具oh-my-zsh

oh-my-zsh可以让我们快速的配置Zsh,话说最开始Zsh乏人问津的原因就是配置过于复杂,oh-my-zsh的网址是https://github.com/robbyrussell/oh-my-zsh,安装oh-my-zsh只需要一条命令:

1
curl -L http://install.ohmyz.sh | sh

2.2 配置别名

zsh的配置主要集中在用户当前目录的.zshrc(~/.zshrc)里。我主要进行了一下配置:

1
2
3
4
alias cls='clear'
alias ll='ls -l'
alias la='ls -a'
alias vi='vim'

2.2 配置颜色

.zshrc里找到ZSH_THEME,就可以设置主题了,默认主题是:

1
ZSH_THEME=”robbyrussell”

oh-my-zsh提供了数十种主题,相关文件在~/.oh-my-zsh/themes目录下,你可以随意选择,我采用了默认主题robbyrussell,不过做了一点小小的改动:

1
2
PROMPT='%{$fg_bold[red]%}➜ %{$fg_bold[green]%}%p%{$fg[cyan]%}%d %{$fg_bold[blue]%}$(git_prompt_info)%{$fg_bold[blue]%}% %{$reset_color%}>>'
#PROMPT='%{$fg_bold[red]%}➜ %{$fg_bold[green]%}%p %{$fg[cyan]%}%c %{$fg_bold[blue]%}$(git_prompt_info)%{$fg_bold[blue]%} % %{$reset_color%}'

对照原来的版本,我把c改为d,c表示当前目录,d表示绝对路径,另外在末尾增加了一个「>>」。

2.2 配置插件

oh-my-zsh项目提供了完善的插件体系,相关的文件在~/.oh-my-zsh/plugins目录下,默认提供了100多种,大家可以根据自己的实际学习和工作环境采用,想了解每个插件的功能,只要打开相关目录下的zsh文件看一下就知道了。插件也是在.zshrc里配置,找到plugins关键字,你就可以加载自己的插件了,系统默认加载git,我的插件配置如下:

1
plugins=(git osx sudo python audojump)

下面简单介绍几个插件:

  • git : 当你处于一个git受控的目录下时,Shell会明确显示gitbranch,另外对git很多命令进行了简化,例如gco=’git checkout’、gd=’git diff’、gst=’git status’、g=’git’等等,熟练使用可以大大减少 git 的命令长度,命令内容可以参考~/.oh-my-zsh/plugins/git/git.plugin.zsh
  • osx : tab增强,quick-look filename可以直接预览文件,man-preview grep可以生成grep手册的pdf版本等。
  • autojump : zshautojump的组合形成了zsh下最强悍的插件,autojump会帮助我们快速的跳到我们访问过的路径,无论我们在哪里。通过以下命令安装:

    brew install autojump 然后在.zshrc中添加一行代码:

    [[ -s ~/.autojump/etc/profile.d/autojump.sh ]] && . ~/.autojump/etc/profile.d/autojump.sh

3. Vim

3.1 颜色配置

话说最好的Vim配色方案是molokai

  • 下载molokai.vim,拷贝到~/.vim/colors目录下,如果没有这个目录就创建这个目录。
  • ~/.vimrc中添加如下代码():

    colorscheme molokai let g:molokai_original = 0 let g:rehash256 = 1

我自己的.vimrc在这里https://github.com/Joywii/TerminalCon

Size Classes是什么

iOS 8在应用界面的可视化设计上添加了一个新的特性-Size Classes,对于任何设备来说,界面的宽度和高度都只分为两种描述:正常紧凑。这样开发者便可以无视设备具体的尺寸,而是对这两类和它们的组合进行适配。这样不论在设计时还是代码上,我们都可以不再受限于具体的尺寸,而是变成遵循尺寸的视觉感官来进行适配。在Xcode中的具体体现如下图:

Alt text

但是我们看到图中的宽度和高度都是Any,Any是什么意思呢?如果weight设为Anyheight设置为Regular,那么在该状态下的界面元素在只要heightRegular,无论weightRegular还是Compact的状态中都会存在。这种关系应该叫做继承关系,具体的四种界面描述与可继承的界面描述如下:

  • w:Compact h:Compact 继承 (w:Any h:Compact , w:Compact h:Any , w:Any h:Any)
  • w:Regular h:Compact 继承 (w:Any h:Compact , w:Regular h:Any , w:Any h:Any)
  • w:Compact h:Regular 继承 (w:Any h:Regular , w:Compact h:Any , w:Any h:Any)
  • w:Regular h:Regular 继承 (w:Any h:Regular , w:Regular h:Any , w:Any h:Any)

我们知道了iOS 8下面设备界面可以描述为4种,但是这么多设备(iPhone4S,iPhone5/5s,iPhone6,iPhone6 Plus,iPad,Apple Watch)具体对应什么描述呢?经过查看官方文档和具体实践得知具体对应关系如下:

  • iPhone4S,iPhone5/5s,iPhone6
    • 竖屏:(w:Compact h:Regular)
    • 横屏:(w:Compact h:Compact)
  • iPhone6 Plus
    • 竖屏:(w:Compact h:Regular)
    • 横屏:(w:Regular h:Compact)
  • iPad
    • 竖屏:(w:Regular h:Regular)
    • 横屏:(w:Regular h:Regular)
  • Apple Watch(猜测)
    • 竖屏:(w:Compact h:Compact)
    • 横屏:(w:Compact h:Compact)

Size Classes手写代码

为了表征Size Classes,Apple在iOS8中引入了一个新的类,UITraitCollection。这个类封装了像水平和竖直方向的Size Class等信息。iOS8的UIKit中大多数UI的基础类(包括UIScreen,UIWindow,UIViewController和UIView)都实现了UITraitEnvironment这个接口,通过其中的traitCollection这个属性,我们可以拿到对应的UITraitCollection对象,从而得知当前的Size Class,并进一步确定界面的布局。和UIKit中的响应者链正好相反,traitCollection将会在view hierarchy中自上而下地进行传递。对于没有指定traitCollection的UI部件,将使用其父节点的traitCollection。这在布局包含childViewController的界面的时候会相当有用。在UITraitEnvironment这个接口中另一个非常有用的是-traitCollectionDidChange:。在traitCollection发生变化时,这个方法将被调用。在实际操作时,我们往往会在ViewController中重写-traitCollectionDidChange:或者-willTransitionToTraitCollection:withTransitionCoordinator:方法(对于ViewController来说的话,后者也许是更好的选择,因为提供了转场上下文方便进行动画;但是对于普通的View来说就只有前面一个方法了),然后在其中对当前的traitCollection进行判断,并进行重新布局以及动画。代码看起来大概会是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection 
              withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
    [super willTransitionToTraitCollection:newCollection 
                 withTransitionCoordinator:coordinator];
    [coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) 
    {
        if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
            //To Do: modify something for compact vertical size
        } else {
            //To Do: modify something for other vertical size
        }
        [self.view setNeedsLayout];
    } completion:nil];
}

在两个To Do处,我们要手写代码针对不同的状态做调整。

Size Classes与Interface Builder

Xcode6中Interface BuilderSize Class有了很强大的支持,xib中可以开启Size Classes如下图:

Alt text

在不同的Size Classes描述下,界面元素可以选择安装还是不安装,具体操作如图:

Alt text

Size Classes与Image Asset

Xcode6中Image Asset也支持了Size Class,也就是说,我们可以对不同的Size Class指定不同的图片了。在Image Asset的编辑面板中选择某张图片,Inspector里现在多了一个WidthHeight的组合,添加我们需要对应的Size Class,然后把合适的图拖上去,这样在运行时SDK就将从中挑选对应的Size的图进行替换了。支持Size ClassImage Asset编辑效果如下:

Alt text

参考链接

1.Super的理解

先看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface Father : NSObject
@end
@implementation Father
@end

@interface Son : Father
@end
@implementation Son
- (id)init
{
    self = [super init];
    if (self)
    {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    
    return self;
}
@end

输出的结果:

1
2
Son
Son

根据[super class]表面的意思我们很容易理解输出应该为Father。真是的情况是这样的,super只是一个编译器指示符,当使用superself发送消息的时候,消息的接收者都是self,本质上通过self发送消息会转化成objc_msgSend方法的调用,objc_msgSend负责从当前类的方法列表开始查询,而通过super发送消息会转化成objc_msgSendSuper方法的调用,objc_msgSendSuper是从当前类的父类的方法列表开始查询方法的。 objc_msgSend定义如下:

1
id objc_msgSend(id receiver, SEL op, ...)

self默认为objc_msgSend方法的第一个参数,所以[self class]的接收者为self,而class方法在NSObject基类中定义,返回消息接收者receiver的。 objc_msgSendSuper定义如下:

1
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

第一个参数是个objc_super的结构体,定义如下:

1
2
3
4
struct objc_super {
    id receiver; 
    Class superClass;
};

可以看到这个结构体包含了两个成员,一个是receiver,这个类似上面objc_msgSend的第一个参数receiver,第二个成员是记录这个类的父类是什么,构建objc_super的时候第一个参数赋值为selfsuperClass赋值为self的父类指针。 最上面的例子中[super class]具体执行过程如下: 1.构建objc_super的结构体,此时这个结构体的第一个成员变量receiver就是Son的实例,和self相同。而第二个成员变量superClass就是指类Father。 2.调用objc_msgSendSuper方法,将objc_super结构体和classsel传递过去。函数里面在做的事情类似这样:从objc_super结构体指向的superClass的方法列表开始找classselector,找到后再以objc_super->receiver去调用这个selector,可能也会使用objc_msgSend这个函数,不过此时的第一个参数receiver就是objc_super->receiver,第二个参数是从objc_super->superClass中找到的selector

id存在的问题

根据Cocoa的命名惯例,Objective-C中init, alloc这类开头的方法,如果以id作为返回类型,会返回消息接收类的实例对象。这些方法有一个相关返回类型,LLVM会进行静态的类型安全检查,但是不是按照这类命名规则命名的方法如果也要返回id,LLVM就不会进行类型安全检查,我们在编译的时候不会发现,而在运行的时候很可能出错,我们现在定义一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface Person : NSObject
- (id)initWithName:(NSString *)name;
+ (id)personName:(NSString *)name;
@end
@implementation Person
- (id)initWithName:(NSString *)name
{
    self = [super init];
    if (self){
    }
    return self;
}
+ (id)personName:(NSString *)name
{
    Person *person = [[Person alloc] init];
    return person;
}
@end

由于personName返回id类型,LLVM不会进行类型安全检查,这样我们就有可能写出错误代码而在编译阶段却发现不了,例如:

1
2
[[[Person alloc] initWithName:@"sss"] count];
[[Person personName:@"sss"] count];

第一行代码就会给出count方法找不到的错误,而第二行不会,由于count不是Person的方法,而personName返回的是id类型,编译器是不会发现问题,只有当运行的时候才会报错。但是initWithName是按照内存管理命名规则定义的方法,同样是返回id,但是该方法就会进行类型安全检查,在编译的时候就会发现问题。

instancetype的出现

instancetype只能用作返回类型,使用instancetype,编译器会进行类型安全检查,就会解决以上问题。

1
2
- (instancetype)initWithName:(NSString *)name;
+ (instancetype)persionName:(NSString *)name;

如果方法的返回类型不是该消息接收类的实例,编译器同样会报错。

1
2
3
4
5
+ (instancetype)personName:(NSString *)name
{
    UIView *view = [UIView alloc] init];
    return person;
}

如果personName方法的返回类型使用id,编译器是认为该代码是没有问题的,而实际上我们想要的是Person的实例。所以,返回类型实例的方法的返回类型尽量用instancetype,以便尽早的在代码的编译阶段发现问题。

OS X的终端开启了一个强大的UNIX实用工具和脚本的世界。如果你是从Linux迁移过来的,你会发现许多熟悉的命令,工作方式和你所期望的方式一样。但是大部分用户不知道OS X自带了一些其他操作系统没有的基于文本的工具。学习这些Mac上独有应用,可以让你在命令行上更高效,帮助你跨越OS X和UNIX之间的鸿沟。

1.open

open打开文件、目录和应用。很令人兴奋是不是,其实open真正有用的是作为命令行的双击命令,例如输入:

1
$ open /Applications/Safari.app/

Safari就会打开,就像你在Finder中双击了Safari图标一样。

如果我们用open打开一个文件,终端就会尝试用相应的GUI程序加载文件,open screenspot.png会在Preview中打开图片。我们可以通过-a来指定打开的应用,或者-e来打开TextEdit编辑文件。

open一个目录将直接在Finder窗口中打开相应的目录,特别打开当前目录open .命令是非常有用的。

请记住,Finder和终端是双向的,我们直接从Finder中拖拽一个文件到命令行,该文件的完整地址就会粘贴到命令行。

2.pbcopy and pbpaste

我们可以用这两个命令在命令行里粘贴和复制,当然我们也可以使用鼠标做到这些,但是pbcopy和pbpaste真正的强大之处在于,他们来自于UNIX命令,可以充分利用管道、重定向和能够在脚本中与其他命令相结合的能力。例如:

1
$ ls ~ | pbcopy

这条命令会拷贝home目录下的所有文件列表到OS X的剪贴板中。我们也可以很简单的获取文件中的内容:

1
$ pbcopy < blogpost.txt

或者疯狂一点的,通过抓取脚本抓取Google涂鸦的连接然后拷贝到剪贴板。

1
$ curl http://www.google.com/doodles#oodles/archive | grep -A5 'latest-doodle on' | grep 'img src' | sed s/.*'<img src="\/\/'/''/ | sed s/'" alt=".*'/''/ | pbcopy

通过pbcopy来获取其他命令的输出是一个非常棒的方法,省去了我们滚动屏幕小心翼翼的选择文本啦。这个命令让我们很容易的分享诊断信息。pbcopy和pbpaste还可以用于自动或加速某些类型的任务。举例来说,如果你想的电子邮件的主题行保存到任务列表中,你可以从Mail.app赋值主题然后运行下面的命令:

1
$ pbpaste >> tasklist.txt

3.mdfind

许多Linux用户尝试在Mac上使用lacate来搜索文件,然后发现这个命令不好使。虽然有一个find命令可以用,但是OSX有一个自己的好用的文件搜索工具Spotlight。所以为什么不把它放到命令行里使用呢?mdfind就是这样的命令。任何Spotlight可以查找的,mdfind都可以查找,包括查找文件和元数据。mdfind有几个方便之处区别于Spotlight。例如-onlyin参数可以严格搜索一个简单的目录:

1
$ mdfind -onlyin ~/Documents essay

mdfind的数据库总是在后台保持最新,但是我们也可以通过mdutil来重构。如果Spotlight出了问题,mdutil -E可以删除所有索引然后重头重构。我们也可以通过mdutil完全的关闭索引。

4.screencapture

screencapture可以获取不同类型的截图,功能类似于Grab.app和快捷键cmd+shift+3cmd+shift+4,但是更灵活。下面展示几种screencapture不同的用法。

1.捕捉屏幕上的内容,包括光标,然后生成的图像(命名为“image.png”)并附加到新的邮件:

1
$ screencapture -C -M image.png

2.用鼠标选中一个窗口,获取窗口内容不包括阴影,然后拷贝到剪切板:

1
$ screencapture -c -W

3.10秒后获取屏幕截图,然后在Preview中打开:

1
$ screencapture -T 10 -P image.png

用鼠标选择屏幕的一个区域,然后截图,保存图片为PDF

1
$ screencapture -s -t pdf image.pdf

更过选项请查看screencapture --help

5.launchctl

launchctl可以让我们和OS X的初始化脚本系统launch进行交互。随着启动保护进程和启动代理,当我们启动电脑的时候控制要开启的服务。我们甚至可以启动一个脚本定期执行或者一定时间间隔后在后台执行,类似于Linux上的cron。例如你想要在开启电脑的时候自动开启Apache Web Server,你可以这样:

1
2
$ sudo launchctl load -w 
/System/Library/LaunchDaemons/org.apache.httpd.plist

运行launchctl list我们可以看到当前系统加载的启动脚本。sudo launchctl unload [path/to/script]命令可以停止或者卸载启动脚本,加入-W标志将从启动序列中永久删除这些脚本。我们可以用这个命令关掉AdobeMicrosoft Office的自动更新。启动脚本存储在以下目录下:

1
2
3
4
5
~/Library/LaunchAgents    
/Library/LaunchAgents          
/Library/LaunchDaemons
/System/Library/LaunchAgents
/System/Library/LaunchDaemons

如果你自己想写启动脚本,Apple在开发者网站上提供了帮助文档。如果你想完全避免命令行来操作的话可以使用Lingon应用。

6.say

这是一个有趣的命令:say可以把文本转化成语音,使用和OS X系统VoiceOver相同的TTS引擎,没有任何选项,就是简单的把文本转化为语音说出来。

1
$ say "Never trust a computer you can't lift."

我们也可以简单的用say-f标志来说一个文本内容,然后使用-o存储最后的音频文件:

1
$ say -f mynovel.txt -o myaudiobook.aiff

say命令非常有用可以用来替代脚本里面的打印日志和警告音。例如,你可以设置一个Automator的或Hazel脚本做文件批处理,然后在任务完成的时候通过say宣布。但是对于say最有趣的功能是:如果你可以使用ssh登录朋友的或者同事的机器,你可以默默的登录他们的机器然后通过命令行打扰他们。给他们一个惊喜。我们也可以改变say命令默认的语言,通过System PreferencesDictation & Speech面板。

7.diskutil

diskutil是OS X自带磁盘工具对应的命令行工具,它不仅可以做到界面工具可以做到的任何功能,也有一些额外的功能-例如用零或者随机数据填充一个磁盘。输入diskutil list就可以看到计算机相关的可移动介质和磁盘的路径。然后可以查看你操作的磁盘的容量,但是使用diskutil一定要小心,它可能会破坏数据。

8.brew

技术上来说brew不是一个原生的命令,但是没有一个Mac高级用户不用Homebrew,官网生成这是OS X缺少的包管理工具,如果你在Linux上使用过apt-get,那么Homebrew就是这样的工具。 brew可以帮助你轻松的访问开源社区上千的开源库和免费工具,举个例子:brew install imagemagick会帮助你安装ImageMagick工具,一个非常有用的工具,可以把GIF图像转换成几十张不同类型的图像。brew install node就会安装NodeJS,一个非常热门的JavaScript服务端开发的工具。你也可以用brew做一些有趣的事情:brew install archey 安装Archey,一个很cool的工具用来展示Mac描述,包含一个彩色的Apple的Logo。Homebrew的定制是非常灵活的,因为它是非常容易的创建formulas,新包会一直被加进来。

但是Homebrew最好的部分是:它总是把所有的文件保存在一个简单的目录下:/usr/local/.这意味着你可以系统软件的最新版本,例如pythonmysql,不用被环境变量干扰。如果你不想用Homebrew安装,删除是很简单的。

原文链接

Eight Terminal Utilities Every OS X Command Line User Should Know

0.Swift开发

1.国内iOS开发博客

2.国内iOS开发网站

3.国外iOS开发博客

4.国外iOS开发网站

5.iOS逆向工程

6.其他