Yong的Blog

技术生活随想

使用 Swift 解析 JSON

| Comments

本文翻译自这篇文章,本文中所有的代码都放在Github

我将在本文中概述一个使用 Swift 完成的处理 JSON 的解析库。一个 JSON 的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var json : [String: AnyObject] = [
  "stat": "ok",
  "blogs": [
    "blog": [
      [
        "id" : 73,
        "name" : "Bloxus test",
        "needspassword" : true,
        "url" : "http://remote.bloxus.com/"
      ],
      [
        "id" : 74,
        "name" : "Manila Test",
        "needspassword" : false,
        "url" : "http://flickrtest1.userland.com/"
      ]
    ]
  ]
]

最具挑战的部分就是如何将该数据转换成如下 Swift 结构体的数组:

1
2
3
4
5
6
struct Blog {
    let id: Int
    let name: String
    let needsPassword : Bool
    let url: NSURL
}

我们首先来看最终的解析函数,它包含两个运算法:>>=<*> 。这两个运算符或许看起来很陌生,但是解析整个 JSON 结构就是这么简单。本文其他部分将会解释这些库代码。下面的解析代码是这样工作的:如果 JSON 是不合法的(比如 name 不存在或者 id 不是整型)最终结果将是 nil 。我们不需要反射(reflection)和 KVO ,仅仅需要几个函数和一些聪明的组合方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
func parseBlog(blog: AnyObject) -> Blog? {
    return asDict(blog) >>= {
        mkBlog <*> int($0,"id")
               <*> string($0,"name")
               <*> bool($0,"needspassword")
               <*> (string($0, "url") >>= toURL)
    }
}
let parsed : [Blog]? = dictionary(json, "blogs") >>= {
    array($0, "blog") >>= {
        join($0.map(parseBlog))
    }
}

上面的代码到底做了什么呢?我们来仔细看看这些最重要的函数。首先来看看 dictionary 函数,它接受一个 StringAnyObject 的字典,返回另一个具有指定 key 的字典:

1
2
3
func dictionary(input: [String: AnyObject], key: String) ->  [String: AnyObject]? {
    return input[key] >>= { $0 as? [String:AnyObject] }
}

例如在前面的 JSON 例子中,我们期望 key = “blogs” 包含一个字典。如果字典存在,上述函数返回该字典,否则返回 nil 。我们可以对 ArrayStringInteger 写出同样的方法(下面只是生命,完整代码见 Github):

1
2
3
func array(input: [String:AnyObject], key: String) ->  [AnyObject]?
func string(input: [String:AnyObject], key: String) -> String?
func int(input: [NSObject:AnyObject], key: String) -> Int?

现在,我们来看一下 JSON 例子的完整结构。它本身就是一个字典,包含一个 key 为 “blogs” 的另一个字典。该字典包含一个 key 为 “blog” 的 Array 。我们可以用下面的代码表达上述结构:

1
2
3
4
5
if let blogsDict = dictionary(parsedJSON, "blogs") {
    if let blogsArray = array(blogsDict, "blog") {
         // Do something with the blogs array
    }
}

我么可以实现一个 >>= 操作来代替,接受一个 optional 参数,当该参数不为 nil 的时候,对其使用一个函数。该操作符使用 flatten 函数,flatten 函数将嵌套的 optional 展开:

1
2
3
4
5
6
7
8
operator infix >>= {}
@infix func >>= <U,T>(optional : T?, f : T -> U?) -> U? {
    return flatten(optional.map(f))
}
func flatten<A>(x: A??) -> A? {
    if let y = x { return y }
    return nil
}

另一个被频繁使用的是 <*> 操作符。例如下面的代码是用来解析单个 blog 的:

1
2
3
4
mkBlog <*> int(dict,"id")
       <*> string(dict,"name")
       <*> bool(dict,"needspassword")
       <*> (string(dict, "url") >>= toURL)

当所有的 optional 参数都是 non-nil 的时候该函数才能正常运行,上面的代码转化成:

1
mkBlog(int(dict,"id"), string(dict,"name"), bool(dict,"needspassword"), (string(dict, "url") >>= toURL))

所以,我们来看看操作符 <*> 的定义。它接受两个 optional 的参数,左边的参数是一个函数。如果两个参数都不是 nil ,将会对右边的参数使用左边的函数参数:

1
2
3
4
5
6
7
8
9
operator infix <*> { associativity left precedence 150 }
func <*><A, B>(f: (A -> B)?, x: A?) -> B? {
    if let f1 = f {
        if let x1 = x {
            return f1(x1)
        }
    }
    return nil
}

现在你有可能想知道 mkBlog 是做什么的吧。它是一个 curried 函数用来包装我们的初始化函数。首先,我们有一个 (Int,String,Bool,NSURL) –> Blog 类型的函数。然后 curry 函数将其类型转化为 Int -> String -> Bool -> NSURL -> Blog

1
2
3
let mkBlog = curry {id, name, needsPassword, url in
   Blog(id: id, name: name, needsPassword: needsPassword, url: url)
}

我们将 mkBlog<*> 一起使用,我们来看第一行:

1
2
3
// mkBlog : Int -> String -> Bool -> NSURL -> Blog
// int(dict,"id") : Int?
let step1 = mkBlog <*> int(dict,"id")

可以看到,用 <*> 将他们两个连起来,将会返回一个新的类型:(String -> Bool -> NSURL -> Blog)? ,然后和 string 函数结合:

1
let step2 = step1 <*> string(dict,"name")

我们得到:(Bool -> NSURL -> Blog)? ,一直这样结合,最后将会得到类型为 Blog? 的值。

希望你现在能明白整个代码是如何在一起工作的了。通过创建一些辅助函数和运算符,我们可以让解析强类型的 JSON 数据变得非常容易。如果不用 optional 类型,那么我们将会使用完全不同的类型,并且包含一些错误信息,但这将是另外的 blog 的话题了。

Facebook Shimmer 实现原理

| Comments

Facebook 最新的 App Paper 包含了很多丰富的动画元素,为此 Facebook 甚至设计了一款 iOS 的动画引擎 pop。整个 Paper 的动画设计中另一个很有特色的动画是文字的闪光效果。

Shimmer

这个效果其实和 iPhone 的锁屏文字效果非常类似。

lock screen text animation

Facebook Shimmer

Facebook 最近在 Paper 中实现这种文字效果的部分开源了出来,就叫做 Shimmer。使用 Shimmer 可以很容易实现这种文字闪光的效果。

但是 Shimmer 不仅仅局限于对文字做删光效果,它可以实现任何 view 的闪光效果,如果使用图片做内容,可以实现图像内容的闪光。这样可以实现某些 logo 的闪光效果,甚至可以用来实现反光效果。

Shimmer 原理分析

Shimmer 是怎么实现这种效果的呢?通过阅读源码可以了解到(Shimmer 代码非常短,很容易阅读),其内部是依靠 Core Animation 来实现的。

Shimmer 的内部包含两个 CALayer, 一个是展示内容的 contentlayer, 另一个是完成闪光效果的 masklayer。

这两个 CALayer 通过两个步骤来实现闪光: 遮罩(mask) 和 滑动(slide)。

1: 遮罩(Mask)

顾名思义,遮罩的作用就是要让闪光效果只是对文字部分起作用,而不是文字所在的整个 view。这一步通过 CALayer 的 mask 属性实现。Shimmer 将 masklayer 设置为 contentlayer 的 mask。先来看看 CALayer 关于 mask 的解释:

The layer’s alpha channel determines how much of the layer’s content and background shows through. Fully or partially opaque pixels allow the underlying content to show through but fully transparent pixels block that content.

The default value of this property is nil nil. When configuring a mask, remember to set the size and position of the mask layer to ensure it is aligned properly with the layer it masks.

Shimmer 中利用这一点,通过设置 masklayer 中 alpha 通道的效果来实现闪光效果中的高亮部分。首先将 contentlayer 中的内容设置为白色,通过 masklayer 的遮罩,对应 masklayer 中 alpha 小的部分将会透出一些 contentlayer 的背景,而 alpha 大的部分将会显示出白色的高亮。剩下的工作就是要让这些高亮动起来了。

2: 滑动

要实现滑动效果,其实非常简单,我们可以将通过设置 masklayer 的 position 来控制其与 contentlayer 做 mask 的部分,那么,我们可以通过将 masklayer 的尺寸放大,然后保证调整 position 的时候能完全遮罩住 contentlayer 即可,这样就实现了闪光部分的移动,从而实现了闪光效果。下面这张图很好的描述了整个滑动的原理(图片取自这里):

通过调整 masklayer 中 alpha 通道的形状,可以实现不同的闪光效果。例如如果使用圆形光斑效果的 alpha 通道,那么就可以实现 iPhone 本身锁屏的文字滑动效果了。

总结

从整个 Shimmer 的实现来看,Core Animation 本身强大的功能确实为 iOS 实现很多漂亮的动画带来了可能。但是大部分 iOS 开发者却没有深度掌握和挖掘出这些潜能来。Facebook Shimmer 对于我们或许能够带来某种启发,iOS 系统中很多看似复杂的动画效果其实都可以用 Core Animation 来实现,而不是某些不存在的私有 API。

另 1:对于想要更深入了解 Core Animation 的可以参考阅读 Objc.io 第12期《iOS Core Animation Advanced Techniques》

另 2:这里是一篇利用 css 实现 iPhone 锁屏文字动画的文章,原理与本文描述类似,可以参考。

iOS App 性能备忘

| Comments

本文译自这里.

本备忘收集了很多可以提高 iOS 中 Objective-C 代码性能的代码片段和配置

这些文档中大部分代码片段和配置包括:将平时随手使用会用到的优先考虑灵活性而不是性能的高级 API,替换为功能相同的底层 API;一些会影响到绘制性能的类属性配置。对于 app 性能来说,好的架构和恰当的多线程总是很重要的,但是有时需要具体问题具体对待。

目录

iOS App 性能备忘 按照 Objective-C framework 组织为不同章节的 Markdown 文件:

Foundation


NSDateFormatter

NSDateFormatter 不是唯一一个创建的开销就很昂贵的类,但是它却是常用的、开销大到 Apple 会特别建议应该缓存和重复使用实例的一个。

Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, it is typically more efficient to cache a single instance than to create and dispose of multiple instances. One approach is to use a static variable.

Source

一种通用的缓存 NSDateFormatter 的方法是使用 -[NSThread threadDictionary](因为 NSDateFormatter 不是线程安全的):

1
2
3
4
5
6
7
8
9
10
11
+ (NSDateFormatter *)cachedDateFormatter {
  NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary];
  NSDateFormatter *dateFormatter = [threadDictionary objectForKey:@"cachedDateFormatter"];
    if (dateFormatter == nil) {
        dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setLocale:[NSLocale currentLocale]];
        [dateFormatter setDateFormat: @"YYYY-MM-dd HH:mm:ss"];
        [threadDictionary setObject:dateFormatter forKey:@"cachedDateFormatter"];
    }
    return dateFormatter;
}
– (NSDate )dateFromString:(NSString )string

这可能是最常见的 iOS 性能瓶颈。经过多方努力寻找,下面是 ISO8601 转成 NSDateNSDateFormatter 的最著名替代品。

strptime
1
2
3
4
5
6
7
8
//#include <time.h>

time_t t;
struct tm tm;
strptime([iso8601String cStringUsingEncoding:NSUTF8StringEncoding], "%Y-%m-%dT%H:%M:%S%z", &tm);
tm.tm_isdst = -1;
t = mktime(&tm);
[NSDate dateWithTimeIntervalSince1970:t + [[NSTimeZone localTimeZone] secondsFromGMT]];

Source

sqlite3
1
2
3
4
5
6
7
8
9
10
11
12
//#import "sqlite3.h"

sqlite3 *db = NULL;
sqlite3_open(":memory:", &db);
sqlite3_stmt *statement = NULL;
sqlite3_prepare_v2(db, "SELECT strftime('%s', ?);", -1, &statement, NULL);
sqlite3_bind_text(statement, 1, [iso8601String UTF8String], -1, SQLITE_STATIC);
sqlite3_step(statement);
int64_t value = sqlite3_column_int64(statement, 0);
NSDate *date = [NSDate dateWithTimeIntervalSince1970:value];
sqlite3_clear_bindings(statement);
sqlite3_reset(statement);

Source

NSFileManager

– (NSDictionary )attributesOfItemAtPath:(NSString )filePath error:(NSError *)error

当试图获取磁盘中一个文件的属性信息时,使用 –[NSFileManager attributesOfItemAtPath:error:] 会浪费大量时间读取你可能根本不需要的附加属性。这时你可以使用 stat 代替 NSFileManager,直接获取文件属性:

1
2
3
4
5
6
7
8
9
10
//#import <sys/stat.h>

struct stat statbuf;
const char *cpath = [filePath fileSystemRepresentation];
if (cpath && stat(cpath, &statbuf) == 0) {
    NSNumber *fileSize = [NSNumber numberWithUnsignedLongLong:statbuf.st_size];
    NSDate *modificationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_mtime];
    NSDate *creationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_ctime];
    // etc
}

NSObjCRuntime

NSLog(NSString *format, …)

NSLog() 写消息到 Apple 的系统日志。当通过 Xcode 变异运行程序时,被写出的日志会展现在调试终端,同事也会写到设备产品终端日志中。此外,系统会在主线程序列化 NSLog() 的内容。即使是最新的 iOS 设备,NSLog() 输出调试信息所花的时间也是无法忽略的。所以在产品环境中推荐尽可能少的使用 NSLog()

Calling NSLog makes a new calendar for each line logged. Avoid calling NSLog excessively.

Source

下面是通常会用到的宏定义,它会根据 debug/production 来选择执行 NSLog()

1
2
3
4
5
6
7
8
#ifdef DEBUG
// Only log when attached to the debugger
#    define DLog(...) NSLog(__VA_ARGS__)
#else
#    define DLog(...) /* */
#endif
// Always log, even in production
#define ALog(...) NSLog(__VA_ARGS__)

Source

NSString

+ (instancetype)stringWithFormat:(NSString *)format,, …

创建 NSString 不是特别昂贵,但是当在紧凑循环(比如作为字典的键值)中使用时, +[NSString stringWithFormat:] 的性能可以通过使用类似 asprintf 的 C 函数显著提高。

1
2
3
4
5
6
NSString *firstName = @"Daniel";
NSString *lastName = @"Amitay";
char *buffer;
asprintf(&buffer, "Full name: %s %s", [firstName UTF8String], [lastName UTF8String]);
NSString *fullName = [NSString stringWithCString:buffer encoding:NSUTF8StringEncoding];
free(buffer);
– (instancetype)initWithFormat:(NSString *)format, …

参考 +[NSString stringWithFormat:]

UIKit


UIImage

+ (UIImage )imageNamed:(NSString )fileName

如果 boundle 中得某个图片只显示一次,推荐使用 + (UIImage *)imageWithContentsOfFile:(NSString *)path,系统就不会缓存该图片。参考 Apple 的文档:

If you have an image file that will only be displayed once and wish to ensure that it does not get added to the system’s cache, you should instead create your image using imageWithContentsOfFile:. This will keep your single-use image out of the system image cache, potentially improving the memory use characteristics of your app.

Source

UIView

@property(nonatomic) BOOL clearsContextBeforeDrawing

UIView 的属性 clearsContextBeforeDrawing 设置为 NO 在多数情况下可以提高绘制性能,尤其是在你自己用绘制代码实现了一个定制 view 的时候。

If you set the value of this property to NO, you are responsible for ensuring the contents of the view are drawn properly in your drawRect: method. If your drawing code is already heavily optimized, setting this property is NO can improve performance, especially during scrolling when only a portion of the view might need to be redrawn.

Source

By default, UIKit clears a view’s current context buffer prior to calling its drawRect: method to update that same area. If you are responding to scrolling events in your view, clearing this region repeatedly during scrolling updates can be expensive. To disable the behavior, you can change the value in the clearsContextBeforeDrawing property to NO.

Source

@property(nonatomic) CGRect frame

当设置一个 UIView 的 frame 属性时,应该保证坐标值和像素位置对齐,否则将会触发反锯齿降低性能,也有可能引起图形界面的边界模糊(译者注:尤其是涉及到绘制文字时将会引起文字模糊不清,非 retina 设备特别明显)。一种简单直接的办法就是使用 CGRectIntegral() 自动将 CGRect 的值四舍五入到整数。对于像素密度大于1的设备,可以将坐标值近似为 1.0f / screen.scale 整数倍。

QuartzCore


CALayer

@property BOOL allowsGroupOpacity

在 iOS7 中,这个属性表示 layer 的 sublayer 是否继承父 layer 的透明度,主要用途是当在动画中改变一个 layer 的透明度时(会引起子 view 的透明度显示出来)。但是如果你不需要这种绘制类型,可以关闭这个属性来提高性能。

When true, and the layer’s opacity property is less than one, the layer is allowed to composite itself as a group separate from its parent. This gives the correct results when the layer contains multiple opaque components, but may reduce performance.

The default value of the property is read from the boolean UIViewGroupOpacity property in the main bundle’s Info.plist. If no value is found in the Info.plist the default value is YES for applications linked against the iOS 7 SDK or later and NO for applications linked against an earlier SDK.

上述引用来源已不存在,可以参考 CALayer.h

(Default on iOS 7 and later) Inherit the opacity of the superlayer. This option allows for more sophisticated rendering in the simulator but can have a noticeable impact on performance.

Source

@property BOOL drawsAsynchronously

drawsAsynchronously 属性会导致 layer 的 CGContext 延迟到后台线程绘制。这个属性对于频繁绘制的 leyer 有很大的好处。

When this property is set to YES, the graphics context used to draw the layer’s contents queues drawing commands and executes them on a background thread rather than executing them synchronously. Performing these commands asynchronously can improve performance in some apps. However, you should always measure the actual performance benefits before enabling this capability.

Source

Any drawing that you do in your delegate’s drawLayer:inContext: method or your view’s drawRect: method normally occurs synchronously on your app’s main thread. In some situations, though, drawing your content synchronously might not offer the best performance. If you notice that your animations are not performing well, you might try enabling the drawsAsynchronously property on your layer to move those operations to a background thread. If you do so, make sure your drawing code is thread safe.

Source

@property CGPathRef shadowPath

如果要操作 CALayer 的阴影属性,推荐设置 layer 的 shadowPath 属性,系统将会缓存阴影减少不必要的重绘。但当改变 layer 的 bounds 时,一定要重设 shadowPath

1
2
3
4
5
6
CALayer *layer = view.layer;
layer.shadowOpacity = 0.5f;
layer.shadowRadius = 10.0f;
layer.shadowOffset = CGSizeMake(0.0f, 10.0f);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRect:layer.bounds];
layer.shadowPath = bezierPath.CGPath;

Letting Core Animation determine the shape of a shadow can be expensive and impact your app’s performance. Rather than letting Core Animation determine the shape of the shadow, specify the shadow shape explicitly using the shadowPath property of CALayer. When you specify a path object for this property, Core Animation uses that shape to draw and cache the shadow effect. For layers whose shape never changes or rarely changes, this greatly improves performance by reducing the amount of rendering done by Core Animation.

Source

@property BOOL shouldRasterize

如果 layer 只需要绘制依此,那么可以设置 CALayer 的属性 shouldRasterizeYES。但是如果该 layer 让然会被移动、缩放或者变形,那么将 shouldRasterize 设置为 YES 会损伤绘制性能,因为系统每次绘制完后会尝试再次重绘。

When the value of this property is YES, the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content. Shadow effects and any filters in the filters property are rasterized and included in the bitmap.

Source

动手创建 NSObject

| Comments

本文译自这里,转载请注明!

在 Cocoa 编程时创建和使用的(几乎)所有类都是以 NSObject 为根基,但它背后都做了什么、是怎么做的呢?今天,我将从零开始创建 NSObject。

根类(root class)的组成

准确地说,根类到底做了什么?以 Objective-C 自身的术语描述,有一个准确的要求:根类的第一个实例变量必须是 isa,它指向一个实例(object)的类(class),在分发消息的时候,它被用来准确找出该实例的类。从严格的编程语言观点来看,这是根类必须要做的。

当然根类只提供这些是不够的,NSObject 提供了更多的。它提供的功能可以分为一下三类:

  1. 内存管理:标准的内存管理方法如 retainrelease 都是在 NSObject 中实现的,alloc 方法也是如此。
  2. 内省:NSObject 提供了很多 Objective-C 运行时方法的包装,例如 classrespondsToSelector:isKindOfClass:
  3. 内置实用方法:有很多每一个实例都需要实现的方法,例如 isEqual:description。为了保证每一个实例都有这些方法,NSObject 提供了这些方法的默认实现。

代码

我接下来将要在 MAObject 中实现 NSObject 的功能,源代码在 Github: https://github.com/mikeash/MAObject

这个代码没有使用 ARC,虽然 ARC 是个好东西,而且应该尽可能使用它,不过在实现根类的时候使用就不太合适,因为根类需要实现内存管理,如果使用 ARC 就把内存管理交给编译器了!

实例变量

MAObject 有两个实例变量,第一个是 isa 指针,第二个是实例的引用计数1

1
2
3
4
@implementation MAObject {
    Class isa;
    volatile int32_t retainCount;
}

我们将使用 OSAtomic.h 中的方法管理引用计数保证线程安全,这就是为什么这里没有用 NSUInteger 或者类似的声明,而是用了个看起来不寻常的方式。

实际上 NSObject 是把引用计数保存在外面。有一个全局的表(table),标中是实例地址和引用计数映射。这样可以节省内存,因为不在表中的实例的引用计数默认就是1。尽管有这些好处,但是这样实现起来有点复杂并且性能有些慢,所以在这里我就不实现这种方法了。

内存管理

MSObject 需要做的第一件事情就是创建实例,主要在 +alloc 中实现完成。(这里我略过了已经弃用的 +allocWithZone: 方法,这个方法实际上会忽略参数,完成 +alloc 同样的工作。)

子类一般很少会重写 +alloc,而是依赖根类来分配内存。这就意味着 MAObject 不仅要为自己分配内存,还要为子类分配内存,完成这个就要利用类方法中的 self 的值实际上就是消息将会被分发到的类这一优势。例如代码为 [SomeSubClass alloc],那么 self 就是指向 SomeSubClass。这个类将会被用来使用运行时方法确定需要的内存大小和正确设置 isa 。同时引用计数的值也会被设为1,与创建一个实例的行为相符:

1
2
3
4
5
6
7
+ (id)alloc
{
    MAObject *obj = calloc(1, class_getInstanceSize(self));
    obj->isa = self;
    obj->retainCount = 1;
    return obj;
}

retain 方法很简单,就是使用 OSAtomicIncrement32 方法将引用计数加1,然后返回 self:

1
2
3
4
5
- (id)retain
{
    OSAtomicIncrement32(&retainCount);
    return self;
}

release 方法干的事情多一点,首先将引用计数减1,如果引用计数降为0了,那么实例需要被销毁,所以要调用 dealloc

1
2
3
4
5
6
- (oneway void)release
{
    uint32_t newCount = OSAtomicDecrement32(&retainCount);
    if(newCount == 0)
        [self dealloc];
}

autorelease 的实现实际上调用 NSAutoreleasePool 将 self 加入当前的 autorelease pool 。Autorelease pool 现在时运行时的一部分,所以这样做不是很直接,但是 autorelease pool 的 API 是私有的,所以这样实现也是目前最好的办法了:

1
2
3
4
5
- (id)autorelease
{
    [NSAutoreleasePool addObject: self];
    return self;
}

retainCount 方法简单返回引用计数值:

1
2
3
4
- (NSUInteger)retainCount
{
    return retainCount;
}

最后是 dealloc 方法,通常类的 dealloc 方法清除所有的实例变量,然后调用父类的 dealloc 。所以根类实际上需要释放占用的内存。这里通过调用 free 来完成:

1
2
3
4
- (void)dealloc
{
    free(self);
}

还有一些辅助的方法。NSObject 为了一致性提供了一个什么也没做的 init 方法,所以子类通常会调用 [super init]:

1
2
3
4
- (id)init
{
    return self;
}

还有一个 new 方法,它只是包装了一下 alloc 和 init :

1
2
3
4
+ (id)new
{
    return [[self alloc] init];
}

还有个空的 finalize 方法。NSObject 把它作为垃圾回收的一部分实现了。不过 MAObject 开始就不支持垃圾回收,不过我在这里加上它只是因为 NSObject 有它:

1
2
3
- (void)finalize
{
}

内省

很多内省的方式只是运行时方法的包装,因为这没太大意思,所以我会简单介绍一下运行时方法背后的工作原理。

最简单的内省方法就是 class ,它只是返回 isa :

1
2
3
4
- (Class)class
{
    return isa;
}

从技术上来讲,这样的实现在 isa 是一个标记指针(tagged pointers)2 时就会发生错误,更合理的实现应该是调用 object_getClass 方法,它在标记指针时也可以正常工作。

实例方法 superclass 和使用类来调用 superclass 的行为一样,我们也是这么实现的:

1
2
3
4
- (Class)superclass
{
    return [[self class] superclass];
}

有很多类方法也是内省的一部分,+class 直接返回 self ,实际上它时一个类实例(class object)。这里有些奇怪,但实际上 NSObject 就是这么工作的。[object class] 返回的是实例的类,[MyClass class] 返回的指针指向 MyClass 本身,看起来不一致,实际上 MyClass 也有一个类,他就是 MyClass 的元类(meta class) 。下面是实现:

1
2
3
4
+ (Class)class
{
    return self;
}

+superclass 方法顾名思义,它是通过调用 class_getSuperclass 实现的,该方法会解析运行时的类结构来找出指向父类的指针:

1
2
3
4
+ (Class)superclass
{
    return class_getSuperclass(self);
}

还有一些方法用来查询某个实例的类是否与指定的类相匹配,最简单的一个就是 isMemberOfClass: ,该方法进行严格匹配,会忽略子类,实现如下:

1
2
3
4
- (BOOL)isMemberOfClass: (Class)aClass
{
    return isa == aClass;
}

isKindOfClass: 方法会检查子类,所以 [subclassInstance isKindOfClass: [Superclass class]] 会返回 YES 。该方法的返回值与 +isSubclassOfClass: 的返回值完全一样,我们也是通过调用它来实现的:

1
2
3
4
- (BOOL)isKindOfClass: (Class)aClass
{
    return [isa isSubclassOfClass: aClass];
}

+isSubclassOfClass: 方法有点意思,它会从 self 开始向上递归,在每一层比较目标类。如果找到相匹配的,就返回 YES ,如果一直找到类继承机构的顶层也没有匹配的,就返回 NO :

1
2
3
4
5
6
7
+ (BOOL)isSubclassOfClass: (Class)aClass
{
    for(Class candidate = self; candidate != nil; candidate = [candidate superclass])
        if (candidate == aClass)
            return YES;
    return NO;
}

你可能已经注意到了这里不是很高效,如果你对一个处在很深的继承关系中的类调用该方法,它在返回 NO 之前将会进行很多次循环。因此 isKindOfClass: 会比通常的消息发送慢,在某种情况下会是性能瓶颈,这也是需要避免使用这类方法的原因之一。

respondsToSelector: 方法只是调用运行时方法 class_respondsToSelector ,该方法在类的方法表中依此查找看是否有匹配项:

1
2
3
4
- (BOOL)respondsToSelector: (SEL)aSelector
{
    return class_respondsToSelector(isa, aSelector);
}

还有另外一个类方法 instancesRespondToSelector: 和上面的方法几乎一样,不过唯一的区别是它的实现传入的是 self 而不是 isa ,在上下文环境中 self 应该是元类(meta class):

1
2
3
4
+ (BOOL)instancesRespondToSelector: (SEL)aSelector
{
    return class_respondsToSelector(self, aSelector);
}

与此类似,也有两个 conformsToProtocol: 方法,一个是实例方法,另一个是类方法。他们也都是对运行时方法的包装,在这里是去遍历所有类遵循的协议(protocol)表,坚持给定的协议是否在其中:

1
2
3
4
5
6
7
8
9
- (BOOL)conformsToProtocol: (Protocol *)aProtocol
{
    return class_conformsToProtocol(isa, aProtocol);
}

+ (BOOL)conformsToProtocol: (Protocol *)protocol
{
    return class_conformsToProtocol(self, protocol);
}

下一个方法是 methodForSelector: ,类方法中类型的方法是 instanceMethodForSelector: 。他们俩都会调用运行时方法 class_getMethodImplementation ,该方法会查找类的函数表,然后返回响应的 IMP :

1
2
3
4
5
6
7
8
9
- (IMP)methodForSelector: (SEL)aSelector
    {
        return class_getMethodImplementation(isa, aSelector);
    }

    + (IMP)instanceMethodForSelector: (SEL)aSelector
    {
        return class_getMethodImplementation(self, aSelector);
    }

有趣的一点是 class_getMethodImplementation 总是返回一个 IMP ,即使参数是一个不存在的 selector 。当类没有实现某个方法时,它会返回一个特殊的转发 IMP ,这个 IMP 包装好了调用 forwardInvocation: 的消息参数。

方法 methodSignatureForSelector: 只是对类方法的包装:

1
2
3
4
- (NSMethodSignature *)methodSignatureForSelector: (SEL)aSelector
{
    return [isa instanceMethodSignatureForSelector: aSelector];
}

而这个类方法也是对运行时方法的包装。它首先获取输入 selector 的 Method 。如果不能得到,那么就表明类没有实现该方法,那么返回 nil 。否则就获取方法类型的 C 字符串表达,并且包装在 NSMethodSignature 中:

1
2
3
4
5
6
7
8
9
+ (NSMethodSignature *)instanceMethodSignatureForSelector: (SEL)aSelector
{
    Method method = class_getInstanceMethod(self, aSelector);
    if(!method)
        return nil;

    const char *types = method_getTypeEncoding(method);
    return [NSMethodSignature signatureWithObjCTypes: types];
}

最后是 performSelector: 方法,还有两个类似方法用 withObject: 去接收参数。他们不是严格意义上的内省方法,但他们都是对底层运行方法的包装。他们只是获取 selector 的 IMP ,强制转换成合适的函数指针类型,然后调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (id)performSelector: (SEL)aSelector
{
    IMP imp = [self methodForSelector: aSelector];
    return ((id (*)(id, SEL))imp)(self, aSelector);
}

- (id)performSelector: (SEL)aSelector withObject: (id)object
{
    IMP imp = [self methodForSelector: aSelector];
    return ((id (*)(id, SEL, id))imp)(self, aSelector, object);
}

- (id)performSelector: (SEL)aSelector withObject: (id)object1 withObject: (id)object2
{
    IMP imp = [self methodForSelector: aSelector];
    return ((id (*)(id, SEL, id, id))imp)(self, aSelector, object1, object2);
}

内置实用方法

MAObject 提供了很多方法的默认实现,我们从 isEqual:hash 两个方法开始,因为它们都是用实例的指针进行唯一性判断:

1
2
3
4
5
6
7
8
9
- (BOOL)isEqual: (id)object
{
    return self == object;
}

- (NSUInteger)hash
{
    return (NSUInteger)self;
}

任何子类想要实现更复杂的相等判断就要重写这些方法,但是如果子类想要只有和自身相等的情况下就可以使用这些方法。

另外一个方便的方法是 description ,我们也有一个默认实现。这个方法只是生成一个类似 <MAObject: 0xdeadbeef> 的字符串,包含了实例的类和指针:

1
2
3
4
- (NSString *)description
{
    return [NSString stringWithFormat: @"<%@: %p>", [self class], self];
}

类方法 description 只需要返回类的名字即可,所以调用运行时方法获取类名然后返回即可:

1
2
3
4
+ (NSString *)description
{
    return [NSString stringWithUTF8String: class_getName(self)];
}

doesNotRecognizeSelector: 是一个知道的人比较少的实用方法。它会通过抛异常来让类看起来不会响应某些方法,这在我们创建一些子类必须重写的方法时很有用:

1
2
3
4
5
- (void)subclassesMustOverride
{
    // pretend we don't actually implement this here
    [self doesNotRecognizeSelector: _cmd];
}

代码很简单,唯一有些技巧的地方就是正确给出方法的名称,我们想为实例输出类似 –[Class method] 的东西,同时类方法前面需要显示 + ,类似 +[Class classMethod] 。为了分辨是哪种情况,我们就需要检查 isa 是不是元类,如果是元类,那么 self 就是类,需要显示 + 。否则 self 就是实例,需要用 。代码其他部分就是用来抛异常:

1
2
3
4
5
- (void)doesNotRecognizeSelector: (SEL)aSelector
{
    char *methodTypeString = class_isMetaClass(isa) ? "+" : "-";
    [NSException raise: NSInvalidArgumentException format: @"%s[%@ %@]: unrecognized selector sent to instance %p", methodTypeString, [[self class] description], NSStringFromSelector(aSelector), self];
}

最后,还有很多显而易见的方法(比如 self 方法),很多让子类安全调用 super 的方法(类似空的 +initialize 方法),很多重写点(例如 copy 就会抛异常)。这些都没太多意思,但都包括在了 MAObject 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
- (id)self
{
    return self;
}

- (BOOL)isProxy
{
    return NO;
}

+ (void)load
{
}

+ (void)initialize
{
}

- (id)copy
{
    [self doesNotRecognizeSelector: _cmd];
    return nil;
}

- (id)mutableCopy
{
    [self doesNotRecognizeSelector: _cmd];
    return nil;
}

- (id)forwardingTargetForSelector: (SEL)aSelector
{
    return nil;
}

- (void)forwardInvocation: (NSInvocation *)anInvocation
{
    [self doesNotRecognizeSelector: [anInvocation selector]];
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    return NO;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return NO;
}

总结

NSObject 其实就是一大堆不同函数的组合,没什么奇怪之处。它的主要功能就是分配和管理内存,你可据此创建实例。同时它也提供了一堆每个方法都应该支持的有用的重写点,也包装了很多有用的运行时 API 。

这里我忽略了 NSObject 的一个块内容:key-value coding ,这块复杂到我需要另写一篇文章了。

iOS 的 ReactiveCocoa 函数式响应编程

| Comments

本文翻译自这里,其中一些语法由于 ReactiveCocoa 的演进我做了修改。

Objective-C 是一种基于C语言、但又常常深陷于C的过时编程方式的编程语言。随着计算机计算能力的提升,编程语言设计也与时俱进,但是Objective-C有时好像留在了过去。

Objective-C和C都属于计算机顺序执行软件指令的指令式编程语言,软件的行为也出自执行这些指令。如果开发者写出来正确顺序的指令,那么软件的行为也满足程序的期望。

但是写出来的代码总是有缺陷,我们需要使用或是手动或是自动的测试去减少这些问题,但是如果能抽象单独的指令,而把注意力放在期望行为上面。这就是声明式编程的由来。

指令范式强制让开发者写程序去完成一些任务,而声明范式解放开发者去描述任务是什么。

ReactiveCocoa是一种让 Objective-C 少一些指令式属性,多一些响应式属性。它将怎样抽象成什么

下面我们用几个例子来展示 Objective-C 中的声明式编程是什么样的。

ReactiveCocoa 的核心是信号 (signal) ,信号代表了不同时间发生的事件流。订阅 (subscribing) 信号允许开发者访问这些事件。下面开看一个基础的例子。

iOS App 中得输入框(Text Field)在输入文字发生改变时会产生的事件会生成信号。ReactiveCocoa 的 UITextField 分类(Category)有一个方法:rac_textSignal,我们可以这样订阅这个事件:

1
2
3
[self.usernameField.rac_textSignal subscribeNext:^(NSString *value) {
  NSLog(@"Text field has been updated: %@", value);
}];

这段代码中,我们声明输入框的文字变化时,将它的新值打印出来。无论何时输入框的信号发送了一个事件,这块代码都将被以新的文本内容为参数调用。

subscription

信号很牛逼的地方在于它可以组合使用。我们可以过滤 rac_textSignal 返回的信号,以保证字符串的长度大于3才能登陆:

1
2
3
4
5
[[self.textField.rac_textSignal filter:^BOOL(NSString *value) {
  return [value length] >= 3;
}] subscribeNext:^(NSString *value) {
  NSLog(@"Text field has been updated: %@", value);
}];

Filter 方法返回一个新的信号。当第一个信号发射了一个事件,这个事件的值将被传递到 filter 代码块。如果这块代发返回 YES,那么新的信号会发射一个事件。后代码订阅的就是这个 filter 返回的信号。

filter

我们来做一些更复杂的吧。我们将两个输入框的两个信号联合 (combine) 起来,将他们的值降 (reduce) 为一个布尔值,然后和另一个按钮的 enable 属性绑定 (bind) 在一起。

1
2
3
4
[[RACSignal combineLatest:@[self.firstNameField.rac_textSignal, self.lastNameField.rac_textSignal]
  reduce:^(NSString *firstName, NSString *lastName){
      return @(firstName.length > 0 && lastName.length > 0);
  }] setKeyPath:@"enabled" onObject:self.button];

按钮的 enable 状态总是由两个输入框的最新的信号所派生。这代表了函数响应式编程众多核心理念中的一个:派生状态(deriving state)。

combine

在上述所有例子中,我们都在 viewDidLoad 中有所声明,在应用运行时这些陈述都保持为真。这里我们没有实现任何代理方法(delegate methods)或者保存任何状态。所有行为都是显式声明而不是隐式的推断。

函数响应式编程非常复杂,而学习 ReactiveCocoa 的丰富细节也需要时间。但是学习这些也会带来具有可预测的、良好定义行为的稳定程序。

软件开发的历史告诉我们软件开发的趋势是朝着更高级别的抽象迈进,诚如我们现在再也不会和穿孔卡片或者汇编语言打交道一样。我相信函数响应式编程是抽象的另一个更高层次,借此程序员可以更快地开发出更好地软件。

我的 2013

| Comments

2013年飞逝而过!对我来说,这一年真如坐过山车,起起伏伏,人生如梦的感慨对我的2013来说最是贴切。这篇日志是对我的2013年的总结,也是对这神奇一年的备忘!

工作

2013年起伏的事情都发生在工作中。2012年成功发布 iPhone Flickr1 后,13年前半段开始了新iPad项目,同时也维护已发布的App。下半年由于公司策略问题,将所有的移动项目拿回总部,我的项目也被拿了回去。我也被换到了新的项目。新的项目是Yahoo!主页推荐系统的后台,这对我来说是个新的挑战,从前端到后端,也捡起来了丢下好久的C++。

工作中有个大的插曲,2013年前几个月一直在忙内部 Transfer,我全家做好了搬到湾区开始新生活的准备。不过由于临走前发生了一起让我躺枪、也让我震惊的事情,我的 Transfer 最终没有成行。这件事情让我重新思考了待人待事的态度方法,期间公司的做法和决定也让我下定决心离开Yahoo!.

2013年后半段开始计划换工作,从准备到结束花了将近3个月,最终皇天不负有心人,我对结果非常满意。

技术

2013年我的技术积累还主要在iOS开发上面。项目重构重写让我重新审视了自己以前的设计和编码,发现弥补了很多不足。这一年我更加注重程序的设计和架构,我认为好的程序不应该能够工作就好了,还要有美感,要能重用、方便扩展。这一年我也更加关注后端的技术,从大规模服务器到大数据处理。好的软件工程师应该了解方方面面,并且有一两个精通的方向。

生活

2013年的生活被工作影响比较大。前面提到的内部Transfer没有成行,对我和我的家庭有很大影响,我们花了1个多月才让生活恢复正常。

一年内我和老婆以及朋友完成了两趟比较长的自驾游。


第一条线路是 北京->承德->塞罕坝->塔里湖->锡林郭勒->草原天路->张家口->北京。这一趟花了7天,把北京北边主要景区都走过了。唯一遗憾的是由于时间是秋天,所以草原的美景不如夏季那么好,不过也是别有一番风情。
第二条线路是 旧金山->Half moon bay->Moterey->Camel->洛杉矶->圣地亚哥->洛杉矶->旧金山,这一趟花了14天。和老婆一起在美国度过了圣诞节和新年,一路风景迷人、气候宜人。

对了,2013年健身的决心无比坚定,已经减肥接近8Kg!

2014希望

全家健康,一切顺顺利利,自己更加强大。


  1. https://itunes.apple.com/hk/app/flickr/id328407587?mt=8

Hello From Octopress

| Comments

2014年决定在这里开始写blog,主要发布一些技术观察,技术总结和个人随想。