IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Reader Submissions - New Year\'s 2015

    mattt@nshipster.com (Mattt Thompson)发表于 2015-01-01 00:00:00
    love 0

    当我们花些时间来回溯我们在过去一年的经历的时候,有一点是清楚的:对专业的苹果开发者来说,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'Brien 的 this Technical Q&A; document 让我们对 AmIBeingDebugged 方法引起了关注:

    #include 
    #include 
    #include types.h><span>
    #include 
    #include sysctl.h><span>
    
    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 上提问:

    有没有人知道在 iOS 8 里面默认的动画时长和使 viewController 用模型方式展示的触发条件?

    — Matthias Tretter (@myell0w) November 21, 2014

    在 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 年快乐!

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



沪ICP备19023445号-2号
友情链接