Reader Submissions - New Year's 2015Mattt April Peng 🚩🌱

当我们花些时间来回溯我们在过去一年的经历的时候,有一点是清楚的:对专业的苹果开发者来说,2014 年是一个令人难以置信的一年。在这么短的时间跨度内发生了这么多的事情,都记不得在 Swift 之前我们跟 Objective-C 的关系,或者还有什么 API 比 iOS 8 或 WatchKit 更让我们着迷。

这有一个 NSHipster 传统问题要问你们,亲爱的读者,请把你在过去一年里最喜欢的技巧发送给我们,我们会在新年假期后公布结果。这一年随着大量新发展的出现,无论从苹果还是整个社区,都为读者分享了很多的有趣花絮。

谢谢 Colin Rofls, Cédric Luthi, Florent Pillet, Heath Borders, Joe Zobkiw, Jon Friskics, Justin Miller, Marcin Matczuk, Mikael Konradsson, Nolan O’Brien, Robert Widmann, Sachin Palewar, Samuel Defago, Sebastian Wittenkamp, Vadim Shpakovski, 和 Zak Remer 的贡献。


成员函数的秘密生活

来自 Robert Widmann:

在 Swift 的类和结构里,使用静态时成员函数类总是有下列类型:

Object -> (Args) -> Thing

比如,你可以用两种方式来对一个数组调用 reverse()

[1, 2, 3, 4].reverse()
Array.reverse([1, 2, 3, 4])()

@( ) 来封装 C-Strings

来自 Samuel Defago:

鉴于文字大部分是用数字和集合关联的,我常常忘记它们可以在 UTF8 下工作良好,并且编码了 NULL, 终结了 C-string,特别是当我使用运行时代码:

NSString *propertyAttributesString =
    @(property_getAttributes(class_getProperty([NSObject class], "description")));
// T@"NSString",R,C

AmIBeingDebugged

Nolan O’Brienthis Technical Q&A document 让我们对 AmIBeingDebugged 方法引起了关注:

#include <assert.h>
#include <stdbool.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/sysctl.h>

static Bool AmIBeingDebugged(void) {
    int mib[4];
    struct kinfo_proc info;
    size_t size = sizeof(info);

    info.kp_proc.p_flag = 0;

    mib[0] = CTL_KERN;
    mib[1] = KERN_PROC;
    mib[2] = KERN_PROC_PID;
    mib[3] = getpid();

    sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0);

    return (info.kp_proc.p_flag & P_TRACED) != 0;
}

使用惰性变量(Lazy Variables)

来自 Colin Rofls:

避免使用 optional。应尽量避免对 optional 进行隐式拆包(implicitly unwraped)。要声明一个变量,但不一定在初始化时赋初始值?就用惰性关键字,在你真的有值之前不调用 getter 方法。

lazy var someModelStructure = ExpensiveClass()

如果你对这个变量调用 set 之前没有调用过 getter,惰性表达式永远不会被执行。比如在视图里直到 viewDidLoad 之前你都不一定要初始化就很棒。

访问添加到 Storyboard 的容器视图(container views)里的子控制器(child controllers)

来自 Vadim Shpakovski:

有一种方便的方式来访问插入到 storyboard 容器视图的子控制器:

// 1. A property has the same name as a segue identifier in XIB
@property (nonatomic) ChildViewController1 *childController1;
@property (nonatomic) ChildViewController2 *childController2;

// #pragma mark - UIViewController

- (void)prepareForSegue:(UIStoryboardSegue *)segue
                 sender:(id)sender
{
    [super prepareForSegue:segue sender:sender];

    // 2. All known destination controllers assigned to properties
    if ([self respondsToSelector:NSSelectorFromString(segue.identifier)]) {
        [self setValue:segue.destinationViewController forKey:segue.identifier];
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];

    // 3. Controllers already available bc viewDidLoad is called after prepareForSegue
    self.childController1.view.backgroundColor = [UIColor redColor];
    self.childController2.view.backgroundColor = [UIColor blueColor];
}

不需要重新编译的重新运行

来自 Heath Borders:

如果你一遍又一遍的调试同样的问题,你可以不重新编译就运行你的应用程序: “Product > Perform Action > Run without Building” (⌘⌃R)。

快速访问 Playground 资源

来自 Jon Friskics:

Swift Playgrounds 跟所有的共享 Playground 数据都在 /Users/HOME/Documents/Shared Playground Data 下可以找到。

如果你喜欢使用很多的 Playgrounds,你会想要把各 Playground 使用到的数据放到该共享文件夹的子文件夹里面,但你得让 Playground 知道去哪里找。下面是我使用的辅助方法来让这事变得简单:

func pathToFileInSharedSubfolder(file: String) -> String {
    return XCPSharedDataDirectoryPath + "/" + NSProcessInfo.processInfo().processName + "/" + file
}

在 NSProcessInfo 的 processName 属性包含了 Playground 文件的名称,所以只要你已经在 Playground 的共享数据文件夹里创建了用相同名字命名的子文件夹,就可以很容易的访问这些文件,就像读本地的 JSON 一样:

var jsonReadError:NSError?
let jsonData = NSFileManager.defaultManager().contentsAtPath(pathToFileInSharedSubfolder("data.json"))!
let jsonArray = NSJSONSerialization.JSONObjectWithData(jsonData, options: nil, error: &jsonReadError) as [AnyObject]

…或者得到一个本地图片:

let imageView = UIImageView()
imageView.image = UIImage(contentsOfFile: pathToFileInSharedSubfolder("image.png"))

今年其余的读者意见的来自 Cédric Luthi,他(像去年之前一样)贡献了很多的技巧和窍门值得占据一整篇文章。非常感谢,Cédric!

揭露 CocoaPods!

有一个快速方法来检查(闭源)应用程序使用的所有源:

$ class-dump -C Pods_ /Applications/Squire.app | grep -o "Pods_\w+"

CREATE_INFOPLIST_SECTION_IN_BINARY

查看 Xcode 中对命令行应用程序的设置 CREATE_INFOPLIST_SECTION_IN_BINARY。它比 -sectcreate__TEXT__info_plist 链接标志(linker flag)更容易使用,而且它把处理了的 Info.plist 文件嵌入了到二进制包中。

这也是在归档雷达的教训。在2006年,此功能被要求以 rdar://4722772 归档,在 7 年后才被认真对待。

阻止 dylib 钩子

来自 Sam Marshall 的这一招使黑客的生活更艰难:> >

把这一行加到你的 “Other Linker Flags” 里:

-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null

NSBundle -preferredLocalizations

有时候,你需要知道你的应用程序在什么语言环境下运行。通常,人们会用 NSLocale +preferredLanguages。不幸的是这除了告诉你应用程序实际上显示的语言之外一无所知。它只是给你 iOS 中 “Settings → General → Language & Region → Preferred Language” 或是 OS X 里 “System Preferences → Language & Region → Preferred Languages” 同样的有序列表。

想象一下如果首选语言顺序是 {英语, 法语} 但是你的应用程序只支持德语。调用 [NSLocale preferredLanguages] firstObject] 会返回英语而不是你想要的德语。

得到应用程序使用的准确语言环境的正确方式是使用 [[NSBundle mainBundle] preferredLocalizations]

文档是这么说的:

一个 NSString 对象的数组包含了 bundle 里的区域语言 ID。这些字符串是按用户系统设置和可用本地化来排序的。

NSBundle.h 里的注释说:

这个 bundle 本地化的子集,会对这个进程的当前执行环境的优先顺序上重新排序;主 bundle 的首选本地化显示了用户是最有可能在 UI 看到的语言(文本)

你大概还需要使用 NSLocale +canonicalLanguageIdentifierFromString: 来确保规范的语言标识。

保护 SDK 头文件

如果你是从 dmg 里安装的 Xcode,参考一下这个来自 Joar Wingfors 的方法,通过保留所有权、权限和硬链接的方式避免不小心修改了 SDK 的头文件:

$ sudo ditto /Volumes/Xcode/Xcode.app /Applications/Xcode.app

检查 void * 的实例变量

因为逆向工程的原因,非常有用的常用方法是查看对象的实例变量。它通常很容易通过 valueForKey: 来达成,因为很少有类会重写 +accessInstanceVariablesDirectly 来禁止变量通过 Key-Value Coding 访问。

但是有一种情况会让这个不起作用:当变量有一个 void * 类型的时候。

这有一个来自 iOS 6.1 里 MediaPlayer 库的摘录:

@interface MPMoviePlayerController : NSObject <MPMediaPlayback>
{
    void *_internal;    // 4 = 0x4
    BOOL _readyForDisplay;  // 8 = 0x8
}

由于 id internal = [moviePlayerController valueForKey:@"internal"] 不工作,有一个硬编码的方式访问内部变量:

id internal = *((const id*)(void*)((uintptr_t)moviePlayerController + sizeof(Class)));

不要发布这样的代码,这是非常不可靠的,因为变量布局可能会改变。只在逆向工程里使用!

NSDateFormatter +dateFormatFromTemplate:options:locale:

友情提示:如果你在使用 NSDateFormatter -setDateFormat: 而不同时使用 NSDateFormatter +dateFormatFromTemplate:options:locale: 那么你很可能做错了。

文档是这样的:

+ (NSString *)dateFormatFromTemplate:(NSString *)template
                             options:(NSUInteger)opts
                              locale:(NSLocale *)locale

不同的语言对时间要素有不同的规范。你用这个方法来得到某个特定语言(通常使用当前的语言 - 参看 currentLocale)给定的时间要素的正确字符串格式。

下面的例子展示了时间在英国英语和美国英语下的不同格式:

NSLocale *usLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
NSLocale *gbLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"];

NSString *dateFormat;
NSString *dateComponents = @"yMMMMd";

dateFormat = [NSDateFormatter dateFormatFromTemplate:dateComponents options:0 locale:usLocale];
NSLog(@"Date format for %@: %@",
    [usLocale displayNameForKey:NSLocaleIdentifier value:[usLocale localeIdentifier]], dateFormat);

dateFormat = [NSDateFormatter dateFormatFromTemplate:dateComponents options:0 locale:gbLocale];
NSLog(@"Date format for %@: %@",
    [gbLocale displayNameForKey:NSLocaleIdentifier value:[gbLocale localeIdentifier]], dateFormat);

// Output:
// Date format for English (United States): MMMM d, y
// Date format for English (United Kingdom): d MMMM y

调试器中得到内部常量

最近,Matthias Tretter 在 Twitter 上提问:

在 UIKit 的类堆栈里搜索 duration,找到了 UITransitionView +defaultDurationForTransition: 方法,然后在那个方法上加个断点:

(lldb) br set -n "+[UITransitionView defaultDurationForTransition:]"

展示一个模式视图控制器,就会进到这个断点,键入 finish 来执行这个方法:

(lldb) finish

defaultDurationForTransition: 执行的那个断点,你可以读到结果(在 xmm0 里):

(lldb) register read xmm0 --format float64
    xmm0 = {0.4 0}

答案:默认时长是 0.4 秒。

DIY 弱关联对象

遗憾的是,关联对象 OBJC_ASSOCIATION_ASSIGN 的政策不支持零弱引用(zeroing weak references)。幸运的是,自己实现也很简单。你只需一个简单的类来封装一个弱引用的对象:

@interface WeakObjectContainter : NSObject
@property (nonatomic, readonly, weak) id object;
@end

@implementation WeakObjectContainter
- (instancetype)initWithObject:(id)object {
    self = [super init];
    if (!self) {
        return nil;
    }

    self.object = object;

    return self;
}
@end

然后,用 OBJC_ASSOCIATION_RETAIN(_NONATOMIC): 关联 WeakObjectContainter

objc_setAssociatedObject(self, &MyKey, [[WeakObjectContainter alloc] initWithObject:object], OBJC_ASSOCIATION_RETAIN_NONATOMIC);

object 属性来访问它以使得把零弱引用关联到需要的对象上:

id object = [objc_getAssociatedObject(self, &MyKey) object];

就是这样,我们迎来了全新的充满可能和机会的一年。大家 2015 年快乐!

祝愿你继续编译你的代码并且得到鼓舞。


除非另有声明,本文采用知识共享「署名-非商业性使用 3.0 中国大陆」许可协议授权。