[{"content":" Your digital wardrobe — catalog, organize, and plan outfits. Closet helps you track everything you own. Add items manually or import from URLs, organize by category, brand, color, or season, and build a smarter wardrobe over time.\nFeatures # Catalog your wardrobe — Add items with photos, details, and purchase info\nSmart organization — Filter by category, brand, color, size, and custom tags\nOutfit planning — Combine pieces into outfits for any occasion\niCloud Sync — Your wardrobe stays in sync across all your devices via CloudKit\nQuick search — Find any item instantly with full-text search\nDark mode — Beautiful interface that adapts to your preference\nComing soon to the App Store\n","externalUrl":null,"permalink":"/apps/closet/","section":"Apps","summary":"Your digital wardrobe — catalog, organize, and plan outfits","title":"Closet","type":"apps"},{"content":" Save, archive, and search your links — across iOS, macOS, and watchOS. Distill is a multi-platform link archiving app that lets you save web content and search through it with powerful tools.\nFeatures # Full-text search — Find anything you\u0026rsquo;ve saved instantly\nHotLinks — Quick-access collection for your most important links\nReader mode — Clean, distraction-free reading experience\nAI insights — Get summaries and key takeaways from saved articles\nShare extension — Save links from any app with one tap\niCloud Sync — Seamless sync across iPhone, iPad, Mac, and Apple Watch\nComing soon to the App Store\n","externalUrl":null,"permalink":"/apps/distill/","section":"Apps","summary":"Save, archive, and search your links — across iOS, macOS, and watchOS","title":"Distill","type":"apps"},{"content":" AI-powered meal tracking — snap a photo, scan a barcode, or describe your food. EatWisely makes calorie and nutrition tracking effortless. Use AI food recognition, barcode scanning, or the USDA database to log meals and understand what you eat.\nFeatures # AI food recognition — Take a photo or describe your meal to get instant nutritional breakdown\nBarcode scanner — Scan packaged food using the Open Food Facts database\nUSDA database — Search 300,000+ foods for accurate nutrition data\nDaily dashboard — Track calories, macros, and daily budget at a glance\nHealthKit integration — Reads active calories and adjusts your targets automatically\nWeekly insights — Trends, macro breakdowns, and meal distribution charts\niCloud Sync — Your food diary syncs seamlessly across devices\nBYOK AI — Bring your own API key for OpenAI, Claude, or custom endpoints\nComing soon to the App Store\n","externalUrl":null,"permalink":"/apps/eatwisely/","section":"Apps","summary":"AI-powered meal tracking — snap, scan, or describe your food","title":"EatWisely","type":"apps"},{"content":" Personal finance tracker with AI insights and bank integration. SpendWisely gives you a clear picture of your money. Connect bank accounts, set budgets, and get AI-powered insights about your spending habits.\nFeatures # Dashboard — Net worth, spending breakdowns, and recent transactions at a glance\nTransaction history — Full history with search, filtering, and categorization\nBudget tracking — Monthly budgets per category with visual progress rings\nAI insights — Spending analysis, savings suggestions, and anomaly detection\nBank integration — Link accounts via Plaid for automatic transaction import\nCharts \u0026amp; visualizations — Beautiful Swift Charts for trends and breakdowns\nMulti-platform — Available on both iOS and macOS\nComing soon to the App Store\n","externalUrl":null,"permalink":"/apps/spendwisely/","section":"Apps","summary":"Personal finance tracker with AI insights and bank integration","title":"SpendWisely","type":"apps"},{"content":" Beautiful JSON formatting Safari extension for macOS and iOS. Structly turns raw JSON in your browser into a clean, color-coded, searchable view. A must-have tool for developers.\nFeatures # Syntax highlighting — Color-coded JSON for easy reading\nSearch — Find keys and values instantly with Cmd+F\nJSON Path queries — Filter with jq-like syntax using Cmd+G\nExport — Convert to YAML, CSV, or TypeScript interfaces\nSmart detection — Auto-formats Unix timestamps, URLs, and Base64 strings\nLight \u0026amp; dark themes — Matches your system appearance\nKeyboard shortcuts — Power-user friendly with full keyboard navigation\nCross-platform — Works in Safari on both iOS and macOS\nComing soon to the App Store\n","externalUrl":null,"permalink":"/apps/structly/","section":"Apps","summary":"Beautiful JSON formatting Safari extension for macOS and iOS","title":"Structly","type":"apps"},{"content":"","date":"2020-11-27","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"2020-11-27","externalUrl":null,"permalink":"/tags/cloudkit/","section":"Tags","summary":"","title":"CloudKit","type":"tags"},{"content":"","date":"2020-11-27","externalUrl":null,"permalink":"/categories/coredata/","section":"Categories","summary":"","title":"CoreData","type":"categories"},{"content":"Integrate your codebase with CloudKit is actually a tricky stuff for me since the first impression the CloudKit gave me several years ago is not good.\nIn recent days, I had to cope with this bad feeling (Never mind, CloudKit) in the development of Sideloader.\nCoreData is not an easy-API framework, as we all know. However, CloudKit defeats CoreData. You can imagine how frustrated I felt when I first use CloudKit to synchronize data stored in local CoreData.\nSetup your CoreData with CloudKit # Since Sideloader is an action-extension based app, the CoreData database is saved within the AppGroup shared folder.\n// Function helps you get specific file under appGroup folder. public extension URL { /// Returns a URL for the given app group an /// d database pointing to the sqlite database. static func storeURL(for appGroup: String, databaseName: String) -\u0026gt; URL { guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { fatalError(\u0026#34;Shared file container could not be created.\u0026#34;) } return fileContainer.appendingPathComponent(\u0026#34;\\(databaseName).sqlite\u0026#34;) } } You can use the NSPersistentStoreDescription to tell the CloudKitContainer where the local database is, just like the code below.\nlet container = NSPersistentCloudKitContainer(name: momdName, managedObjectModel: managedObjectModel) let storeURL = URL.storeURL(for: Constants.groupContainerName, databaseName: \u0026#34;Sideloader\u0026#34;) let storeDescription = NSPersistentStoreDescription(url: storeURL) storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: \u0026#34;iCloud.app.chen.ios.Sideloader\u0026#34;) container.persistentStoreDescriptions = [storeDescription] container.viewContext.automaticallyMergesChangesFromParent = true Migration from your local store to Cloud-based store # My complete CoreDataStack codes are shown below:\npublic final class PersistentManager { private init() { // Register the Transformer used in Custom CoreData Models YoutubeVideoMetaInfoTransformer.register() } public static let shared = PersistentManager() // MARK: - Core Data Saving support public let momdName: String = \u0026#34;Sideloader\u0026#34; public lazy var managedObjectModel: NSManagedObjectModel = { // Resource generated by compiler will be \u0026#34;Sideloader.momd\u0026#34; let bundle = Bundle(for: PersistentManager.self) guard let modelURL = bundle.url(forResource: momdName, withExtension: \u0026#34;momd\u0026#34;) else { fatalError(\u0026#34;Error loading model from bundle\u0026#34;) } guard let mom = NSManagedObjectModel(contentsOf: modelURL) else { fatalError(\u0026#34;Error initializing mom from: \\(modelURL)\u0026#34;) } return mom }() lazy var persistentContainer: NSPersistentCloudKitContainer = { // NSPersistenCloudKitContainer is a subclass of NSPersistentContainer, adding the CloudKit capability. let container = NSPersistentCloudKitContainer(name: momdName, managedObjectModel: managedObjectModel) let storeURL = URL.storeURL(for: Constants.groupContainerName, databaseName: \u0026#34;Sideloader\u0026#34;) let storeDescription = NSPersistentStoreDescription(url: storeURL) storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: \u0026#34;iCloud.app.chen.ios.Sideloader\u0026#34;) container.persistentStoreDescriptions = [storeDescription] container.viewContext.automaticallyMergesChangesFromParent = true container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. /* Typical reasons for an error here include: * The parent directory does not exist, cannot be created, or disallows writing. * The persistent store is not accessible, due to permissions or data protection when the device is locked. * The device is out of space. * The store could not be migrated to the current model version. Check the error message to determine what the actual problem was. */ fatalError(\u0026#34;Unresolved error \\(error), \\(error.userInfo)\u0026#34;) } }) return container }() lazy var managedObjectContext: NSManagedObjectContext = { return persistentContainer.viewContext }() // MARK: - Core Data Saving support func saveContext () { let context = managedObjectContext if context.hasChanges { do { try context.save() } catch { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. let nserror = error as NSError fatalError(\u0026#34;Unresolved error \\(nserror), \\(nserror.userInfo)\u0026#34;) } } } } Handle Synchronization # CloudKit can do all the synchronizations silently in the background. If you are debugging with Xcode, you can see lots of CloudKit-related logs in the console view. There you can see every model we create using CoreData will be saved in iCloud as CKRecord.\nThough CloudKit can do almost everything for you automatically, a problem exists which you should take care of.\nThink about a scene our users definitely would encounter. They are using your app on their iPhones, while CloudKit synchronize changes to your local store in the background, say a piece of data on the list has been deleted. When the bad-luck user tap the cell with this piece of data, what would happen? CloudKit documentation tells as below:\nIsolate the Current View from Store Changes\nYes, we do not have to show the users every change CloudKit fetch. Keep current data view from the remote one is really necessary. You should pin a context\ntry? persistentContainer.viewContext.setQueryGenerationFrom(.current) ","date":"2020-11-27","externalUrl":null,"permalink":"/post/coredata-with-cloudkit/","section":"Posts","summary":"Integrate your codebase with CloudKit is actually a tricky stuff for me since the first impression the CloudKit gave me several years ago is not good.\nIn recent days, I had to cope with this bad feeling (Never mind, CloudKit) in the development of Sideloader.\n","title":"CoreData With CloudKit","type":"post"},{"content":"I\u0026rsquo;m Chen, an iOS / macOS developer. I build indie apps, write about Swift and Apple platform development, and occasionally translate great technical articles into Chinese.\nCheck out my apps or browse my blog posts.\n","date":"2020-11-27","externalUrl":null,"permalink":"/","section":"I make stuff","summary":"I’m Chen, an iOS / macOS developer. I build indie apps, write about Swift and Apple platform development, and occasionally translate great technical articles into Chinese.\nCheck out my apps or browse my blog posts.\n","title":"I make stuff","type":"page"},{"content":"","date":"2020-11-27","externalUrl":null,"permalink":"/tags/icloud/","section":"Tags","summary":"","title":"ICloud","type":"tags"},{"content":"","date":"2020-11-27","externalUrl":null,"permalink":"/post/","section":"Posts","summary":"","title":"Posts","type":"post"},{"content":"","date":"2020-11-27","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2020-08-10","externalUrl":null,"permalink":"/tags/bundleid/","section":"Tags","summary":"","title":"BundleID","type":"tags"},{"content":"","date":"2020-08-10","externalUrl":null,"permalink":"/categories/macos/","section":"Categories","summary":"","title":"MacOS","type":"categories"},{"content":"","date":"2020-08-10","externalUrl":null,"permalink":"/tags/plist/","section":"Tags","summary":"","title":"Plist","type":"tags"},{"content":"我们知道无论是 iOS 还是 macOS 上的应用，其配置信息都是通过 plist 文件组织的，该应用的 BundleID 就藏在这个文件中，通过读取 plist 文件就能够知道其 Bundle ID 以及应用版本号等信息。我们以 App Store.app 为例，其 plist 文件就存储在如下路径，其他应用也是如此。\n打开该 plist 文件就能找到该应用的配置信息。如下图所示。\n大家知道 plist 本质上是 XML 格式的文件，我们需要找到键为 CFBundleIdentifier 的值，在上图中是 51 行， 52 行为其值，为 com.apple.AppStore.\n知道了原理，就好办多了，macOS 内置有 plist 的工具 PlistBuddy，我们输入 plist 文件的路径，抽取某个 Key 的值就行。\n详细的 PlistBuddy 描述可以看下 What is PlistBuddy?\n","date":"2020-08-10","externalUrl":null,"permalink":"/post/reveal-bundleid-application/","section":"Posts","summary":"我们知道无论是 iOS 还是 macOS 上的应用，其配置信息都是通过 plist 文件组织的，该应用的 BundleID 就藏在这个文件中，通过读取 plist 文件就能够知道其 Bundle ID 以及应用版本号等信息。我们以 App Store.app 为例，其 plist 文件就存储在如下路径，其他应用也是如此。\n","title":"获取某个 Mac 应用的 BundleID","type":"post"},{"content":"","date":"2019-09-29","externalUrl":null,"permalink":"/tags/alamofire/","section":"Tags","summary":"","title":"Alamofire","type":"tags"},{"content":"今天继续来一篇水文，关于 Alamofire 和 RxSwift 两个组件中扩展后缀 .af 以及 .rx 是怎么实现的。相信用过的 (￣ω￣(￣ω￣〃 (￣ω￣〃)ゝ 都看过了。我这里权当自己的记录流水账了。\n先来看 Alamofire 中关于 af 扩展的实现：\n使用 Alamofire 扩展的方式如下：\nstruct Demo {} extension Demo: AlamofireExtended {} extension AlamofireExtension where ExtendedType == Demo { func printSomething() { print(\u0026#34;Something arrived.\u0026#34;) } } let d = Demo() d.af.printSomething() 这里主要有两点：\n为将要使用的类 Demo 扩展 AlamofireExtended 协议，也就是增加 af 扩展属性，其返回的是一个封装对象 AlamofireExtension； 为 AlamofireExtension 对象（1 中返回的封装类型）扩展专属于 Demo 这个类型的方法； 说实话，这里用来封装元类型信息的结构体名称和扩展协议名称一样，我初次看的时候确实是蒙的。相对于 Alamofire 的命名来说，个人更喜欢 RxSwift 的命名。\n接下来是 RxSwift 中 Rx 扩展的实现：\n可以看到，整个使用方式上是完全相同的。仅仅是各项名字不同。\n所以我们可以总结这种方式的使用路径，如何为我们自己开发的组件扩充命名空间，具体有三个地方：\n声明扩展属性 # 为了能够支持实例变量和类型两者，需要分别增加两个计算属性：\n一个是 static 的，提供给类型使用； 一个是实例变量，提供给单个实例来用； 封装元类型信息 # 为了能够使得 .af/.rx 这些扩展属性也能够获得当前实例或者类型的元信息，不约而同的使用了结构体封装的形式，通过泛型类型实现。我们知道这个封装对象内部就是记录的就是 self\n我们可以通过编译器提示来看下扩展返回的类型是什么。\n从 print 这个方法可以推断出，这个类型是个 AlamofireExtension 对象（因为我们是针对该类型增加的扩展方法），并且其内部有个变量 type 就是我们扩展的类型 Demo。而我们为该对象扩展的方法 printSomething 也能够在实例是 Demo 类型的时候触发。\n扩展目标类 # 我们已经知道扩展后缀拿到的是一个携带了关联类型的对象，因为有了关联类型，所以才能够通过 Where 子句写出针对不同类型存在的专属方法。\n按照这个步骤，我们可以在自己的工程中写属于自己模块的扩展。\nimport Foundation // We implemented a module named Wonderful. // Extension name will be named as wf // Step 0. // Encapsulate a type which is unknown until now. struct Extensible\u0026lt;EncapsulatedType\u0026gt; { let type: EncapsulatedType init(_ type: EncapsulatedType) { self.type = type } } // Step 1. // Extension a type to add two properties. a static var and a var protocol WonderfulExtension { associatedtype ExtendedType // static entry point static var wf: Extensible\u0026lt;ExtendedType\u0026gt;.Type { get set } // instance entry point var wf: Extensible\u0026lt;ExtendedType\u0026gt; { get set } } // Step 2. extension WonderfulExtension { static var wf: Extensible\u0026lt;Self\u0026gt;.Type { set { } get { return Extensible\u0026lt;Self\u0026gt;.self } } var wf: Extensible\u0026lt;Self\u0026gt; { set { } get { return Extensible(self) } } } // Step3. // Extend the type we want to, which the type will have wf var. struct Ordinary {} extension Ordinary: WonderfulExtension {} // Step4. // Restrict methods in the scope of type we want to extend extension Extensible where EncapsulatedType == Ordinary { func wow() { print(\u0026#34;Wow, Wonderful extension.\u0026#34;) } } // Use methods of wf extension let obj = Ordinary() obj.wf.wow() 总体上对于这种形式，带来两个显著的好处：\n明确作用域，告知调用者调用的专属方法属于该组件相关； 提供该扩展的组件自身就更容易被扩展了（打上了烙印），也有了 RxSwift 所衍生的各种组件； 所以，这段代码中涉及的 Swift 类型系统中的几个关键支持：\n关联类型， Associated Type 泛型系统， Generic 扩展的默认实现， Extension Default Implementation ","date":"2019-09-29","externalUrl":null,"permalink":"/post/alamofire-rxswift-af-rx/","section":"Posts","summary":"今天继续来一篇水文，关于 Alamofire 和 RxSwift 两个组件中扩展后缀 .af 以及 .rx 是怎么实现的。相信用过的 (￣ω￣(￣ω￣〃 (￣ω￣〃)ゝ 都看过了。我这里权当自己的记录流水账了。\n","title":"Alamofire 和 RxSwift 中的 .af 以及 .rx 扩展是怎么实现的","type":"post"},{"content":"","date":"2019-09-29","externalUrl":null,"permalink":"/tags/associatedtype/","section":"Tags","summary":"","title":"AssociatedType","type":"tags"},{"content":"","date":"2019-09-29","externalUrl":null,"permalink":"/tags/extension/","section":"Tags","summary":"","title":"Extension","type":"tags"},{"content":"","date":"2019-09-29","externalUrl":null,"permalink":"/tags/generic/","section":"Tags","summary":"","title":"Generic","type":"tags"},{"content":"","date":"2019-09-29","externalUrl":null,"permalink":"/tags/protocol/","section":"Tags","summary":"","title":"Protocol","type":"tags"},{"content":"","date":"2019-09-29","externalUrl":null,"permalink":"/tags/rxswift/","section":"Tags","summary":"","title":"RxSwift","type":"tags"},{"content":"","date":"2019-09-29","externalUrl":null,"permalink":"/categories/swift/","section":"Categories","summary":"","title":"Swift","type":"categories"},{"content":"","date":"2019-09-15","externalUrl":null,"permalink":"/tags/dispatch/","section":"Tags","summary":"","title":"Dispatch","type":"tags"},{"content":"","date":"2019-09-15","externalUrl":null,"permalink":"/tags/inline/","section":"Tags","summary":"","title":"Inline","type":"tags"},{"content":"","date":"2019-09-15","externalUrl":null,"permalink":"/tags/ios/","section":"Tags","summary":"","title":"IOS","type":"tags"},{"content":"","date":"2019-09-15","externalUrl":null,"permalink":"/tags/pwt/","section":"Tags","summary":"","title":"PWT","type":"tags"},{"content":"","date":"2019-09-15","externalUrl":null,"permalink":"/tags/swift/","section":"Tags","summary":"","title":"Swift","type":"tags"},{"content":" 什么是消息派发？ # 消息派发，英文名称 Method Dispatch，是指程序在运行过程中调用某个方法的时候决议使用哪个具体指令的过程。消息派发的行为在我们代码中时时刻刻的在发生。了解消息派发的机制对于我们日常写出相对高效的代码也是有利的，日常 Coding 的时候遇到一些派发相关的问题，也能做到心里有数。\n对于编译型语言来讲，有主要三种类型的方法派发方式：Direct Dispatch，Table Dispatch 以及 Message Dispatch，前者也被称作 Static Dispatch，后两个为 Dynamic Dispatch。\n方法派发类型 # 本质上来讲，派发的目的就是告诉 CPU，在某个特定方法被调用的时候去内存的哪个地址去读执行代码。下面我们先了解下这三种类型的派发形式，这三种可以说各有优劣。\nDirect Dispatch # 直接派发也叫静态派发。它是这三种形式中最快速的，不仅仅是底层翻译的汇编指令少，也是因为编译器可以针对这种情况做更多优化的事情，比如说函数内联等。C 语言就是这种的典型代表，然而，对于日渐复杂的编程场景来说，静态派发限制非常大，我们必须在代码运行之前把事情安排的明明白白的，这个极大的限制了代码书写的灵活性。一句话总结，编译器在编译期间就已经确定了的推断路径，运行期间按照既定路线走就行。\nTable Dispatch # 函数表派发这种是编译型语言中最常见的动态派发实现形式，编译器层面使用一个表格结构来存储类型声明中的每一个函数指针。C++ 语言把这种表格称作虚函数表（virtual table，简称 V-Table），而在 Swift 中，针对拥有继承的 Class 类型来说，依然采用了 V-Table 这种实现形式达到对多态的支持，而针对值类型由于 Protocol 产生的多态而采用了另外一种函数表派发形式，这个表格被称为协议目击表 （Protocol Witness Table，简称 PWT），这个暂时略去不表。\nV-Table # 对于 V-Table 的应用场景下，每一个类都会维护一个函数表，里面记录着该类所有的函数指针，主要包含：\n由父类继承而来的方法执行地址； 如果子类覆写了父类方法的话，表格里面就会保存被重载之后的函数。 子类新添加的函数会插入到这个表格的末尾 在程序运行期间会根据这个表去查询真正要调用的函数是哪一个。这种特性就极大的提升了代码的灵活性，也是 Java，C++ 等语言得以支持多态这种语言特性的基石。当然，相对于静态派发而言，表格派发则多了很多的隐式花销，因为函数调用不再是直接调用了，而是需要通过先行查表的形式找到真实的函数指针来执行，编译器也无法再进行诸如 inline 等编译期优化了。\n我们以 WWDC 的某个例子来大概说明下 V-Table 的具体派发过程，如下代码，共有三个 Class，其中 Drawable 是基类，子类化的 Point 和 Line。\n在 drawables 真正调用的时候（大家注意下，数组的这个结构中除了 refCount 做引用计数之外，每个元素都是 1 个字长的引用，也就是每一个具体实例的地址），先通过指针找到在堆上的实例，通过实例中存储的类型信息中的虚函数表（V-Table）找到具体的函数执行地址。\n每一个类的元类型信息中存储着虚函数表，表中就是所有该类型真实的函数地址。\nPWT # 对于 Swift 来说，还有更为重要的 Protocol，对于符合同一协议的对象本身是不一定拥有继承关系的，因此 V-Table 就没法使用了。这里，Swift 使用了 Protocol Witness Table 的数据结构达到动态查询协议方法的目的。如果将上面的例子中的 Drawable 抽象成协议。\n简单来说，Swift 会为每一个实现了该协议的对象生成一个大小一致的结构体，这个结构体被称为 Existential Container，它内部就包含了 PWT，而这个 Table 中的每一个条目指向了符合该协议的类型信息，而除了 PWT，该结构体中还保留了三个字长的 valueBuffer 用以存储数据成员，一个 Value Witness Table 存储着一组函数的执行地址，这些函数都是针对前面数据成员的具体操作方法，细节这里不展开讲了。 PWT 中包含着该实例实现的协议方法实现地址。\n所以，本质上来讲，Protocol 的消息派发要比 V-Table 更加复杂，但是还是基于这种表格查询的形式找到真正需要执行的方法地址。\nMessage Dispatch # 第三种就是消息派发类型，作为一个使用 Objective-C 这么多年的老同志，想必对 sendMessage 不能更熟悉了，消息派发是目前最为动态的一种方式，这个也是整个 Cocoa 架的基础。像平时我们常用的 KVO ，Target-Action 等都建立在消息派发的基础之上，这也才有了 Objective-C 中常炒不衰的黑魔法 ── method swizzling ，你可以用这个调换函数执行地址。\n关于基于 OC 层面运行时库的核心代码估计大家都已经看过。运行时通过查找该类的方法列表，同时通过 super class 回溯一直查找到该方法即可，这部份核心内容是 objc runtime。而我们知道 Objective-C 运行时的核心方法是 obj_msgSend，其会在类的继承链查找所有可能的方法。整个运行时消息的转发过程再发一次。\n大家体会一下消息派发模式和之前的表格派发的区别，表格派发查询的表是固定的，子类也会将父类的可见方法继承过来（这也相对安全），而消息派发还可以动态的回溯继承链，子类无需复制父类方法，而是通过 superClass 指针遍历完整个继承链的方法。\n介绍完这三种方法派发形式之后，大家有了一个概念之后，我们看下几个主流语言目前是什么情况。\n大家可能会说 Objective-C 不是能和 C++ 混编么？那它也支持 Table Dispatch 吧，这个是误解。 Objc 的历史地位和 C++ 是类似的，都是在 C 语言的基础之上的一门语言，而天然 Objective-C 就是采用的 Message Dispatch 形式，但是因为是基于 C 语言的，包括它的核心运行时都是使用 C 语言构建的，因而能很好的兼容 C，也自然支持 C 的 Direct Dispatch。 而 C++ 语言能够和 Objective-C 混编也算是一厢情愿吧，想想被 Objective-C++ 支配的恐惧。\nSIL # 在讲解 Swift 的方法派发之前，我们先了解一下 SIL，SIL 全程是 Swift Intermediate Language，它是为 Swift 编程语言而设计的高级中间语言，它工作在编译器前端，其作为 AST 和 LLVM IR 的中间过程的主要产物，主要针对 Swift 语言做语义分析，代码优化，泛型特化等事情。从 SIL 的生成文件我们能够一窥方法派发的一些门道。\n下图是一张 WWDC 上讲述 Swift 语言的编译前端的几个阶段。\n可以使用 swiftc 生成某个 Swift 文件对应的 SIL 文件。\nswiftc -emit-sil test.swift \u0026gt; test.sil 比如针对下面这份文件会生成对应的 SIL 如下。\n生成的 SIL 文件如下：\nSIL 几个方法 # 因为我们在讲述方法派发，因此我们关心几个相关的 SIL 语法，主要有如下四个：\nclass_method # 关于 class_method，我们可以在VTables 这一 Section 找到说明，证明其表示当前 Swift 语义指令使用 V-Table 进行动态派发。\nSIL represents dynamic dispatch for class methods using the class_method, super_method, objc_method, and objc_super_method instructions.\nThe potential destinations for class_method and super_method are tracked in sil_vtable declarations for every class type.\nobjc_method # 这个字面很明显，使用 Objective-C 的运行时进行派发。在 Github 主页也有说明\nPerforms Objective-C method dispatch using objc_msgSend().\nwitness_method # 这个方法的意思表示的语义是在协议目击表（Protocol Witness Table）中查询方法。\nfunction_ref # 代表一个函数的引用。\napply # 可以将 apply 理解为调用某个函数。\n如果想了解完整的其他详细参数，可以到 Swift 的 Github 页面查看。\nOK，了解完 SIL 之后，进入正题。\nSwift 中的方法派发是什么样子的 # 首先一句话， Swift 中支持以上三种派发方式，首先，因为其背负的历史负担，必须支持 Objective-C Runtime，因此一定会支持消息派发方式，又由于值类型和传统类类型的存在，Swift 支持了以上三种。\n首先我们需要先知道影响目前 Swift 中方法派发形式的几个要素：\n声明方法的地方 修饰符修改 编译器的可见优化 声明位置 # 首先，不同位置的方法声明，派发的时候各不相同，先看个例子，我们为类和其扩展分别定义了两个方法。\n针对上面这段代码，实际上 MyClass 使用的是函数表派发，而其分类中的方法是使用的静态派发。\n值类型毫无疑问进行直接派发； 存在继承关系可能的类的初始声明下的方法采用虚函数表派发（V-Table）； 对协议的扩展或者类的扩展进采用直接派发（非 NSObject 下的 extension 的方法均为直接派发）; 协议的初始声明下的方法均采用协议目击表派发（PWT） 案例 # 这其中有个问题是日常开发过程中也经常会遇到的，如下代码。\n我们在某个类型的扩展中定义了方法，协议扩展中也定义了同名方法，在进行调用的时候，因为声明类型的不同，表现完全不一样，通过调用的结果就能知道是静态派发。\n而当我们将该同名方法声明在 MyProtocol 中的时候，这也变成了原始的协议表格派发形式了，通过 PWT 来查找该协议方法的具体实现。\n协议扩展是严格的静态派发的，因为没有虚函数表可以把方法的实现地址放进去。扩展能够实现协议的默认实现，那是因为符合这个协议的类型会把扩展的实现方法存到自己的协议目击表里（PWT），而且只有在这些类型没有实现这些协议方法的时候。\n因为协议目击表中包含定义在 Protocol 中的方法而已，因此协议扩展方法（不应该算作 Protocol 的一部分）并不知道把这些实现置于何处。因此调用协议扩展方法是不经过虚函数表派发的，它们唯一能做的事情就是静态调用扩展中的方法实现。而且因为是静态派发，也就不存在重载这一支持。\n唯一能够让协议扩展方法进行虚函数表派发的方式就是为 Extension 增加虚函数表，但是在编译期间，符合该协议的类型并没有必要获知该协议所有扩展中的方法，因此没办法说将所有扩展的方法均添加到自身的 PWT 中。\n当然除非将所有协议派发均改为动态派发，但是 Swift 团队的人并不希望出现这种情况。\n[Protocol extensions] are strictly static typing, all the way. Which makes sense, because there’s no virtual function table (or in Swift terms, protocol witness table) to put the methods into. Extensions can provide default implementations of protocol methods because the type that conforms to the protocol puts the extension method implementation into its own protocol witness table (and they only do this if the type doesn’t already implement the method itself). Since the protocol witness table only contains things defined in the protocol itself, protocol extension methods that aren’t part of the protocol don’t get put anywhere. So invoking one of those methods has no possible virtual function table to check, the only thing it can do is statically invoke the method from the extension. And this is why you can’t override them (or rather, why your override isn’t called if the method resolution is done via the protocol). The only way to make protocol extension methods work via virtual dispatch is to define a new protocol witness table for the extension itself, but types that were defined without being able to see the extension won’t know to create and populate this protocol witness table. […] The only other solution that comes to mind is turning all protocol dispatch into dynamic dispatch, which I hope you’ll agree is not a good idea.\n针对这个情况，大家记住一个规则就行：\n如果一个变量的类型被推断为协议 Protocol，那么依据是否在 Protocol 中声明了该方法来做决策，如果声明了，就调用类型自身的实现（无论协议扩展中有无默认实现），如果未声明，一定是调用的扩展中的实现； 如果一个变量的类型被推断为实际的类型，那么一定执行的是该类型内部的基于实现； 显式指定派发方式 # Swift 语言自身提供了一些修饰符，能够对方法的派发方式做变更，主要有如下几种。\n大家都知道，Swift 语言在生成编译器前端这一侧会先生成 SIL，让编译器进行一些优化，这个阶段我们能看到具体编译器做了哪些优化。 我们先看下\n在文件末尾有 V-Table 信息，如下所示：\n默认的类方法出现，意味着默认类的方法采用表格派发； 标记了 final 的方法没有出现在虚函数表中； @objc 标记的方法和默认的方法相同； dynamic 标记的方法没有出现在虚函数表中； 那我们那几句执行代码如今都什么样子，我们一个一个来看下。\ndefault class method # MyClass 的默认方法的 SIL 内容如下：\nfinal # final 关键字能够确保方法进行静态派发，添加了该关键字的方法也会对 Objective-C 运行时隐藏该方法，使得 OC 运行时不会为该方法生成 selector.\n在上面 SIL 文件里找到 final 标记的方法：\n这里可以看到，function_ref 表明直接拿到函数指针，将实例对象传递给该方法，apply 就是执行的意思。\ndynamic # dynamic 是确保该方法能够进行消息派发，也就是说该确保该方法是通过 obj_msgSend() 来调用的。在 Swift 4 之前，dynamic 关键字是和 @objc 关键字一起出现的，到了Swift4 之后，官方将这两个关键字的功能拆开了。@objc 只确保 Objective-C 可见。\n我们看到 MyClass 的虚函数表中，使用 dynamic 修饰的方法 performDynamicOperation 并不在其中，这也证明了该方法的派发已经不是 Table Dispatch 了。\n而我们再看下实际生成的调用处的代码： 看到这里有 objc_method ，我们实际上是将实例对象直接传递给 objc_method 来执行了。从这里首先我们能知道 dynamic 修饰的方法不是直接派发，也不是表格派发，是将该实例对象转换成 Objc 运行时的代码进行执行。\n@objc # 这两个关键字是来确保方法是否能够被 Objective-C 运行时看到。@objc 这个经常是为了暴露 Swift 方法给 Objc 来使用。一定要知道 @objc 并不是改变派发方式，在 SIL 文件中我们看到 performOcOperation 和默认类方法 performOperation 一样均在 V-Table 中出现。\n从生成的 SIL 来看，使用 @objc 标记的方法和默认的方法完全一致，都是进行表格派发。\n关于内联 # 内联是自从 C 语言开始，编译器针对函数调用的一种优化措施。一旦编译器判定某个函数能够进行内联优化，则会将一次函数调用直接在当前调用处展开，就如同不存在函数本身，而直接将函数中的代码插入到当前调用处。我们拿个例子来看。\n编译器如果判定该方法进行内联的话，在编译期就会将代码转换成如下：\n因为一次函数调用的代价是存在的，虽然一次函数调用的代价受到多种因素影响，但是有几个关键的因素，比如说函数调用栈的开辟，间接指针跳转以及分配寄存器。\n当然，编译器是需要很多多种情况衡量这个方法是否适合进行内联优化的，具体的因素在下面的参考文献里已经列举出来了，我就不详细说了，感兴趣的同学可以看下这篇文章了解下，我这里大致列举了几种：\n函数体中有循环； 函数体过长（想象一下，要是都展开了，无形中增加了包体积，重复代码）； 递归函数； 内联也不是都是好处，比如说增大二进制体积等。所以针对内联与否，编译器是有一套完整的考量标准的。当然，所以 Swift 而言，有内联参数来主动干预， @inline(__always) 以及 @inline(never).\nSwift 支持内联，该参数告知编译器进行派发优化，使用直接派发（静态派发）的形式。当然具体能不能做成内联函数还是要看编译器怎么抉择了。\nOK，通过修饰符来主动修改派发方式大体是这样，\n可见性带来的优化 # 通过前面的介绍，我们知道如果能够给编译器传递很多的信息，编译器就能按照既定规则来帮助开发者做一定的优化，如何给予编译器很多信息呢？那就是合适的控制 Visibility 了。对于 Swift 而言，从 2.0 开始提供了 Whole Module Optimizations 开关，很大程度上提升了 Swift 的整体性能。\n我们摘录 Swift Blog 中的一个例子来说明这个参数的意义，假如我们的 Module 中只有utils.swift 和 main.swift 两个文件，如下所示:\n和以往大家对编译文件的认识一样，编译器是针对独立的每一个源文件进行编译，优化等一系列操作。当编译器优化当前文件的时候，只会考虑当前的文件，比如 main.swift 文件，编译器只知道有个函数为 getElement，但是不知道其具体实现。而在编译器准备优化 utils.swift 文件时，因为它并不知道具体类型 T 是什么，这个时候它就没法做泛型参数具体化，只能生成一个泛型版本的函数实现，然后会在运行时阶段动态的根据传递具体的类型来确定返回什么。\n再举个例子，我们前面说内联，内联优化的前提是编译器能够解析当前函数体来确定是否进行内联，而如果这种分离优化的情况下，内联优化也是做不到的，比如你把函数定义在其他独立的文件，当前调用该函数的文件就没法做内联展开。这一切都是因为编译器仅关心当前文件而无法获知其他文件信息而导致的。而开启了 WMO ，理论很简单就是使得编译器全局考虑整个模块，显式的让编译器在编译阶段全局考察当前 Module 内部所有的 Swift 文件。\n所以上面的例子，开启了 WMO 之后，编译器就会针对这两个文件做一些优化工作，比如 utils.swift 中定义的方法就会被直接特化，而使用该方法的 main.swift 的方法中的使用也会进行内敛优化，如下所示。\n那这个对类型派发的意义在哪里呢？ 首先通过上面的例子中的内联展开就已经体现出了优化策略。将原本相对高代价的动态派发转换成了静态派发。\n我们再以官方 WWDC 的 Demo 为例了解下这个优化点，加深一下影响。\n默认 Dog 的 noise 方法，在独立编译 Dog.swift 文件的时候，编译器是无法保证其不被其他类继承的，因此它只能默认其是需要进行重载的形式进行 V-Table 派发。而一旦开启了 WMO，如上图的例子，Dog 自身作为 Internal 已经没有其他 Module 继承的可能性，而纵观整个 Module ，编译器发现如果没有任何继承自 Dog 的类，那么它就被优化为直接派发，省略一次间接跳转的代价。\n之前因为单个文件做优化，编译器并不知道 Dog 的方法有没有被重载而不得不将 noise 方法存入虚函数表中做 Table Dispatch，而现在通过全局掌控，编译器明确知道作为 Internal 控制的 Dog 在当前 Module 内并无任何子类出现，此时其 noise 方法的调用就会被直接更改为 Direct Dispatch。\n因此通过开启 Whole Module Optimizations 开关，使得 Swift 编译器能够获取很多信息，包括类型的继承关系以及 Access Control 的情况来进行优化。\n总结 # 通过上面的讲解，我们可以列出目前所有的方法派发情况：\n虽然现在编译器优化方面做的已经足够优秀了，但是对于代码开发者的我们还是要比编译器更能应付业务逻辑的复杂代码，而这些复杂代码导致可以优化的地方被编译器忽略。因此，还是需要 Coder 自己能够主观上有认识到这些可以优化的一些点，从小处着手，写出更加高效的代码。\nReferences # Virtual method table Proposal: Universal dynamic dispatch for method calls Increasing Performance by Reducing Dynamic Dispatch - Swift Blog @objc and dynamic - Swift Unboxed Whole-Module Optimization in Swift 3 Inline Functions in C++ - GeeksforGeeks The Cost - A Function Call Swift Intermediate Language ","date":"2019-09-15","externalUrl":null,"permalink":"/post/messagedispatchinswift/","section":"Posts","summary":" 什么是消息派发？ # 消息派发，英文名称 Method Dispatch，是指程序在运行过程中调用某个方法的时候决议使用哪个具体指令的过程。消息派发的行为在我们代码中时时刻刻的在发生。了解消息派发的机制对于我们日常写出相对高效的代码也是有利的，日常 Coding 的时候遇到一些派发相关的问题，也能做到心里有数。\n","title":"Swift 中的消息派发","type":"post"},{"content":"Protocol 作为 Swift 生态的最重要的组成部分（没有之一），其搭建起了整个语言生态的各个组成部分。\n相对于继承而言，Protocol 的几个比较直观的优势：\n无需必须去强制继承某个类； 通过协议可以改造已经存在的类型； 适用范围更大一些，使得 struct 和 enum 这些值类型也能够继承能力 Swift 语言本身是不支持多继承的，你需要花费精力去纠结继承哪个 class，一旦出现不同的能力分散在不同父类的情况，就更加纠结了，你甚至需要去修改原有的父类； 继承的话，还需要考虑 override 父类方法的问题，比如调用 super 方法的时机问题； 这篇文章想记录下 Protocol Extension 中的一点区别。\n我们知道 Swift Protocol 中是没有 optional 关键字的，因此只要是在 Protocol 定义的方法，即为 Protocol Requirements，符合该协议的类型就必须实现，否则是不完整的。比如，我们自己实现了 TableView，定义了这么多 delegate 协议方法，要求调用方全都提供，就太不人道了😢\n因此，我们会通过对 Protocol 进行扩展，给该协议方法增加缺省实现，剩余的方法则是你需要调用方必须实现的方法。\n如果针对某个协议 P，我们扩展 P 给其增加某个协议方法有两种形式：\n直接在 Protocol 定义里定义新方法，该方法成为实现该协议必须实现的方法； 为 Protocol 增加扩展方法，该方法并不一定要求符合该协议的类型进行实现； 这两者有什么区别呢？ 我们看代码\n发现 s 调用的是我们为 S 结构体增加的扩展方法，而 s2 调用的是 P 协议的扩展方法，尽管实体对象的动态类型是 S，但是因为声明的静态类型不一样，调用结果也不同。\n对于 1 中的方法，运行时进行动态派发，也就是真实根据其具体类型来做决议，2 中的方法是进行静态派发，也就是说你声明的该对象是什么类型，就调用该类型的方法。 以上述的代码为例，s1 的静态类型由编译器推导为 S，而 s2 的静态类型声明为 P ，因此二者在调用扩展协议方法时候的表现不一致。\n如果想确保调用方法时候不因为静态类型不同而导致不同的结果，可以将协议扩展方法在 P 协议定义的地方加入，自此所有符合该协议的类型就必须强制实现该协议，此时该方法则是通过动态决议来派发了。\n而我们了解完这些之后，对上面 TableView 的栗子就有了更深刻的理解，Protocol 中的方法定义使得所有符合该协议的类型在运行时进行决议（也就是对象的动态类型），而这个过程中如果发现该对象已经实现了。\n","date":"2019-08-08","externalUrl":null,"permalink":"/post/protocol-extension/","section":"Posts","summary":"Protocol 作为 Swift 生态的最重要的组成部分（没有之一），其搭建起了整个语言生态的各个组成部分。\n","title":"Protocol Extension","type":"post"},{"content":"","date":"2019-07-04","externalUrl":null,"permalink":"/tags/launchddaemon/","section":"Tags","summary":"","title":"LaunchdDaemon","type":"tags"},{"content":"","date":"2019-07-04","externalUrl":null,"permalink":"/tags/letsmove/","section":"Tags","summary":"","title":"LetsMove","type":"tags"},{"content":"当我们从网络上下载某个 mac app 之后，一般都会默认在 Downloads 目录里，但是某一些 App 是需要放置到 Application 目录才行，最常见的就是自更新功能是受到当前所处目录的限制的，这时候很多下载到本地的 .app 文件打开之后都会弹出提示框，让用户选择是否移动到 Applications 目录，LetsMove 就是大部分 mac app 参考或者集成的开源库，封装了这套逻辑。\n这个库已经好多年了，最近的一次 Commit 是 2017年，其中有几个点可以学习借鉴。\n大致思路 # 虽然叫 Move，实际上是先行 Copy 一份当前的 App Bundle 到 Application 目录，然后杀掉自己并删除源目录的 App 文件。\nMove to Applications Folder 确认之后 复制当前 App 文件到 /Applications 目录中 删除当前已经打开 App （Mac 上是可行的，不同于 Windows，不存在占用文件句柄的情况） 执行子进程执行重启过程，具体后文会讲。 权限获取（提权） # 检查文件路径是否可以写入的方法是 NSFileManager 的 isWritableFileAtPath ，如果返回 false，LetsMove 会主动进行权限获取。\n获取权限所使用的接口是 AuthorizationExecuteWithPrivileges，该接口实际在 OSX 10.7 Lion 之后就已经标记废弃了，但是一直到目前的 macOS 10.14 还一直可以使用。\n而作者为了避免苹果直接移除该代码导致提权接口不可用，使用了动态查找方法的形式，利用 dlsym 查找方法指针来获取，从而避免如果完全移除该方法导致功能不稳定的情况发生。\n通过该方法获取了 rm 和 cp 命令在指定目录的权限，这个过程中用户会收到输入 Admin Password 的提示的。\n当然现有的提权官方有建议使用一个 helper 程序来负责或者使用 Service Management framework 来做，这两者在做 App 自启动的时候已经了解过了，确实不如这一个方法来的简单。有兴趣可以去看下 LaunchdDaemon 这个东东。\nUse a launchd-launched helper tool and/or the Service Mangement framework for this functionality.\nRelaunch # Relaunch 方法是将应用移动到 /Applications 目录后的后续动作。\n其中 NSTask 重启进程的 Shell 脚本如下：\n该方法涉及两个主要职能：\n将拷贝到 /Applications 目录下的 App 文件增加扩展属性 执行 Shell 脚本，杀掉当前父进程，并重启刚刚 Copy 到 /Applications 目录下的 App 文件 关于第一点，使用 kill 命令来杀掉当前主进程，但是使用的是 kill -0，不是任何有效的 signal，关于该命令只是查询当前你是否有权限来做 kill 这件事情，这一块也没看明白该方法的意义在哪里，知道的同学可以告知下；\n关于第二点，用到了 xattr 命令，该命令是用来针对文件的扩展属性做操作的。上方的命令就是为当前已经移动到 /Applications 目录下的 App 文件去除 com.apple.quarantine 属性。不知道大家记得不，从某些网站下完 App 之后，比如 Github Desktop 客户端，打开会提示如下窗口：\n这个是 macOS 自身的安全校验提示，当然一般从 web 下载的 App 本身都会被加上 com.apple.quarantine 的扩展属性，这里是移除了该属性，防止执行了 open 之后弹出该提示叨扰用户。\n当然，这一切都是在非沙盒应用中才能够实现，关键原因在于沙盒环境下授权相关的 API 都是受到严格限制的。如果想针对沙盒应用做，一般会制作成 dmg 文件格式来供用户显式操作。\nReferences # What does kill -0 do? Extended file attributes - Wikipedia Doesn\u0026rsquo;t work with sandboxed apps ","date":"2019-07-04","externalUrl":null,"permalink":"/post/letsmove/","section":"Posts","summary":"当我们从网络上下载某个 mac app 之后，一般都会默认在 Downloads 目录里，但是某一些 App 是需要放置到 Application 目录才行，最常见的就是自更新功能是受到当前所处目录的限制的，这时候很多下载到本地的 .app 文件打开之后都会弹出提示框，让用户选择是否移动到 Applications 目录，LetsMove 就是大部分 mac app 参考或者集成的开源库，封装了这套逻辑。\n","title":"LetsMove 中的几个点","type":"post"},{"content":"","date":"2019-07-04","externalUrl":null,"permalink":"/tags/shell/","section":"Tags","summary":"","title":"Shell","type":"tags"},{"content":"","date":"2019-07-04","externalUrl":null,"permalink":"/tags/sourcecode/","section":"Tags","summary":"","title":"SourceCode","type":"tags"},{"content":"","date":"2019-07-04","externalUrl":null,"permalink":"/tags/xpc/","section":"Tags","summary":"","title":"XPC","type":"tags"},{"content":" 关于 XPC # 因为最近在做的工作涉及到这一块，就大致的记录一下，针对 XPC 技术也做个总结。\nXPC 是 macOS 上一种进程间通信的技术统称，其使得我们可以将 Mac 应用的功能模块拆分成不同的多进程模式，关于进程之间的通信也被系统封装完整了，你只需要按照既定的模式做即可。其带来的最大的好处是\n提高 App 自身的稳定性 进行权限隔离 拿我目前在做的工作来讲，确实有针对以上这两点的需求。上层业务通过执行脚本命令获取执行结果，执行脚本命令使用的就是 NSTask，大概结构如下：\n对于执行 NSTask 的这层命令行执行模块来讲，拆分到独立的进程中使得安全性得以提高，即使 XPC 进程挂掉，一般情况下再次向 XPC 通信，系统会自行恢复启用新的 XPC 进程，而主应用无感知。\nXPC 架构 # 在 Apple 官方文档中有如下图，很清楚的展示了整个 NSXPC 的架构。\n主应用和 XPC Service 之间是通过 NSXPCConnection 对象保持通信的，其通信接口由 Protocol 定义，然后暴露给外界一个符合了该协议的对象。\n所有的 XPC Service 本身是由系统级别程序 launchd 来管理和维护的。\n开发过程 # 整体上主应用和 XPC 通信是标准的 Client-Server 模式。客户端询问 Server 端数据，Server 端应答即可。这之间的数据序列化完全交由系统管理。\n具体步骤 # 新建 XPC Service 的 Target # 这里要说一点，Xcode 提供的 XPC Service 创建的时候是没有语言选项的，默认都是创建的 Objective-C 的样板文件，但是你是可以把所有的代码文件删除，然后建立 main.swift 来写的，一样能够开发。\n建立 Listener，监听连接 # 实际上，当你建立完毕 Targe 之后，系统默认生成的文件里已经将对应的代码大致过程已经通过注释和代码列出来了。我将这部分逻辑在 Swift 文件里也写了出来，如下：\n每个 Service 会启动 NSConnectionListener，其代理方法是 NSXPCListenerDelegate，一旦主应用发起连接，该代理方法就会启动来做连接前的准备，而代理方法中的主要逻辑就三点：\n告诉 Connection 对象，Service 暴露的接口（协议）； 传递给 Connection 对象一个符合了前面协议的对象； 启动连接； 返回是否允许本次连接建立； 其中 ServiceDemo 就是具体实现接口的对象。\n主应用发起连接 # 一般会在 App 启动之后启动连接，具体步骤如下，\n创建 NSXPCConnection 对象，指定连接哪个 Service 指定使用哪个协议接口（Protocol 的 Membership 需要主应用和 XPC Service 的Targe 共享） resume 但是此时 Service 里的监听实际上还未执行，直到你发起第一次 Request。向 XPC Service 发起动作的具体就是从刚刚创建的 NSXPCConnection 中获取对应的接口以及执行该接口来和 Service 交互。\n整体过程如 Apple 文档里的图示过程，\n需要注意的几点 # NSXPCInterface 定义的方法回调只能有一个 Block，如果多于一个就会报异常。 XPCDemo[46250:3466789] NSXPCInterface: Only one reply block is allowed per selector (XPCDemo.DragonServiceProtocol / fireWithTimes:withSuccess:withFailure:) 关于一次 Request 通信多次 Response 类似于下载任务要获取下载进度的问题，在迁移 NSTask 执行层的过程中，面临的一个需求是针对 XPCService 端 NSTask 执行过程中的持续性的输出都要能够回调给 Client 端，而 Reply 回调只能响应一次。\n我们在主应用端希望能够通过一个 Block 不断获取当前最新的数据，可是，不幸的是，XPC 并不支持如此。比如，我们希望能够通过回调实时获取状态。\n具体可以参考 https://forums.developer.apple.com/thread/35731 论坛中 Apple 人员的回复，总结起来两点：\nClient 以及 Service 自行维护状态，将一次请求多次回调，拆分为多次请求多次回调即可； Client 转移要交付的第三方给 Service 直接交互（一般比如 API ） References # Creating XPC Services XPC · objc.io XPC\u0026hellip;Can\u0026rsquo;t call a reply block multiple times? |Apple Developer Forums ","date":"2019-07-04","externalUrl":null,"permalink":"/post/xpcservice/","section":"Posts","summary":"关于 XPC # 因为最近在做的工作涉及到这一块，就大致的记录一下，针对 XPC 技术也做个总结。\n","title":"XPC Services","type":"post"},{"content":"","date":"2019-06-10","externalUrl":null,"permalink":"/tags/codable/","section":"Tags","summary":"","title":"Codable","type":"tags"},{"content":"During my learning of Swift, many interesting things I will find. Codable is one of them.\nToday, I defined a model with a tuple type, then Xcode told me some error. Codes may like as below.\nYou may want Xcode automatically complete all the codable stuff. However, life is hard. Codes like these can even not be compiled. Xcode will tell you name cannot be synthesize the Person because of the FullName.\nerror: default.playground:5:8: error: type \u0026#39;Person\u0026#39; does not conform to protocol \u0026#39;Decodable\u0026#39; struct Person: Codable { ^ Swift.Decodable:2:12: note: protocol requires initializer \u0026#39;init(from:)\u0026#39; with type \u0026#39;Decodable\u0026#39; public init(from decoder: Decoder) throws ^ default.playground:6:9: note: cannot automatically synthesize \u0026#39;Decodable\u0026#39; because \u0026#39;FullName\u0026#39; (aka \u0026#39;(firstName: String, secondName: String)\u0026#39;) does not conform to \u0026#39;Decodable\u0026#39; var name: FullName ^ error: default.playground:5:8: error: type \u0026#39;Person\u0026#39; does not conform to protocol \u0026#39;Encodable\u0026#39; struct Person: Codable { ^ Swift.Encodable:2:17: note: protocol requires function \u0026#39;encode(to:)\u0026#39; with type \u0026#39;Encodable\u0026#39; public func encode(to encoder: Encoder) throws ^ default.playground:6:9: note: cannot automatically synthesize \u0026#39;Encodable\u0026#39; because \u0026#39;FullName\u0026#39; (aka \u0026#39;(firstName: String, secondName: String)\u0026#39;) does not conform to \u0026#39;Encodable\u0026#39; var name: FullName ^ So, We know that if a struct or class is codable implicitly, it must not contain properties which are not codable. Tuple is one of them.\nSome people argue that why tuple cannot be designed as a codable type? Yeah, the hope is that tuples could conform to protocols in future. This is covered in the generics manifesto as \u0026ldquo;Extensions of Structural Types 28\u0026rdquo;.\nExtensions of structural types Currently, only nominal types (classes, structs, enums, protocols) can be extended. One could imagine extending structural types\u0026ndash;particularly tuple types\u0026ndash;to allow them to, e.g., conform to protocols. For example, pulling together variadic generics, parameterized extensions, and conditional conformances, one could express \u0026ldquo;a tuple type is Equatable if all of its element types are Equatable\u0026rdquo;:\nextension\u0026lt;...Elements : Equatable\u0026gt; (Elements...) : Equatable { // extending the tuple type \u0026#34;(Elements...)\u0026#34; to be Equatable } There are some natural bounds here: one would need to have actual structural types. One would not be able to extend every type:\nextension\u0026lt;T\u0026gt; T { // error: neither a structural nor a nominal type } And before you think you\u0026rsquo;re cleverly making it possible to have a conditional conformance that makes every type T that conforms to protocol P also conform to protocol Q, see the section \u0026ldquo;Conditional conformances via protocol extensions\u0026rdquo;, below:\nextension\u0026lt;T : P\u0026gt; T : Q { // error: neither a structural nor a nominal type } So for now, you have to synthesize the tuple type by yourself.\nReferences # Codable Tuples Swift GenericsManifesto.md ","date":"2019-06-10","externalUrl":null,"permalink":"/post/tuple-codable/","section":"Posts","summary":"During my learning of Swift, many interesting things I will find. Codable is one of them.\nToday, I defined a model with a tuple type, then Xcode told me some error. Codes may like as below.\nYou may want Xcode automatically complete all the codable stuff. However, life is hard. Codes like these can even not be compiled. Xcode will tell you name cannot be synthesize the Person because of the FullName.\n","title":"Codable \u0026\u0026 Tuple","type":"post"},{"content":"","date":"2019-06-10","externalUrl":null,"permalink":"/tags/tuple/","section":"Tags","summary":"","title":"Tuple","type":"tags"},{"content":"In the last days, I created all the ViewControllers through the storyboard or the Nib (for views). Today, when I created a demo project without any Nib file, the ViewController did not show as I expected.\nWTF, Can you believe that? I cannot even create a ViewController now.\nThere exist some differences between iOS and macOS.\niOS # In the previous development on the iOS platform, create a ViewController, specifically UIViewController instance, is dead simple as below.\nimport UIKit class ViewController: UIViewController {} let vc = ViewController() print(\u0026#34;vc.view.frame = \\(vc.view.frame)\u0026#34;) You can create a UIViewController without a nib file, and customize the view\u0026rsquo;s properties as you like. Nothing unexpected happen and ViewController has a default view.\nmacOS (Cocoa) # However, when you want to create a NSViewController in the same way, something wrong occur.\nimport Cocoa class ViewController: NSViewController {} let viewController = ViewController() print(\u0026#34;vc.view.frame = \\(vc.view.frame)\u0026#34;) It looks like the same as what we do for UIViewController. And it should work like UIViewController.\nHowever, an exception is thrown, which shows that \u0026lsquo;could not load the nibName\u0026rsquo;. Why?\n[General] -[NSNib _initWithNibNamed:bundle:options:] could not load the nibName: Demo.ViewController in bundle (null). So, It looks like that NSViewController will not create a default rootView for us. We must create one by ourselves. The key is the loadView. Just override the loadView() method, then create a NSView instance.\nimport Cocoa class ViewController: NSViewController { override func loadView() { self.view = NSView() } } That\u0026rsquo;s it.\nNow, I have found that macOS development has many differences with iOS. You may encounter many fundamental pitfalls, and when you indeed stumble, be patient. To read the Apple\u0026rsquo;s documentations is you top priority, then Google please.\nReferences # Instantiate a UIViewController without Nib Instantiate a NSViewController without Nib ","date":"2019-06-03","externalUrl":null,"permalink":"/post/create-nsviewcontroller-without-nib/","section":"Posts","summary":"In the last days, I created all the ViewControllers through the storyboard or the Nib (for views). Today, when I created a demo project without any Nib file, the ViewController did not show as I expected.\nWTF, Can you believe that? I cannot even create a ViewController now.\n","title":"How to generate a NSViewController without a Nib?","type":"post"},{"content":"","date":"2019-06-03","externalUrl":null,"permalink":"/tags/nsviewcontroller/","section":"Tags","summary":"","title":"NSViewController","type":"tags"},{"content":"","date":"2019-06-03","externalUrl":null,"permalink":"/tags/uiviewcontroller/","section":"Tags","summary":"","title":"UIViewController","type":"tags"},{"content":"","date":"2019-05-14","externalUrl":null,"permalink":"/tags/metatype/","section":"Tags","summary":"","title":"MetaType","type":"tags"},{"content":"","date":"2019-05-14","externalUrl":null,"permalink":"/tags/overload/","section":"Tags","summary":"","title":"Overload","type":"tags"},{"content":"","date":"2019-05-14","externalUrl":null,"permalink":"/tags/quiz/","section":"Tags","summary":"","title":"Quiz","type":"tags"},{"content":"这两天在 Twitter 上看到一道题目，主要是考察 overload 和 type(of:) 的知识点，本文仅做记录，关于 MetaType 会单独写一篇文章来总结。\n// Swift Quiz class View {} class A: View {} class B: View {} func add(_ v: View) { print(type(of: v)) } func add(_ v: A) { print(type(of: v)) } func add(_ v: B) { print(type(of: v)) } func build() -\u0026gt; View { return B() } add(build()) // What is the output? - \u0026#34;View\u0026#34; - \u0026#34;A\u0026#34; - \u0026#34;B\u0026#34; 答案的分布说明，大家对这个输出结果还是有一定的疑惑🤔\n这里使用多个 add 函数，实际上是起到了一定的迷惑作用，那针对具备不同静态类型的 add 函数来讲，其重载哪个函数就是依据其传入参数的静态类型而决定，因此在本 Quiz 中 build 函数返回了实例的静态类型即为 View，所以一定是会重载具备 View 类型参数的函数。 而进入函数体内部，主要就是 type(of:) 方法的执行结果了，而该方法的官方定义里有如下说明：\nfunc type\u0026lt;T, Metatype\u0026gt;(of value: T) -\u0026gt; Metatype You can use the type(of:) function to find the dynamic type of a value, particularly when the dynamic type is different from the static type. The static type of a value is the known, compile-time type of the value. The dynamic type of a value is the value’s actual type at run-time, which can be a subtype of its concrete type.\n可以看到，type(of:) 可以获取到当前所传值的动态类型，也就是其原始的宿主类型。对于本例来讲，传入到该方法的值实质上是由 B 类型实例化而来，因此其动态类型应该是 B。\nReferences # type(of:)\n","date":"2019-05-14","externalUrl":null,"permalink":"/post/a-swift-quiz/","section":"Posts","summary":"这两天在 Twitter 上看到一道题目，主要是考察 overload 和 type(of:) 的知识点，本文仅做记录，关于 MetaType 会单独写一篇文章来总结。\n","title":"一道 Swift Quiz","type":"post"},{"content":"","date":"2019-04-26","externalUrl":null,"permalink":"/tags/framework/","section":"Tags","summary":"","title":"Framework","type":"tags"},{"content":"","date":"2019-04-26","externalUrl":null,"permalink":"/tags/library/","section":"Tags","summary":"","title":"Library","type":"tags"},{"content":"","date":"2019-04-26","externalUrl":null,"permalink":"/tags/mach-o/","section":"Tags","summary":"","title":"Mach-O","type":"tags"},{"content":"Library 和 Framework 的概念大家应该脑海里都有一些，本文旨在讲述下基本概念，没有对每个字节都了如指掌。关于基本的编译过程在 Build Process 一文中也大概讲述了一些。\n在链接 Library 以及 Framework 之前，我们需要先了解一下 Mach—O。\n提到 Mach-O ，需要明确下 object file（目标文件）的概念，其实质上是指那些由源代码编译之后生成的，还未进行链接的中间文件（比如 Linux 下的 .o 以及 Windows 下的 .obj），它和可执行文件的内容和结构很相似，所以一般上会跟可执行文件格式采用一样的格式存储。\n那在 Unix 系统上的 COFF 是当时发明的在 Unix 表示一个目标文件的存取格式，后来 Linux 基于 COFF 发明了 ELF，而 Windows 基于 COFF 发明了 PE 格式。广义上来说，目标文件与可执行文件几乎一样。在 Linux 下统称为 ELF 文件，Windows 上统称为 PE-COFF 文件，而在 iOS/macOS 上我们的目标文件就是 Mach-O 格式，其在目标文件的大体组成上和其他两种很相似。而 Mach-O 的全称就是 Mach Object 的缩写。\n我们以 Linux 上 ELF 结构的一张图来大概说明一下情况，不同的源文件通过分类，将代码和数据，以及变量放入不同的职责段，代码段 _TEXT，数据段 _DATA 等。\n而 Mach-O 文件格式如下图（源自于 Apple 文档）, 和 ELF 格式类似，其也是将不同职责的代码和数据放置于不同的 Section 段，然后再集中成一个一个的 Segment，而 Load Commands 是用来控制每一个 Segment 是如何被加载的，数据是从哪里取的，等等。\n我们通过将如下代码编译输出成可执行文件来看，\n// // main.m // MachO // // Created by chen he on 2019/4/22. // Copyright © 2019 chen he. All rights reserved. // #import \u0026lt;stdio.h\u0026gt; int main(int argc, const char * argv[]) { printf(\u0026#34;Hello World.\\n\u0026#34;); return 0; } 通过 clang 工具套件编译源文件生成可执行文件。\n// output the main.o clang -c main.m // link main.o with c lib // default output file is a.out, you can specify another name with -o clang main.o -Wl,`xcrun --show-sdk-path`/usr/lib/libSystem.B.tbd 接着可以使用 MachOView 来查看刚才 a.out 以及 main.o 文件。\nMachOView 是一款开源软件，用以查看目标文件格式的，不过已经好几年没有更新了，所有有的 Load Command 识别不出。\n我们再看看 main.o 这个目标文件的结构是怎么样的。\nLibrary # 在 这篇文章 中说明了，每个源文件最终都会生成对应的目标文件，当工程项目变大之后，我们希望能够将具体协同工作的单一组件内的目标文件一起进行交付，对于功能稳定的代码无需参与再次编译的过程，我们需要将同一个组件内部的所有目标文件归拢到一起，这也是初期库（Library） 出现的由来。简单而言，库就是一堆目标文件的集合。\n通过简单的 ar 命令即可将多个 .o 文件整合成一个 .a 文件，同时使用 ranlib 命令更新静态库 .a 的符号索引。\nar -r libfoo.a foo.o bar.o baz.o ranlib libfoo.a Static Library # 关于静态和动态的由来，是因为链接过程的不同而产生，基于静态链接的库被称作静态库，基于动态链接的库被称作动态库（共享库）。\n静态库实质上是目标文件的集合。它是把很多目标文件捆绑在一起形成一个文件，再加上一些索引，你可以简单地把它理解为一个包含有很多目标文件的文件包。也就是上面我们介绍的 .a 文件。\n静态库的存在形式使得不同的成员开发者和部门都能够独立的自行开发自己的功能功能和程序模块，在一定阶段和程度上是大大提高了开发效率的，但是静态库也有一些问题：\n浪费内存和磁盘空间 模块更新难 以上方的 HelloWorld 程序为例，其中 printf 的符号是定义在 C 的标准库文件中的，如果要进行静态链接，也就是 C 的标准库要以静态库的方式参与链接到程序内部，假如平均一个 C 程序会使用到大概 1MB 的空间（磁盘以及内存），如果我们操作系统运行 100 个这样简单的程序就需要近 100MB 的空间，这个不太可取，而且这些标准库本身所有的程序都需要一人带一份，不太可取。\n另外一个问题是模块更新问题，因为最终要在链接阶段直接链接到可执行文件中，因此一旦某个模块发生变化，我们需要将该静态库重新进行编译，链接，发布，而且整个可执行文件都做了变更，因此需要完整的替换掉当下的可执行文件。\n例如一个程序如果包含 20 个模块，每个模块 1 MB，每次更新任何一个模块，用户需要重新获取 20MB 的程序。\n要想解决以上两个问题，最简单的办法就是将程序的每个模块隔离开，形成单独的文件，而不再将其都混合在一起。简单的来说，就是不再对那些组成程序的目标文件进行链接，而是等到程序用的时候再实时的进行链接，这个也是动态链接的基本思想，而基于动态链接产生的 Library 支持，即为共享库（动态链接库）\nDynamic Library # 动态链接最大的变化在于可执行程序和动态链接的目标文件是独立的两个文件，只是在程序初始执行的目标文件（比如 main 入口所在的文件）内部持有动态链接库的符号，当真正运行起来，需要用到对应的符号时，链接器才去查找对应目标文件，然后链接执行。\n当运行程序需要更新的时候，我们不需要完整的编译二进制可执行文件，而只需要在动态库的接口不变的前提下，仅仅更新那部分包含了所需升级功能的动态库而已。这一点，在 Windows 系统上体现的尤为明显。\n当然动态链接也存在自身的问题，很常见的一个问题，当程序所使用的其中一个模块发生更新之后，很容易造成库接口不兼容导致程序无法运行，这也是为什么有的时候你更新完系统，原来使用好好的 App 打开发生闪退或者某个功能无法使用了的缘故。\n另外一个缺点就是，由于所需调用的符号不再和可执行文件在一起，在进行方法调用的时候，过程要比之前复杂的多，要先找对应的动态库文件，如果在内存中不存在，还需要再去磁盘中实时加载，导致动态库的方法调用时间要比静态链接的情况下慢。\n在 Mac / iOS 上所有的动态库都是通过 dyld 来进行动态加载的，这里不详细说明了，大概列一下动态库的加载顺序：\n首先会加载系统级别的dylib, 目录在设备的/usr/lib/, 文件:libsqlite3.dylib、libc++.1.dylib... 然后加载系统级别的framework, 目录在设备的/System/Library/Frameworks, 文件:Foundation.framework 再引入runtime、gcd存放的dylib, 目录在设备的/usr/lib/, 文件:libSystem.B.dylib、libobjc.A.dylib 再引入自己注入的dylib, @executable_path/(目录存放在当前可执行文件底下) 而对于 App 来讲，实际上的路径是通过 @rpath 来指定的，该符号说白了就是占位符，当你的 App 成功安装到 iOS 系统之后，@rpath 就是该 App 的安装路径，以某个 iap 文件内部的可执行文件为例，使用 otool 查看其依赖的动态库如下：\notool -L osee2unifiedRelease.app/osee2unifiedRelease osee2unifiedRelease.app/osee2unifiedRelease: /usr/lib/libbz2.1.0.dylib (compatibility version 1.0.0, current version 1.0.5) /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 400.9.4) /usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0) /usr/lib/libicucore.A.dylib (compatibility version 1.0.0, current version 62.1.0) /usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 1.0.0) /usr/lib/libsqlite3.dylib (compatibility version 9.0.0, current version 274.20.0) /usr/lib/libxml2.2.dylib (compatibility version 10.0.0, current version 10.9.0) /usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.11) /System/Library/Frameworks/AVFoundation.framework/AVFoundation (compatibility version 1.0.0, current version 2.0.0) /System/Library/Frameworks/Accelerate.framework/Accelerate (compatibility version 1.0.0, current version 4.0.0) /System/Library/Frameworks/AdSupport.framework/AdSupport (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/AudioToolbox.framework/AudioToolbox (compatibility version 1.0.0, current version 492.0.0) /System/Library/Frameworks/CFNetwork.framework/CFNetwork (compatibility version 1.0.0, current version 975.0.3) /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (compatibility version 150.0.0, current version 1560.10.0) /System/Library/Frameworks/CoreGraphics.framework/CoreGraphics (compatibility version 64.0.0, current version 1245.9.2) /System/Library/Frameworks/CoreLocation.framework/CoreLocation (compatibility version 1.0.0, current version 2245.8.12) /System/Library/Frameworks/CoreMotion.framework/CoreMotion (compatibility version 1.0.0, current version 2245.8.12) /System/Library/Frameworks/CoreSpotlight.framework/CoreSpotlight (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/CoreTelephony.framework/CoreTelephony (compatibility version 1.0.0, current version 0.0.0) /System/Library/Frameworks/CoreText.framework/CoreText (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/ExternalAccessory.framework/ExternalAccessory (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1560.10.0) /System/Library/Frameworks/GLKit.framework/GLKit (compatibility version 1.0.0, current version 103.2.0) /System/Library/Frameworks/ImageIO.framework/ImageIO (compatibility version 1.0.0, current version 0.0.0) /System/Library/Frameworks/MediaPlayer.framework/MediaPlayer (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/MobileCoreServices.framework/MobileCoreServices (compatibility version 1.0.0, current version 935.2.0) /System/Library/Frameworks/OpenGLES.framework/OpenGLES (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/Photos.framework/Photos (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/QuartzCore.framework/QuartzCore (compatibility version 1.2.0, current version 1.11.0) /System/Library/Frameworks/Security.framework/Security (compatibility version 1.0.0, current version 58286.222.2) /System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration (compatibility version 1.0.0, current version 963.200.27) /System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 61000.0.0) @rpath/VideoEdit.framework/VideoEdit (compatibility version 1.0.0, current version 1.0.0) @rpath/VideoPlayer.framework/VideoPlayer (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/VideoToolbox.framework/VideoToolbox (compatibility version 1.0.0, current version 1.0.0) @rpath/ZmFFmpeg.framework/ZmFFmpeg (compatibility version 1.0.0, current version 1.0.0) @rpath/du.framework/du (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/Metal.framework/Metal (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/CoreMedia.framework/CoreMedia (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/WebKit.framework/WebKit (compatibility version 1.0.0, current version 606.2.104) /System/Library/Frameworks/Social.framework/Social (compatibility version 1.0.0, current version 87.0.0) /System/Library/Frameworks/Accounts.framework/Accounts (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/MessageUI.framework/MessageUI (compatibility version 1.0.0, current version 3445.100.42) /System/Library/Frameworks/AddressBook.framework/AddressBook (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/StoreKit.framework/StoreKit (compatibility version 1.0.0, current version 1.0.0) /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.200.5) /System/Library/Frameworks/AssetsLibrary.framework/AssetsLibrary (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/Contacts.framework/Contacts (compatibility version 0.0.0, current version 0.0.0) /System/Library/Frameworks/CoreData.framework/CoreData (compatibility version 1.0.0, current version 866.2.0) /System/Library/Frameworks/CoreImage.framework/CoreImage (compatibility version 1.0.0, current version 5.0.0) /System/Library/Frameworks/CoreVideo.framework/CoreVideo (compatibility version 1.2.0, current version 1.5.0) /System/Library/Frameworks/EventKit.framework/EventKit (compatibility version 1.0.0, current version 100.0.0) /System/Library/Frameworks/LocalAuthentication.framework/LocalAuthentication (compatibility version 1.0.0, current version 425.222.1) /System/Library/Frameworks/MapKit.framework/MapKit (compatibility version 1.0.0, current version 14.0.0) /System/Library/Frameworks/MetalKit.framework/MetalKit (compatibility version 1.0.0, current version 113.0.0) /System/Library/Frameworks/NetworkExtension.framework/NetworkExtension (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/PassKit.framework/PassKit (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/SafariServices.framework/SafariServices (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/UserNotifications.framework/UserNotifications (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/iAd.framework/iAd (compatibility version 1.0.0, current version 1.0.0) @rpath/libswiftAVFoundation.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftAccelerate.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftContacts.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftCore.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftCoreAudio.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftCoreData.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftCoreFoundation.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftCoreGraphics.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftCoreImage.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftCoreLocation.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftCoreMedia.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftDarwin.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftDispatch.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftFoundation.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftGLKit.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftMapKit.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftMediaPlayer.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftMetal.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftModelIO.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftPhotos.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftQuartzCore.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftUIKit.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftos.dylib (compatibility version 1.0.0, current version 1000.11.42) @rpath/libswiftsimd.dylib (compatibility version 1.0.0, current version 1000.11.42) dyld 是开源的，感兴趣可以一看。\nopensource-apple/dyld\nFramework # 在库刚出现的时候，我们将自己创建的 Library 提供给对方的时候，不仅需要将 .a 文件提供给对方，还需要同时提供 Header，用以开发人员在高级语言层面能够直观的使用该库提供的功能。后来，我们的模块越来越大，不仅仅包含代码文件，还加入了资源文件，Apple 引入了 framework 的概念用来将一个独立功能模块所需提供的内容打包提供。\nA framework is a bundle (a structured directory) that contains a dynamic shared library along with associated resources, such as nib files, image files, and header files.\n所以，你要明白，framework 本质上只是一种 Bundle 存在的形式，类似于一种目录结构，其并无动静之分。 内部包含的库文件，如果是静态库，其被称作为静态 Framework，如果内部为动态库，就被称作动态 Framework。由于官方在 iOS 上推出 Framework 的时候直接以 Dynamic Framework 的形式推出，通过 Xcode 创建的 Target 我们也可以了解。\nText Based .dylib Stubs # 自 Xcode 7 引入，目前未查找到官方正式文档说明，但是在官方论坛中有 Apple 官方人员针对 .tbd 文件的回应如下：\nFor those who are curious, the .tbd files are new \u0026ldquo;text-based stub libraries\u0026rdquo;, that provide a much more compact version of the stub libraries for use in the SDK, and help to significantly reduce its download size.\n以 libc++ 为例，在我们 iOS 开发过程中链接的 libc++ 的动态库是通过 libc++.tbd 引入的，可以查到其原始路径位于每个 SDK 文件夹下。\n/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/libc++.tbd 使用文本编辑器即可打开查看：\n--- !tapi-tbd-v3 archs: [ i386, x86_64 ] uuids: [ \u0026#39;i386: A9FCC99A-209C-348E-BB79-0A29A5FCB82B\u0026#39;, \u0026#39;x86_64: 66B692F1-FA7E-3CBB-817A-73A7FE29A765\u0026#39; ] platform: ios install-name: \u0026#39;/usr/lib/libc++.1.dylib\u0026#39; current-version: 400.9.4 objc-constraint: none exports: - archs: [ i386 ] symbols: [ __ZNKSt3__18messagesIcE6do_getEiiiRKNS_12basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE, __ZNKSt3__18messagesIcE8do_closeEi, __ZNKSt3__18messagesIwE6do_getEiiiRKNS_12basic_stringIwNS_11char_traitsIwEENS_9allocatorIwEEEE, __ZNKSt3__18messagesIwE8do_closeEi, __ZNSt3__111this_thread9sleep_forERKNS_6chrono8durationIxNS_5ratioILx1ELx1000000000EEEEE, __ZNSt3__112strstreambuf6__initEPciS1_, __ZNSt3__112strstreambufC1EPKai, __ZNSt3__112strstreambufC1EPKci, __ZNSt3__112strstreambufC1EPKhi, __ZNSt3__112strstreambufC1EPaiS1_, __ZNSt3__112strstreambufC1EPciS1_, __ZNSt3__112strstreambufC1EPhiS1_, __ZNSt3__112strstreambufC1Ei, __ZNSt3__112strstreambufC2EPKai, __ZNSt3__112strstreambufC2EPKci, __ZNSt3__112strstreambufC2EPKhi, __ZNSt3__112strstreambufC2EPaiS1_, __ZNSt3__112strstreambufC2EPciS1_, __ZNSt3__112strstreambufC2EPhiS1_, __ZNSt3__112strstreambufC2Ei, __ZNSt3__113basic_istreamIcNS_11char_traitsIcEEE3getEPci, __ZNSt3__113basic_istreamIcNS_11char_traitsIcEEE3getEPcic, __ZNSt3__113basic_istreamIcNS_11char_traitsIcEEE4readEPci, __ZNSt3__113basic_istreamIcNS_11char_traitsIcEEE6ignoreEii, __ZNSt3__113basic_istreamIcNS_11char_traitsIcEEE7getlineEPci, __ZNSt3__113basic_istreamIcNS_11char_traitsIcEEE7getlineEPcic, __ZNSt3__113basic_istreamIcNS_11char_traitsIcEEE8readsomeEPci, __ZNSt3__113basic_istreamIwNS_11char_traitsIwEEE3getEPwi, __ZNSt3__113basic_istreamIwNS_11char_traitsIwEEE3getEPwiw, 其中将本身的动态链接库所提供出的符号（函数，全局变量等）列出，因为在 macOS 以及 iOS 中系统目录下已经集成了这些动态库，因为为了避免不必要的下载和提供重复的动态链接库，因而提供了 tbd 这种格式文件用来做桥接。实质上当真正可执行文件进行链接的时候，会加载系统原有的动态链接库，这个具体的动态链接库位置由 install-name 给出。\n简而言之，tbd 是 Apple 用来减小 SDK 体积大小所提供的中间文件。\n所以，当你下载 Xcode 的时候，其自带的 SDK 中包含的动态链接库大部分均为 tbd 格式，而真正我们运行起来可执行文件之后加载的动态链接库会直接使用系统目录下的（无论是 iOS 还是 macOS）。\n参考链接 # MachOView Mach-O Executables PARSING MACH-O FILES ","date":"2019-04-26","externalUrl":null,"permalink":"/post/library_framework/","section":"Posts","summary":"Library 和 Framework 的概念大家应该脑海里都有一些，本文旨在讲述下基本概念，没有对每个字节都了如指掌。关于基本的编译过程在 Build Process 一文中也大概讲述了一些。\n","title":"关于 Library 和 Framework","type":"post"},{"content":"","date":"2019-04-25","externalUrl":null,"permalink":"/tags/build/","section":"Tags","summary":"","title":"Build","type":"tags"},{"content":"编程语言的处理过程大致会有五个阶段，其每个阶段均有对应的工具：\n预处理器 Preprocessor 编译器 Compiler 汇编器 Assembler 链接器 Linker 加载器 Loader 我们以一个简单的源文件，来看看具体这几个步骤都做了哪些事情。\n// // main.m // MachO // // Created by chen he on 2019/4/22. // Copyright © 2019 chen he. All rights reserved. // #import \u0026lt;stdio.h\u0026gt; int main(int argc, const char * argv[]) { printf(\u0026#34;Hello World.\\n\u0026#34;); return 0; } 预处理器 Preprocessor # 符号化 宏定义展开 头文件引入展开 在命令行执行如下命令，我们导出输出到本地文件\nxcrun clang -E helloworld.c \u0026gt; preprocessed.txt 打开 preprocessed.txt 看输出如下：\n# 1 \u0026#34;helloworld.c\u0026#34; # 1 \u0026#34;\u0026lt;built-in\u0026gt;\u0026#34; 1 # 1 \u0026#34;\u0026lt;built-in\u0026gt;\u0026#34; 3 # 361 \u0026#34;\u0026lt;built-in\u0026gt;\u0026#34; 3 # 1 \u0026#34;\u0026lt;command line\u0026gt;\u0026#34; 1 # 1 \u0026#34;\u0026lt;built-in\u0026gt;\u0026#34; 2 # 1 \u0026#34;helloworld.c\u0026#34; 2 # 1 \u0026#34;/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/stdio.h\u0026#34; 1 3 4 # 64 \u0026#34;/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/stdio.h\u0026#34; 3 4 # 1 \u0026#34;/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/_stdio.h\u0026#34; 1 3 4 # 68 \u0026#34;/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/_stdio.h\u0026#34; 3 4 # 1 \u0026#34;/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/sys/cdefs.h\u0026#34; 1 3 4 // 省略一万字 extern int __vsnprintf_chk (char * restrict, size_t, int, size_t, const char * restrict, va_list); # 412 \u0026#34;/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/stdio.h\u0026#34; 2 3 4 # 2 \u0026#34;helloworld.c\u0026#34; 2 int main(int argc, char *argv[]) { printf(\u0026#34;Hello World!\\n\u0026#34;); return 0; } 可以看到预编译处理之后的源文件，仅仅只有几行的源文件进行头文件展开之后能增加到几百行，而我们仅仅只引入了 stdio.h 一个头文件。当然，你可以使用 Xcode 来执行预处理命令。\n而在 Swift 提出的同时，Module 的概念被同时引入 C 系语言中，在 Xcode 的 build settings 中能找到其开关。\nModules 默认是开启的（当然这个开关也只是针对 C 系语言，Swift 原生就是以 Module 形式存在的），大家可以在开启的情况下再进行预处理命令看结果，已经由原来的 548 行减少到了 14 行，这也是 LLVM 极力推荐 Module 的其中一个原因，大大减少了预编译处理时间。\n# 1 \u0026#34;/Users/chen/Desktop/Share/Code/MachO/MachO/1/helloworld.m\u0026#34; # 1 \u0026#34;\u0026lt;built-in\u0026gt;\u0026#34; 1 # 1 \u0026#34;\u0026lt;built-in\u0026gt;\u0026#34; 3 # 374 \u0026#34;\u0026lt;built-in\u0026gt;\u0026#34; 3 # 1 \u0026#34;\u0026lt;command line\u0026gt;\u0026#34; 1 # 1 \u0026#34;\u0026lt;built-in\u0026gt;\u0026#34; 2 # 1 \u0026#34;/Users/chen/Desktop/Share/Code/MachO/MachO/1/helloworld.m\u0026#34; 2 # 10 \u0026#34;/Users/chen/Desktop/Share/Code/MachO/MachO/1/helloworld.m\u0026#34; @import Darwin.libc; int main(int argc, const char * argv[]) { printf(\u0026#34;Hello World.\\n\u0026#34;); return 0; } 当进行预处理完成之后，下一步就是基于展开的源码进行 AST 语法树解析，生成中间代码，并且输出目标机器代码的过程。这就是编译器做的事情。\n编译器 Compiler # 编译器自身就是个二进制程序，用来将代码源文件转换成语义上完全一致的等价语言的，该等价语言就是机器码（machine code）。目前在 Apple 的生态里完成这件事情的就是 LLVM（出现就是为了替换掉 GCC）。我们平时所说的编译器实质上是一个广义上的概念，其还包括编译器前端，中间代码生成器，代码优化器，编译器后端。\n以目前 Apple 生态下编译器（核心是 LLVM 套件）进行细化之后，可以看到详细的划分阶段如下所示。\n其中 Clang 以及 swiftc / swift 实质上是 LLVM 编译链条中的前端，虽然是前端，但是目前工具集本身已经内置了一些注入汇编器，代码优化等功能。\n无论是 Clang 还是 swiftc 最终都会在编译前端将源文件生成中间代码 LLVM Intermediate representation （LLVM IR），然后会经历中间代码优化等阶段，最后交由汇编器生成对应目标指令集的目标文件。\nXcode 使用两种不同的编译器前端，一种是针对 Objective-C/Objective-C++ 或者 C/C++ 代码使用的，另一种就是在 Swift 出现之后针对 Swift 语言使用的。前者就是 clang，后者是 swiftc / swift。\n因为 swift 语言本身是没有预处理器的，因此忽略前一步。在预编译阶段，我使用了 Clang 作为演示，而 Swift 文件是无需进行预编译处理的。最终编译器生成的文件，称之为目标文件，在 Apple 平台中以特殊的格式存在，这种格式就是 Mach-O 格式，下文会讲。 而每个目标文件中都有暴露给其他目标文件所使用的符号，这些也就是外部符号，这些符号被管理起来，以一个符号表的形式存在，对于变量和函数来说，其符号表中的 Key 就是这些变量和函数的名字，而 Value 就是其在目标文件中的地址。\n汇编器 Assembler # 汇编器实际上是将中间过程生成的中间代码翻译成指定机器指令集的汇编代码，其实 Clang 工具集中已经集成了\n链接器 Linker # 每个源文件就按照上面的步骤，生成了一个一个我们称之为目标文件的二进制，接下来我们需要将所有的目标文件整合成一个可执行文件。比如上面的例子中，我们有两个文件，Foo.o 以及 main.o 文件，当然还有依赖的其他库，Linker 该出场了。整个链接过程正是基于每个目标文件中的符号才能正确完成，链接过程中很关键的一部分就是符号的管理。\n我们新建两个文件，分别是 main.m，Foo.m 以及其对应的头文件 Foo.h。分别经历预处理，编译生成对应目标文件，给 clang 传递 -c 是用来完成以上所有工作集合，直接生成目标文件的。\nxcrun clang -c main.m xcrun clang -c Foo.m 生成产物为两个对应的目标文件，main.o 以及 Foo.o ，使用链接器进行链接，传统的链接器，无论是 Mac 还是 Linux 上都会有 ld 这个工具，在 /usr/bin/ 下，我们先使用 ld 将对应两个目标文件链接起来，如下：\nld Foo.o main.o 但是报错，如下所示:\nld: warning: No version-min specified on command line Undefined symbols for architecture x86_64: \u0026#34;_OBJC_CLASS_$_NSObject\u0026#34;, referenced from: _OBJC_CLASS_$_Foo in Foo.o \u0026#34;_OBJC_METACLASS_$_NSObject\u0026#34;, referenced from: _OBJC_METACLASS_$_Foo in Foo.o \u0026#34;__objc_empty_cache\u0026#34;, referenced from: _OBJC_METACLASS_$_Foo in Foo.o _OBJC_CLASS_$_Foo in Foo.o \u0026#34;_objc_msgSend\u0026#34;, referenced from: _main in main.o \u0026#34;_printf\u0026#34;, referenced from: -[Foo hello] in Foo.o ld: symbol(s) not found for inferred architecture x86_64 其中，报了一堆 OBJC 库找不到的问题，因为是静态链接，我们内部使用了 Foundation 的一些内容，这里也在侧面证明了，想要完成静态链接，所有未定义的符号都需要在静态链接阶段进行修正，因此我们还需要将这些库也链进来，当然，还有 _printf 也找不到，我们还需要把 C++ 相关的库也同时一并链接，最后的链接命令如下：\nld main.o Foo.o `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation `xcrun --show-sdk-path`/usr/lib/libSystem.B.tbd 这样就能够顺利的生成我们所需要的目标文件了。当然，如果你要是使用 clang 提供的默认命令来做，如下，\nxcrun clang main.o Foo.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation 会看到你不需要把一些基础的 C 库引入，是因为 clang 本身工具帮你做了一堆环境路径查找的事情，感兴趣可以加上 -v 指令来看完整的输出，a.out 是链接器默认生成的文件名称。\nnm # nm - display name list (symbol table)\n接下来，引入一个新的命令 nm，其是用来展示目标文件中的符号表信息的。我们使用 nm 命令来查看该可执行文件的符号表信息。\nxcrun nm -nm Foo.o (undefined) external _OBJC_CLASS_$_NSObject (undefined) external _OBJC_METACLASS_$_NSObject (undefined) external __objc_empty_cache (undefined) external _printf 0000000000000000 (__TEXT,__text) non-external -[Foo hello] 0000000000000040 (__DATA,__objc_const) non-external l_OBJC_METACLASS_RO_$_Foo 0000000000000088 (__DATA,__objc_const) non-external l_OBJC_$_INSTANCE_METHODS_Foo 00000000000000a8 (__DATA,__objc_const) non-external l_OBJC_CLASS_RO_$_Foo 00000000000000f0 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo 0000000000000118 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo 同时，我们再查看下 main.o 这个目标文件。\nxcrun nm -nm main.o (undefined) external _OBJC_CLASS_$_Foo (undefined) external _objc_msgSend 0000000000000000 (__TEXT,__text) external _main 再查看下最终的目标输出文件的符号信息，如下：\nxcun nm -nm a.out (undefined) external _OBJC_CLASS_$_NSObject (from libobjc) (undefined) external _OBJC_METACLASS_$_NSObject (from libobjc) (undefined) external __objc_empty_cache (from libobjc) (undefined) external _objc_msgSend (from libobjc) (undefined) external _printf (from libSystem) (undefined) external dyld_stub_binder (from libSystem) 0000000000001000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header 0000000000001ef0 (__TEXT,__text) external _main 0000000000001f50 (__TEXT,__text) non-external -[Foo hello] 0000000000002100 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo 0000000000002128 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo 大家会看到每一个目标文件符号表信息中会存储着 undefined 字样的符号，名字均为我们没有在代码中所体现出来的函数名字或者字符变量（本例没有引入变量）名称，在生成对应的可执行文件的过程中，链接器会查找所有输入的目标文件中每一个的符号表，然后组成全局符号表，相互找所需要的符号，并且对非动态库文件的符号进行定位。\n在进行链接之后，所有原来标记为 undefined 的符号依然是 undefined，但是会发现在对应符号后方会标记出该符号来自于哪里。这个也就是当我们可执行文件需要使用该符号的时候，对应去哪里加载对应的 dylib。\notool # llvm-otool - the otool-compatible command line parser for llvm-objdump. The command line shim llvm-otool takes all the same options as the original otool(1) command and executes an equivalent objdump(1) command.\n接下来再介绍一个命令行工具 otool(object file displaying tool)，其底层是对 objdump 的封装，而 objdump 是 Linux 下查看二进制文件的工具。\n我们就用 otool 来查看目标文件所需要使用的库的地址在哪里。\n// -L print shared libraries used xcrun otool -L a.out a.out: /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1560.12.0) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.200.5) /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0) 当然，我们也可以查看一下其中的 libSystem.B.dylib 的情况，其实 macOS 上的 C 基础库。\nxcrun otool -L /usr/lib/libSystem.B.dylib /usr/lib/libSystem.B.dylib: /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.250.1) /usr/lib/system/libcache.dylib (compatibility version 1.0.0, current version 81.0.0) /usr/lib/system/libcommonCrypto.dylib (compatibility version 1.0.0, current version 60118.250.2) /usr/lib/system/libcompiler_rt.dylib (compatibility version 1.0.0, current version 63.4.0) /usr/lib/system/libcopyfile.dylib (compatibility version 1.0.0, current version 1.0.0) /usr/lib/system/libcorecrypto.dylib (compatibility version 1.0.0, current version 602.250.23) /usr/lib/system/libdispatch.dylib (compatibility version 1.0.0, current version 1008.250.7) /usr/lib/system/libdyld.dylib (compatibility version 1.0.0, current version 655.1.1) /usr/lib/system/libkeymgr.dylib (compatibility version 1.0.0, current version 30.0.0) /usr/lib/system/liblaunch.dylib (compatibility version 1.0.0, current version 1336.251.2) /usr/lib/system/libmacho.dylib (compatibility version 1.0.0, current version 927.0.2) /usr/lib/system/libquarantine.dylib (compatibility version 1.0.0, current version 86.220.1) /usr/lib/system/libremovefile.dylib (compatibility version 1.0.0, current version 45.200.2) /usr/lib/system/libsystem_asl.dylib (compatibility version 1.0.0, current version 356.200.4) /usr/lib/system/libsystem_blocks.dylib (compatibility version 1.0.0, current version 73.0.0) /usr/lib/system/libsystem_c.dylib (compatibility version 1.0.0, current version 1272.250.1) /usr/lib/system/libsystem_configuration.dylib (compatibility version 1.0.0, current version 963.250.1) /usr/lib/system/libsystem_coreservices.dylib (compatibility version 1.0.0, current version 66.0.0) /usr/lib/system/libsystem_darwin.dylib (compatibility version 1.0.0, current version 1.0.0) /usr/lib/system/libsystem_dnssd.dylib (compatibility version 1.0.0, current version 878.250.4) /usr/lib/system/libsystem_info.dylib (compatibility version 1.0.0, current version 1.0.0) /usr/lib/system/libsystem_m.dylib (compatibility version 1.0.0, current version 3158.200.7) /usr/lib/system/libsystem_malloc.dylib (compatibility version 1.0.0, current version 166.251.2) /usr/lib/system/libsystem_networkextension.dylib (compatibility version 1.0.0, current version 1.0.0) /usr/lib/system/libsystem_notify.dylib (compatibility version 1.0.0, current version 172.200.21) /usr/lib/system/libsystem_sandbox.dylib (compatibility version 1.0.0, current version 851.250.12) /usr/lib/system/libsystem_secinit.dylib (compatibility version 1.0.0, current version 30.220.1) /usr/lib/system/libsystem_kernel.dylib (compatibility version 1.0.0, current version 4903.251.3) /usr/lib/system/libsystem_platform.dylib (compatibility version 1.0.0, current version 177.250.1) /usr/lib/system/libsystem_pthread.dylib (compatibility version 1.0.0, current version 330.250.2) /usr/lib/system/libsystem_symptoms.dylib (compatibility version 1.0.0, current version 1.0.0) /usr/lib/system/libsystem_trace.dylib (compatibility version 1.0.0, current version 906.250.5) /usr/lib/system/libunwind.dylib (compatibility version 1.0.0, current version 35.4.0) /usr/lib/system/libxpc.dylib (compatibility version 1.0.0, current version 1336.251.2) 甚至查看 a.out 可执行文件在执行时所需要依赖的所有依赖。\n(export DYLD_PRINT_LIBRARIES=; ./a.out ) 可以通过结果看到，一个简单的程序所需要链接的 dylib 有多少，就是因为有这么多动态库要加载，在可执行程序加载到内存中执行的时候，不断的需要装载动态库进入内存，所需要花费的时间其实很可观，所以系统层面提供了共享缓存，也即会提前加载到内存中的动态链接库集合，当需要链接到对应动态库的时候就省略了从磁盘写入内存的过程。\n总结一句话 # 整个编译过程可以归结为： 头文件约定，编译器信任彼此，链接器验证\n","date":"2019-04-25","externalUrl":null,"permalink":"/post/xcode-build-system/","section":"Posts","summary":"编程语言的处理过程大致会有五个阶段，其每个阶段均有对应的工具：\n预处理器 Preprocessor 编译器 Compiler 汇编器 Assembler 链接器 Linker 加载器 Loader 我们以一个简单的源文件，来看看具体这几个步骤都做了哪些事情。\n","title":"Build Process","type":"post"},{"content":"","date":"2019-04-25","externalUrl":null,"permalink":"/tags/compile/","section":"Tags","summary":"","title":"Compile","type":"tags"},{"content":"","date":"2019-04-25","externalUrl":null,"permalink":"/tags/preprocess/","section":"Tags","summary":"","title":"Preprocess","type":"tags"},{"content":"","date":"2019-04-25","externalUrl":null,"permalink":"/tags/xcode/","section":"Tags","summary":"","title":"Xcode","type":"tags"},{"content":"","date":"2019-03-30","externalUrl":null,"permalink":"/tags/applescript/","section":"Tags","summary":"","title":"AppleScript","type":"tags"},{"content":"","date":"2019-03-30","externalUrl":null,"permalink":"/tags/menu/","section":"Tags","summary":"","title":"Menu","type":"tags"},{"content":"","date":"2019-03-30","externalUrl":null,"permalink":"/tags/nstask/","section":"Tags","summary":"","title":"NSTask","type":"tags"},{"content":"","date":"2019-03-30","externalUrl":null,"permalink":"/tags/process/","section":"Tags","summary":"","title":"Process","type":"tags"},{"content":"最近看到开发圈某热点，做了个小工具，总结下最近的几个 CocoaApp 上的功能，仅做练手。现在只包含了两个开关：切换系统主题和隐藏桌面 icon 的功能。\n功能点 # 切换系统主题 显式或者印象桌面图标 完全的 Menu Only 应用 开机自启动 关于第三点的实现在另外一篇文章有讲过，第四点的实现在 这一篇 中有讲。\n系统主题切换 # 关于系统主题切换主要是基于 AppleScript 所写，\ntell application \u0026#34;System Events\u0026#34; tell appearance preferences set dark mode to not dark mode end tell end tell 你完全可以自行执行这段 Apple Script 来切换主题，你可以点击下面链接尝试。\nClick Here to run\n显式/隐藏桌面图标 # 关于隐藏桌面图标，实际上是执行系统的 Command 来实现的，Cocoa 应用可以显式使用 Process（也就是 NSTask）来执行任务，我们也是通过 NSTask 执行了一段如下的命令行达到的目的：\ndefaults write com.apple.finder CreateDesktop false killall Finder 你可以自行点击以下链接尝试。\nClick here to hide Desktop Icons\nClick here to show Desktop Icons\n感兴趣可以去看下代码，领会意思即可。 Releases\nTODO # Add more switch Add User-Customized switch setting Links # Checkout all command lines macOS Support\n","date":"2019-03-30","externalUrl":null,"permalink":"/post/swwwitch/","section":"Posts","summary":"最近看到开发圈某热点，做了个小工具，总结下最近的几个 CocoaApp 上的功能，仅做练手。现在只包含了两个开关：切换系统主题和隐藏桌面 icon 的功能。\n","title":"写个小工具 Swwwitch","type":"post"},{"content":"","date":"2019-03-18","externalUrl":null,"permalink":"/tags/agent/","section":"Tags","summary":"","title":"Agent","type":"tags"},{"content":"","date":"2019-03-18","externalUrl":null,"permalink":"/tags/cocoa/","section":"Tags","summary":"","title":"Cocoa","type":"tags"},{"content":"","date":"2019-03-18","externalUrl":null,"permalink":"/tags/dock/","section":"Tags","summary":"","title":"Dock","type":"tags"},{"content":"","date":"2019-03-18","externalUrl":null,"permalink":"/tags/login/","section":"Tags","summary":"","title":"Login","type":"tags"},{"content":"开机自启动是 Cocoa 应用最常见的一种功能，尤其是针对需要常驻 Menu 的服务来说更是如此，今天我们对开机启动项的功能加入做个梳理。\n在 Daemons and Services Programming Guide 中我们能找到关于自启动项的开发说明：\nApplications can contain a helper application as a full application bundle, stored inside the main application bundle in the Contents/Library/LoginItems directory. Set either the LSUIElement or LSBackgroundOnly key in the Info.plist file of the helper application’s bundle.\nUse the SMLoginItemSetEnabled function (available in OS X v10.6.6 and later) to enable a helper application. It takes two arguments, a CFStringRef containing the bundle identifier of the helper application, and a Boolean specifying the desired state. Pass true to start the helper application immediately and indicate that it should be started every time the user logs in. Pass false to terminate the helper application and indicate that it should no longer be launched when the user logs in. This function returns true if the requested change has taken effect; otherwise, it returns false. This function can be used to manage any number of helper applications.\nIf multiple applications (for example, several applications from the same company) contain a helper application with the same bundle identifier, only the one with the greatest bundle version number is launched. Any of the applications that contain a copy of the helper application can enable and disable it.\n如文档中描述的那样，你可以在主应用中包含一个辅助应用，并且路径固定为 Contents/Library/LoginItems，\n另外一种方式就是使用 Shared File List，其相关 API 在 Launch Services Reference 能找到，其实具体的 API 就在 LSSharedFileList.h 中。\n沙盒应用 # 在官方文档 App Sandbox Design Guide 中有如下针对开机自启动的描述：\nTo create a login item for your sandboxed app, use the SMLoginItemSetEnabled function (declared in ServiceManagement/SMLoginItem.h) as described in Adding Login Items Using the Service Management Framework.\n(With App Sandbox, you cannot create a login item using functions in the LSSharedFileList.h header file. For example, you cannot use the function LSSharedFileListInsertItemURL. Nor can you manipulate the state of Launch Services, such as by using the function LSRegisterURL.)\n其实上面提及的方式也就是在第一小节中提到的两种方式中的第一种，而且第二种共享文件列表的 API 是无法针对沙盒应用使用的，而且 LSSharedFileList.h 已经在 10.10 系统版本之后标记为废弃了。\n综合上面的说明，目前在 macOS 上加入自启动项的方式也有且仅有一种方式，也就是加入辅助应用来引导主应用启动。整个思路应该是如下：\n将辅助应用加入系统启动项中； 系统启动，进而自启动辅助应用； 辅助应用引导主应用启动； 主应用启动完成，干掉辅助应用 是的，就是这么绕，具体要怎么做呢？ 主要分为如下几个步骤：\n创建辅助应用，其作为主应用的 Target 新建出来 将辅助应用的 Info.plist 文件中指定属性 LSBackgroundOnly 为 YES； 在辅助应用 Target 的 build setting 中设置 Skip Install 为 YES（关于其作用主要是不需要 Xcode archive 执行将 Product 拷贝到最终的包里，因为我们执行加入了步骤 4）; 在主应用的 build phase 中加入 Copy Files 阶段， 指定 destination 为 Wrapper 指定 subpath 为 Contents/Library/LoginItems 加入辅助应用的 Product 大家第一眼看到这些步骤的时候是不是头都大了，没错，这仅仅是写代码之前的参数配置工作。\n启动项支持 # 指定 CocoaApp\n指定 Product ID 为 StartAtLoginLauncher,该 Target 的 BundleID 为 app.chen.osx.demo.StartAtLoginLauncher。\n然后，修改 StartAtLoginLauncher 的 Info.plist 文件，指定 LSBackgroundOnly 为 YES 修改 StartAtLoginLauncher Target 的 Build Setting 中 Skip Install 为 YES\n紧接着是设置主应用 StartAtLogin Target，为其加入 Copy Files Build Phase，如下设置，路径是固定的 Contents/Library/LoginItems，Copy 对象为 StartAtLoginLauncher\n至此，所有设置均已完成，你可以 Command+B 产出一个 Product 看看，在主应用里是否已经将启动项目包含进去了。\n还没有结束，因为 StartAtLoginLauncher 应用是指在后台运行，我们不希望辅助应用启动的时候弹出 UI，因此还需要删除相关的 UI 代码，在 Main.storyboard 中，删除 Window 以及 ViewController，只保留 Application Scene 即可\n至此，所有写代码之前的工作已经完成，我们已经为主应用生成了对应的辅助应用，帮助其启动。\n加入启动项 # 代码核心逻辑包含两部分：\n主应用启动之后杀掉辅助应用，因为其已经完成了使命； 助应用启动之后将主应用唤醒 主应用 # extension Notification.Name { static let killLauncher = Notification.Name(\u0026#34;killLauncher\u0026#34;) } func applicationDidFinishLaunching(_ aNotification: Notification) { // Insert code here to initialize your application let launcherAppId = \u0026#34;app.chen.osx.demo.StartAtLoginLauncher\u0026#34; let runningApps = NSWorkspace.shared.runningApplications let isRunning = !runningApps.filter { $0.bundleIdentifier == launcherAppId }.isEmpty if isRunning { DistributedNotificationCenter.default().post(name: .killLauncher, object: Bundle.main.bundleIdentifier!) } } 主应用在完成启动之后，检查当前正在执行的 Application 列表中是否包含了我们的辅助应用，如果包含，发送通知，让其 Terminate\n辅助应用 # func applicationDidFinishLaunching(_ aNotification: Notification) { let mainAppIdentifier = \u0026#34;app.chen.osx.demo.StartAtLogin\u0026#34; let runningApps = NSWorkspace.shared.runningApplications let isRunning = !runningApps.filter { $0.bundleIdentifier == mainAppIdentifier }.isEmpty if !isRunning { DistributedNotificationCenter.default().addObserver(self, selector: #selector(self.terminate), name: .killLauncher, object: mainAppIdentifier) let path = Bundle.main.bundlePath as NSString var components = path.pathComponents components.removeLast() components.removeLast() components.removeLast() components.append(\u0026#34;MacOS\u0026#34;) components.append(\u0026#34;StartAtLogin\u0026#34;) //main app name let newPath = NSString.path(withComponents: components) NSWorkspace.shared.launchApplication(newPath) } else { self.terminate() } } 辅助应用启动之后，查询主应用是否已经运行，如果已经运行，就自觉干掉自己。如果没有运行，我们唤醒主 App，在此之前设置监听，等到主应用启动之后会发给自己通知，然后再自杀 😂\n这其中我们使用了 DistributedNotificationCenter，和平时我们使用的 NotificationCenter 不同，其发出的通知是跨任务（进程间）的，也就是其他进程如果注册了同样的通知，也是能够收到监听通知的。 系统的日夜间通知就是这种类型，其会在所有 Task 之间进行广播，该通知的 NotificationName 是 AppleInterfaceThemeChangedNotification.\nprivate static let notificationName = NSNotification.Name(\u0026#34;AppleInterfaceThemeChangedNotification\u0026#34;) func reigsterThemeChangedNotification() { DistributedNotificationCenter.default().addObserver(self, selector: #selector(selectorHandler), name: notificationName, object: nil) } @objc private static func selectorHandler() { print(\u0026#34;Theme Changed!\u0026#34;)\t} 因此 Demo 中的通知名字只是示例，在实际开发中，尽可能的确保通知的唯一性。\n切换自启动状态 # 关于自启动状态的设置包含两个主要的 API：\nSMCopyAllJobDictionaries SMLoginItemSetEnabled SMCopyAllJobDictionaries # 获取当前我们的启动项设置情况是通过 SMCopyAllJobDictionaries 方法，如下定义。\n/*! * @function SMCopyAllJobDictionaries * @abstract * Copy the job description dictionaries for all jobs in the given domain. * * @param domain * The job\u0026#39;s domain (e.g. {@link kSMDomainSystemLaunchd} or * {@link kSMDomainUserLaunchd}). * * @result * A new array containing all job dictionaries, or NULL if an error occurred. * Must be released by the caller. * * @discussion * SMCopyAllJobDictionaries returns an array of the job description dictionaries * for all jobs in the given domain, or NULL if an error occurred. This routine * is deprecated and will be removed in a future release. There will be no * provided replacement. * * For the specific use of testing the state of a login item that may have been * enabled with SMLoginItemSetEnabled() in order to show that state to the * user, this function remains the recommended API. A replacement API for this * specific use will be provided before this function is removed. */ __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_6, __MAC_10_10, __IPHONE_3_0, __IPHONE_8_0) XPC_EXPORT CFArrayRef SMCopyAllJobDictionaries(CFStringRef domain); 该方法虽然标记 10.10 系统开始废弃，但是到目前的 10.14 版本还未提供替换的 API，所以还是可以继续使用的（文档所说）。\n传入的参数可以理解就是指定获取任务类型的，我们使用 kSMDomainUserLaunchd 来获取所有加入到用户启动项列表中的任务，其中每一个 Job 都是一个字典结构，内容大概类似：\n我们可以通过 Label 来查找我们需要的 Job，\nlet launchHelperIdentifier = \u0026#34;app.chen.osx.demo.StartAtLoginLauncher\u0026#34; let jobs = SMCopyAllJobDictionaries(kSMDomainUserLaunchd).takeRetainedValue() as? [[String: AnyObject]] var autoLaunchRegistered = jobs?.contains(where: { $0[\u0026#34;Label\u0026#34;] as! String == launchHelperIdentifier }) ?? false SMLoginItemSetEnabled # 设置启动项是通过 SMLoginItemSetEnabled 方法，参数为要自启动的应用的 BundleID 以及自启动状态。\n要记住，这里我们进行更改的是针对 Launch Helper 的设置。\nvar startAtLogin = true // .... let launchHelperIdentifier = \u0026#34;app.chen.osx.demo.StartAtLoginLauncher\u0026#34; SMLoginItemSetEnabled(launchHelperIdentifier as CFString, startAtLogin) 测试 # 至此，关于自启动项的工作已经完成，想要测试，可以先 Archive 出一个安装包，然后将 Demo App 拖到 /Applications 目录，启动之后，设置 Start At Login 选项 checked 状态。\n如果不放心，退出登录之前，Quit 掉测试应用，并且取消 Reopen 选项。\n然后，Log Out 当前用户，之后再次登录进来，看 Demo 应用是否被启动了。在我的电脑上测试再次启动之后 Demo 应用就会被顺利启动了。\n其中还有一点是关于 Target 的 Sandbox 属性，作为目前唯一可行的自启动官方方案，其同时适用于沙盒应用和非沙盒应用的。\n工具推荐 # 推荐下 Github 上 sindresorhus 写的小工具 LaunchAtLogin，简化了上述的步骤。\n参考链接 # App Sandbox Design Guide Daemons and Services Programming Guide Launch Services Programming Guide NSDistributedNotificationCenter What the Skip-Install mean? ","date":"2019-03-18","externalUrl":null,"permalink":"/post/autostartwhenlogin/","section":"Posts","summary":"开机自启动是 Cocoa 应用最常见的一种功能，尤其是针对需要常驻 Menu 的服务来说更是如此，今天我们对开机启动项的功能加入做个梳理。\n","title":"关于 Mac 应用的自启动是如何做到的","type":"post"},{"content":"","date":"2019-03-13","externalUrl":null,"permalink":"/tags/dockless/","section":"Tags","summary":"","title":"Dockless","type":"tags"},{"content":"Menu Only 算是 Cocoa App 中最常见的一项，它使得 App 不占用你的 Dock 栏，在多 workspace 的时候也不影响正常使用，随时都可以在屏幕的菜单栏中执行快捷操作。尤其是针对一些需要便捷性要求比较高的应用来讲，Menu bar 的功能必不可少。本文就简单介绍一下关于 Menu App 中关键的几个开发步骤。\n介绍 # 以 Shimo 这个 App 举例，\n在其设置选项卡中能看到 Show Shimo in 的选项菜单，其中有三项：\nMenubar only Dock only Menubar \u0026amp; Dock 这也是常见的 Cocoa 应用的模式支持，很多常见的 App 都支持，比如 DayOne，Dash，Todoist 等。\n其实核心功能有两点：\n可以显示或者隐藏 Dock 图标； 可以显示或者隐藏 Menu 菜单这两者的组合。 核心步骤 # Dock # 普通的 Cocoa Application 创建之后，默认都是 Dock 上展示的，如果想隐藏 Dock 图标，首先它需要有这个能力，这个能力是通过 info.plist 文件中的 Key 来指定的，这个 Key 就是 LSUIElement，我们将其值设置为 true\n\u0026lt;key\u0026gt;LSUIElement\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; 在可视化展示 plist 的时候能看到针对该 Key 的描述是标识 Application is agent(UIElement)\n之后，再次打开 App，会发现 Dock 上已经看不到该应用的图标了，这就是 UIElement 的作用，其实际上就是声明我们的 Cocoa App 是 UIElement（也即 agent） application，Dock 不显示，允许有一定的用户界面。在方法 TransformProcessType 的头文件中能看到 Cocoa Application 的几种常见类型：\n/* * TransformProcessType() * * Summary: * Changes the \u0026#39;type\u0026#39; of the process specified in the psn parameter. * The type is specified in the transformState parameter. * * Discussion: * Given a psn for an application, this call transforms that * application into the given type. Foreground applications have a * menu bar and appear in the Dock. Background applications do not * appear in the Dock, do not have a menu bar ( and should not have * windows or other user interface ). UIElement applications do not * have a menu bar, do not appear in the dock, but may in limited * circumstances present windows and user interface. If a foreground * application is frontmost when transformed into a background * application, it is first hidden and another application is made * frontmost. A UIElement or background-only application which is * transformed into a foreground application is not brought to the * front (use SetFrontProcess() after the transform if this is * required) nor will it be shown if it is hidden ( even if hidden * automatically by being transformed into a background-only * application ), so the caller should use ShowHideProcess() to show * the application after it is transformed into a foreground * application. Applications can only transform themselves; this * call cannot change the type of another application. extern OSStatus TransformProcessType( const ProcessSerialNumber * psn, ProcessApplicationTransformState transformState) AVAILABLE_MAC_OS_X_VERSION_10_3_AND_LATER; 上方的注释写的非常清楚，我们日常的 Cocoa Application 主要包含三种类型：\nForeground applications， 拥有一个 menu bar，并且会在 Dock 上出现； Background applications，Dock 上不存在并且没有 menu bar，并且不应该存在任何 UI 交互界面（建议） UIElement applications 有和 Background applications 相同的情况，但是允许在某些情况下展示用户界面。 那上方这个方法 TransformProcessType 就是进行这几种模式切换的。\nfunc toggleDock(show: Bool) -\u0026gt; Bool { // ProcessApplicationTransformState let transformState = show ? // show to foreground application // or not show to background application ProcessApplicationTransformState(kProcessTransformToForegroundApplication) : ProcessApplicationTransformState(kProcessTransformToUIElementApplication) // transform current application type. var psn = ProcessSerialNumber(highLongOfPSN: 0, lowLongOfPSN: UInt32(kCurrentProcess)) return TransformProcessType(\u0026amp;psn, transformState) == 0 } 这里实际上还有一种方案也是很多开发者选用的方案，通过指定 App 的ActivationPolicy来实现的，核心的 API 是 setActivationPolicy:\n/* Attempts to modify the application\u0026#39;s activation policy. In OS X 10.9, any policy may be set; prior to 10.9, the activation policy may be changed to NSApplicationActivationPolicyProhibited or NSApplicationActivationPolicyRegular, but may not be changed to NSApplicationActivationPolicyAccessory. This returns YES if setting the activation policy is successful, and NO if not. */ @available(OSX 10.6, *) open func setActivationPolicy(_ activationPolicy: NSApplication.ActivationPolicy) -\u0026gt; Bool 而针对 ActivationPolicy 的详细解释也可以在其头文件注释中看到不同的 activation policy 意味着什么。\n/* The following activation policies control whether and how an application may be activated. They are determined by the Info.plist. */ public enum ActivationPolicy : Int { /* The application is an ordinary app that appears in the Dock and may have a user interface. This is the default for bundled apps, unless overridden in the Info.plist. */ case regular /* The application does not appear in the Dock and does not have a menu bar, but it may be activated programmatically or by clicking on one of its windows. This corresponds to LSUIElement=1 in the Info.plist. */ case accessory /* The application does not appear in the Dock and may not create windows or be activated. This corresponds to LSBackgroundOnly=1 in the Info.plist. This is also the default for unbundled executables that do not have Info.plists. */ case prohibited } 实际上不同的 activation policy 和 Info.plist 文件中写入不同元素的效果是对等的。regular policy 的应用就是常规引用的形式，会出现在 Dock 上，accessory policy 的应用就是指定当前应用为 agent，不再 Dock 出现。\n显示或者隐藏 Dock 的功能就可以通过切换当前的激活策略（activation policy来实现，如下代码所示：\nfunc toggleDock2(show: Bool) -\u0026gt; Bool { return show ? NSApp.setActivationPolicy(.regular) : NSApp.setActivationPolicy(.accessory) } Menu bar # 一旦我们可以通过以上形式隐藏 Dock 图标之后，我们还需要为应用加上菜单栏按钮，具体做法是通过 NSStatusItem 这个类，其代表一个系统菜单栏上的条目。具体操作如下：\nlet statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength) 之后系统会在 Menu bar 上创建一个选项按钮，不过我们还需要设置该菜单选项的 UI，如下设置：\nif let button = statusItem.button { button.image = NSImage(named:NSImage.Name(\u0026#34;ic_dock\u0026#34;)) button.action = #selector(doWhatYouWantToDo(_:)) } 此时应用启动之后菜单栏就会有图标展示了，详细可以参考 Raywenderlich 家的教程，不再赘述。 [Menus and Popovers in Menu Bar Apps for macOS]\n有个关于菜单栏按钮的库 CCNStatusItem 提供了比较完整的需求，包括支持 menu 按钮定制，点击弹窗以及 Dragging and Drop 等支持。 不过该库已经很久没有维护了，仅做参考吧。\n参考文献 # Show/Hide dock icon on macOS App NSStatusItem Menus and Popovers in Menu Bar Apps for macOS CCNStatusItem ","date":"2019-03-13","externalUrl":null,"permalink":"/post/dockless-cocoaapps/","section":"Posts","summary":"Menu Only 算是 Cocoa App 中最常见的一项，它使得 App 不占用你的 Dock 栏，在多 workspace 的时候也不影响正常使用，随时都可以在屏幕的菜单栏中执行快捷操作。尤其是针对一些需要便捷性要求比较高的应用来讲，Menu bar 的功能必不可少。本文就简单介绍一下关于 Menu App 中关键的几个开发步骤。\n","title":"Mac 平台上那些 Dockless 的 App 都是如何实现的？","type":"post"},{"content":"","date":"2019-03-13","externalUrl":null,"permalink":"/tags/evernote/","section":"Tags","summary":"","title":"Evernote","type":"tags"},{"content":"","date":"2019-03-13","externalUrl":null,"permalink":"/tags/macos/","section":"Tags","summary":"","title":"MacOS","type":"tags"},{"content":"","date":"2019-03-13","externalUrl":null,"permalink":"/categories/productivity/","section":"Categories","summary":"","title":"Productivity","type":"categories"},{"content":"","date":"2019-03-13","externalUrl":null,"permalink":"/tags/%E5%8D%B0%E8%B1%A1%E7%AC%94%E8%AE%B0/","section":"Tags","summary":"","title":"印象笔记","type":"tags"},{"content":"因为 Evernote 在国内使用了不同的账户体系，去年团队也彻底独立出来，但是 Safari 的剪藏插件是同一个，下载地址，但是当你系统是英文系统的时候，使用 Safari 剪藏有点尴尬，就是根本找不到印象笔记的登录入口，该文做个记录。\n首先 Safari 默认是没有 Develop 菜单的，因此先开启 Develop 模式，进入 Safari 的选项设置面板中，最后 Advanced 选项卡最下方如图勾选。\n此时，在 Safari 的顶部菜单中会多出 Develop 菜单入口，如下图所示：\n安装印象笔记的插件之后，点击 Safari 上的印象笔记剪藏按钮，你会发现直接带到 Evernote 的登录界面，没有渠道让我登录印象笔记，即使你 Safari 已经登录了印象笔记，登录的 Session 和 Cookie 信息并没有任何用。\n这时，你就登录国际版 Evernote 账户，如果你没有，那就注册一个。总之，你需要先登录进去，因为我们必须让剪藏菜单展示出来才行。\n登录 Evernote 之后，打开一个页面进行剪裁，之后如图，我们终于看到了剪藏菜单：\n点击下方的 Options，打开设置选项\n此时，因为刚才开启了 Develop 菜单选项卡，此时右键点击该窗口，\n点击 Inspect Element 打开页面元素查看器，在下方元素展示区为焦点的前提下，Command +F 搜索 DeveloperContainer 关键字。\n所以大概猜得出来，这个面板实际上隐藏了一些开发者选项，这里通过 display:none 执行隐藏了，我们直接把 display:none 删掉，双击就能选中。\n我们看到这个 div 下面包含很多 div（是一些开发者功能选项，就像很多程序一样，这些功能都被隐藏了，平时看不到），发现后面有一个 style =“display:none;”这是前端里面经常看到的一个样式语句，作用就是隐藏下面这个表里面的内容，然后按回车确定。\n你会发现上方印象笔记的剪藏插件刷新了，之后你会发现之前被隐藏的选项出现了，如下图所示：\nDeveloper Options 这个选项表单出现了，就在不远处看到了选项 Simulate Simplified Chinese，选中后面复选框。之后剪藏插件会自动退登当前的 Evernote 账户，这时候再次点击剪藏按钮，就会触发印象笔记的登录了，地址会被带入地址 https://app.yinxiang.com 域下。\n登录完成之后，你的剪藏插件已经是印象笔记的账户了。\nEnjoy it.\n","date":"2019-03-13","externalUrl":null,"permalink":"/post/using-yinxiangbiji-system-english/","section":"Posts","summary":"因为 Evernote 在国内使用了不同的账户体系，去年团队也彻底独立出来，但是 Safari 的剪藏插件是同一个，下载地址，但是当你系统是英文系统的时候，使用 Safari 剪藏有点尴尬，就是根本找不到印象笔记的登录入口，该文做个记录。\n","title":"在英文语言系统的 Safari 中使用印象笔记","type":"post"},{"content":"","date":"2019-03-11","externalUrl":null,"permalink":"/tags/backgroundcolor/","section":"Tags","summary":"","title":"BackgroundColor","type":"tags"},{"content":"","date":"2019-03-11","externalUrl":null,"permalink":"/tags/nsview/","section":"Tags","summary":"","title":"NSView","type":"tags"},{"content":"NSView 作为 Cocoa 中最基本的构成元素，是构成整个 Mac App 视图体系的基础，和 UIView 在 iOS 世界中的位置一样重要，可是在 UIView 里司空见惯的背景色设置，在 NSView 中却不见身影。\n在 iOS 中，设置 UIView 的背景色很简单。\nimport UIKit let frame = CGRect(origin: .zero, size: CGSize(width: 100, height: 100)) let view = UIView(frame: frame) view.backgroundColor = .blue 而在 macOS 上，使用 NSView 背景色需要如下设置：\nimport Cocoa let frame = CGRect(origin: .zero, size: CGSize(width: 100, height: 100)) let view = NSView(frame: frame) view.wantsLayer = true view.layer?.backgroundColor = NSColor.blue.cgColor UIView 自身有一个 backgroundColor 的属性，而在 NSView 中是没有的，你需要做的是设置 View 所包含的 layer 的背景色。而且 layer 是 Optional 的，也就意味着 NSView 默认是不包含 layer 的，所以你还需要设置 wantsLayer 为 true 让 NSView 创建 layer 出来。\n在 Apple 的官方文档\u0010 有针对 wantsLayer 的一些说明\nSetting the value of this property to true turns the view into a layer-backed view—that is, the view uses a CALayer object to manage its rendered content. Creating a layer-backed view implicitly causes the entire view hierarchy under that view to become layer-backed. Thus, the view and all of its subviews (including subviews of subviews) become layer-backed. The default value of this property is false.\n","date":"2019-03-11","externalUrl":null,"permalink":"/post/nsview+backgroundcolor/","section":"Posts","summary":"NSView 作为 Cocoa 中最基本的构成元素，是构成整个 Mac App 视图体系的基础，和 UIView 在 iOS 世界中的位置一样重要，可是在 UIView 里司空见惯的背景色设置，在 NSView 中却不见身影。\n","title":"为 NSView 增加 backgroundColor","type":"post"},{"content":"","date":"2019-02-26","externalUrl":null,"permalink":"/categories/rxswift/","section":"Categories","summary":"","title":"RxSwift","type":"categories"},{"content":"文中所用插图均出自书籍 《RxSwift - Reactive Programming with Swift》\nSubject 在 Rx 的世界里是这么一种存在，其既可以作为观测者，也可以作为被观测者。自然而然想到的是 Subject 本身就可以作为一种过渡桥接信号的手段，它订阅某个信号，一旦信号收到序列，转头它就又把信号散发给自己的观测者了。\n在 RxSwift 的世界里涉及几种 Subject，这里先行做个记录：\nPublishSubject BehaviorSubject ReplaySubject Variable （预计在 5.0 废弃） PublishSubject # PublishSubject 只给订阅者发送新元素，也就是订阅者只能接受到订阅之后发出的信号。一图胜千言，看👇\n其中第一排是某个作为 Observable 角色的 Subject，自左至右为严格时间线， 2 和 3 排均为订阅了该 Subject 的观测者，向上的虚线表明订阅的时机，可以看到观测者只能接收到订阅之后由 Subject 新发射出的信号。\nBehaviorSubject # BehaviorSubject 会在订阅者订阅之后发送最新的一个信号元素，自然需要你在初始化该对象的时候给其设定初始化值（否则对于第一次订阅的观测者来说，哪来的最新发射的元素呢？）。如果初始化的时候无法提供默认值，那可能你就需要用到上面的 PublishSubject 了。其中常用的 Variable 和 BehaviorRelay 都是对其的封装。\n从图中可以看到和上方 Publish 区别的是其在观测者订阅之后会收到最新的最后一个信号。\nReplaySubject # 顾名思义，ReplaySubject 会重新发射在订阅者订阅之间发出的信号，具体发多少是由 bufferSize 决定的，其指定需要 cache 多少个信号，如下图中所示，Buffer Size 为 2，因此当第二个订阅者订阅 Subject 之后，其会收到最新的 1 和 2 两个信号。\n/// 指定发射多少最新信号 let subject = ReplaySubject\u0026lt;String\u0026gt;.create(bufferSize: 2) 需要注意一点：所有被 Cache 的信号本身都是在内存中的，因此需要考虑到缓存信号的最大个数以及每个信号的负载，比如你的每个信号本身是 Image 数据，然后又 Cache 了比较多的数目，这时候内存压力就会很大，所以使用这个 ReplaySubject 的时候需要注意内存问题。\nVariable # Variable 自身实质上是 BehaviorSubject 的封装，所以它具备 Behavior 每次订阅就会接收到最新信号元素的属性，但是和 BehaviorSubject 不同在于其不会因为收到 Error 事件导致整个序列停止（也就是说只有 completion 信号才会让该信号终结），而它的信号完成是在其 deinit 方法中。\n我们在日常使用中喜欢 Variable 的一个最大原因可能就是直接获取它的值，而不需要像常规信号一般需要进行订阅才能取得它的值，而且不需要进行订阅，想用即取，而且其生命周期完全和 ARC 结合，无需主动取消订阅。\nvar variable = Variable(\u0026#34;Initial value\u0026#34;) variable.value = \u0026#34;New initial value\u0026#34; print(variable.value) 目前在 RxSwift 中 Variable 已经被标记为废弃，如果你使用了 Variable 类型，编译器会在 Console 中打印出以下内容：\n`Variable` is planned for future deprecation. Please consider `BehaviorRelay` as a replacement. Read more at: https://git.io/vNqvx 目前官方给出的推荐是使用 BehaviorRelay，详细解释如下：\n/// Current recommended replacement for this API is `RxCocoa.BehaviorRelay` because: /// * `Variable` isn\u0026#39;t a standard cross platform concept, hence it\u0026#39;s out of place in RxSwift target. /// * It doesn\u0026#39;t have a counterpart for handling events (`PublishRelay`). It models state only. /// * It doesn\u0026#39;t have a consistent naming with *Relay or other Rx concepts. /// * It has an inconsistent memory management model compared to other parts of RxSwift (completes on `deinit`). 首先 Variable 是 RxSwift 为 Apple 生态加入的一个名词，并不是 Rx 生态共有的概念，而且名字上也显得和其他概念格格不入，因此严格意义算起来并不是 Rx 生态的一部分了；\n另外，它功能上几乎和 BehaviorRelay 一样，使用 BehaviorRelay 也能实现同样的功能。\n最后一点，其内存管理语义和 Rx 其他概念也不一样，其他的信号终结是严格按照 completion 和 error 信号，但是 Variable 直到对象被释放掉才默认触发 completion 信号。\n详细官方讨论在这里\n","date":"2019-02-26","externalUrl":null,"permalink":"/post/rxswift-subjects/","section":"Posts","summary":"文中所用插图均出自书籍 《RxSwift - Reactive Programming with Swift》\nSubject 在 Rx 的世界里是这么一种存在，其既可以作为观测者，也可以作为被观测者。自然而然想到的是 Subject 本身就可以作为一种过渡桥接信号的手段，它订阅某个信号，一旦信号收到序列，转头它就又把信号散发给自己的观测者了。\n","title":"RxSwift 中的几种 Subject","type":"post"},{"content":"","date":"2019-02-26","externalUrl":null,"permalink":"/tags/subject/","section":"Tags","summary":"","title":"Subject","type":"tags"},{"content":"","date":"2019-02-26","externalUrl":null,"permalink":"/tags/variable/","section":"Tags","summary":"","title":"Variable","type":"tags"},{"content":" 问题背景 # 最近在修改某个 Mac 应用，其原理就是通过执行一段 AppleScript 获取 OmniFocus 的信息，然后进行可视化展示，但是总取不到数据。\n原因 # 关键数据获取阶段执行的 AppleScript 的时候总是取不到数据。\nset theProgressDetail to \u0026#34;\u0026#34; tell application \u0026#34;OmniFocus\u0026#34; tell front document set theModifiedProjects to every flattened project repeat with a from 1 to length of theModifiedProjects set theCompletedTasks to (every flattened task of (item a of theModifiedProjects) where its number of tasks = 0) if theCompletedTasks is not equal to {} then repeat with b from 1 to length of theCompletedTasks set theProgressDetail to theProgressDetail \u0026amp; completion date of (item b of theCompletedTasks) \u0026amp; return end repeat end if end repeat set theInboxCompletedTasks to (every inbox task where its number of tasks = 0) repeat with d from 1 to length of theInboxCompletedTasks set theProgressDetail to theProgressDetail \u0026amp; completion date of (item d of theInboxCompletedTasks) \u0026amp; return end repeat end tell end tell display dialog theProgressDetail return theProgressDetail 在 Cocoa 中是通过执行如下代码执行：\nlet myAppleScript = \u0026#34;I am a piece of applescript\u0026#34; if let scriptObject = NSAppleScript(source: myAppleScript) { var error: NSDictionary? let output = scriptObject.executeAndReturnError(\u0026amp;error) } 进行 Debug 启动 App 或者 Archive 安装包执行可执行文件，均无法正常获取执行结果，直接报错。 具体错误原因：\n\u0026#34;NSAppleScriptErrorMessage\u0026#34; : \u0026#34;Not authorized to send Apple events to OmniFocus.\u0026#34; 很显然，没有权限执行该脚本来发送 Apple Event 给 OmniFocus 从而执行对应操作。\n针对 Apple Event 的权限控制， 是 macOS Mojave 系统带入 macOS 的，Apple 把权限控制进一步收缩，并且要求开发者显示告知用户你的 App 要干什么。当然 iOS 开发同学肯定很清楚，我们无论是拿摄像头，读取相册，甚至是 FaceID 等都是要明确的在 App 中声明的，也就是要在 Info.plist 文件中声名的。\n对于 macOS Mojave 系统而言，新增了针对 Apple Event 的权限控制，需要开发者在 plist 文件中增加 NSAppleEventsUsageDescription 信息，并且明确告知用户 Apple Event 会用来做什么事情。这也是去年 WWDC 2018 Session \u0026ldquo;Your Apps and the Future of macOS Security\u0026rdquo; 中提到的，所有给其他 App 发送 AppleEvent 的 Cocoa App 均会受到影响，这就是 AppleEvent sandboxing。 如果你不添加该声明字段，App 就默认不进行任何显示授权行为并且默认无权限进行相关操作，也就是本文前面出现的情况。\n话不多说，在 info.plist 中加入 NSAppleEventsUsageDescription 字段，并且填入具体权限声明,\n再次启动 App 之后就会有如下弹窗提示：\n前面执行 Apple Script 的方法会同步等待授权结果，用户点击允许之后，App 就能正常获取数据了。\n参考文档 # WWDC 2018 Session 702\n","date":"2019-02-21","externalUrl":null,"permalink":"/post/appleevents-usage-description/","section":"Posts","summary":"问题背景 # 最近在修改某个 Mac 应用，其原理就是通过执行一段 AppleScript 获取 OmniFocus 的信息，然后进行可视化展示，但是总取不到数据。\n","title":"Apple Event Sandboxing","type":"post"},{"content":"","date":"2019-02-21","externalUrl":null,"permalink":"/tags/sandbox/","section":"Tags","summary":"","title":"Sandbox","type":"tags"},{"content":"","date":"2019-01-28","externalUrl":null,"permalink":"/tags/safari/","section":"Tags","summary":"","title":"Safari","type":"tags"},{"content":"","date":"2019-01-28","externalUrl":null,"permalink":"/tags/shadowsocks/","section":"Tags","summary":"","title":"Shadowsocks","type":"tags"},{"content":"","date":"2019-01-28","externalUrl":null,"permalink":"/tags/traffic/","section":"Tags","summary":"","title":"Traffic","type":"tags"},{"content":" 2019-01-29 更新 # 早上一到办公室连接上公司网络，网速直接就飚到了 600KB/s，果不其然，还是 com.apple.Safari.SafeBrowsing.Service 这个服务进程。这就尴尬了，看来并不是说你不用 Safari 就不会触发。\n直接把 Surge 规则中的 Rule 由 Direct 改成了 Reject。\n问题追踪 # 在 2018 年 12 月底的时候，偶然间登录 AgentNEO 查看刚买的流量使用情况。但是发现，刚在前几天购买的 SS 流量在短短的 4 天之内就耗了将近 95GB，就在 1 月 1 日元旦当天就耗了接近 75GB，要知道，我平时的月均流量也就维持在 10 GB 不到，毕竟平时在公司办公，业余时间 Youtube 也看的相对少。\n在最初看到这个统计数据之后，我一度怀疑是他们网站的流量统计有问题。\n后续几天，我实际上对流量耗损比较关注，有一天注意到，Surge 的流量监控速率一直持续不断的显示达五六百 KB/s 的下载流量，但是我知道自己当时并没有下载任何东西。\n打开 Surge Dashboard，看到如下的情形：\n可以看到当时在持续不断的进行下载动作的进程名称是 com.apple.Safari.SafeBrowsing.Service，看请求地址是 safebrowsing.googleapis.com。\n从进程名字来看应该是 Safari 和安全流量相关的服务，通过 Snitch 的数据库查到对其的说明：\nSafari has built-in support for Google’s Safe Browsing service to identify fraudulent and unsafe websites. Right before Safari navigates to a certain website, the website gets checked for possible security concerns using Google’s Safe Browsing online database. Accessing the online database requires connections to Google servers.\n说的大概是该服务是针对 Safari 浏览器启用的，在 Safari 要帮你导航到下一级页面的时候，会识别该页面是否是欺诈🐶或者不安全⚠️的网站，我们在 Safari 的 Security 菜单中可以找到启用关于欺诈网站的检测功能的开关。以我本机上看到的内容如下\nSafari uses Tencent Safe Browsing and Google Safe Browsing to identify fraudulent websites.\n大概意思就是 Safari 使用腾讯的安全浏览服务和 Google 的安全浏览服务来鉴别欺诈网站，腾讯应该是本地化的产物。\n通过针对该进程的出口请求 host 也可以验证这一点，该进程发起的网络请求会有如下两个 host 出口：\nsafebrowsing.googleapis.com safebrowsing.urlsec.qq.com 其实在 iOS 设备上也有同样的进程来做这件事情，抓包有时候也能捕捉到这两个请求（国行）：\n而这次出问题的就是 google 提供的欺诈网站特征库。 尝试关闭 Surge 作为系统流量代理之后，看到 Activity Monitor 中进程又开始了下载，如下图所示，只是连接请求主体从原来 Surge 切换到了独立进程而已（Surge 会接管网络流量，因此之前该任务的下载会算到 Surge 的头上），但是针对该 host 的下载任务一致持续不断。\n说起来，Little Snitch Network Monitor 这款软件也是当时为了查流量丢失问题，才买的。\n而在我关闭 Surge 作为代理之前，可以看到该进程大概在 7 个小时之内耗了 12.4 GB 的流量。\n而在网络上目前未看到针对 safebrowsing 进程的大量讨论，国内论坛 V2EX 里也看到有人遇到该问题。\n目前暂时不清楚是官方 Bug 还是我电脑安装了什么插件或者软件导致。暂时先停掉使用 Safari 了，用 Chrome 用上一段时间之后再用 Snitch 看下情况吧。\n另一方面，因为不放心，在 Surge 的自定义规则中加了一条：\nNAME,com.apple.Safari.SafeBrowsing.Service,DIRECT 针对该进程的所有流量都直连，不用代理了。 后续有任何进展会更新到 Blog 中。\n","date":"2019-01-28","externalUrl":null,"permalink":"/post/%E5%85%B3%E4%BA%8E%E4%B8%80%E6%AC%A1-ss-%E6%B5%81%E9%87%8F%E4%B8%A2%E5%A4%B1%E7%9A%84%E8%BF%87%E7%A8%8B%E8%AE%B0%E5%BD%95/","section":"Posts","summary":"2019-01-29 更新 # 早上一到办公室连接上公司网络，网速直接就飚到了 600KB/s，果不其然，还是 com.apple.Safari.SafeBrowsing.Service 这个服务进程。这就尴尬了，看来并不是说你不用 Safari 就不会触发。\n","title":"关于一次 SS 流量丢失的过程记录","type":"post"},{"content":"","date":"2019-01-03","externalUrl":null,"permalink":"/tags/cocoapods/","section":"Tags","summary":"","title":"CocoaPods","type":"tags"},{"content":"","date":"2019-01-03","externalUrl":null,"permalink":"/tags/module/","section":"Tags","summary":"","title":"Module","type":"tags"},{"content":" 问题描述 # 我们在开发线上诊断工具需求的时候，是以单个 Pod 的形式提供支持，并且代码文件中只有纯 Swift 文件，但是其中需要用到系统的 C 库的一些功能，本次就是使用了系统 C 库中 resolv.h 这个文件来进行 DNS 解析所用。\n当后期 Pod 功能完善之后，在 Example 工程中也已经编译通过之后，接入主项目中之后遇到了下面这个编译错误：\n具体文字错误信息如下：\n/Users/chen/Repos/Work/ZHDiagnosisTool/ZHDiagnosisTool/Classes/Core/Network/ZHDiagnosisTool-Network-Header.h:8:10: Include of non-modular header inside framework module \u0026#39;Diagnosis.ZHDiagnosisTool_Network_Header\u0026#39;: \u0026#39;/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.1.sdk/usr/include/resolv.h\u0026#39; 大概的意思就是在 Umbrella Header 中加入的 Bridging header 指定了 include 一个 C 文件，而该 C 文件本身并不是 Modular Header， 因此编译无法通过。这就涉及了目前 iOS 生态中普遍使用的 Modules 概念。\n几个概念 # Modules # Modules 的概念是 XCode 5 带到 iOS 开发者面前的，从那时起 LLVM 编译器就已经内在支持了，具体关于其历史在 WWDC 2013 Session笔记 - Xcode5和ObjC新特性 这篇文章中已经讲解的非常详细了，不再赘述，总结一句话就是：\nModules 是被引入 LLVM 层面的，用以解决之前 C/C++ 系中 #include 和 #import 带来的引用泛滥以及编译时间过长的问题的一种手段。\n尤其是在 Swift 引入之后，Module 的概念应该已经深入人心。大家可能已经习惯直接使用 @import 来引用某个 Module，或者其中某个功能单元。尤其是使用 CocoaPods 集成开发的时候，其内部实际上也是做了一些 module 的工作，在 Pods 目录中充斥着 .modulemap 的身影，想必大家经常看到。\n而 一个 module 的属性是由定义它的 .modulemap 文件来决定的，其简单语法大概如下：\nmodule module_name [system] { header \u0026#34;header.h\u0026#34; link \u0026#34;linked_library\u0026#34; export * } 如果对其中 Modules 的语法感兴趣，可以到 Clang 的官方文档下通读下，你肯定会对 Modules 这一套有更清楚的认识。\nUmbrella Header # 说起来，Umbrella Header 是在 Framework 的概念被引入的，你可以理解为一个模块均存在一个 Umbrella Header 用来将那些你想暴露给模块外界调用的头文件包裹在一起。避免使用者在使用该模块的时候需要手动输入多个 Header 的一种解决方案。 其实 Mac 端很早就有这个概念，iOS 中特指 iOS 8 开始官方加入 Dynamic Framework 以后的概念。\n如下所示，没有 Umbrella Header 的情况下你需要将所有需要引入的头文件依次写出。\n#import \u0026lt;XYZModule/XYZCustomCell.h\u0026gt; #import \u0026lt;XYZModule/XYZCustomView.h\u0026gt; #import \u0026lt;XYZModule/XYZCustomViewController.h\u0026gt; 在使用了 Umbrella Header 之后，你只需要下面一行即可。\n#import \u0026lt;XYZModule/XYZModule.h\u0026gt; 当然，还存在 umbrella framework，感兴趣大家可以到官方文档下观看。\nBridging-Header # 桥接文件是在 Swift 推出之后，Apple 引入iOS 生态的用以桥接 Swift 和 Objective-C 相互调用的一种方式 （Mix and Match），如下图所示，如果在 Swift 中想使用 Objective-C 类定义的内容，就需要建立 Bridging Header，然后在其中定义你想要暴露的 OC 头文件。\n具体的操作在官方文档有针对在同一个 App Target 和在同一个 Framework 内两种情况均有说明。\n在梳理这些概念的过程中，发现自己之前对桥接文件有个误区，如果你在 App 的主 Target 中需要进行 OC 和 Swift 的混编，使用 Bridging Header 是必选，如果你是在同一个 Framework 中进行的混编，Bridging-Header 是并不需要的，你需要做的只是在 Umbrella Header 里加入你想暴露给 Swift 文件使用的 OC 头文件即可，之前自己一直是显式的建立一个 Bridging-Header，然后在 Umbrella Header 中引入该头文件，想来这种方式只是将你想暴露在外的头文件进行了二次包裹而已，因为 Cocoapods 是将所有 public header 均加入到 umbrella header 里了，因此单个 framework 内部开发，即使没有 bridge header 也是能够将符号暴露给 Swift 的。\nCocoapods # 我们每次执行 pod install 的时候，如果你使用 —verbose 来看详细安装过程就能看到，针对每一个将要被安装的 Pod 均会执行生成 module map 和 umbrella header 这两个阶段，如下所示部分 Install 指令的代码，代码位于 lib/cocoapods/installer/xcode/pods_project_generator/aggregate_target_installer.rb\ndef install! UI.message \u0026#34;- Installing target `#{target.name}` #{target.platform}\u0026#34; do native_target = add_target create_support_files_dir create_support_files_group create_xcconfig_file(native_target) if target.host_requires_frameworks? create_info_plist_file(target.info_plist_path, native_target, target.version, target.platform) create_module_map(native_target) create_umbrella_header(native_target) elsif target.uses_swift? create_module_map(native_target) create_umbrella_header(native_target) end # Some Code # Some Code # Some Code end 很明确，Cocoapods 在 install 某个 Pod 的时候会执行创建 module map 文件以及 umbrella header 文件的工作。详细的代码大家可以直接到 Cocoapods 源码下 lib/cocoapods/generator 目录下看，分别是 module_map.rb 和 umbrella_header.rb 文件。\n其中在各自文件的注释部分也有针对生成文件的说明，比如 module_map.rb 中说明了 module map 的作用，\nGenerates LLVM module map files. A module map file is generated for each Pod and for each Pod target definition that is built as a framework. It specifies a different umbrella header than usual to avoid name conflicts with existing headers of the podspec.\n在 umbrella_header.rb 中说明了 umbrella header 的作用，\nGenerates an umbrella header file for clang modules, which are used by dynamic frameworks on iOS 8 and OSX 10.10 under the hood. If the target is a +PodTarget+, then the umbrella header is required to make all public headers in a convenient manner available without the need to write out header declarations for every library header.\n问题原因 # 首先解答：为什么 Example 工程能够编译通过 而用了主工程是无法编译通过？\n因为目前主工程都要求我们自行开发的 pod 以动态 framework 的形式参与，但是在 Example 工程开发的时候我们的 podfile 并未指定 use_frameworks! 用以产出动态 framework，因此未暴露出该问题，也就是说只有以 framework 的形式的 module 才会有该问题，准确的说，应该是 Dynamic Framework\n了解了以上概念之后，我们再来看我们的问题，首先我们的问题是在 framework 内部调用系统 C 库代码出现的问题，从 Xcode 的报错，我们知道我们通过 bridging header 引入的 C 文件并不以 module 的形式存在，因此编译器报错。\n解决过程 # 其实吧，大家都能想到，使用 Objective-C 做个封装，或者干脆直接调用 C 文件的类用 OC 重写不就完了么，可是如果下次你再遇到了呢？ 或者要改造了一个纯 Swift 库呢？知其所以然，才能避免再次落坑吧。\n因为 Objective-C 自身编译器是帮你做了 modular 化的，当然，如果你选择了前者，还有个限制，你并不能把上面 C 的头文件放到你的 Objective-C 的头文件中，因为本质上，最后这个头文件还是要暴露给 Umbrella Header 的。\n首先，能否允许编译器支持在 module 中引入非 modular 的头文件呢？\nXcode 在 build setting 中提供了 Allow Non-modular Includes In Framework Modules 来控制是否允许在当前 framework 中支持非 modular 头文件引入，其并不适用于纯 Swift 项目，而且即使适用，其会导致所有用到该 framework 的大的编译单元都不能再使用 module 形式引入头文件了，也就是必须适用平坦式的 #include \u0026quot;\u0026quot; 形式。\n最后，通过只能从根源上来解决该问题了，既然需要引入的文件是 modular header，我们就要想办法来将其包裹为 module。 在查找解决方案的过程中发现，就在 Swift 刚推出的时候，大家已经遇到这个问题了，遇到最多的就是CommonCrypto 经常被使用到的 C 库，常见的 MD5 计算就是其提供的 API，但是在前几年官方并未将该动态库 module 化，因此导致你无法在 Swift 文件中直接使用类似 #import \u0026lt;CommonCrypto/CommonCrypto.h\u0026gt; 这种写法。自然而言多了很多解决方案，如下出自 StackOverflow 上 Importing CommonCrypto in a Swift framework 的帖子下面就提供了多种解决方案：\n还是那句话，你是可以通过 Objective-C 做桥接来达到同样效果的。毕竟 Swift 本身就是一门心思想抛开 C 的历史包袱，因此想拥有一个纯纯的 Swift 代码不沾染一点 C 文件气息，你就要做一些工作。\n其中根本问题就是为 CommonCrypto 这个 C 编译单元定义 module，上面也提到了 LLVM 是通过 modulemap 文件来识别的，所以只要通过 .modulemap 来定义即可，\nmodule CommonCrypto [system] { header \u0026#34;/usr/include/CommonCrypto/CommonCrypto.h\u0026#34; export * } 定义的 modulemap 自然可以放到任意目录，只要让编译单元在编译的时候能够搜索的到即可，Xcode 选项的 Build Settings 下的 Swift Compiler - Search Paths 。添加 .modulemap 文件所在路径即可。在编译的时候 LLVM 自然会查找到 .modulemap 文件自动生成 Module 信息。你此时就可以在使用 CommonCrypto 的地方使用 modular header 了。\n过了几年之后，官方才将该库定义为 module，在 Xcode 自带的 iOS SDK 中 /usr/include/CommonCrypto 下可以看到 module.modulemap 文件：\nmodule CommonCrypto [system] [extern_c] { umbrella header \u0026#34;CommonCrypto.h\u0026#34; export * module * { export * } module Error { header \u0026#34;CommonCryptoError.h\u0026#34; export * } module Random { header \u0026#34;CommonRandom.h\u0026#34; export * } } 大家也注意到了在 CommonCrypto 的同级目录中实际上还有很多的系统 C 库代码，并且也有一个 module.modulemap 文件，我裁剪一段代码大家看下：\nmodule Compression [system] [extern_c] { header \u0026#34;compression.h\u0026#34; export * link \u0026#34;compression\u0026#34; } module Darwin [system] [extern_c] [no_undeclared_includes] { // Headers that are repeatedly included, and therefore should not be // assigned to any given module. exclude header \u0026#34;_structs.h\u0026#34; exclude header \u0026#34;sys/_structs.h\u0026#34; // C standard library module C { textual header \u0026#34;assert.h\u0026#34; module setjmp { header \u0026#34;setjmp.h\u0026#34; export * } module signal { header \u0026#34;signal.h\u0026#34; export * } module stdio { header \u0026#34;stdio.h\u0026#34; export * } } module zlib [system] [extern_c] { header \u0026#34;zlib.h\u0026#34; export * link \u0026#34;z\u0026#34; } module SQLite3 [system] [extern_c] { header \u0026#34;sqlite3.h\u0026#34; link \u0026#34;sqlite3\u0026#34; explicit module Ext { header \u0026#34;sqlite3ext.h\u0026#34; export * } export * } 这也是为什么我们在 Swift 代码里可以直接使用类似 import Darwin.C.stdio 写法的原因。但是还是存在一些 C 库并没有被定义为 module，而本次用来做 DNS 解析的 resolv.h 就是其中一员。\n具体到我们解决思路里来看就是要解决 modulemap 如何定义，如何和我们目前使用的 cocoapods 整合的问题了。既然是在作为 pod 进行开发，自然是需要将 module map 文件加入到 pod 的代码管理中去。比如将其放置于单个 pod 根目录下，然后在 podspec 中配置好路径以便编译的时候 Swift Search Paths 中有它。自然，我们可以建立 resolv.modulemap 如下：\nmodule Resolv [system] { header \u0026#34;/usr/include/resolv.h\u0026#34; export * } 然后在对应的 podspec 文件中配置 build setting 中的参数，如下所示\ns.subspec \u0026#39;Core\u0026#39; do |core| core.source_files = [\u0026#39;ZHDiagnosisTool/Classes/Core/**/*\u0026#39;] core.preserve_paths = \u0026#39;ZHDiagnosisTool/Classes/Core/ModuleMap\u0026#39; core.pod_target_xcconfig = { \u0026#39;SWIFT_INCLUDE_PATHS[sdk=macosx*]\u0026#39; =\u0026gt; \u0026#39;$(PODS_ROOT)/ZHDiagnosisTool/Classes/Core/ModuleMap\u0026#39;, \u0026#39;SWIFT_INCLUDE_PATHS[sdk=iphoneos*]\u0026#39; =\u0026gt; \u0026#39;$(PODS_ROOT)/ZHDiagnosisTool/Classes/Core/ModuleMap\u0026#39;, \u0026#39;SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*]\u0026#39; =\u0026gt; \u0026#39;$(PODS_ROOT)/ZHDiagnosisTool/Classes/Core/ModuleMap\u0026#39;, } end 可是，问题出在 PODS_ROOTS 这个路径上，如果我们进行本地开发，该路径就是 Cocoapods 执行完毕之后生成的临时 Pods 目录，并不是原始 podspec 文件所在的地方，而官方也表明了，不会对这种 Local Pods 特定提供一个环境变量来获取， 如下链接可以看到：\nlocal pod development on a project that includes libraries · Issue #809 · CocoaPods/CocoaPods\n如图中，指定 $(PODS_ROOT) 路径实际上在开发 Local Pod 的时候就会找不到，所以就需要按照上面链接中的方式自行手动拼接路径，所以，我们尽可能不用 PODS_ROOT 这个路径，需要另外找一个不会因为 Pod 位置而变化的路径。\n最终参考了以下 repo 中工程的解决方案，\nonmyway133/Arcane\n其中作者使用 Pod 的 script phase 来完成 .modulemap 的操作，这样会更加灵活。\n这样，针对我们自己本次的需求来看，.podspec 中的 script 如下所写即可：\n# Create FRAMEWORK_DIR=\u0026#34;${BUILT_PRODUCTS_DIR}/RESOLV.framework\u0026#34; if [ -d \u0026#34;${FRAMEWORK_DIR}\u0026#34; ]; then echo \u0026#34;${FRAMEWORK_DIR} already exists, so skipping the rest of the script.\u0026#34; exit 0 fi mkdir -p \u0026#34;${FRAMEWORK_DIR}/Modules\u0026#34; echo \u0026#34;module RESOLV [system] { header \\\u0026#34;${SDKROOT}/usr/include/resolv.h\\\u0026#34; export * }\u0026#34; \u0026gt; \u0026#34;${FRAMEWORK_DIR}/Modules/module.modulemap\u0026#34; # Generate fake header... [ -d \u0026#34;${FRAMEWORK_DIR}/Headers\u0026#34; ] || mkdir \u0026#34;${FRAMEWORK_DIR}/Headers\u0026#34; touch \u0026#34;${FRAMEWORK_DIR}/Headers/resolv.h\u0026#34; # Soft link C header to local framework Headers ln -sf \u0026#34;${SDKROOT}/usr/include/resolv.h\u0026#34; \u0026#34;${FRAMEWORK_DIR}/Headers/resolv.h\u0026#34; 每次在编译运行前，我们会自行创建一个 Resolv.framework，其中 Header 目录中我们放一个空白文件并且软链接到真正想要链接的头文件上，然后在该 framework 中创建 modulemap 文件。\n之后，我们在 .podspec 中指定 script phase 为编译前即可，如下所示，\ns.script_phase = { :name =\u0026gt; \u0026#39;Resolv\u0026#39;, :script =\u0026gt; script_above, :execution_position =\u0026gt; :before_compile } 这样，通过 Resolv.framework 的桥接，Swift 代码中就可以直接通过 import RESOLV 来使用了。当然，链接这一环你可以通过以下两种形式达到：\n直接在 podspec 文件中指定依赖，如 core.library = \u0026quot;resolv\u0026quot; 在 .modulemap 文件中显式的 link，如下所示： module Resolv [system] { header \u0026#34;/usr/include/resolv.h\u0026#34; link \u0026#34;resolv\u0026#34; export * } 而且通过将 module map 文件放置于 product 产出目录这里也解决了路径指定的问题，因为 product folder 是默认在编译的搜索路径下的。\n总结 # 总结来看，就是想要在纯 Swift 的项目中引入系统 C 库文件（这里指的是未被 modular 化的文件，因为有些 C 文件已经被系统默认封装成了 module 了）。以上只是一些简单的概念讲解，整个编译系统以及套件都是长时间不断演化的结果，这里也只是简单的讲述，有一些概念实际上也只是点到为止，大家如果感兴趣可以多找一些相关资料阅读下，亲自试一试。\n参考资料 # Modules LLVM - 维基百科，自由的百科全书 Modules - Clang 8 documentation WWDC 2013 Session笔记 - Xcode5和ObjC新特性 Importing CommonCrypto in a Swift framework Adding CommonCrypto to custom Swift framework |Apple Developer Forums How to call C code from Swift - The.Swift.Dev. Using a C library inside a Swift framework - Swift and iOS Writing - Medium ","date":"2019-01-03","externalUrl":null,"permalink":"/post/swift-and-modules/","section":"Posts","summary":"问题描述 # 我们在开发线上诊断工具需求的时候，是以单个 Pod 的形式提供支持，并且代码文件中只有纯 Swift 文件，但是其中需要用到系统的 C 库的一些功能，本次就是使用了系统 C 库中 resolv.h 这个文件来进行 DNS 解析所用。\n","title":"在 Swift Framework 中使用 C 文件的过程探索","type":"post"},{"content":"","date":"2018-12-21","externalUrl":null,"permalink":"/categories/ios/","section":"Categories","summary":"","title":"IOS","type":"categories"},{"content":"","date":"2018-12-21","externalUrl":null,"permalink":"/tags/keychain/","section":"Tags","summary":"","title":"Keychain","type":"tags"},{"content":"","date":"2018-12-21","externalUrl":null,"permalink":"/tags/userdefaults/","section":"Tags","summary":"","title":"UserDefaults","type":"tags"},{"content":"Apple 提供了几种持久化方案，其中 UserDefaults 和 Keychain 是 App 开发过程中使用频率最高的方案，而且从以往和同事的探讨过程中发现对这两个概念中有一些细节还是理解不太透彻，因此本文会针对这二者展开讲一讲。\nUserDefaults # 首先，阅读完 Apple 关于 UserDefaults 一节的文档描述之后，我觉得有两个需要注意的点：\nUserDefaults 的构成 UserDefaults 的目的 UserDefaults 构成 # 我们可以看看具体 UserDefaults 存储的地方，如下图，我们在应用中写入 Demo 数据：\n打印 Library 路径，在 Finder 中打开路径:\n如下展示内容，可以看到在 Preferences 目录中存在某个文件，例如该 App Library 目录下存放的文件为 app.chen.ios.PersistenceDemo.plist\n该 plist 内容如图所示，即为我们之前在 App 中写入的 Key-Value。\n所以这也证明了，我们针对 UserDefaults 的读写实质上是针对 plist 文件的读写。\niOS 系统会自动帮你做 plist 文件的读入以及 Cache，根据情况会把你在内存中所做的操作同步到 plist 文件（UserDefaults 同步到内存是同步的，同步到 Database 是异步的， iOS 8 开始，会有一个常驻进程 cfprefsd 来负责同步）。\n所以你会看有iOS 面试题目会问题： 系统的 UserDefaults 的本质以及和 plist 文件的直接读写的区别？（这题目太 TM 偏了。。。）\nUserDefaults caches the information to avoid having to open the user’s defaults database each time you need a default value. When you set a default value, it’s changed synchronously within your process, and asynchronously to persistent storage and other processes.\n所以，实质上，你是可以直接针对 UserDefaults 的最终产物 plist 文件进行操作的，当然，这是有风险的，而且无法保障正常使用的。官方在文档中也提醒了开发者。\nDon’t try to access the preferences subsystem directly. Modifying preference property list files may result in loss of changes, delay of reflecting changes, and app crashes. To configure preferences, use the defaults command-line utility in macOS instead.\n那既然本质上 UserDefaults 是使用 plist 文件进行存储，那也要求了我们能存储的 Value 只能支持 plist 所支持的格式，例如 String，Number，Array，Data，Date 等，当然如果你要存储自定义的类，其需要遵守 Codable 协议（实质也是要归档为 Data）\nUserDefaults 目的 # The defaults system allows an app to customize its behavior to match a user’s preferences. For example, you can allow users to specify their preferred units of measurement or media playback speed. Apps store these preferences by assigning values to a set of parameters in a user’s defaults database.\n一般我们在 UserDefaults 存储的数据都是用户的某些配置项，不因为用户使用过程中出现意外而丢失，比如是否开启了日夜间模式了，是否开启了大图模式了等等。或者存储一些对安全方面不敏感的数据\n不建议往 UserDefaults 里存储较大的数据，例如直接存储一张图片。而对于这种需要存储较大文件的需求，你可以将文件本身存储到本地，而 UserDefaults 里只存储该文件的路径。\n毕竟在 App 启动之后，UserDefaults 会进行 IO ，读取本地 plist 文件，因此一定程度上，也会较少 App 启动之后 UserDefaults API 针对 plist 文件 IO 的时间，纯属个人揣测，没有经过实验验证。\n关于 Extension 中 UserDefaults 的应用 # iOS 上 Extension 和 Host App 之间做数据共享也是通过 UserDefaults，开启 App Group 之后，这二者就可以修改同一份配置。具体 Extension 和 Host App 之间的内在关系可以先看下 Apple 文档。本质上如下所示，\nExtension 和 Host App 之间通过 Shared Container 来做数据共享，该 SharedContainer 私有，因此不存在于单一 App 的沙盒内。应该是系统会单独开辟一块空间用以做共享数据的存储。\nAfter you enable app groups, an app extension and its containing app can both use the NSUserDefaults API to share access to user preferences. To enable this sharing, use the initWithSuiteName: method to instantiate a new NSUserDefaults object, passing in the identifier of the shared group.\n因为 UserDefaults 既然是暴露在本地能够访问的文件当中的，因此不要在 UserDefaults 里存储任何 Security-Sensitive 的数据。 如果要存储保密级别较高的数据，就要用到另外一种持久化方案 ── Keychain\nKeychain # 关于 Keychain，是 Apple 提供给开发者用来存储 Security-Sensitive 的数据了，比如登录密码，用户标识，加密数据等等。 官方示意图如下，其实中间 Keychain 的 API 还进行了 Decrypt 和 Encrypt 的动作。\nApple 提供的 Keychain API 大部分都是 C 语言写成，使用起来相对不便，因此基本上我们都会使用二次加工过的库，比如知乎用的就是 SMKeychain。\nKeychain 中的数据完全交由系统保管并加密过( AES 128 in GCM (Galois/Counter Mode))的，因此能够保证安全性。\n如果对加密这一块感兴趣，可以看下苹果的白皮书中 Keychain data protection 这一节。\n另外，Keychain 的数据并不存放在 App 的 Sanbox 中，即使删除了 App，资料依然保存在 keychain 中。如果重新安装了app，还可以从 keychain 获取数据。\n同一个 App 内部共享 # 类似 UserDefaults ，Keychain 也支持 Extension 和 host app 之间的共享，需要在每个 Target 的 Capability 下开启 Keychain Sharing 功能，并且设置为同一个 Group。\n不同 App 之间共享 # 和 UserDefaults 不同的是，Keychain 的数据是可以跨 App 获取的，但是限于一个开发者的 App，也就是需要确保这些 App 所属的 Team ID 是相同的。\n这个也是App 进行 SSO 登录的基础，比如知乎日报使用知乎主 App 登录也是基于此原理。\n在需要进行 Keychain 共享的 App 内开启 Keychain Sharing 的能力，所属同一个 Team ID 下的 App 本身都可以读取该 Keychain 的内容，类似于 AppGroup，这里也会产生 Keychain Group。\nAllows this application to share passwords from its keychain with other applications made by your team.\n参考文档 # https://developer.apple.com/documentation/foundation/userdefaults https://www.reddit.com/r/jailbreak/comments/2qpqi9/ios_812_has_protection_against_plist_file_editing/ http://iphonedevwiki.net/index.php/PreferenceBundles https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html https://developer.apple.com/documentation/security/keychain_services https://github.com/soffes/SAMKeychain https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps ","date":"2018-12-21","externalUrl":null,"permalink":"/post/userdefaults-and-keychain/","section":"Posts","summary":"Apple 提供了几种持久化方案，其中 UserDefaults 和 Keychain 是 App 开发过程中使用频率最高的方案，而且从以往和同事的探讨过程中发现对这两个概念中有一些细节还是理解不太透彻，因此本文会针对这二者展开讲一讲。\n","title":"UserDefaults and Keychain","type":"post"},{"content":"","date":"2018-11-26","externalUrl":null,"permalink":"/tags/2do/","section":"Tags","summary":"","title":"2Do","type":"tags"},{"content":"","date":"2018-11-26","externalUrl":null,"permalink":"/tags/bookmark/","section":"Tags","summary":"","title":"Bookmark","type":"tags"},{"content":"","date":"2018-11-26","externalUrl":null,"permalink":"/tags/omnifocus/","section":"Tags","summary":"","title":"OmniFocus","type":"tags"},{"content":"","date":"2018-11-26","externalUrl":null,"permalink":"/tags/things/","section":"Tags","summary":"","title":"Things","type":"tags"},{"content":"使用鼠标拖拽下面这个链接到你的 Favorites Bar 上，默认点击行为实际上是模拟在当前页面进行导航，而目标地址由 avascript:window.location 来指定，目标地址实际上就是各个 App 的 URL Scheme。 你可以在 AppTalk查看各个主流 App 的 URL Scheme。\n2Do # Send to 2Do\n然后编辑地址，将其替换为如下：\njavascript:window.location=\u0026#39;twodo://x-callback-url/add?task=\u0026#39;+encodeURIComponent(document.title)+\u0026#39;\u0026amp;note=\u0026#39;+encodeURIComponent(window.location)+\u0026#39;\u0026amp;action=url:\u0026#39;+encodeURIComponent(window.location) 所以，本质上是执行了一段 JS 代码，结合当前文档的上下文信息，调用 URL Scheme，其中\ndocument.title 为标题 window.location 为当前页面链接 Things # 我们知道 Things 的 URL Scheme 的规则如下：\n/// add a todo due when tomorrow things:///add?title=iamtitle\u0026amp;notes=iamnotes\u0026amp;when=tomorrow Add a todo to Things\n比如你可以把链接内容改为支持 Send To Things：\njavascript:window.location=\u0026#39;things:///add?title=\u0026#39;+encodeURIComponent(document.title)+\u0026#39;\u0026amp;notes=\u0026#39;+encodeURIComponent(window.location)+\u0026#39;\u0026amp;when=today\u0026#39; OmniFocus # 同理，可以有 Send To OmniFocus ，如下：\njavascript:window.location=\u0026#39;omnifocus:///add?note=\u0026#39;+encodeURIComponent(window.location)+\u0026#39;\u0026amp;name=\u0026#39;+encodeURIComponent(document.title) Add a todo to OmniFocus\n或者直接去 OmniFocus 页面 用相同方式把起已经设置好正确 Value 的链接拖到上方工具条上即可。\n如果你找不到 Favorites Bar，点击 Safari 菜单栏上的 View 菜单就能看到（中文系统叫视图）\n","date":"2018-11-26","externalUrl":null,"permalink":"/post/send-to-2do/","section":"Posts","summary":"使用鼠标拖拽下面这个链接到你的 Favorites Bar 上，默认点击行为实际上是模拟在当前页面进行导航，而目标地址由 avascript:window.location 来指定，目标地址实际上就是各个 App 的 URL Scheme。 你可以在 AppTalk查看各个主流 App 的 URL Scheme。\n","title":"如何制作 Send to 2Do 的 Safari 书签","type":"post"},{"content":"","date":"2018-10-16","externalUrl":null,"permalink":"/tags/devonthink/","section":"Tags","summary":"","title":"DEVONThink","type":"tags"},{"content":"","date":"2018-10-16","externalUrl":null,"permalink":"/tags/marginnote/","section":"Tags","summary":"","title":"MarginNote","type":"tags"},{"content":"","date":"2018-10-16","externalUrl":null,"permalink":"/tags/pdfexpert/","section":"Tags","summary":"","title":"PDFExpert","type":"tags"},{"content":"最近统一了一下自己的阅读流程，更新一下，具体用到的工具有\nDEVONThink Pro PDFExpert MarginNote 其中 1 是输入和归档源，3 是输出源。 大概就是如下所示：\n采集素材 # 在平时浏览网页的时候，看到自己喜欢的文章之后，想进行一些标记或者做笔记，大概有几个选择：\n印象笔记直接 clip 当前网页，之后在印象笔记里进行阅读或者标记 放进稍后读服务中，比如 Pocket 或者 Instapaper 等； 用 Safari 自带的 Save as PDF 之后再用单独的 PDF 工具查看； 而最近几个月，因为 MarginNote 3 的发布，由于其提供的大纲脑图的自动生成，给我提供了一种便利的拆书功能，因此会把平时看到的文章都归集到一处来看。\n借助 DEVONthink 的 Clip To DEVONthink 的插件可以很方便的把文章归集到 DEONThink 中，最主要的是因为它能保存 WebArchive，即使后续文章被删除也没关系。\n当按下 Cmmd + S 进行归档之后，DEVONThink 对应 Group 里就会把该原始页面保存起来。\n由于 WebArchive 无法做 Highlight 标记，这时候就用到了 DEVONthink 提供的 OCR 功能，将 WebArchive 提取为 PDF 文件。\n二次加工处理 # 当你使用 DEVONthink 生成 PDF 之后，就可以直接用其自带的 PDF 阅读工具进行阅读了，它默认提供工具已经完全满足，不过我想利用 MarginNote 来阅读，因此这时候你可以选择直接发送到 MarginNote 来阅读。\n其实更多时候，我会选择先使用 PDF Expert 进行 PDF 文件的裁剪，因为大多数时候网页正文内容两侧留白太多，想裁剪掉。\n裁剪掉多余的部分之后，pdf 文件就可读性也会极大提高。 阅读输出 # 到了这一步，我们就可以把文档导入到 MarginNote 中，如图所示： 使用 Add to Study，将自己的标记和笔记生成笔记本，MarginNote 强大就在于能够很好的自动帮你把重点归集到一起，生成脑图关联，你还可以针对某个部分做笔记。\n而且很多时候，我们看一篇文章很大可能会展开这个主题，比如看下该文章下给的参考链接，这些往往都是同样主题的内容，我们希望相关的文章能够整体归集到一个笔记本下。MarginNote 可以很方便做到，比如下图，我在阅读 Apple 官方 URL Session Programming Guide 的时候会查看很多相关文章，也都是相关内容，我会把笔记整理在一起。\n我单独在 MarginNote 里建了一个 Folder ，叫 Article。专门阅读我从 DEVONthink 中采集到的文章。阅读完毕，还可以将阅读笔记归档，MarginNote 支持分享到 印象笔记，Word 或者 DEVONthink 进行归档。\n到了这里，基本上对我来讲一个 输入 ── 处理 ── 阅读 ── 输出 的整个流程就结束了，基本上是一个闭环。\n上面提到的三个工具，均有 iOS 版本，也就意味着即使拿着手机也能够不破坏整个阅读环节，不过其中 DEVONthink 提供的 iOS 版本是没办法进行 WebArchive 向 PDF 的转换的，处理这一步最后还是得放在 Mac 端处理，不过解决了输入和输出也就意味着随时随地可以捕获想看的内容放到稍后读列表，然后自动同步到 Mac 端进行处理，然后在 MarginNote 对应的 iOS 版本，平时在家的时候还是习惯用 iPad Pro 查看，体验也不错。\n","date":"2018-10-16","externalUrl":null,"permalink":"/post/reading-process/","section":"Posts","summary":"最近统一了一下自己的阅读流程，更新一下，具体用到的工具有\nDEVONThink Pro PDFExpert MarginNote 其中 1 是输入和归档源，3 是输出源。 大概就是如下所示：\n","title":"阅读流程","type":"post"},{"content":"","date":"2018-07-17","externalUrl":null,"permalink":"/tags/singleton/","section":"Tags","summary":"","title":"Singleton","type":"tags"},{"content":"","date":"2018-07-17","externalUrl":null,"permalink":"/categories/translation/","section":"Categories","summary":"","title":"Translation","type":"categories"},{"content":"原文：Testing Swift code that uses system singletons in 3 easy steps\n原作者 @johnsundell\n大部分在 Apple 平台开发的 App 都会依赖基于单例的 API。从 UIScreen 到 UIApplication，再到 NSBundle，而 Foundation，UIKit 以及 AppKit 里到处充斥着静态的 API。\n尽管单例非常方便，并且随时随地都可以很轻易的获取到特定的 API，但是当它们一旦要面临代码解耦和测试的时候就会出现挑战。单例同时也是平时遇到 Bug 里最常见的一种，在单例中状态会以共享的方式终结，那些针对状态做的变更并不能很好的广播到整个系统层面。\n然而，尽管我们能够重构自己的代码，让它们只在真正需求的地方使用单例，我们针对系统 API 也做不了太多。但是，好消息是，这里有一些技巧让你使用系统单例的代码依然变得容易管理和测试。\n让我们看一些使用了 URLSession.shared 单例的代码：\nclass DataLoader { enum Result { case data(Data) case error(Error) } func load(from url: URL, completionHandler: @escaping (Result) -\u0026gt; Void) { let task = URLSession.shared.dataTask(with: url) { (data, response, error) in if let error = error { return completionHandler(.error(error)) } completionHandler(.data(data ?? Data())) } task.resume() } } 上面这个 DataLoader 现在就变得很难测试，因为它内部会自主调用共享的 URL Session 来执行一个网络调用。这就需要我们为测试代码中增加一些等待和超时代码，然后很快这部分代码就会变得糟糕和不稳定。\n抽象成一个协议 # 我们第一个任务就是把我们需要的 URLSession 的部分移到一个协议里，这样我们在测试中也能够很容易的进行 mock。在作者的一次 talk中作者建议尽可能的避免 mock，尽管它是一种很好的策略。但是当你和系统的单例打交道的时候，mock 就是一个增加可预测性的重要工具。\n让我们创建一个 NetworkEngine 协议，然后让 URLSession 符合它:\nprotocol NetworkEngine { typealias Handler = (Data?, URLResponse?, Error?) -\u0026gt; Void func performRequest(for url: URL, completionHandler: @escaping Handler) } extension URLSession: NetworkEngine { typealias Handler = NetworkEngine.Handler func performRequest(for url: URL, completionHandler: @escaping Handler) { let task = dataTask(with: url, completionHandler: completionHandler) task.resume() } } 如上所见，我们使得 URLSessionDataTask 成了 URLSession 的一个实现细节。这样，我们就避免了在测试代码中不得不创建不同的 mock ，而只需要关注 NetworkEngine 就行。\n协议中把单例作为默认值 # 现在，让我们更新我们的 DataLoader 来使用新的 NetworkEngine 协议，把它作为依赖项注入。我们把 URLSession.shared 作为默认参数传递，以此做到了向后兼容并且和之前一样方便。\nclass DataLoader { enum Result { case data(Data) case error(Error) } private let engine: NetworkEngine init(engine: NetworkEngine = URLSession.shared) { self.engine = engine } func load(from url: URL, completionHandler: @escaping (Result) -\u0026gt; Void) { engine.performRequest(for: url) { (data, response, error) in if let error = error { return completionHandler(.error(error)) } completionHandler(.data(data ?? Data())) } } } 通过使用默认参数，我们依然可以像之前一样很容易的生成 DataLoader，而不需要提供一个 NetworkEngine。\n在你的测试中 Mock 协议 # 最后，让我们写个测试，在这里我们通过 mock NetworkEngine 使得我们的测试快速，可预测并且容易维护。\nfunc testLoadingData() { class NetworkEngineMock: NetworkEngine { typealias Handler = NetworkEngine.Handler var requestedURL: URL? func performRequest(for url: URL, completionHandler: @escaping Handler) { requestedURL = url let data = “Hello world”.data(using: .utf8) completionHandler(data, nil, nil) } } let engine = NetworkEngineMock() let loader = DataLoader(engine: engine) var result: DataLoader.Result? let url = URL(string: “my/API”)! loader.load(from: url) { result = $0 } XCTAssertEqual(engine.requestedURL, url) XCTAssertEqual(result, .data(“Hello world”.data(using: .utf8)!)) } 如上所见，作者试图让 mock 尽可能的简单。不需要创建包含大量逻辑的复杂 mock，你只需要让自己的代码返回一些 hardcode 的值通常是一个好主意，这样，你能够在自己的测试中进行断言。然而，风险就是你可能最后测试的是自己的 mock 而不是生产环境的代码。\n最后 # 我们现在通过以下三个步骤让这些依然方便的使用了系统的单例可被测试：\n抽象协议 协议中把单例作为默认值 在你的测试中 Mock 该协议 ","date":"2018-07-17","externalUrl":null,"permalink":"/post/testing-swift-code-that-uses-system-singletons-in-3-easy-steps/","section":"Posts","summary":"原文：Testing Swift code that uses system singletons in 3 easy steps\n原作者 @johnsundell\n大部分在 Apple 平台开发的 App 都会依赖基于单例的 API。从 UIScreen 到 UIApplication，再到 NSBundle，而 Foundation，UIKit 以及 AppKit 里到处充斥着静态的 API。\n","title":"三个简单步骤让你测试使用系统单例的代码","type":"post"},{"content":"原文：Avoiding singletons in Swift 原作者 \u0026amp; Copyright @johnsundell\n“我知道单例不好，但是\u0026hellip;”，这是开发者常常在讨论代码的时候会提到的。貌似社区大家有共识 ── 单例不好。但是同时，包括 Apple 和第三方的 Swift 开发者还是在 App 内部或者共享的 frameworks 里不断在用它们。\n这周，让我们好好看一看单例的问题到底在哪里，探索更多的技巧以便我们日常能够避免使用它们。这就开始吧！\n为什么单例这么流行？ # 首先一点我们要想的是问问自己，为什么单例这么流行？热过大多数开发者都认同，单例应该被避免的话，为什么它们还不断的出现呢？\n我认为有两部分原因。\n我觉得日常开发 Apple 平台上的 App 时候大量使用单例，最大的原因是苹果自己也在经常的用。作为第三方开发者来讲，我们常常会把 Apple 的做法作为最佳实践来用，任何 Apple 自己常常使用的模式在社区也会发展的很迅速。\n这个谜题的第二个原因是单例非常方便。单例因为它可以在任何地方获取到，因而经常会扮演一个获取特定核心值或者对象的快捷方式。可以看下下面这个例子，在这个例子中，我们想在 ProfileViewController 中展示当前登录用户的名字，同时当某个按钮按下的时候，退登当前用户。\nclass ProfileViewController: UIViewController { private lazy var nameLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() nameLabel.text = UserManager.shared.currentUser?.name } private func handleLogOutButtonTap() { UserManager.shared.logOut() } } 按照上面这样，把一个 UserManager 单例的用户和账户处理的功能封装起来，确实很方便（而且很常见！），那这种模式的使用到底不好在哪里呢？\n为什么单例会很糟糕？ # 当讨论类似模式和架构这些东西的时候，很容易陷入一个特别理论化的圈套。虽然让我们的代码理论上“正确”，遵循最佳实践和原则是很美好的，但是现实经常会很受打击，我们需要寻找一些中间方案。\n所以，关于单例引起的实质性问题是什么呢？为什么它们要应该被避免呢？这里我倾向于三个原因：\n这些单例是全局的可变的共享状态。它们的状态是在整个 App 里自动共享的，因此当其状态发生不可预期的变化的时候，Bug 也就可能开始发生了； 单例之间的关系以及依赖这些单例的代码常常不那么好定义。因为单例是如此方便和容易获得，大量的使用它们常常会导致很难维护面条式代码，这些代码中对象和对象之间没有清晰的隔离； 管理这些单例的生命周期非常的难。因为单例本身会在应用的整个生命周期内存活，管理它们就变得异常困难，因此常常会不得不依赖可选值来跟踪值的变化。这也使得依赖单例的代码变得很难测试，因为你没法在一个测试 case 里把状态重置。 在我们前面的 ProfileViewController 的例子中，我们已经看到了有这三个问题的信号出现。首先它依赖 UserManager 导致这二者关系很不清晰，其次，它不得不让 currentUser 作为可选值出现，因为我们没办法在编译期就能够确定视图控制器实际上出现的时候这个数据一定在。听起来就感觉要有 bug 发生的感觉 😬!\n依赖注入 # 相对于使用 ProfileViewController 通过单例来获取它所需的依赖项，我们要在其初始化方法中将依赖项传入。这里我们是将当前的 User 作为非可选传入的，同样，传入一个 LogOu\u0010tService 来进行登出操作：\nclass ProfileViewController: UIViewController { private let user: User private let logOutService: LogOutService private lazy var nameLabel = UILabel() init(user: User, logOutService: LogOutService) { self.user = user self.logOutService = logOutService super.init(nibName: nil, bundle: nil) } override func viewDidLoad() { super.viewDidLoad() nameLabel.text = user.name } private func handleLogOutButtonTap() { logOutService.logOut() } } 这样的话，代码就会变得更加清晰，并且容易管理。我们的代码现在安全的依赖其 Model，并且有一个清晰的 API 进行登出的交互。一般情况下，将各种单例以及管理器重构成清晰分离的服务能够使得一个 App 的核心对象之间的关系更加清晰。\nServices # 举个例子，让我们更近一些来看下 LogOutService 这个类是如何实现的。它内部也对其依赖的服务使用了依赖注入，并且提供了一个优雅的，定义清晰的 API 来只做一件事情 ── 登出。\nclass LogOutService { private let user: User private let networkService: NetworkService private let navigationService: NavigationService init(user: User, networkService: NetworkService, navigationService: NavigationService) { self.user = user self.networkService = networkService self.navigationService = navigationService } func logOut() { networkService.request(.logout(user)) { [weak self] in self?.navigationService.showLoginScreen() } } } 翻新 # 从一个重度使用单例的设置到完全使用服务，依赖注入以及本地状态来改造会非常的困难，会花费大量时间。而且，会很难认为花大量时间在这上面是合理的，而且有些时候可能会需要一次更大规模的重构才行。\n谢天谢地，我们可以使用相近的技术，在Testing Swift code that uses system singletons in 3 easy steps这篇文章中也使用到过，其允许我们可以以一种非常容易的方式单例开始。和很多其他解决方案类似 ── 协议来救场！\n我们不是一次性重构所有的单例，而是创建 Service 类，我们可以很简单的把我们的 Service 定义成 Protocol，如下所示：\nprotocol LogOutService { func logOut() } protocol NetworkService { func request(_ endpoint: Endpoint, completionHandler: @escaping () -\u0026gt; Void) } protocol NavigationService { func showLoginScreen() func showProfile(for user: User) ... } 然后我们就能通过将单例符合我们新创建的 Service 协议来使其翻新为一堆 Service。在很多情况下，我们甚至不需要改变任何实现，只是简单的传递它们 shared 实例作为一个 Service 即可。\n同样的技巧也能够被用作重构我们 app 中其他核心对象，那些对象我们也许也都正在以某种单例形式在使用着，例如使用 AppDelegate 来做导航。\nextension UserManager: LoginService, LogOutService {} extension AppDelegate: NavigationService { func showLoginScreen() { navigationController.viewControllers = [ LoginViewController( loginService: UserManager.shared, navigationService: self ) ] } func showProfile(for user: User) { let viewController = ProfileViewController( user: user, logOutService: UserManager.shared ) navigationController.pushViewController(viewController, animated: true) } } 我们现在通过使用依赖注入以及 Service 的方式，开始使得我们工程处在 Singleton free 的状态，而不需要进行特别巨大的工程改造和重写。接下来我们就能够使用 Service 或者其他类型的 API 逐个替换掉单例，比如说使用这篇博文中的技巧。\n结论 # 单例也不是都是不好的，只是在很多情况下，它们会引发一系列问题，这些问题可以通过使用依赖注入的方式为你程序里的对象建立良好的关系来解决。\n如果你现在正在开发的 App 里在使用大量的单例，那你一定也在经历或者已经经历过它们所带来的 bug 了吧。希望这篇文章能够给你一些灵感让你没有那么慌乱的开始远离它们。\n","date":"2018-07-16","externalUrl":null,"permalink":"/post/avoiding-singletons-in-swift/","section":"Posts","summary":"原文：Avoiding singletons in Swift 原作者 \u0026 Copyright @johnsundell\n“我知道单例不好，但是…”，这是开发者常常在讨论代码的时候会提到的。貌似社区大家有共识 ── 单例不好。但是同时，包括 Apple 和第三方的 Swift 开发者还是在 App 内部或者共享的 frameworks 里不断在用它们。\n","title":"避免在 Swift 中使用单例","type":"post"},{"content":"原文：Different flavors of dependency injection in Swift\n原作者 \u0026amp; Copyright @johnsundell\n在之前的几篇博客中，我们已经了解了几种使用依赖注入方式使得某个 swift app 拥有一个更加解耦可测试的架构。比如在 在 Swift 中使用工厂模式进行依赖注入 中和工厂模式结合，以及在避免在 Swift 中使用单例中替换程序中的单例对象等方式进行依赖注入。\n到目前为止，大部分我的博文以及例子中都使用了基于初始化的依赖注入方式。然而，就像大部分的编程技巧一样，还有很多 “口味” 的进行依赖注入的方式 ── 每一种都有其优缺点。 这周，让我们来看看其中三种方式以及如何在 Swift 中使用它们。\n基于初始化方法 # 让我们快速回顾一下用的最为广泛的依赖注入方式 ── initializer-based。这个想法是在某个类被初始化的时候传入其所需的依赖的方式。这种形式最大的好处就是，它保证了能够立即满足该类完成其功能所需要的所有东西。\n让我们建一个类 ── FileLoader 来从磁盘加载文件。为了实现该 Loader 的功能，需要两个依赖项，系统所提供的实例对象 ── FileManager 和 Cache。使用基于初始化的依赖注入的执行代码如下所示：\nclass FileLoader { private let fileManager: FileManager private let cache: Cache init(fileManager: FileManager = .default, cache: Cache = .init()) { self.fileManager = fileManager self.cache = cache } } 可以关注下初始化方法里默认参数的设置，其可以避免每次都需要自行创建的问题。这样能够简化我们在生产环境下使用 FileLoader 类创建文件加载器的工作，而且也能够能够传入 mock 数据或者我们的测试代码中的创建的实例进行测试。\n基于属性 # 虽然前一种基于初始化的依赖注入形式常常给我们自定义类带来很多好处，但是有时候，当你不得不继承自某个系统类的时候会遇到一些困难。其中一个例子就是，当我们构建 view controller，尤其是你在使用 XIB 以及 Storyboard 类定义它们的时候，因为此时你不再能够掌控它们的初始化方法了。\n对于这些情况，基于属性的依赖注入方式是一个更好的选择。相对于初始化的时候进行对象的依赖项注入，基于属性的形式可以在之后通过简单的赋值来做到，其能够让你减少模板代码的书写，尤其是在你确实没有必要进行注入的时候能够有更好的默认实现。\n让我们再来看一个例子，在这个例子中，我们在构建一个 PhotoEditorViewController ，这个视图控制器使得用户编辑它们照片库中的图片。为了达到这个功能，该视图控制器需要用到系统提供的 PHPhotoLibrary 类型的一个实例（该类本身是一个单例）以及我们自己实现的类 PhotoEditorEngine 的一个实例。那为了不通过自定义初始化方法进行依赖注入的话，我们可以创建一些具有默认值的可变属性，例如下面这样：\nclass PhotoEditorViewController: UIViewController { var library: PhotoLibrary = PHPhotoLibrary.shared() var engine = PhotoEditorEngine() } 使用 Testing Swift code that uses system singletons in 3 easy steps 这篇文章中的手法，通过使用一个协议提供一个更为抽象的 PhotoLibrary 接口来获取系统的图片库。这样会使得测试和 Mock 数据特别的容易。\n上面这些工作比较好的是我们依然能够通过简单的给视图控制器来赋值，从而在我们的测试中简单的注入 Mock 代码：\nclass PhotoEditorViewControllerTests: XCTestCase { func testApplyingBlackAndWhiteFilter() { let viewController = PhotoEditorViewController() // Assign a mock photo library to gain complete control over // what photos are stored in it let library = PhotoLibraryMock() library.photos = [TestPhotoFactory.photoWithColor(.red)] viewController.library = library // Run our testing commands viewController.selectPhoto(atIndex: 0) viewController.apply(filter: .blackAndWhite) viewController.savePhoto() // Assert that the outcome is correct XCTAssertTrue(photoIsBlackAndWhite(library.photos[0])) } } 基于参数 # 最后一种，让我们来看下基于参数的依赖注入形式。这种方式尤其在你想让旧有代码更加可测试，而不需要对现存的结构进行更多改动的时候尤其有效。\n许多时候，我们仅需要某个特定的依赖项一次，或者我们仅仅需要在某些特定条件下进行 mock。不需要改变某个对象的初始化方法或者暴露可变属性（通常都不是一个好主意），我们可以开发出一个接受一个依赖项作为参数的特定 API。\n让我们看一个 NoteManager 类，该类是某个笔记类应用的一部分。它的工作就是管理所有用户已经书写的笔记，提供一个 API 让用户能够查询笔记。考虑到这个操作可能耗时（\u0010如果用户有许多笔记，通常情况下是这样），我们把该动作正常放置于一个后台线程执行，如下：\nclass NoteManager { func loadNotes(matching query: String, completionHandler: @escaping ([Note]) -\u0026gt; Void) { DispatchQueue.global(qos: .userInitiated).async { let database = self.loadDatabase() let notes = database.filter { note in return note.matches(query: query) } completionHandler(notes) } } } 尽管上面的代码在我们的生产环境里也算是一个很好的解决方案，但在测试中我们正常情况下是想避免一些异步代码的，尽可能的平行化以避免 flakiness. 如果要类似于基于初始化或者基于属性的依赖注入能够指定一个显式的队列提供给 NoteManager 来用的方式，需要对该类进行很多改变，这是我们在当下无法做或者不愿意做的。\n这时候，基于参数的依赖注入方式的实现就能够达到。相对于不得不对整个类进行重构，我们通过插入队列相关代码使得 loadNotes 方法在该队列上执行：\nclass NoteManager { func loadNotes(matching query: String, on queue: DispatchQueue = .global(qos: .userInitiated), completionHandler: @escaping ([Note]) -\u0026gt; Void) { queue.async { let database = self.loadDatabase() let notes = database.filter { note in return note.matches(query: query) } completionHandler(notes) } } } 这样就使得我们能够很容易的在测试代码中使用某个定制的我们可等待的队列。这样也几乎使得我们把上面的接口转变成了一个同步接口，使得一切变得更加容易和可预测。\n另外一个基于参数的依赖注入的使用案例是当你想测试某个静态的 API 的时候。因为静态的 API 我们是不存在初始化方法的，同时我们理想情况下也是不应该维护任何静态的状态的，那基于参数的依赖注入方式就是一个很好的选择。让我们看一个静态的类 MessageSender， 该类当前依赖某个单例：\nclass MessageSender { static func send(_ message: Message, to user: User) throws { Database.shared.insert(message) let data: Data = try wrap(message) let endpoint = Endpoint.sendMessage(to: user) NetworkManager.shared.post(data, to: endpoint.url) } } 当然，一个长远的解决方案更可能是重构 MessageSender 这个类，让它变成一个非静态的，能够在其所有被使用的地方被正确的注入依赖。但是基于我们能够更容易的测试它（比如，重现或者验证某个 Bug），我们能简单的使用参数作为依赖项进行注入，而不是基于某个单例：\nclass MessageSender { static func send(_ message: Message, to user: User, database: Database = .shared, networkManager: NetworkManager = .shared) throws { database.insert(message) let data: Data = try wrap(message) let endpoint = Endpoint.sendMessage(to: user) networkManager.post(data, to: endpoint.url) } } 我们又再次使用了缺省的参数，不仅仅更加方便而已，更重要的是，能够在 100% 向后兼容的情况下为我们的代码增加测试支持。\n结论 # 所以，哪一种依赖注入的方案是最好的呢？就像大部分时候一样，我的答案很无聊的： 看情况！我在该博客中所视图去做的一件事情就是针对某个特定问题给出需要不同的解决方案。原因很简单，我真的不相信又所谓银弹的存在。我认为，按照我们意愿具备多个工具或者对于特定技巧的多种解决方案能够使我们变得更好，在写代码的时候也能够游刃有余。\n","date":"2018-07-16","externalUrl":null,"permalink":"/post/different-flavors-of-dependency-injection-in-swift/","section":"Posts","summary":"原文：Different flavors of dependency injection in Swift\n原作者 \u0026 Copyright @johnsundell\n在之前的几篇博客中，我们已经了解了几种使用依赖注入方式使得某个 swift app 拥有一个更加解耦可测试的架构。比如在 在 Swift 中使用工厂模式进行依赖注入 中和工厂模式结合，以及在避免在 Swift 中使用单例中替换程序中的单例对象等方式进行依赖注入。\n","title":"Swift 中几种不同的依赖注入方式","type":"post"},{"content":"","date":"2018-07-11","externalUrl":null,"permalink":"/tags/app/","section":"Tags","summary":"","title":"App","type":"tags"},{"content":"","date":"2018-07-11","externalUrl":null,"permalink":"/tags/mac/","section":"Tags","summary":"","title":"Mac","type":"tags"},{"content":"记录一下自己常用的 Mac Apps，每次重装之后到了用的时候发现忘记安装了(不是每次都用 TimeMachine 恢复)，而且强烈简易大家如果某个 App 有对应的独立安装版本的话，购买其独立安装版本为好，沙盒环境以及和 Apple ID 绑定的特性就使得功能性以及灵活度大减。\n更新时间 2018-10-07 更换图床，更新图片\n更新时间 2018-08-10 不久前，已经订阅了一年的 Setapp，发现上面好多 App 自己都之前买过了，Sigh！ 如果你使用 Ulysses 或者 MoneyWiz 3.0 的话，订阅 Setapp 就会非常的值得！\n办公工具 # 离开这些工具，貌似无法办公的。\nAirMail # 下载地址\n邮箱客户端，看中 AirMail 主要是因为其强大的第三方服务集成。\nSlack # 下载地址\n公司的日常沟通交流主要战场，其实整个操作体验我是不太适应的，不过它强大在第三方服务的集成上，可以做很多自动化的事情。\nFantastical 2 # 下载地址\n日历工具，查看满满的一屏幕日程，你会感觉很舒服的。\nTunnelblick # 下载地址\n公司需要使用该 App 来接入办公室网络。远程办公必备。\nOffice 三件套 # 微软的 Office 三件套，Word，Excel，PowerPoint。 用的频率不高，但是用的时候得有。\nGTD # 日常 GTD 工具，基本就是各家评测常提起的几款 App。\n2Do # 下载地址\n最近才完全转向 2Do，这个 GTD 工具是我最早买入的，但是后来因为其自动化太弱，被打入冷宫。最近几个月又开始捡起来了，吸引我的几个点：\nColorful Tab Mail AutoCapture Smart List 还有一点就是，其同步目前稳定的就只能使用 Dropbox 了，在国内基本对大部分人就是个障碍。\nThings 3 # Things 2 之前也有，不过因为功能太简单，基本处于买了看看的状态，后来 Things 3 的出现主要看中它的设计元素。\nOmniFocus 3 # 使用它最主要的原因是它支持 AppleScript 比较方便。尤其是结合 Keyboard Maestro 来用太方便。比如我会建立一些定时任务，每周五下午下班前自动生成一张周报表单，列出来本周完成的 Task 等。\n最近刚又从 2 升级到 3 代，半价折扣 39.99$\n3 主要将之前 Context 更换为支持多属性的 Tag，也算补充了一个竞争点吧。\n系统工具 # 基本是系统缺少的或者功能难以满足日常使用的一些 App。\n1 Blocker # 下载地址\niStat Menus # 下载地址\nBartender 3 # 下载地址\nSurge # Surge\n最近 Yachen Liu 发布了 Surge 3 的 Mac 版本预览版本\n实质上是个网络调试工具，但是大部分情况下被用来作为爱国上网工具了。 而且定价策略也是引发了很多口水战，不过商品嘛，如果你觉得不值就不买就行了，不必要想用还骂他定价高，对吧。\nWallpaper Wizard # 下载地址\nMacPaw 家的壁纸应用，限时免费的时候入的。\nTinkerTool # 下载地址\n一个系统增强工具，可以对 Dashboard 和 Docker 等进行定制化修改。\nCapto # 下载地址\n截屏和录制视频工具。\n效率工具 # 1Password 7 for Mac # 下载地址\nAlfred 3 # 下载地址\nLaunchBar 6 # 下载地址\nBetterTouchTool # 下载地址\nKeyboard Maestro # 下载地址\nYoink # 下载地址\nUnclutter # 下载地址\nToothFairy # 下载地址\nPopClip # 下载地址\nCopied # 下载地址\n一款剪贴板工具，应该是目前 Mac 上最强大的剪贴板工具了。\n写作 # Day One # 下载地址\n日记应用，之前买了 iOS 和 Mac 双版本，改成订阅之后自动升级为 Plus 会员了，功能已经完全足够。最近的 Premium 会员增加了音频日记以及 Dark Theme 的主题了。\nUlysses # 下载地址\n这个完全是冲动消费，就是想看看大家捧杀这么狠到底优秀在哪里，说实话其自定义的 Markdown 格式我还是用不太习惯，不过用了每几次。从非订阅用到了订阅\nMWeb # 推荐安装其非 MAS 版本，下载地址\n前一阵子刚又升级到了 3.0 版本，虽然 UI 上和 Ulysses 差得远，不过因为本地化做的好，各项服务接入的比较完整，比如图床，静态网站自动生成等都是比较吸引人的地方。\nNotability # 下载地址\nGoodNotes # 下载地址\n![GoodNotes](http://7xilk1.com1.z0.glb.clouddn.com/2018-07-13-2400x2400bb -2-.jpg)\nMindNode 5 # 下载地址\n阅读 # DEVONThink Pro Office # 下载地址\n已经完全把 DENONThink 家的产品作为构建我知识图谱的工具了。必备。\nReeder 3 # 下载地址\nMarginNote 3 # 下载地址\n开发工具 # IDE # Xcode # 话不多说，老老实实的从 App Store 下载吧。\nAppCode # 下载地址\nAppCode 强大就强大在 Jetbrain 团队一贯的 Refactor 功能。不过在 15寸 中配 MacBook Pro 上依然会时不时卡顿，不清楚时不时因为其基于 JVM 那一套所致。推荐个人开发者或者项目不太庞大的团队或者个人使用。\n编辑器 # Visual Studio Code # 下载地址\n目前当之无愧最好使的编辑器了，当然可以作为大量脚本语言的轻量 IDE 使用了。\n代码管理 # Tower # Tower 是一款多平台支持的 Git GUI，使用命令行虽然某些程度上比较快，也显得 Geek 范儿，但是在查看 Diff 等功能上确实不太直观，因此一款 Git 的 UI 工具显得很重要。Tower 是一款收费软件，最近 3.0 转为了订阅式。刚订阅了一年基础版再接着用，毕竟切换工具的代价有时候蛮大的。\n下载地址\nOhMyStar # 下载地址\nSnippetsLab # 下载地址\n设计 # Sketch # 下载地址\nSip for Mac # 下载地址\nPaintCode # 下载地址\n主要针对一些复杂的设计图形，用代码绘制会非常复杂，比如一个需要使用 Bezier 绘制复杂的图形，如果人工画，费时费力。使用 PaintCode 能够解决这个痛点，支持 Objective-C 和 Swift 两种语言。\n结合 Playground 的话你感受一下。\nEagle # 设计师用来收集素材所用\n调试 # Charles # 下载地址\n使用公司的序列号，手机版本我也入了，不过在 iOS 生态里其并没有 Mac 上在同等功能上的地位，在 iOS 上我更喜欢使用 surge 或者 Thor 来抓包，而后者的功能基本上目前是市面上最完整的 App 了。 但是在 Mac 平台，Charles 的地位依然无人撼动。\nPaw # 下载地址\n强大的 API 调试工具\nReveal # 下载地址\n其他 # Monodraw # 下载地址\n代码注释加强工具，可以用来对代码进行注释。\nCuteBaby # Model 序列化工具，解决的痛点就是后端给你一个 JSON 结构，你需要自行映射成工程中的类，这个重复工作有时候会让您炸掉，因为有的接口返回的结构复杂，字段繁杂。不过网络上有很多开源的项目其实也可以做到一定程度。\n社交 # Telegram # 下载地址\nTweetbot 3 # 下载地址\n其他 # RescueTime # 买了 Premium 会员，记录我日常在 Mac 上的工作记录。已经记录了大概有 2 年多的数据了，很直观。\nPCalc # 也算是老牌的计算器了\n","date":"2018-07-11","externalUrl":null,"permalink":"/post/my-favorite-mac-apps/","section":"Posts","summary":"记录一下自己常用的 Mac Apps，每次重装之后到了用的时候发现忘记安装了(不是每次都用 TimeMachine 恢复)，而且强烈简易大家如果某个 App 有对应的独立安装版本的话，购买其独立安装版本为好，沙盒环境以及和 Apple ID 绑定的特性就使得功能性以及灵活度大减。\n","title":"那些我恢复 Mac 系统之后要安装的 Apps","type":"post"},{"content":"","date":"2018-07-10","externalUrl":null,"permalink":"/tags/compiler/","section":"Tags","summary":"","title":"Compiler","type":"tags"},{"content":"","date":"2018-07-10","externalUrl":null,"permalink":"/tags/ir/","section":"Tags","summary":"","title":"IR","type":"tags"},{"content":"","date":"2018-07-10","externalUrl":null,"permalink":"/tags/llvm/","section":"Tags","summary":"","title":"LLVM","type":"tags"},{"content":" 什么是 LLVM？隐藏在 Swift，Rust，Clang 等语言背后的奥秘 # 了解编译器是如何生成机器原生代码会使得倒腾新语言或者加强已经存在的编程语言变得比以往更加容易了。\n新的编程语言，针对现存语言的优化如同雨后春笋般产生。 Mozilla 的 Rust， Apple 的 Swift，JetBrain 的 Kotlin等语言给开发者提供了在速度，安全性，便捷性，可移植性以及性能等方面更多的选择。\n为什么是这个时间点呢？一个重大的原因是那些构建具体语言相关的新式工具的出现。在这一堆工具中，最重要的就是 LLVM，其全称是 Low-Level Virtual Machine，它是一个开源项目，其最早是由 Swift 语言的发明人 Chris Lattner 在伊利诺斯州州立大学期间的一个研究项目而来。\nLLVM 不仅仅使得创建新式语言更加容易，也使得针对现存编程语言进行增强完善更加便捷。它提供了一堆工具使得创造语言的过程中需要的那些令人头疼的事情变得自动化：创建一个编译器，输出代码需要适配多平台和架构，编写代码处理语言当中通常都存在的那些比较晦涩的部分，比如异常。LLVM 的自由开发许可使得它可以被自由的作为一个软件组件或者被部署成服务来使用。\n使用了 LLVM 的编程语言花名册种有很多熟悉的名字。Apple 的 Swift 语言使用 LLVM 作为其编译器框架，Rust 语言使用 LLVM 作为其工具链中的一个核心组件。同样，许多编译器也有其 LLVM 版本，例如 Clang，一个 C/C++ 编译器，它是和 LLVM 功能特别相近的一个项目。还有 Kotlin 语言，其名义上是一门 JVM 语言，也正在开发该语言的一个版本： Kotlin Native，其使用 LLVM 编译成机器原生代码。\nLLVM 是什么 # LLVM 最核心的功能就是，可以通过编码方式创造机器原生代码。一个开发者可以使用其 API 创造指令，该指令是一种 intermediate representation 格式，简称 IR。之后，LLVM 能够将 IR 编译成独立的二进制文件，或者对其执行即时编译以运行在另外一个程序，例如某编程语言的解释器的上下文中。\n很多编程语言中很常见的结构体或者模式，LLVM 的 API 都提供了原始语义的支持。例如，几乎所有的语言都有函数或者全局变量的概念，LLVM 在其 IR 中把函数和全局变量作为独立元素定义出来，因此你不需要花太多时间和精力来重复造这些特定的轮子，你只需要使用 LLVM 的实现，专注于你的语言最需要关注的地方即可。\nLLVM: 专为移植性而设计 # 涉及 LLVM 的一点就是，经常在讨论 C 语言的时候被提及： C 语言有时候会被描述为一门可移植的高阶汇编语言，就是因为其具备能被映射成很接近硬件设备的指令，而且它已经被移植到几乎所有的系统架构上。但是 C 语言只能作为一种可移植的汇编语言实际上也有其副作用，这个确实也不是其设计目标。\n作为对比，LLVM 的 IR 从最初就是被设计作为一个可移植的汇编。体现的第一点就是其提供独立于特定机器架构的语义，例如，integer 类型并不会被限制在特定硬件设备上的最大位数（比如 32 位 或者 64 位）。你可以按照你的需要的位数来创建你所需要的 integer 类型，比如 一个 128 位的 integer。你也不需要担心针对特定处理器指令集来再加工，LLVM 已经为了考虑了这些。\n如果你想看到 LLVM IR 长的什么样子，可以去看一下 ELLCC 项目，尝试一下 Demo 直接使用浏览器将 C 代码转换为 LLVM IR。\n编程语言是如何使用 LLVM 的 # 最常用的场景就是作为某一种语言的编译器前端（ahead-of-time (AOT) compiler），除了如此，LLVM 还有其他很多可能性。\n使用 LLVM 进行即时编译 # 一些情形需要我们的代码在运行时即时生成，而不是提前编译好。比如 Julia 语言，JIT 编译其代码，因为其需要运行很快，能够使得用户通过交互式解析器（REPL）或者可交互弹窗来交互。Mono，使用 .Net 实现，能够利用 LLVM 后端编译为机器原生代码。\nNumba，一个为 Python 提供的数学计算加速包，JIT 将选择的 Python 函数编译为机器代码。它也能提前编译 Numba 修饰的代码，但是（例如 Julia），Python 由于其解释性语言的特性能够提供快速的开发工作。使用 JIT 编译能够产出比提前编译更好的 Python 的交互工作流。\n其他的语言还有用非传统方式把 LLVM 作为 JIT 来实验的语言，比如 JIT-Compiling SQL Queries in PostgreSQL Using LLVM，能够达到高达 5 倍的性能提升。\n利用 LLVM 进行自动化的代码优化 # LLVM 不仅仅是将 IR 编译成原生机器代码。你也可以通过编码引导其进行更大粒度的优化工作，整个工作会贯穿整个链接过程。优化工作可以非常激进，包括对函数进行内联，移除死代码（包含无用的类型声明以及函数参数）或者展开循环等。\n这次依然，上面这些你并不需要都自己来做。 LLVM 能够帮你处理这些事情，你也可以按需关闭这些功能。举例来讲，如果你想牺牲一部分性能来减小二进制的大小，你可以使用自己的编译器前端来告知 LLVM 来关闭循环展开。\n使用 LLVM 的领域特定语言 # LLVM 已经被用以为许多跨领域通用计算机语言产生编译器，但是它在生成非常垂直或者解决某些具体问题的语言方面也非常有用。在某些程度上，这才是 LLVM 最闪光的点所在，因为它移除了一大堆创建该语言过程中的单调烦躁的工作，并且使得它表现的更好。\n比如，Emscripten 项目，采用了 LLVM IR 代码，将其转换成 JavaScript，理论上允许任何具备 LLVM 后端的语言可以导出在浏览器中运行的代码。虽然 Emscripten 的长期计划是能够拥有可生成 WebAssembly 的基于 LLVM 的后端，但是它是一个展示 LLVM 灵活性的很好的例证。\n另外一种 LLVM 可以被使用的方式就是为已存在的编程语言增加特定领域的扩展。Nvidia 使用 LLVM 创造了Nvidia CUDA Compiler，其能够让编程语言增加针对 CUDA 的原生支持，而不是通过加载某个库来唤起。\n在多种语言中使用 LLVM # 和 LLVM 打交道的一种典型方式就是找到你很舒服的一种编程语言的代码来体会，当然，这种编程语言要支持 LLVM。\n两种常见选择的语言是 C 和 C++。许多 LLVM 的开发者默认选择这二者之一有以下几个原因：\nLLVM 本身就是 C++ 写的； LLVM 的 API 能够在 C 和 C++ 中无缝使用； 需要语言都倾向于基于 C 和 C++ 进行开发。 尽管，这两种语言并不是唯一的选择。许多语言能够原生调用 C 库的方法，所以理论上是可以使用这些语言来进行 LLVM 开发的，但是其也有助于在某种语言中实际存在一个库，能够优雅的封装 LLVM 的 API。幸运的是，许多语言和运行时都有这些库，包括 C#/.Net/Mono, Rust, Haskell, OCAML, Node.js, Go, 和 Python。\n不过，你需要注意的一点是，这些语言中有一些针对 LLVM 的绑定支持并不完整。以 Python 语言举例，虽然存在多种选择，但是它们都会在完整性和功效性上有差别：\nLLVM 项目下维护有针对 LLVM 的 C 版 API 的绑定集合，但是它们已经不再被维护了；\nllvmpy 在 2015 年停止的维护，这对于任何的软件项目来讲都不是一件好事。在使用 LLVM 的时候更是如此，考虑到每一个版本的 LLVM 所带来的变化数量。\nllvmlite 是由 Numba 的开发团队创建的，已经成为了当下在 Python 中做 LLVM 开发的有力竞争者。它基于 Numba 项目的需求，只实现了一小部分 LLVM 的功能。但是这个功能子集已经能够满足大部分 LLVM 开发者使用了。\nllvmcpy 旨在更新 Python 为 C 库提供的绑定，使得它们能够以自动化的形式保持更新，并且 Python 的原生语法使得它们能够很方便的使用。llvmcpy 依然处于初级阶段，但是已经做了很多和 LLVM API 打交道的基础工作。\n如果你很好奇如何使用 LLVM 库构建一门语言的话，LLVM 的创造者们提供了教程，该教程使用 C++ 或者 OCAML ，指导你从从 0 到 1 创造一门简单的语言 Kaleidoscope，而且这个示例已经被移植到其他语言上了：\nHaskell: 和原始教程最接近的移植；\nPython：一个版本和该教程非常接近，另外一个版本 更激进一些，重写了一个交互命令行。两者都使用了 llvmlite 作为和 LLVM 的绑定；\n\u0010Rust 和 Swift：似乎不可避免，我们会得到这两个语言版本的移植版本教程，因为 LLVM 本身更就是因为这二者才得以创造的。\n最后，这个教程也有其他人类语言的版本。它已经被翻译成中文了，有使用 C++ 的，也有 Python。\nLLVM 做不到哪些 # 以上提到的都是 LLVM 能够提供的功能，它还有一些无法做到的事情，了解一下会比较有用。\n举例而言，LLVM 不会进行语法解析。许多工具，比如 lex/yacc，ﬂex/bison 以及 ANTLR 等都已经做了这些工作。解析语法就意味着要和编译行为进行解耦，所以，LLVM 没有试图去做这部分工作也不意外。\nLLVM 也不会试图解决围绕某种特定语言更大范围的周边行为。你需要自行安装编译器二进制，管理安装过程中的包以及升级工具链。\n最后一点，也是最重要的是，依然又很多语言通用的部分 LLVM 没有原生支持的。许多编程语言都存在垃圾回收内存管理的行为，要么作为管理内存的主要方式，要么作为某种策略，例如 RAII（C++ 和 Rust 语言中使用的）的辅助。LLVM 不提供垃圾回收机制，但是它确实提供一些工具，能够允许代码可以使用元数据来做标记使得编写垃圾收集器更加容易。\n尽管如此，LLVM 还是存在最终添加原生支持执行垃圾回收机制的可能性。LLVM 正在以每 6 个月一个大版本更新在快速发展。而这个发展步速很大程度上因为目前很多主流编程语言已经把其作为它们开发过程的核心一环。\n","date":"2018-07-10","externalUrl":null,"permalink":"/post/what-is-llvm/","section":"Posts","summary":"什么是 LLVM？隐藏在 Swift，Rust，Clang 等语言背后的奥秘 # 了解编译器是如何生成机器原生代码会使得倒腾新语言或者加强已经存在的编程语言变得比以往更加容易了。\n","title":"What is LLVM","type":"post"},{"content":"","date":"2018-05-24","externalUrl":null,"permalink":"/tags/iap/","section":"Tags","summary":"","title":"IAP","type":"tags"},{"content":"对于在 App Store 中上架的应⽤来说，应⽤内购买(In-App Purchase，简称 IAP) 应该是一个避不开的话题，尤其是去年微信打赏和 Apple 之间的争执更让 IAP 火了一把，不仅仅大公司，作为个人开发者来讲，IAP 也是非常重要的，说不定就是你养家糊口的工具呢。\n整个 IAP 的过程，在客户端的实现依赖于 StoreKit 这个框架，所有的支付相关操作都是交由 StoreKit 来完成的。\n首先，我们先复习一下 IAP 的整个流程。\nIn App Purchase 的整体流程 # 阶段 1 # 加载 In-App Identifier 客户端从 AppStore 中获取本地化的商品信息。 把 IAP 的购买界⾯面展示给⽤用户，⽤用户可以同意购买并点击购买按钮。 ⽤用户授权购买，客户端向服务器器发送购买请求。 服务器器处理理购买请求，并把结果返回给 StoreKit。 如果购买请求验证通过，客户端此时解锁内容或者提供金币。 至此，整个交易易流程结束。 阶段 2 # 具体到和 Apple Store 打交道的话，如下所示：\n阶段 3 # 如果涉及到自己 App 端 Server 的参与，基本如下图所示：\n正常多出的几步：\n获取 Product 标识列表； 客户端上传 Receipt 数据给 Server，Server 自行校验或者使用 Apple 的接口校验； Server 端告知给客户端付费内容 注册商品 # 商品注册是通过 iTunes Connect 后台进行的，其中商品类型需要提前明确好。\n商品类型 # 参与 App 内支付动作的商品有四种类型：\nConsumable products （消耗型商品） Non-consumable products （非消耗型商品） Auto-renewable subscriptions （自动续约订阅） Non-renewable subscriptions （非自动续约订阅） 其中关于非自动续约订阅，App 的开发者有义务同步服务到用户的所有设备上。\n如下，是官方文档列出来的四种商品类型的一些主要属性。\nProduct type Non-consumable Consumable Auto-renewable Non-renewing Users can buy Once Multiple times Multiple times Multiple times Appears in the receipt Always Once Always Always Synced across devices By the system Not synced By the system By your app Restored By the system Not restored By the system By your app 其中需要注意的几点：\n除了非消耗型商品只能买一次，再次购买应该会失败以外，其他三种类型（消耗型，连续/非连续订阅）都是可以无限制购买的，Apple 是不会报错的； 除了消耗型商品在 Receipt 中出现一次，其他三种类型（非消耗型，连续/非连续订阅）是始终都会在其中的，这也是后续做订阅校验至关重要的一环； 获取 Product ID # 这里获取的是 Product 在 iTunes Connect 后台注册商品时填写的 Product ID\nIn-App Identifier 为每个可销售的商品提供一个唯一标识。在客户端上，我们可以直接写死代码:\nlet identifiers = [\u0026#34;com.myCompany.myApp.product1\u0026#34;, \u0026#34;com.myCompany.myApp.product2\u0026#34; ] 或者从 Server 端获取。\n获取商品信息 # 然后通过上一步取得的 Product ID 来获取具体某个 Product 的详细信息，包括价格，描述等等。\n// 获取一批商品的信息 let request = SKProductsRequest(productIdentifiers: identifierSet) request.delegate = self request.start() 在代理回调中处理获取的结果，进行展示或者 Cache。\nfunc productRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { for product in response.products { } } 官方建议是不要进行缓存，因为 Product 的信息有可能会更新，比如用户切换 AppStore 的区域，价格信息也会因为相应的汇率发生变化。不过！在国内，被牺牲最大的用户体验不是这种边界情况，而是获取 Product 信息是需要和 Apple Server 打交道的，这就有个问题 —— 慢！\n我们会在开机的时候提前预取所有 Product 信息，并且缓存起来。下次弹出充值面板的时候就直接列出商品信息即可。\n展示支付 UI # 接下来就看什么时候进行购买了，展示商品面板进行购买。用户在面板上进行选择之后，就进入了下一阶段 ── 支付\n发出购买请求 # 这⼀步也很简单， 两⾏行代码就可以搞定。只需要把之前拿到的商品对象传到 SKPayment 的初始化方法中，构造一个 SKPayment 实例，再把这个实例加⼊到购买队列中即可:\nlet payment = SKPayment(product: product) SKPaymentQueue.default().add(payment) 之后就交给 Apple 了，此时应用内会弹出苹果设计的购买窗口，用户只要使⽤指纹或者输入密码即可同意付款。\n处理购买结果 # 当⽤户的购买请求经过 StoreKit 和苹果服务器的验证后，开发者可以在回调函数中接收到。Apple 具体的回调会通过 SKPaymentQueue 来进行，如下，我们需要在支付之前就向 SKPaymentQueue 中加入代理监听。\nSKPaymentQueue.default().add(self) 你可以把具体的监听放在一个独立的类中完成。在该 Observer 中，实现代理方法：\n// 处理理 SKPaymentQueueObserver 事件 // MARK: - SKPaymentTransactionObserver func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions:[SK PaymentTransaction]) { for transaction in transactions { switch transaction.transactionState { case .purchased: // Validate the purchase // Locate the file guard let url = Bundle.main.appStoreReceiptURL else { // Handle failure return } // Read the contents let receipt = Data(contentsOf: url) } } } 对于每一个你提交的 Payment，就一定会有一个相应的 Transaction 生成以便其后续处理。通过检查 transaction 的状态，我们可以指定每种状态下的处理逻辑。如果状态显示已购买， 我们还是应该和⾃己的服务器器进行一次校验，确保交易真实有效⽽而不是通过越狱后的某些插件完成的。被检验的，是一种被苹果称之为收据(receipt)的凭证，就像我们在超市购物或者饭店就餐后拿到的收据一样，每⼀个购买的商品都有⾃己的收据。这个收据由苹果签发，保存在客户端本地。\n目前 Apple 官方提供两种方式，一种是本机校验，一种是 Server 端校验。\nFinal，投递商品 # 当 Server 端检验完成之后，说明本地交易已经成功完成，因此将用户购买的内容提供给用户使用。当然，对于业务方，购买完成不代表业务完成，因此大多数情况下还有一步进行商品业务校验的过程。\n需要注意的一点是：\nApple 也明确告诉开发者有几个关键的路径节点需要注意：\n监听交易状态的代码一定要越早越好，比如在 App 完成启动之后进行注册，确保整个生命周期内都有代码逻辑来处理监听到的交易，根据监听到的 Transaction 的状态来分发处理逻辑。因为很有可能你监听的时机太晚，导致 Apple 通知有一些之前未完成的交易的时候你无法捕获。说白了就是用户的购买流程跟 App 生命周期不挂钩导致的，典型的有以下几个场景\n用户杀死了 App 用户需要更新帐号中的付费信息（此时已跳出 App ） App 闪退 用户进行了订阅续期 用户进入了推介促销价的流程 用户跳出 App 输入推广码 结束交易 # 在最后一步中，如果是正常的交易，那么简单的结束它们就⾏了。但对于其他异常的交易易，⽐比如出现了一些 error，也要妥善的结束它们。如果是⾃动续期的订阅，也会经过这一步。如果 交易没有被正确的结束，它们会一直停留留在上文提到的队列中，每次回调函数被调用时，队列 中都会有这些没有被结束的交易。也就是为什么需要提前监听的原因了。\n结束交易的代码⾮常简单，只有一行:\nSKPaymentQueue.default().finishTransaction(transaction) 关于 Transaction 和 Receipt 的区别和联系 # 很多同学在进行 IAP 开发的时候常常有一个很大的困惑就是 Transaction 和 Receipt 傻傻分不清楚，这里重新梳理了一下。\n关于 Transaction # 其实前面讲 Payment 的操作的时候已经表明了， Transaction 是和具体的一次 Payment 对应的，也就是发生一次 Payment 就会生成 Transaction，但是，并不是 1 对 1 的关系，比如本次 Payment 对应的 Transaction 已经被 finish 了，用户换了手机，登陆同样 AppleID 的时候使用 Restore 功能，Apple 会生成 Transaction 给你，本次的 Transaction 就对应的之前的 Payment，只是状态标记为 .restored。\nTransaction 的状态主要有以下这么几种：\n状态 含义 .purchasing 不需要做什么，继续等待 SKPaymentTransactionState 的状态流转 .purchased 用户已完成付费，处理付费后的流程并调用 finishTransaction 方法 .failed 用户付费失败，处理付费失败的流程并调用 finishTransaction 方法 .restored 用户付费成功，处理付费后的流程并调用 finishTransaction 方法 .deferred 不需要做什么，继续等待 SKPaymentTransactionState 的状态流转 Note: 对于 finishTransaction 的几点说明：\n对于那些依赖苹果下载服务的，比如存储在 iTunes Connect 上的付费内容要下载到本地，如果下载完成之前就进行了 finish 动作会导致 Apple 阻断所有的下载流程，并且无法重新下载； Transaction 需要配合 Receipt 验证是否合法，单纯靠前端是不靠谱的，因此交由业务后端验证 Receipt 更安全合理，业务后端来决定是否需要 finish 文档里说：Your app needs to finish every transaction, regardless of whether the transaction succeeded or failed. 所以我们需要将 Transaction Finish 掉，否则会始终出现在 PaymentQueue 里，BUT，中间态的交易是不需要 finish 的，比如 .purchasing 和 .deferred 关于 Receipt # Receipt 是 App Store 签发的，是你的 App 上和该 App 上发生的支付行为记录。它存储在设备上的某个固定地址，StoreKit 并不会生成它，它是从 App Store 拉取下来的一个文件。如图所示：\nguard let url = Bundle.main.appStoreReceiptURL else { // handle failure return } // read the contents let receipt = Data(contentsOf: url) 如果本地该地址里没有，我们还可以进行刷新，实质上就是从 App Store 请求 Receipt 数据。请求如下：\nlet request = SKReceiptRefreshRequest() request.delegate = self request.start() 这里有几个需要注意的点： # 恢复已经购买的商品 # 这里有一个需要注意的事情就是：如果一个用户试图去购买一个之前购买过的商\u0010品，而不是使用你App 提供的 Restore 功能恢复的话，App Store 会依然创建一个正常的新交易。用户是不会被再次收费的，这里的问题是，Transaction 的状态可不是 restored，而是完全新的 Transaction，你可以按照之前的正常流程走，当然对于自带账户体系的 App 来讲，需要自行判断和计算。\n参考资料： # In App Purchase In-App Purchase Programming Guide Receipt Validation Programming Guide How to detect refunded IAPs from receipts? Check if a non-renewable subscription was refunded by Apple? How does Apple notify iOS apps of refunds of in-app purchases (IAP)? ","date":"2018-05-24","externalUrl":null,"permalink":"/post/in-app-purchase/","section":"Posts","summary":"对于在 App Store 中上架的应⽤来说，应⽤内购买(In-App Purchase，简称 IAP) 应该是一个避不开的话题，尤其是去年微信打赏和 Apple 之间的争执更让 IAP 火了一把，不仅仅大公司，作为个人开发者来讲，IAP 也是非常重要的，说不定就是你养家糊口的工具呢。\n","title":"In App Purchase","type":"post"},{"content":"","date":"2018-05-24","externalUrl":null,"permalink":"/tags/receipt/","section":"Tags","summary":"","title":"Receipt","type":"tags"},{"content":"","date":"2017-11-19","externalUrl":null,"permalink":"/tags/carthage/","section":"Tags","summary":"","title":"Carthage","type":"tags"},{"content":"原文：Modular Xcode projects\n原作者 \u0026amp; Copyright @pepibumur\n翻译：@OgreMergO\n使用 Xcode 构建模块化的工程就需要对工程结构以及其基础概念有很好的理解才行。\n我们平时不怎么关注工程结构本身，只有在工程逐渐变大，要添加更多依赖的时候才会注意的到。而即使到了这个时候，我们大多数的工程都会使用 CocoaPods 来设置那些依赖项，或者 Carthage, 后者虽然没有帮我们做依赖性的设置，但是使得我们会更容易的，通过在工程的 build phase 选项中添加一些内容，达到同样的目的。当配置项越来越复杂，我们就很容易产生困惑，这是因为我们并没有完全掌握 Xcode 工程中所涉及的所有元素。我经常被问到的问题如下：\n我能不能在工程里同时使用 Carthage，Cocoapods 以及自己个人的依赖设置？ 我添加了依赖，但是当模拟器打开 App 的时候 Crash 了。 为什么我需要只在某些 targets 里嵌入 framework？ 我的 framework 应该是静态的还是动态的？ 在这篇博文中，我会引导你遍历 Xcode Project 中的各个元素，指导你如何通过改变这些元素来模块化你的设置项。我希望下次你遇到上面这些问题的时候，你不需要再花大量时间取 Stack Overflow 上查这些确定的答案。\nElements ⚒ # Target # 工程（Projects）都是由多个更小的叫做 target 的单元组成的。这些 target 包含编译特定平台产品，比如 frameworks, libraries, apps, testing bundles, extensions 等所需要的配置。 你可以在这里看到 target 所有可用的类型。 Target 可以相互依赖，当一个 target A 依赖另外一个 target B 的时候，target B 就会被先构建出来以便 target A 使用其产出。而 target 的配置项会涉及以下几个地方：\nInfo.plist 文件: 该文件包含产出特定的设置项，比如 版本、App 的名字或者 App 的类型，你可以在这里详细了解这个文件。 Entitlements: 其指定了应用的能力。如果在授权文件中指定的能力和开发者平台上设置的无法匹配，签名过程就会出错。 Build settings: 如其名字所描述的那样，这些都是构建 target 所必要的设置项。构建设置项要么在 target 自身定义或者在 xcconfig 文件中定义。一个 target 的配置项可以继承而来，首先是配置文件本身，其次是 target 的配置项，最后是 project 配置项。 Build phases: 构建流水线由 build phase 定义。当一个 target 被创建出来之后，其包含默认的构建阶段（包含 构建源码、拷贝资源等），但是你可以自行添加你需要的。举个例子，这些阶段里，有个 shell script phase 允许你在构建过程中执行一些脚本。这些脚本可以读取 Xcode 暴露出来的那些构建参数。 基于.xcconfig文件的可组合性以及其可重用性的考虑，强烈建议你在这些文件中定义你那些编译设置。Target 的配置，比如 build setting 、build phase 等的变更都体现在 .pbxproj 文件中，这个文件是一种特殊的plist 文件，当我们使用 Git 管理我们的工程的时候，这个文件很容易出现冲突。当然，更新 pbxproj 文件中配置的最简单方式就是使用 Xcode，其了解如何从这些文件中读取配置和向其中写入配置。如果你对不使用 Xcode 更新 pbxproj 文件感兴趣的话，你可以试试 Xcodeproj 或者 xcproj。\n构建这些 target 的输出要么是比如 app，extension 或者测试文件等 bundles，要么就是intermediate products ，例如 library 或者那些封装了代码和资源文件用来给别的 target 使用的 framework。这些 Target 的输出内容你可以在工程文件中的 Products 的 Group 下找到，如果有红色的文件引用表示没有 product 输出，很大可能是你还没有构建过这个 target。\nScheme # Xcode 工程中另外一个要素是 scheme。 一个工程可以包含多个 scheme，他们可以被共享，作为工程的一部分被人们使用。这些 scheme 指定了 Xcode 中每个具体动作的配置，这些动作包括：run，test，profile，analyze 以及 archive。 细的来讲，可以指定哪些 target 需要被构建，以什么顺序构建甚至针对每一种动作指定不同的配置。\n关于 scheme 的编译配置有一些东西要讲。当我们指定针对哪些动作构建哪些 target 的时候，在下面两种情况下，我们不需要指定每个 target 的依赖项：\n如果依赖项是是相同 project 中的一部分，并且已经在 Target dependencies 的 Build Phases 中定义过； 开启了 Find implicit dependencies。 第 2 点中开启的标识，构建过程必须找到 target 的依赖项，并且先行构建。另外，如果你开启了Parallelize build 的话，一旦 target 相互之间没有依赖的话，就能够并行构建，因而会节省一部分时间。\n一个有问题的构建配置会导致你编译 target 的时候出现错误，比如 Framework XXX not found。如果你曾经或者当前遇到了类似的报错，检查一下在构建每个 scheme 的时候，你的 target 的所有依赖是否已经被构建。\nscheme 文件定义是存储在 Project.xcodeproj/xcshareddata/xcodeproj.xcscheme 路径下的一个标准的 XML 文本，因此你可以很容易的使用任意 XML 编辑器来修改它。\nWorkspace # 多个 project 文件被组合成一个 workspace。当 project 被添加到一个 workspace 的时候：\n其 schemes 会出现在 workspace 的 scheme 列表中； project 彼此可以产生依赖关系，后文会讲到。 和 scheme 类似，workspace 也是普通的 XML 文件，修改起来很方便。\nDependencies 🌱 # 每个 target 都可以有依赖，这些依赖是 target 需要链接的那些 framework 以及 library等，其包含了能够被 target 共享的源代码以及资源。这些依赖项可以被静态或者动态的链接。\n静态链接：\n发生在编译阶段； 库（Library）中的代码文件会被包含到应用的二进制文件中（会增大应用的二进制大小）； 库使用 .a 作为文件后缀，其来自 (ar)chive file3 type； 如果相同的库被多次链接，编译器会由于 duplicated symbols 编译失败。 动态链接：\n模块在应用启动或者运行过程中被加载； 应用或者扩展的 target 都可以共享相同的动态库（仅被复制一份） 关于 framework 和 library（无论是静态链接还是动态链接）的区别在于前者可以在相同的 bundle 中包含多个版本，还可以包含额外的资源文件。\n一个 Library 是一个 .a 文件，其来源于 归档（archive）文件类型。一个单一的归档文件仅支持单一的架构。如果需要打包多个架构，则需要将其打包成胖Match-O二进制（fat Match-O binary），该二进制文件是一种容器格式，其将支持不同架构的Mach-O打包在一起。如果我们想生成、修改一个胖二进制文件或者从其中提取某个特定架构的库的话，可以使用命令行工具lipo。\n你可以在这里了解更多关于 frameworks/libraries 以及 static/dynamic 的内容。\n应用的依赖项分为预编译过的（precompiled）和未经编译过（not-compiled）两类。\nPrecompiled dependencies # Carthage 是这类型依赖的典型代表。某些 SDK，比如 Firebase 就是作为编译过的依赖来发布的。当预编译过的依赖是库（library）的时候，这些依赖就包含 .a 的库及一个公共头文件，包含了该库所暴露出的公共接口。当这些依赖是 framework 的时候，这些依赖就以包含了库和资源文件的 .framework 文件发布。\n当我们的 app 依赖的是预编译依赖的时候，很重要的一点是，这些依赖也是依照我们 app 所构建架构来构建出来的。一旦其中缺失某个架构的代码，我们就会在编译 app 的时候收到编译错误。一会儿后文会看到，Carthage 使用 lipo 工具生成那些包含模拟器或者真机所必须的架构的 framework 的，同时根据构建配置来剔除掉那些不需要的 framework。\nNon-compiled dependencies # CocoaPods 是该种类型的典型代表。依赖项被定义在我们要链接的 frameworks/libraries 的 target 中。这里有多种方式在 Xcode 中指定我们的 target 依赖其他 target 的输出。\n如果这些 target 分布在同一个 project 中，你可以在 Build Phase 的Target dependencies 中指定依赖。 Xcode 会在编译该 target 的时候首先编译这些指定的依赖项； 如果这些 target 分布在不同的 project 中，我们就可以使用Scheme来定义这些 target 之间的依赖关系。在 scheme 的 Build 部分，我们可以定义要被构建的 target 以及以什么顺序构建（基于他们之间的依赖关系）。如果你开启了Find implicit dependencies标识，Xcode 能够根据每个 target 的输入输出来猜测依赖。如果 scheme 中有错误配置，你就会得到类似xxxx.framework not found的错误。如果在 framework 之间出现了循环依赖也会报类似的错误。 关于依赖项和配置项有个需要注意的地方：所有依赖项的配置一定要完全匹配。如果你在使用 Alpha 配置项构建你的 app，但是其依赖项中但凡出现了不包含这种配置，编译过程都会因为找不到某个 framework 而失败。当这种情况发生的时候，Xcode 不会编译该 framework 但是不报任何错误。\nLinking with Xcode # Target 本身可以链接其他 target 的输出，我们可以使用 Xcode 中的工具，比如 scheme 或者 target dependencies 来指定依赖，但是，我们是如何通过定义这些依赖的链接关系来将它们融为一体的？\n1. 动态或者静态链接 libraries 和 frameworks # 我们可以通过以下的方式定义链接：\n一个构建阶段（build phase）：，在所有可用的 build phase 中，有一个是定义链接的，Link Binary With Libraries。你可以在这里添加某个 target 的依赖项，这些依赖项可以来自于同一个 project，也可以来自同一个 workspace 中的其他 project。这个 build phase 被 Xcode 用来识别 target 被构建时所需的依赖项； **编译器构建设置：**一个 build phase 中所定义的内容会被转换成编译器参数。其中某些内容你也可以通过定义编译设置项做到： FRAMEWORK_SEARCH_PATHS：定义编译器所链接的 framework 所在路径 LIBRARY_SEARCH_PATHS：定义编译器所链接的 library 所在路径 OTHER_LDFLAGS (Other Linker Flags)：我们可以使用-l参数指定链接的 library，比如-l\u0026quot;1PasswordExtension\u0026quot; -l\u0026quot;Adjust\u0026quot;。如果需要链接一个 framework，就需要使用-framework参数，比如：-framework \u0026quot;GoogleSignIn\u0026quot; -framework \u0026quot;HockeySDK\u0026quot;。如果我们尝试链接一个无法在上方指定路径中找到的 framework 或者 library 的话，编译过程就会失败。 2. 暴露库的头文件 # Library 的头文件需要暴露给依赖该库的 targe。为了做到这个，有一个编译设置项：HEADER_SEARCH_PATHS用来指定头文件所在路径。如果我们链接某个库，但是忘记暴露该库的头文件，编译过程就会因为找不到其头文件而失败。\n3. 将 Framework 嵌入到应用中 # App 的 target 链接动态 framework，需要把这些依赖项复制到应用的 bundle 中。这个过程被称作 framework embedding。为了达到这个目的，我们需要使用 Xcode 的Copy Files Phase，其拷贝 这些 framework 到 Frameworks目录中。不仅仅需要把这些直接依赖项嵌入应用中，还包括直接依赖所依赖的项目。如果缺少任意的 framework，当我们尝试打开 app 的时候都会抛出错误。\n案例学习 👨‍💻 # 在这个部分，我们会分析以下 Cocopods 和 Carthage 是如何贯彻上面这些概念来管理你的工程依赖的。\nCocoaPods # Cocoapods 解析你的工程依赖，并将它们融合到你的工程中。虽然直接修改你的工程配置是不太推荐的，但是它从最初的版本已经有了很大的提升，用这种方式，我们几乎不需要对 project 做很多改变。那么它底层到底是怎么做到的？\n它创建一个工程（project）(*Pods.xcodeproj*) ，其包含了所有的依赖项，每个依赖项以 target 的形式存在。每个 target 各自编译需要被链接到 app 中的依赖项； 它创建一个额外的 target，其依赖于其他所有的依赖项。该 target 是一个 umbrella target，用来触发其他 target 的编译。这样做也最小程度的减少了你的 project 中所需要的改变。通过链接这个 target，Xcode 会先编译其所有依赖项，然后是你的 app； 它创建了一个 workspace，包含了你的 project 以及 Pods project； Frameworks 和 libraries 使用.xcconfig文件链接。这些文件被加到了你的 project 群组中，并且被设置为你 project 中 target 的配置项； 嵌入过程是通过一个构建阶段脚本（build phase script）来做到的。类似的，所有的 framework 所需要的资源也通过一个构建阶段（build phase）来完成。 下面这张图展示了整个设置过程：\nCarthage # Carthage 的方式和 CocoaPods 比起来大不同。除了依赖项的解析，该工具是还一种去中心化的模式，其生成那些需要被链接或者嵌入到 app 的依赖项的预编译版本。\nCarthage 解析依赖项，并且编译它们生成你能够链接到 app 中的动态 framework，或者为了调试所需要的符号。这些 framework 是 fat framework，支持模拟器和真机的架构； 这些 framework 被用户使用 Link Binary With Libraries 的构建阶段（build phase）手动的链接； 嵌入过程使用 Carthage 提供的脚本完成。这个脚本会剔除那些我们正在构建目标所不必要的架构版本； 使用同样的脚本，复制符号到合适的文件夹，使得调试能够正常进行。 References # Framework vs Library Static and dynamic libraries Xcode Build Settings Reference Embedding Frameworks in an App Introduction to Framework Programming Guide Skippy Copy Phase Strip ","date":"2017-11-19","externalUrl":null,"permalink":"/post/modular-xcode-projects/","section":"Posts","summary":"原文：Modular Xcode projects\n原作者 \u0026 Copyright @pepibumur\n翻译：@OgreMergO\n使用 Xcode 构建模块化的工程就需要对工程结构以及其基础概念有很好的理解才行。\n","title":"模块化 Xcode 工程","type":"post"},{"content":"原文：Dependency injection using factories in Swift\n原作者 \u0026amp; Copyright @johnsundell\n依赖注入是一项使得代码更加可测试的关键工具。我们不需要持有某些对象，或者创建这些对象的自有依赖，或者通过单例来获取他们，而是尽可能使那些对象能够正常运转所必须的一切内容（其他对象）通过外界传入，这样做的好处在于，一方面能清晰的看得到某个对象的所有依赖项，另一方便也使得测试工作变得更为简单（因为我们可以模拟这些依赖项来捕获、验证状态以及值。）\n然而，尽管依赖注入确实很有用，但是当在工程中广泛使用的时候还是会有一些痛点。随着某个对象的依赖项越来越多，初始化该对象就变得越来越蹩脚。虽然使得代码可测没毛病，但是如果像下面这种每次需要这样来写初始化方法，也太不爽了。\nclass UserManager { init(dataLoader: DataLoader, database: Database, cache: Cache, keychain: Keychain, tokenManager: TokenManager) { // ... } } 所以，这周咱们来深入了解一下某种依赖注入的技巧，使得我们的代码不失去可测性，我们也不需要再强迫自己去写一团初始化方法或者复杂的依赖管理的代码。\n传递依赖项 # 我们遇到上面代码 demo 中的问题，最主要的原因是我们需要把这么多依赖项传递给某个对象，以便之后来使用。举例来说，我们在构建一个消息收发的 App，这里有一个 view controller 需要展示某个用户所有的消息：\nclass MessageListViewController: UITableViewController { private let loader: MessageLoader init(loader: MessageLoader) { self.loader = loader super.init(nibName: nil, bundle: nil) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) loader.load { [weak self]() messages in self?.reloadTableView(with: messages) } } } 如上代码能看到，我们给 MessageListViewController 传入某个依赖项 MessageLoader，之后其被用来加载数据。这里其实并没有太大的问题，因为仅仅只有一个依赖而已。然而，我们的列表视图并不是一个死气沉沉的展示而已，某些状态下还需要们进行导航到某视图控制器的工作。\n具体来讲，我们想让用户在点击消息列表中某个 cell 的时候，导航到一个新的视图中。我们为这个新的视图创建一个视图控制器 MessageViewController，使得用户能够单独查看某条消息，并且能够回复该消息。为了该功能，我们实现了 MessageSender 类，当创建该类的时候，我们将前面那个新的视图控制器传递给他，代码类似下面这样：\noverride func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let message = messages[indexPath.row]() let viewController = MessageViewController(message: message, sender: sender) navigationController?.pushViewController(viewController, animated: true) } 问题来了，MessageViewController 需要有一个 MessageSender 实例，我们也需要使得 MessageListViewController 看到该类。一种办法就是简单的，将 sender 加入到 列表视图控制器的初始化方法中，如下所示：\nclass MessageListViewController: UITableViewController { init(loader: MessageLoader, sender: MessageSender) { ... } } 一旦如上面这样开始写代码，我们就逐步的进入庞大初始化方法的不归路上咯，然后使得 MessageListViewController 会变得越来越难用（也会让调用这很困惑，为什么一个列表视图控制器还需要关心某个发送消息的人？）。\n另外一个可能的解决方案（也是一个很常用的解决方案），就是把 MessageSender 做成一个单例，这样的话，我们可以很容易在任何地方取到他的值，也可以随时将单例对象注入MessageViewController 中：\nlet viewController = MessageViewController( message: message, sender: MessageSender.shared ) 然而，就如 Avoiding singletons in Swift 这篇文章中讲的，单例这种方式会伴随一些明显的缺陷，导致我们会难以看清依赖关系，从而对整个框架都难以理解。\n工厂模式来救场 # Wouldn\u0026rsquo;t it be nice if we could just skip all of the above, and enable MessageListViewController to be completely unaware of MessageSender, and all other dependencies that any subsequent view controllers might need?\n如果我们能够避免掉上面这些问题，能够使得 MessageListViewController 完全不关心 MessageSender，甚至是后续的视图控制器的其他依赖，岂不是很爽？\n如果我们有某种形式的工厂，我们可以给其传入指定的 message，然后很方便的产出一个 MessageViewController 出来，类似下面这样，就能够很方便并且简洁的实现上面的理想：\nlet viewController = factory.makeMessageViewController(for: message) 如 Using the factory pattern to avoid shared state in Swift 这篇文章中我们看到的，关于工厂模式中，我最喜欢的一点就是，他能够使得你将某个对象的创建和使用两者解耦，也能使得许多对象和这些对象的依赖之间有一个相对解耦的关系，进而能使得我们想重构代码或者修改某些部分的时候相对更容易一些。\n那我们该怎么做呢？ # 首先，我们定义一个工厂协议，该协议使得我们能够在并不知道某个视图控制器的依赖项或者其初始化方法的前提下，很容易的在我们的应用中创建出我们需要的任意的视图控制器。\nprotocol ViewControllerFactory { func makeMessageListViewController() -\u0026gt; MessageListViewController func makeMessageViewController(for message: Message) -\u0026gt; MessageViewController } 到这里我们还不能停止。我们同样为工厂添加一些附件的协议用来创建视图控制器的依赖，比如下面这个协议，使得我们可以为某个列表视图控制器生成一个 MessageLoader 出来：\nprotocol MessageLoaderFactory { func makeMessageLoader() -\u0026gt; MessageLoader } 单例依赖 # 一旦我们准备好这些工厂协议之后，回到上面 MessageListViewController 的地方，重构这段代码，无需使用其依赖项的实例而是简单的引入一个工厂实例即可。\nclass MessageListViewController: UITableViewController { typealias Factory = MessageLoaderFactory \u0026amp; ViewControllerFactory private let factory: Factory private lazy var loader = factory.makeMessageLoader() init(factory: Factory) { self.factory = factory super.init(nibName: nil, bundle: nil) } } 通过上面这么做，我们可以做到两点：\n我们将一堆依赖项简化成了一个单一的工厂； MessageListViewController 无需再需再关心 MessageViewController 的依赖项了 一个使用 Container 的例子 # 接下来，我们该实现工厂协议了。首先，我们需要定义一个 DependencyContainer，该对象会包含我们应用中那些正常情况下会被直接用来作为依赖的核心工具对象们。这些不仅仅包括类似之前 MessageSender，也包括更加底层的业务逻辑上的类，比如我们可能会用到 NetworkManager。\nclass DependencyContainer { private lazy var messageSender = MessageSender(networkManager: networkManager) private lazy var networkManager = NetworkManager(urlSession: .shared) } 从上面这段代码，你能看到，我们使用了懒加载属性以便能够在初始化该对象的时候能够引用相同类中的其他属性。这是设置你依赖关系的一种非常方便而且优雅的方式，你可以利用编译器帮助你避免比如引用循环等问题。\n最后，我们为 DependencyContainer 实现我们的工厂协议，使得我们能够将该工厂注入各种视图控制器或其他对象中：\nextension DependencyContainer: ViewControllerFactory { func makeMessageListViewController() -\u0026gt; MessageListViewController { return MessageListViewController(factory: self) } func makeMessageViewController(for message: Message) -\u0026gt; MessageViewController { return MessageViewController(message: message, sender: messageSender) } } extension DependencyContainer: MessageLoaderFactory { func makeMessageLoader() -\u0026gt; MessageLoader { return MessageLoader(networkManager: networkManager) } } 分布式的所有权 # 最后一步了，我们在哪里实际储存依赖存储器，谁应该拥有它？它应该在哪里设置？这里有些比较 cool 的事情就是，由于我们把依赖容器作为对象们所需要的工厂的一种实现，而对象们强持有其工厂，所以，我们其实无需在任何地方储存该依赖容器。\n举例来说，如果 MessageListViewController 是我们应用的初始化视图控制器，我们可以很简单的创建一个 DependencyContainer 的实例传入：\nlet container = DependencyContainer() let listViewController = container.makeMessageListViewController() window.rootViewController = UINavigationController( rootViewController: listViewController ) 无需保留任何全局的变量或者在 app delegate 中使用可选属性。\n总结 # 使用工厂协议和容器配置依赖注入是一种很好的方式，其可以避免需要传递大量依赖而创建很复杂的初始化方法。它可以使得依赖注入使用起来更加方便，使得你能够对自己创建的对象实际的依赖关系有很明晰的判断，也使得测试更加简单。\n因为我们能够把工厂定义为协议，因此可以很容易的在测试中通过给定不同测试指定版本的具体实现来模拟输出。未来我会写大量关于模拟数据以及如何在测试中充分利用依赖注入的博文。\n","date":"2017-11-19","externalUrl":null,"permalink":"/post/dependency-injection-using-factories-in-swift/","section":"Posts","summary":"原文：Dependency injection using factories in Swift\n原作者 \u0026 Copyright @johnsundell\n依赖注入是一项使得代码更加可测试的关键工具。我们不需要持有某些对象，或者创建这些对象的自有依赖，或者通过单例来获取他们，而是尽可能使那些对象能够正常运转所必须的一切内容（其他对象）通过外界传入，这样做的好处在于，一方面能清晰的看得到某个对象的所有依赖项，另一方便也使得测试工作变得更为简单（因为我们可以模拟这些依赖项来捕获、验证状态以及值。）\n","title":"在 Swift 中使用工厂模式进行依赖注入","type":"post"},{"content":"","date":"2017-11-14","externalUrl":null,"permalink":"/tags/optional/","section":"Tags","summary":"","title":"Optional","type":"tags"},{"content":"原文：Handling non-optional optionals in Swift 原作者 \u0026amp; Copyright @johnsundell\n可选值（optionals）无可争议的是 swift 语言中最重要的特性之一，也是和其他语言，例如 Objective-C 的最大区别。通过强制处理那些有可能出现 nil 的地方，我们就能写出更有预测性的以及更健壮的代码。\n然而，有些时候可选值可能会致你于尴尬的境地，尤其是你作为开发者了解（甚至是有些猜测的成分在），有的特定变量始终是非空（non-nil）的，即使它是一个可选类型。例如，我们在一个视图控制器中处理视图的时候：\nclass TableViewController: UIViewController { var tableView: UITableView? override func viewDidLoad() { super.viewDidLoad() tableView = UITableView(frame: view.bounds) view.addSubview(tableView!) } func viewModelDidUpdate(_ viewModel: ViewModel) { tableView?.reloadData() } } 这也是对于很多 Swift 程序员争论比较激烈的地方，程度不亚于讨论 tabs 和 spaces 的用法。有的人会说：\n既然它是一个可选值，你就应该时刻使用 if let 或者 guard let 的方式进行解包。\n然而另外一些人则采用完全相反，说：\n既然你知道这个变量在使用的时候不会为 nil，使用 ! 强制解包多好。崩溃也要比让你的程序处于一个未知状态要好吧。\n本质上来讲，我们这里讨论的是要不要采用防御性编程（defensive programming）的问题。我们是试图让程序从一个未知状态恢复还是简单的放弃，然后让它崩溃掉？\n如果非得让我对这个问题给出一个答案的话，我更倾向于后者。未知状态真的很难追踪 bug，会导致执行很多不想执行的逻辑，采用防御性编程就会使得你的代码很难追踪，出现问题很难追踪。\n但是，我不太喜欢给出一个二选一的答案。相反，我们可以寻找一些技术手法，用更精妙的方式的解决上面提到的问题。\n它真的可选的吗？ # 那些可选类型的，但是被代码逻辑真实需要的变量和属性，实际上是架构瑕疵的一个体现。如果在某些地方确实需要它，但是它又不在，就会使得你的代码逻辑处于未知状态，那么它就不应该是可选类型的。\n当然，在某些特定场景下，可选值确实很难避免（尤其是和特定的系统 API 交互的时候），那对于大部分这种情况，我们有一些技术来处理从而避免可选值。\nlazy 要比非可选的可选值更好 # 某些属性的值需要在其父类创建之后再生成（比如视图控制器中的那些视图，应该在 loadView()或者 viewDidLoad()方法中被创建），对于这种属性要避免其可选类型的方法就是使用 lazy 属性。一个lazy属性是可以是非可选类型的，同时也不在其父类的初始化方法里被需要，它会在其第一次被获取的时候创建出来。\n让我们改一下上面的代码，使用 lazy 来改造 tableView 属性：\nclass TableViewController: UIViewController { lazy var tableView = UITableView() override func viewDidLoad() { super.viewDidLoad() tableView.frame = view.bounds view.addSubview(tableView) } func viewModelDidUpdate(_ viewModel: ViewModel) { tableView.reloadData() } } 这样，没有可选值了，也不会有未知状态咯🎉\n适当的依赖管理要比非可选的可选值要好 # 可选值类型另外一种常用的场景就是用来打破循环依赖（circular dependencies）。有的时候，你就陷入 A 依赖 B，B 又依赖 A 的情况，如下：\nclass UserManager { private weak var commentManager: CommentManager? func userDidPostComment(_ comment: Comment) { user.totalNumberOfComments += 1 } func logOutCurrentUser() { user.logOut() commentManager?.clearCache() } } class CommentManager { private weak var userManager: UserManager? func composer(_ composer: CommentComposer didPostComment comment: Comment) { userManager?.userDidPostComment(comment) handle(comment) } func clearCache() { cache.clear() } } 从上面的代码，我们可以看到，UserManager 和 CommentManager 之间有一个循环依赖的问题，它们二者都没法假设自己拥有对方，但是它们都在各自的代码逻辑里依赖彼此。这里就很容易产生 bug。\n那要解决上面的问题，我们创建一个 CommentComposer 来做一个协调者，负责通知UserManager 和 CommentManager二人一个评论产生了。\nclass CommentComposer { private let commentManager: CommentManager private let userManager: UserManager private lazy var textView = UITextView() init(commentManager: CommentManager, userManager: UserManager) { self.commentManager = commentManager self.userManager = userManager } func postComment() { let comment = Comment(text: textView.text) commentManager.handle(comment) userManager.userDidPostComment(comment) } } 通过这种形式，UserManager 可以强持有 CommentManager 也不产生任何依赖循环。\nclass UserManager { private let commentManager: CommentManager init(commentManager: CommentManager) { self.commentManager = commentManager } func userDidPostComment(_ comment: Comment) { user.totalNumberOfComments += 1 } } 我们又一次的移除了所有的可选类型，代码也更好预测了🎉。\n优雅的崩溃（Crashing gracefully） # 通过上面几个例子，我们通过对代码做一些调整，移除了可选类型从而排除了不确定性。然而，有的时候，移除可选类型是不可能的。让我们举个例子，比如你在加载一个本地的包含针对你 App 的配置项的 JSON 文件，这个操作本身一定会存在失败的情况，我们就需要添加错误处理。\n继续上面这个场景，加载配置文件失败的时候继续执行代码就会使得你的 app 进入一个未知状态，在这种情况下，最好的方式让它崩溃。这样，我们会得到一个崩溃日志，希望这个问题能够在用户感知之前早早的被我们的测试人员以及 QA 处理掉。\n所以，我们如何崩溃。。。最简单的方式就是添加 ! 操作符，针对这个可选值强制解包，就会在其是 nil 的时候发生崩溃：\nlet configuration = loadConfiguration()! 虽然这个方法比较简单，但是它有个比较大的问题，就是一旦这段代码崩溃，我们能得到的只有一个错误信息：\nfatal error: unexpectedly found nil while unwrapping an Optional value\n这个错误信息并不告诉我们为什么发生这个错误，在哪里发生的，给不了我们什么线索来解决它。这个时候，我们可以使用 guard 关键字，结合 preconditionFailure() 函数，在程序退出的时候给出定制消息。\nguard let configuration = loadConfiguration() else { preconditionFailure(\u0026#34;Configuration couldn\u0026#39;t be loaded. \u0026#34; + \u0026#34;Verify that Config.JSON is valid.\u0026#34;) } 上面这段代码发生崩溃的时候，我们就能获得更多更有效的错误信息：\nfatal error: Configuration couldn’t be loaded. Verify that Config.JSON is valid.: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17\n这样，我们现在有了一个更清晰的解决问题的办法，能够准确的知道这个问题在我们代码里的哪个未知发生的。\n引入 Require 库 # 使用上面的 guard-let-preconditionFailure 的方案还是有一些冗长，确实让我们呃代码更难驾驭。我们也确实不希望在我们的代码里占很多篇幅去些这种代码，我们想更专注于我们的代码逻辑上。\n我的解决方案就是使用 Require。它只是简单的在可选值添加简单的 require() 方法，但能够使得调用的地方更简洁。用这种方法来处理上面加载 JSON 文件的代码就可以这样写：\nlet configuration = loadConfiguration().require(hint: \u0026#34;Verify that Config.JSON is valid\u0026#34;) 当出现异常的时候，会给出下面的错误信息：\nfatal error: Required value was nil. Debugging hint: Verify that Config.JSON is valid: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17\nRequire 的另一个优势就是它和调用 preconditionFailure() 方法一样也会抛异常 NSException，就能使得那些异常上报工具能够捕获异常发生时候的元数据。\n你如果想在自己代码中使用的话，Require 现在在 Github 上开源了\n总结 # 所以，总结来看，在 Swift 语言里处理那些非可选的可选值，我有几点自己的贴心小提示给大家：\nlazy 属性要比非可选的可选值要更好 适当的依赖管理要比非可选的可选值要好 当你使用非可选的可选值的时候，优雅的崩溃 如果有任何问题、建议或者反馈，都欢迎随时在 Twitter 上联系我，我非常乐意听到你们希望我在接下来的文章里谈论哪些主题哦。\n谢谢阅读。\n","date":"2017-11-14","externalUrl":null,"permalink":"/post/handling-non-optional-optionals-in-swift/","section":"Posts","summary":"原文：Handling non-optional optionals in Swift 原作者 \u0026 Copyright @johnsundell\n可选值（optionals）无可争议的是 swift 语言中最重要的特性之一，也是和其他语言，例如 Objective-C 的最大区别。通过强制处理那些有可能出现 nil 的地方，我们就能写出更有预测性的以及更健壮的代码。\n","title":"处理 Swift 中非可选的可选值类型","type":"post"},{"content":"","date":"2017-11-13","externalUrl":null,"permalink":"/tags/document/","section":"Tags","summary":"","title":"Document","type":"tags"},{"content":"","date":"2017-11-13","externalUrl":null,"permalink":"/tags/uigesturerecognizer/","section":"Tags","summary":"","title":"UIGestureRecognizer","type":"tags"},{"content":"","date":"2017-11-13","externalUrl":null,"permalink":"/tags/uiresponder/","section":"Tags","summary":"","title":"UIResponder","type":"tags"},{"content":"Apps 是通过响应者（responder）对象来接收和处理事件的。一个响应者对象是 UIResponder 类的一个实例，我们常见的 UIView，UIViewController 以及 UIApplication 都是 UIResponder 的子类。 UIKit 自动帮你管理着这些 responder 相关的行为，包括事件是如何从一个 responder 传递给另一个 responder 的等等。当然，你也可以修改你的 app 中事件传递的默认行为。\nUIKit 会把大部分的事件都传递给最适合的 responder 对象来处理。如果该 responder 无法处理该事件，UIKit 就会继续把该事件沿着当前的响应者链传递到下一个 responder。响应者链就是你的 App 中所有响应者的动态配置，也因为其是动态的，你的 App 中不可能只存在单一的响应者链。由于事件总是从特定的响应者那里流转到更通用的响应者那里，因此很容易确定某响应者链中下一个响应者是谁。举个例子，一个 view 的下一个响应者是其 superview 或者负责管理它的 view controller。事件就是这样在响应者链中传递直到其被处理掉。\n下图展示了一个界面中包含了 一个 Label，一个 TextField，一个 Button 以及两个容器 view 的 App 中响应链是什么样子的。如果 TextField 不处理某个事件，UIKit 就会把该事件发送给 TextField 的父级 UIView 对象，同样的，如果该对象依然处理不了，就会传递给该 view 对象的 window。如果 window 对象也依然无法处理该事件，UIKit 最终会把该事件传递给 UIApplication 对象，一般上该 UIApplication 对象是 App 的 delegate 对象并且是 UIResponder 实例，当然这个时候已经脱离了响应者链了。\n针对每一个事件，UIKit 都会指定一个第一响应者（first responder），然后把该事件首先发送给对象处理。这个第一响应者基于事件类型而不同。\nTouch events. 第一响应者是 Touch 发生所在的 view。 Press events. 第一响应者是当前焦点所在的响应者。 Motion event. 第一响应者是你显式指定用以处理事件的对象。 Core Motion 处理所有的和加速器、气压仪以及磁力计相关的事件。Motion events 不随响应者链流动。 Shake-motion events. 第一响应者是你或者 UIKit 框架指定的对象。 Remote-control events. 第一响应者是你或者 UIKit 框架指定的对象。 Editing-menu messages. 第一响应者是你或者 UIKit 框架指定的对象。 Controls 向其关联的对象发送动作消息本身不是事件，但是依然能够享受到响应者链的好处。当一个 Control 的 target object 是 nil 的时候，UIKit 就会在该 target object 的响应者链上寻找合适的对象用以妥善处理动作消息。例如，UIKit 编辑按钮使用这种行为来寻找响应者对象来执行cut:, copy: 或者 paste: 等方法。\n如果一个 view 自身还有附加的手势识别器的话，该手势识别器会延迟针对该 view 的 touch 和 press 事件的传递。delaysTouchesBegan, delaysTouchesEnded 以及 UIGestureRecognizer 的 cancelsTouchesInView 属性都是用来决定这些 touches 什么时间以及以什么方式被延迟处理。\n识别包含 Touch 事件的响应者 # UIKit 使用基于视图的碰撞检测（hit-testing）来决定 touch 事件发生的地点。具体而言，UIKit 拿该 touch 的位置和视图层级中所有的视图对象的 bounds 进行比较。UIView 的 hitTest:withEvent: 方法会游历整个视图层级，找到该 touch 事件发生所在的视图树最底端的视图，其也就是处理该 touch 事件的第一响应者。\n如果一个 touch 的位置发生在某个视图的外围，hitTest:withEvent:方法就会忽略该视图和其所有子视图。所以，如果你将 view 的clipsToBounds属性设置为 NO 的化，即使其子视图将该 touch 包含在自己领域也是无效的。\n就这样，UIKit 持续不断的把每个 touch 指派给包含该 touch 的视图。当 touch 发生的时候，UIKit 创建一个 UITouch 对象，直到 touch 结束该对象才会被释放。当 touch 位置或者其他参数发生变化的时候，UIKit 就更新这个 UITouch 对象的信息。当然，其中有的属性当然是不会变化的，比如该 touch 附属的 view，甚至当 touch 位置已经超出原始 view 的外围的时候，UITouch 对象中的 view 属性依然保持和之前一样。\n变更响应者链 # 你可以通过覆写响应者的 nextResponder 属性来改变响应者链。许多 UIKit 的类已经覆写了该方法并且返回了特定的对象。\n如果你覆写了任意类的 nextResponder 属性，那该对象的下一个响应者就是你返回的那个； UIView 2.1 如果该视图是某个视图控制器的根视图（root view），其下一个响应者就是该视图控制器； 2.2 如果该视图不是某个视图控制器的根视图，其下一个响应者就是该视图的父视图； UIViewController 3.1 如果该视图控制器的视图是某个 window 的根视图（root view），其下一个响应者就是 window； 3.2 如果该视图控制器是由另一个视图控制器展示出来的，其下一个响应者就是那个视图控制器（presenting view controller）； UIWindow window 的下一个响应者就是 UIApplication 对象 UIApplication UIApplication 对象的下一个响应者是 App delegate，而且要求该 delegate 必须是 UIRespponder 的实例并且不能是一个视图、视图控制器或者 UIApplication 对象自己。 ","date":"2017-11-13","externalUrl":null,"permalink":"/post/understanding-responders-and-the-responder-chain/","section":"Posts","summary":"Apps 是通过响应者（responder）对象来接收和处理事件的。一个响应者对象是 UIResponder 类的一个实例，我们常见的 UIView，UIViewController 以及 UIApplication 都是 UIResponder 的子类。 UIKit 自动帮你管理着这些 responder 相关的行为，包括事件是如何从一个 responder 传递给另一个 responder 的等等。当然，你也可以修改你的 app 中事件传递的默认行为。\n","title":"理解响应者和响应链","type":"post"},{"content":"原文：Capturing objects in Swift closures 原作者 @johnsundell\n自从 Block 在 iOS4 被引入 Objective-C 的世界之后就成为了 Apple 各平台上最时髦的 API 的重要组成部分了。当 Swift 语言出现的时候，blocks 的概念就摇身一变通过 closure 的形式引入，成为了目前我们可能每一天都在用的语言特性之一了。\nClosure 目前已经被我们广泛的使用了，即使如此，我们在使用它的时候还是需要有很多需要注意的点，并且需要做很多额外的操作。这篇文章，我们来近距离的了解 closure，主要是了解其捕获变量的机制以及那些能够更好的让我们来处理变量捕获的技术。\n伟大的 escape # Closure 有两种类型：escaping 和 non-escaping。当一个 closure 是 escaping（使用 @escaping 修饰闭包参数）的，也就意味着其会被以各种形式存储下来（无论是通过 property 还是被其他 closure 捕获）。相反，Non-Escaping 的　closure 意味着其不能被存储，而且在使用它的地方就必须直接被执行。\n译者注： 可以参见 《@autoclosure \u0026amp;\u0026amp; @escape》 一文。\n一个显而易见的例子就是当你在一个集合类型上使用函数式操作的时候，例如 forEach：\n[1, 2, 3].forEach { number in ... } 如上代码所示，closure 都直接作用于集合的每一个元素上，也就无需把该闭包变为 escaping 的。\n而 escaping 的 closures 最常见的就是在那些异步 API 中，例如 DispatchQueue。例如，当你异步执行某 closure 的时候，这个 closure 就会 escape。\nDispatchQueue.main.async { ... } 所以，这两种的区别在哪里呢？ 由于 escaping 的 closure 会被以某种形式存储下来，因此，这些 closure 会同时存储当前其所处的上下文，同时就会把上下文中用到的值或者对象都捕获（capture）到，以至于当该 closure 被执行的时候，所需用到的内容没有丢失。实践中最常见的就是在 closure 中使用 self 的 API，此时，我们就需要某种办法显式的捕获 self。\n捕获 \u0026amp; 引用循环 # 因为 escaping 的 closures 会自助捕获在其内部使用的任何值和对象，因此很容易发生引用循环。举个例子，下面描述的是一个 view controller 被其存储的 viewModel 的 closure 捕获的情况：\nclass ListViewController: UITableViewController { private let viewModel: ListViewModel init(viewModel: ListViewModel) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) viewModel.observeNumberOfItemsChanged { // This will cause a retain cycle, since our view controller // retains its view model, which in turn retains the view // controller by capturing it in an escaping closure. self.tableView.reloadData() } } } 最常见的方式，也就是你们大部分人也都会用的解决方式，通过弱引用的方式打破这个循环引用。\nviewModel.observeNumberOfItemsChanged { [weak self] in self?.tableView.reloadData() } 捕获 context 而不是捕获 self # 上面提到的 [weak self] 的解决方案已经是你希望避免引用循环的最常用，也常常是最有效的解决方案了。但是这种方式也有一些问题：\n很容易忘掉写，尤其是编译器又没检查出来潜在的引用循环的时候； 当你希望从 weak self 中强持有 self 的时候还需要写一堆代码（weak strong dance），例如下面这段代码所示： dataLoader.loadData(from: url) { [weak self] data in guard let strongSelf = self else { return } let model = try strongSelf.parser.parse(data, using: strongSelf.schema) strongSelf.titleLabel.text = model.title strongSelf.textLabel.text = model.text } 这里其实有一个可选的解决方案，也就是不要捕获 self，而去捕获那些闭包中所需要的对象即可。例如上面例子中的 labels 和 schema 等，我们可以直接捕获它们而不至于引发引用循环（因为其也并不持有 closure 本身），下面是个解决方案，通过使用 context 的 tuple 来解决。\n// We define a context tuple that contains all of our closure\u0026#39;s dependencies let context = ( parser: parser, schema: schema, titleLabel: titleLabel, textLabel: textLabel ) dataLoader.loadData(from: url) { data in // We can now use the context instead of having to capture \u0026#39;self\u0026#39; let model = try context.parser.parse(data, using: context.schema) context.titleLabel.text = model.title context.textLabel.text = model.text } 通过显式传递参数而不是隐式的捕获 # 这里，还有另外一种捕获对象的方式，就是显式的把这些对象通过参数传入。这种手法我在设计我的 Image Engine 项目中的 Event API 的时候用到了，这个 API 就是当使用 closure 来监听 event 的时候，需要你传递一个 observer 给它。如下所示，你把 self 传入进来的同时也使得其被传递到了 event 的 closure 中了，这也使得 self 被隐式的带入，你也无需手动的捕获它了。\nactor.events.moved.addObserver(self) { scene in ... } 我们回到之前的 ListViewController 的例子中，看一看当我们要监听其 viewModel 的时候，我们是如何通过上面这种手法来实现同样的 API 的。这种方式正好使得我们可以将要 reload 的 tableView 作为观测者传递，实现一个很优雅的调用：\nviewModel.numberOfItemsChanged.addObserver(tableView) { tableView in tableView.reloadData() } 当然，需要实现上面这段代码，我们还需要做一些事情，就像 Image Engine 的事件系统如何工作类似。我们首先定义一个简单的 Event 类型，其可以记录那些观测闭包。\nclass Event { private var observers = [() -\u0026gt; Void]() } 然后，我们添加一个方法，该方法会传两个参数进来，一个是引用类型的观测者，另外一个是一个闭包，当观察动作一旦触发，该闭包就会被调用。核心就在这里，我们会封装该闭包，并且在内部闭包中弱捕获该观测者：\nfunc addObserver\u0026lt;T: AnyObject\u0026gt;(_ observer: T, using closure: @escaping (T) -\u0026gt; Void) { observers.append { [weak observer] in observer.map(closure) } } 这样就使得我们只需要做这么一次 weak/string 的操作，也不影响其调用的地方。\n最后，我们添加一个 trigger 方法来使得我们能够触发事件本身。\nfunc trigger() { for observer in observers { observer() } } 然后回到 ListViewModel，为 numberOfItemsChanged 方法添加事件，当某个条件满足的时候，就会触发该事件。\nclass ListViewModel { let numberOfItemsChanged = Event() var items: [Item] { didSet { itemsDidChange(from: oldValue) } } private func itemsDidChange(from previousItems: [Item]) { if previousItems.count != items.count { numberOfItemsChanged.trigger() } } } 如上面所看到的，基于 event 的 API 的最大优势就是最大程度上避免了引用循环的发生。我们也可以在我们的代码中为任意类型的观察事件重用相同的执行代码。当然，上面的 demo 中 Event 实现非常简单，缺乏一些高级特性，比如针对观察者的移除等等，但是对于简单使用已经足够了。\n我们会在之后的博文中详细的讲述事件驱动的编程范式，你也可以详细看下在 Image Engine 项目中的 Event 类型的完整实现。\n总结 # Closure 自动捕获其内部所使用的对象和值本身是一个非常棒的特色，它使得 closure 本身变得非常好用。但是，捕获同时也引入了一些 bug 和引用循环的问题，甚至最后使得代码变得复杂和难以理解。\n当然，我并不是建议大家在所有的场景下去避免捕获发生，而是想通过这篇文章提供给大家一些捕获 self 的选择。在某些场景下，使用经典的 [weak self] 是最有效的解决方案，另外一些场景则可以使用某些手法来帮助你把自己的闭包代码写的更容易使用，也更容易理解吧。\n","date":"2017-11-12","externalUrl":null,"permalink":"/post/capturing-objects-in-swift-closures/","section":"Posts","summary":"原文：Capturing objects in Swift closures 原作者 @johnsundell\n自从 Block 在 iOS4 被引入 Objective-C 的世界之后就成为了 Apple 各平台上最时髦的 API 的重要组成部分了。当 Swift 语言出现的时候，blocks 的概念就摇身一变通过 closure 的形式引入，成为了目前我们可能每一天都在用的语言特性之一了。\n","title":"Capturing objects in Swift closures","type":"post"},{"content":"","date":"2017-11-12","externalUrl":null,"permalink":"/tags/closure/","section":"Tags","summary":"","title":"Closure","type":"tags"},{"content":"我们知道在 swift 中，闭包（closure）是一等公民，因此可以被当作参数传递，在学习 swift 的过程中经常会看到某些关键字修饰该闭包，@autoclosure， @escape 就是其中比较常见的两种关键字。\n@escape 和 @nonescape # 当一个闭包被当作参数传递给一个函数，但是当该函数内容执行完毕返回之后，该闭包才会被执行，我们就称该闭包要 escape 某个函数，那 @escape 关键字就是用来表示该闭包是允许在函数返回之后被调用的。\n我们用 swift 官方文档的例子来看，如下所示\nvar completionHandlers: [() -\u0026gt; Void] = [] func someFunctionWithEscapingClosure(completionHandler: @escaping () -\u0026gt; Void) { completionHandlers.append(completionHandler) } someFunctionWithEscapingClosure(_:) 的参数是一个闭包，函数内部会把传入的闭包存到之前声明的数组里以便之后进行调用，可以看到，在函数参数的声明部分添加了 @escaping 关键字，如果这里不添加的话，就会在编译的时候报错：\nerror: passing non-escaping parameter \u0026#39;completionHandler\u0026#39; to function expecting an @escaping closure completionHandlers.append(completionHandler) 针对标记了 @escaping 关键字含义代表你必须在该闭包内部显式的使用 self 关键字，官方文档中又列举了另外一个例子，如下所示：\nfunc someFunctionWithNonescapingClosure(closure: () -\u0026gt; Void) { closure() } class SomeClass { var x = 10 func doSomething() { someFunctionWithEscapingClosure { self.x = 100 } someFunctionWithNonescapingClosure { x = 200 } } } let instance = SomeClass() instance.doSomething() print(instance.x) // Prints \u0026#34;200\u0026#34; completionHandlers.first?() print(instance.x) // Prints \u0026#34;100” someFunctionWithEscapingClosure(_:) 是一个可逃逸的闭包，意味着你需要显示的调用 self 关键字， 而 someFunctionWithNonescapingClosure(_:) 是非逃逸的闭包，意味着你可以隐式的调用 self。\n@autoclosure # 例子讲解 # 通过一个🌰来说明 @autoclosure 关键字到底起到什么作用。\n考虑下面这个函数 f，其需传入一个参数，类型是 ()-\u0026gt; Bool 的闭包。\nfunc f(predicate: () -\u0026gt; Bool) { if predicate() { print(\u0026#34;It\u0026#39;s true\u0026#34;) } } 然后通过传入符合此类型的闭包进行调用\nf(predicate: {2 \u0026gt; 1}) // \u0026#34;It\u0026#39;s true\u0026#34; 但是，如果我们忽略传入闭包的 { 和 } ，编译就会错误。\nf(predicate: 2 \u0026gt; 1) // error: \u0026#39;\u0026gt;\u0026#39; produces \u0026#39;Bool\u0026#39;, not the expected contextual result type \u0026#39;() -\u0026gt; Bool\u0026#39; 如文档中所说，一个 autoclosure (自主闭包？)是这样一种闭包: 当某个表达式被当做参数传递给一个函数的时候会被 wrap 成一个闭包，该闭包没有任何参数，其被调用的时候，返回的是其 wrap 的表达式的值。swift 提供这种语法糖就能够让你省略 {} 而直接写一个表达式。\n结合上面的例子来看，当你写个表达式类似 2 \u0026gt; 1 传给函数 f 的时候，该表达式会被自动包裹到一个闭包中，会自动处理为 { 2 \u0026gt; 1 } 而传递给函数 f。\nfunc f(predicate: @autoclosure () -\u0026gt; Bool) { if predicate() { print(\u0026#34;It\u0026#39;s true\u0026#34;) } } f(predicate: 2 \u0026gt; 1) // It\u0026#39;s true ⚠️ @autoclosure 并不支持带有输入参数的写法，也就是说只有形如 () -\u0026gt; T 的参数才能简化\nDelay Evaluation # swift 提供了 ?? 操作符，如下所示：\nlet nickName: String? = nil let fullName: String = \u0026#34;John Appleseed\u0026#34; let informalGreeting = \u0026#34;Hi \\(nickName ?? fullName) 如果某 Optional 存在就会返回其值，如果没有就会返回后面的默认值，当我们去看 ?? 的实现的时候能看到如下定义：\nfunc ??\u0026lt;T\u0026gt;(optional: T?, defaultValue: @autoclosure () -\u0026gt; T?) -\u0026gt; T? func ??\u0026lt;T\u0026gt;(optional: T?, defaultValue: @autoclosure () -\u0026gt; T) -\u0026gt; T 看得出来 ?? 是一个二元操作符，optional 指代 ?? 前面的输入，defaultValue 指代 ?? 后面的参数，那我们就会想，我们上面的例子中 fullName 只是一个 String，怎么变成 () -\u0026gt; T 类型的呢？ 这个就看前面的 @autoclosure 的威力了，前面讲过了，该关键字把表达式的值封装成闭包并且返回该表达式的值了。 其实传入该方法的第二个参数是 { fullName }。\n所以可以想到该方法的实现应该如下所示，（当然 fullName 为 String 类型，应该会重载第二个函数实现）\nfunc ??\u0026lt;T\u0026gt;(optional: T?, defaultValue: @autoclosure () -\u0026gt; T) -\u0026gt; T { switch optional { case .Some(let value): return value case .None: return defaultValue() } } 这里我们还需要注意一点的是，使用 @autoclosure 来修饰的表达式可以实现延迟计算，也就是说直到该闭包被调用之前，闭包里所被包裹的表达式都不会进行取值计算，也就避免了一定的开销，尤其是上面默认值是复杂计算得到的话。\n","date":"2017-11-12","externalUrl":null,"permalink":"/post/autoclosure-escape/","section":"Posts","summary":"我们知道在 swift 中，闭包（closure）是一等公民，因此可以被当作参数传递，在学习 swift 的过程中经常会看到某些关键字修饰该闭包，@autoclosure， @escape 就是其中比较常见的两种关键字。\n","title":"@autoclosure \u0026\u0026 @escape","type":"post"},{"content":"","date":"2017-05-20","externalUrl":null,"permalink":"/tags/notification/","section":"Tags","summary":"","title":"Notification","type":"tags"},{"content":" 概览 # 推送通知我们大家都不陌生，可以说几乎每个使用智能手机的人每天都会被不同的通知 打扰 到，正式因为合适的推送是吸引用户注意力的利器，其成为了各 App 吸引用户，将用户带回到 App 本身，提升用户的活跃度的一种必要的方式。\n当然，过度的推送本身则是一件对用户影响特别大的事情，毕竟注意力被打断，因此合适的推送时机也是各个 App 开发者所要注意的，否则他的 App 就会成为用户勿扰名单里的一员了。\n之前刚开始学习 iOS 开发的时候还整理了下当时部署 iOS 远程推送的流程，详见：iOS 远端推送部署详解\n接下来，我们大致回顾一下 iOS 平台关于推送都有哪些历程？\n历史 # \u0026lt;= iOS 6\n远程推送通知 （iOS 3） 本地通知 （iOS 4） iOS 7 参考\n引入 Silent Remote Notifications iOS 8 参考 WWDC 2014 Session 713\n引入 Actionable Notifications 修改 Notification 的权限请求 iOS 9 参考 WWDC 2015 Session 720\n引入 Text Input Action UIUserNotificationActionBehavior iOS 10 参考 WWDC 707 Introduction to Notifications \u0026amp;\u0026amp; WWDC 708 Advanced Notifications\nUserNotification Framework Extensions WWDC 2016 大会上，Apple 在 iOS 10 上引入了 UserNotification 框架，可以说是对之前的各种代码做了一次重构。该框架统一了通知的行为，尤其是针对远程推送和本地推送不再有两套完全不同的使用方式了。\n变化 # 关于 iOS 10 上 UserNotification 框架的变化，主要从几个方面来讲述：\n权限申请 推送内容变更 推送管理 Extensions 权限申请 # UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if granted { // 用户允许进行通知 } } 远程推送 # // 向 APNs 请求 token： // iOS 10 support if #available(iOS 10, *) { UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .alert, .sound]){ (granted, error) in } application.registerForRemoteNotifications() } // iOS 9 support else if #available(iOS 9, *) { UIApplication.shared.registerUserNotificationSettings(UIUserNotificationSettings(types: [.badge, .sound, .alert], categories: nil)) UIApplication.shared.registerForRemoteNotifications() } 关于注册远程通知的回调方法一致，\n// AppDelegate.swift func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let tokenString = deviceToken.hexString print(\u0026#34;Get Push token: \\(tokenString)\u0026#34;) } Payloads # \u0026lt; iOS 10\n{ \u0026#34;aps\u0026#34;:{ \u0026#34;alert\u0026#34;:\u0026#34;Test\u0026#34;, \u0026#34;sound\u0026#34;:\u0026#34;default\u0026#34;, \u0026#34;badge\u0026#34;:1 } } iOS 10 系统提供了更为丰富的结构，比如可以指定 Title，Subtitle 等\n{ \u0026#34;aps\u0026#34;:{ \u0026#34;alert\u0026#34;:{ \u0026#34;title\u0026#34;:\u0026#34;This is a title\u0026#34;, \u0026#34;subtitle\u0026#34;:\u0026#34;This is a subtitle\u0026#34;, \u0026#34;body\u0026#34;:\u0026#34;This is body\u0026#34; }, \u0026#34;sound\u0026#34;:\u0026#34;default\u0026#34;, \u0026#34;badge\u0026#34;:1 } } 甚至，现在可以支持多媒体的展示了，在 payload 上也有相应的体现，例如下面几例：\n支持图片\n{ \u0026#34;aps\u0026#34;:{ \u0026#34;alert\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;Title: Notification Demo\u0026#34;, \u0026#34;subtitle\u0026#34;: \u0026#34;Subtitle: show iOS 10 support!\u0026#34;, \u0026#34;body\u0026#34;: \u0026#34;The Main Body For Notification!\u0026#34; }, \u0026#34;mutable-content\u0026#34;: 1 }, \u0026#34;image\u0026#34; : \u0026#34;https://pic1.zhimg.com/v2-0314056e4f13141b6ca2277078ec067c.jpg\u0026#34;, } 支持音频\n{ \u0026#34;aps\u0026#34;:{ \u0026#34;alert\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;Title: Notification Demo\u0026#34;, \u0026#34;subtitle\u0026#34;: \u0026#34;Subtitle: show iOS 10 support!\u0026#34;, \u0026#34;body\u0026#34;: \u0026#34;The Main Body For Notification!\u0026#34; }, \u0026#34;mutable-content\u0026#34;: 1, }, \u0026#34;audio\u0026#34; : \u0026#34;http://hao.1015600.com/upload/ring/000/982/d9924a7f4e4ab06e52a11dfdd32ffae1.mp3\u0026#34;, } 具体所有的 Key 可以参考官方文档 Payload Key Reference\n注意到有个 key: launch-image ，可以指定用户点击通知启动 App 的时候的 Launch Image\n可以撤销和更新通知了！ # UserNotification 框架 API 提供了通知的更新和撤销的接口。具体功能主要包含几个部分：\n删除未展示的通知\nlet trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false) let identifier = Constants.pendingRemoveNotificationIdentifier let request = UNNotificationRequest(identifier: identifier, content: title1Content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error = error { print(\u0026#34;remove pending notification error: \\(error)\u0026#34;) } else { print(\u0026#34;Notification request added: \\(identifier)\u0026#34;) } } delay(2) { print(\u0026#34;Notification request removed: \\(identifier)\u0026#34;) UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier]) } 更新未展示的通知\nlet trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false) let identifier = Constants.pendingUpdateNotificationIdentifier let request = UNNotificationRequest(identifier: identifier, content: title1Content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error = error { print(\u0026#34;update pending notification error: \\(error)\u0026#34;) } else { print(\u0026#34;Notification request added: \\(identifier) with title1\u0026#34;) } } delay(2) { let newTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) // Add new request with the same identifier to update a notification. let newRequest = UNNotificationRequest(identifier: identifier, content: self.title2Content, trigger: newTrigger) UNUserNotificationCenter.current().add(newRequest) { error in if let error = error { print(\u0026#34;update delivered notification error: \\(error)\u0026#34;) } else { print(\u0026#34;Notification request updated: \\(identifier) with title2\u0026#34;) } } } 删除已经展示的通知\nlet trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false) let identifier = Constants.pendingUpdateNotificationIdentifier let request = UNNotificationRequest(identifier: identifier, content: title1Content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error = error { print(\u0026#34;update pending notification error: \\(error)\u0026#34;) } else { print(\u0026#34;Notification request added: \\(identifier) with title1\u0026#34;) } } delay(2) { let newTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) // Add new request with the same identifier to update a notification. let newRequest = UNNotificationRequest(identifier: identifier, content: self.title2Content, trigger: newTrigger) UNUserNotificationCenter.current().add(newRequest) { error in if let error = error { print(\u0026#34;update delivered notification error: \\(error)\u0026#34;) } else { print(\u0026#34;Notification request updated: \\(identifier) with title2\u0026#34;) } } } 更新已经展示的通知\nlet trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false) let identifier = Constants.deliveredUpdateNotificationIdentifier let request = UNNotificationRequest(identifier: identifier, content: title1Content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error = error { } else { print(\u0026#34;Notification request added: \\(identifier) with title1\u0026#34;) } } delay(4) { let newTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) // Add new request with the same identifier to update a notification. let newRequest = UNNotificationRequest(identifier: identifier, content: self.title2Content, trigger: newTrigger) UNUserNotificationCenter.current().add(newRequest) { error in if let error = error { } else { print(\u0026#34;Notification request updated: \\(identifier) with title2\u0026#34;) } } } 当然，上述均是针对本地通知的操作，关于远程推送通知，目前只支持更新通知，远程推送可以进行通知的更新，在使用 Provider API 向 APNs 提交请求时，在 HTTP/2 的 header 中 apns-collapse-id key 的内容将被作为该推送的标识符进行使用。多次推送同一标识符的通知即可进行更新。\nNotification Extension # iOS 10 中最重要的一个变化就是 Extension，从 iMessage Extension 到 SiriKit 中提供的 Intent Extension 等，那对于 UserNotification 来讲就是下面这两种 Extension：\nService Extension 具体而言，就是我们可以在收到通知之后，在展示给用户之前 ，也就是以Banner 或者 Alert 的形式 或者 进入通知中心之前，给我们一次截取并处理的机会，这样就给开发者提供了针对推送通知再加工的手段，并且远程推送多媒体也是通过 Service Extension 来实现的。\n使用在本机截取推送并替换内容的方式，可以完成端到端 (end-to-end) 的推送加密。你在服务器推送 payload 中加入加密过的文本，在客户端接到通知后使用预先定义或者获取过的密钥进行解密之后再显示。这样一来，即使推送信道被第三方截取，其中所传递的内容也还是安全的。使用这种方式来发送密码或者敏感信息，对于一些金融业务应用和聊天应用来说，应该是必备的特性。\n生成 Extension 之后，系统已经为我们提供了模板，代码如下：\nclass NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -\u0026gt; Void)? var bestAttemptContent: UNMutableNotificationContent? // 1 override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -\u0026gt; Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) if let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } // 2 override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your \u0026#34;best attempt\u0026#34; at modified content, otherwise the original push payload will be used. if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } } Content Extension iOS 10 SDK 新加的另一个 Content Extension 可以用来自定义通知的详细页面的视图。 下图中就是几款市面上目前已经实现过自定义 Content Extension 的 App 截图。需要注意的是： 第一个 PriceTag App 的通知样式，实际上是默认的系统显示行为。\n其中需要注意的一点是。如果不想默认显示通知内容，需要在 Content Extension 的 info.plist 文件中添加 UNNotificationExtensionDefaultContentHidden 并设置为 YES\n支持多媒体资源在通知中心的展示 # iOS 10 中另一个比较显著的特点就是支持了多媒体的推送和展示。开发者现在可以在通知中嵌入图片、音频甚至视频，这极大丰富了推送内容的可读性和趣味性。\n本地推送支持 本地通知添加多媒体比较简单一些，只需要通过本地磁盘上的文件 URL 创建一个 UNNotificationAttachment 对象，然后将这个对象放到数组中赋值给 content 的 attachments 属性就行了： let content = UNMutableNotificationContent() content.title = \u0026#34;Image Notification\u0026#34; content.body = \u0026#34;Show me an image!\u0026#34; if let imageURL = Bundle.main.url(forResource: \u0026#34;image\u0026#34;, withExtension: \u0026#34;jpg\u0026#34;), let attachment = try? UNNotificationAttachment(identifier: \u0026#34;imageAttachment\u0026#34;, url: imageURL, options: nil) { content.attachments = [attachment] } 远程推送支持 首先需要在 payloads 结构中添加对富媒体的支持，aps 字典中添加字段mutable-content并置为 1 来标识该远程通知是需要支持。 然后可以在 payloads 中添加资源地址，可以是本地的资源，也可以是需要 App 下载的。\n{ \u0026#34;aps\u0026#34;:{ \u0026#34;alert\u0026#34;:{ \u0026#34;title\u0026#34;:\u0026#34;Image Notification\u0026#34;, \u0026#34;body\u0026#34;:\u0026#34;Show me an image from web!\u0026#34; }, \u0026#34;mutable-content\u0026#34;:1 }, \u0026#34;zh_image\u0026#34;: \u0026#34;https://pic1.zhimg.com/v2-0314056e4f13141b6ca2277078ec067c.jpg\u0026#34; } 具体支持多媒体文件的富媒体类型以及每一种类型所支持的大小详官方文档 UNNotificationAttachment\n一旦远程推送在 aps 中添加 mutable-content 的key 并设置为 1 之后，iOS 系统接收到该推送之后就会唤起我们配套的 Service Extension 来做进一步处理，我们可以在其中下载对应 zh_image 链接的图片，然后生成 Attachment 再丢给系统处理即可。 详细做法就是在上一节讲 Service Extension 中的 didReceive 中写相应逻辑即可，具体代码如下所示：\n当然，这里的处理时间是存在时间限制的，如果处理超时系统就会回收该 Extension，并且调用 serviceExtensionTimeWillExpire 方法，因此每个人收到带有附件的推送之后不一定展示相同，例如下图所示的情况，第一张和第二张就属于正常设置了附件的情况，而第三张就是没有在有限时间内正确设置附件的情况。\n推送证书的配置 # 下图是获取推送通知证书并将其注册到 Leancloud 的流程，最关键的地方其实就是需要在本机生成 CSR 文件提交到 Apple Developer Website 生成 Push Certification 文件。\n主要有几个步骤：\nAppID 关于 Remote Push 的 注册 生成推送证书 转换证书为 P12 文件，并提供给 Leancloud XCode 8 的 Auto Signing 已经省去了难以名状的复杂，但是还是有一点小事情我们需要做。\n![iOS App IDs - Apple Developer Google Chrome, 今天 at 下午5.01.22](http://7xilk1.com1.z0.glb.clouddn.com/iOS App IDs - Apple Developer Google Chrome, 今天 at 下午5.01.22.png)\n![Add - iOS Certificates - Apple Developer Google Chrome, 今天 at 下午5.07.47](http://7xilk1.com1.z0.glb.clouddn.com/Add - iOS Certificates - Apple Developer Google Chrome, 今天 at 下午5.07.47.png)\n我们需要生成 CSR 文件 ，按照指定步骤按部就班来就行，最后会生成 默认文件 CertificateSigningRequest.certSigningRequest\n![Finder Finder, 今天 at 下午4.59.55](http://7xilk1.com1.z0.glb.clouddn.com/Finder Finder, 今天 at 下午4.59.55.png) ![证书助理 证书助理, 今天 at 下午5.05.42](http://7xilk1.com1.z0.glb.clouddn.com/证书助理 证书助理, 今天 at 下午5.05.42.png)\n接着之前的步骤，选择 CSR 文件上传。\n![Add - iOS Certificates - Apple Developer Google Chrome, 今天 at 下午5.09.07](http://7xilk1.com1.z0.glb.clouddn.com/Add - iOS Certificates - Apple Developer Google Chrome, 今天 at 下午5.09.07.png)\n上传成功之后就会生成 Push 证书，下载到本机安装上。\n![Add - iOS Certificates - Apple Developer Google Chrome, 今天 at 下午5.09.58](http://7xilk1.com1.z0.glb.clouddn.com/Add - iOS Certificates - Apple Developer Google Chrome, 今天 at 下午5.09.58.png)\nDemo 演示 # 可以使用 Knuff 进行通知推送测试。\n参考资料\nLocal and Remote Notification Programming Guide UserNotifications 官方文档 iOS App 签名的原理 iPhone 的后台刷新 ","date":"2017-05-20","externalUrl":null,"permalink":"/post/ios-notification/","section":"Posts","summary":"概览 # 推送通知我们大家都不陌生，可以说几乎每个使用智能手机的人每天都会被不同的通知 打扰 到，正式因为合适的推送是吸引用户注意力的利器，其成为了各 App 吸引用户，将用户带回到 App 本身，提升用户的活跃度的一种必要的方式。\n","title":"关于 iOS10 Notification 的那些事儿","type":"post"},{"content":"","date":"2017-02-12","externalUrl":null,"permalink":"/categories/book/","section":"Categories","summary":"","title":"Book","type":"categories"},{"content":"","date":"2017-02-12","externalUrl":null,"permalink":"/tags/garbage-collection/","section":"Tags","summary":"","title":"Garbage Collection","type":"tags"},{"content":"","date":"2017-02-12","externalUrl":null,"permalink":"/tags/python/","section":"Tags","summary":"","title":"Python","type":"tags"},{"content":"前一阵子 《垃圾回收的算法与实现》 这本书比较火，正好本人也对垃圾回收这个概念挺感兴趣的，就耐着性子一点一点啃，到今天只能说磕磕绊绊的看了大部分，实现篇只看了 Python 的部分，剩余的关于 Dalvik VM、Rubinius 以及 V8 的垃圾回收并未多看，主要还是自己对 Javascript，Ruby 等语言未有深入学习，我深以为只有结合这种语言本身的语言特性来看对应的垃圾回收实现才有意义。这篇文章主要是总结下学习到的一些主要的知识点。\n知识点总结 # 对于实现篇思维导图里也未有展开，之后等把 Ruby、Javascript 等语言熟悉之后再做深入的阅读吧。。。\n","date":"2017-02-12","externalUrl":null,"permalink":"/post/reading-garbage-collection/","section":"Posts","summary":"前一阵子 《垃圾回收的算法与实现》 这本书比较火，正好本人也对垃圾回收这个概念挺感兴趣的，就耐着性子一点一点啃，到今天只能说磕磕绊绊的看了大部分，实现篇只看了 Python 的部分，剩余的关于 Dalvik VM、Rubinius 以及 V8 的垃圾回收并未多看，主要还是自己对 Javascript，Ruby 等语言未有深入学习，我深以为只有结合这种语言本身的语言特性来看对应的垃圾回收实现才有意义。这篇文章主要是总结下学习到的一些主要的知识点。\n","title":"阅读《垃圾回收的算法与实现》","type":"post"},{"content":"","date":"2015-12-02","externalUrl":null,"permalink":"/tags/animation/","section":"Tags","summary":"","title":"Animation","type":"tags"},{"content":"","date":"2015-12-02","externalUrl":null,"permalink":"/tags/objective-c/","section":"Tags","summary":"","title":"Objective-C","type":"tags"},{"content":"原文：Elastic view animation, or how I built DGElasticPullToRefresh 原作者 @gontovnik\nDGElasticPullToRefresh 展示了如何实现一个弹性效果。效果如下图所示：\n开发环境： Xcode 7 Swift 2.0\n要求： 开发者至少对 UIBezierPath 和 UIGestureRecognizer 有一定的了解.\n理解业务逻辑 # 你可能从上面的效果图上可以看到一些端倪，这个动画中我们主要使用到了UIBezierPath来实现上面这种效果。 我们首先创建一个贝塞尔曲线路径的CAShapeLayer，然后当你的手指在屏幕上移动的时候我们将移动所有的控制点来呈现动画。每一个控制点会使用一个可见的UIView来表示。下面有几张图来演示它们是如何工作的，我将所有的控制点标识成了红色：\n第二章图片中将每个表示控制点的 View 的变量名称标记出来了，如L3，L2等。\n当你的手指释放的时候，我们就播放Spring动画，让所有的控制点向其初始位置以一定的回弹效果移动。当所有的视图在动画播放过程中，我们需要时刻计算并我们的贝塞尔曲线（每一帧都进行计算）。因此，我们准备使用CADisplayLink，CADisplayLink在主循环中运行而且每一帧都会去执行指定的方法。\nA CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display. - Apple doc\n让我们开始Code吧~\nBuild # 创建一个单视图控制器，然后将下面这些代码粘贴到viewController.swift文件的类声明中。\n// MARK: - // MARK: Vars private let minimalHeight: CGFloat = 50.0 private let shapeLayer = CAShapeLayer() // MARK: - override func loadView() { super.loadView() shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: view.bounds.width, height: minimalHeight) shapeLayer.backgroundColor = UIColor(red: 57/255.0, green: 67/255.0, blue: 89/255.0, alpha: 1.0).CGColor view.layer.addSublayer(shapeLayer) view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: \u0026#34;panGestureDidMove:\u0026#34;)) } // MARK: - // MARK: Methods func panGestureDidMove(gesture: UIPanGestureRecognizer) { if gesture.state == .Ended || gesture.state == .Failed || gesture.state == .Cancelled { } else { shapeLayer.frame.size.height = minimalHeight + max(gesture.translationInView(view).y, 0) } } override func preferredStatusBarStyle() -\u0026gt; UIStatusBarStyle { return .LightContent } 以上代码中，我们做的工作主要有以下几点：\n声明了两个变量：shapeLayer 和 minimalHeight。前者是用来表示贝塞尔曲线的，后者是用来定义shapeLayer最小的高度值； 将shapeLayer添加到view的layer中； 添加了一个pan手势到view中，并为该手势添加panGestureDidMove目标方法，当手指移动的时候就会被调用，我们在该方法中修改shapeLayer的高度； 重写父类的preferredStatusBarStyle方法来确保我们的UI更美观一些。 然后编译你的程序，确保实现了以下效果：\n每件事情都在按着我们期待的呈现，不过有一点瑕疵。那就是shapeLayer的高度变化有一定的延迟（动画），那是因为隐式动画的原因。我们确定不需要这样的动画，因此在将shapeLayer添加到view的sublayers中之前禁用该layer关于position、bounds以及path的隐式动画。\nshapeLayer.actions = [\u0026#34;position\u0026#34; : NSNull(), \u0026#34;bounds\u0026#34; : NSNull(), \u0026#34;path\u0026#34; : NSNull()] 然后再次运行，会看到延迟的动画效果没有了。\n接下来，我们就需要将最开始所说的那些控制点（L3, L2, L1, C, R1, R2, R3）加入到程序里，然后加入必要的逻辑。\n让我们一步一步来：\n首先，声明一个maxWaveHeight变量，定义这个变量的原因仅仅是希望让我们最终的效果好看一些而已。如果你不指定这个最大值，最终的效果会看起来很丑的； private let maxWaveHeight: CGFloat = 100.0 为我们的控制点视图声明变量； private let l3ControlPointView = UIView() private let l2ControlPointView = UIView() private let l1ControlPointView = UIView() private let cControlPointView = UIView() private let r1ControlPointView = UIView() private let r2ControlPointView = UIView() private let r3ControlPointView = UIView() 将上面定义的控制点视图设置为3*3的size，然后背景色设置为红色（这里我们只是暂时希望能够更方便演示，最终完成的时候会让它们不可见的），然后将这些控制点添加到View子视图中。将下面这些代码拷贝到loadView方法的尾部； l3ControlPointView.frame = CGRect(x: 0.0, y: 0.0, width: 3.0, height: 3.0) l2ControlPointView.frame = CGRect(x: 0.0, y: 0.0, width: 3.0, height: 3.0) l1ControlPointView.frame = CGRect(x: 0.0, y: 0.0, width: 3.0, height: 3.0) cControlPointView.frame = CGRect(x: 0.0, y: 0.0, width: 3.0, height: 3.0) r1ControlPointView.frame = CGRect(x: 0.0, y: 0.0, width: 3.0, height: 3.0) r2ControlPointView.frame = CGRect(x: 0.0, y: 0.0, width: 3.0, height: 3.0) r3ControlPointView.frame = CGRect(x: 0.0, y: 0.0, width: 3.0, height: 3.0) l3ControlPointView.backgroundColor = .redColor() l2ControlPointView.backgroundColor = .redColor() l1ControlPointView.backgroundColor = .redColor() cControlPointView.backgroundColor = .redColor() r1ControlPointView.backgroundColor = .redColor() r2ControlPointView.backgroundColor = .redColor() r3ControlPointView.backgroundColor = .redColor() view.addSubview(l3ControlPointView) view.addSubview(l2ControlPointView) view.addSubview(l1ControlPointView) view.addSubview(cControlPointView) view.addSubview(r1ControlPointView) view.addSubview(r2ControlPointView) view.addSubview(r3ControlPointView) 创建一个UIView的扩展，将其放到ViewController的声明上方； extension UIView { func dg_center(usePresentationLayerIfPossible: Bool) -\u0026gt; CGPoint { if usePresentationLayerIfPossible, let presentationLayer = layer.presentationLayer() as? CALayer { return presentationLayer.position } return center } } 当UIView从一帧到另外一帧进行动画的时候，你会试图去取得UIView.frame，而UIView.center会提供给你动画最终的值而不是每一帧的过渡值。因此我们需要定义一个扩展方法提供给我们UIView.layer.presentationLayer的实时位置； presentationLayer的详细信息可以在官方文档 中查看\n声明一个currentPath()方法； private func currentPath() -\u0026gt; CGPath { let width = view.bounds.width let bezierPath = UIBezierPath() bezierPath.moveToPoint(CGPoint(x: 0.0, y: 0.0)) bezierPath.addLineToPoint(CGPoint(x: 0.0, y: l3ControlPointView.dg_center(false).y)) bezierPath.addCurveToPoint(l1ControlPointView.dg_center(false), controlPoint1: l3ControlPointView.dg_center(false), controlPoint2: l2ControlPointView.dg_center(false)) bezierPath.addCurveToPoint(r1ControlPointView.dg_center(false), controlPoint1: cControlPointView.dg_center(false), controlPoint2: r1ControlPointView.dg_center(false)) bezierPath.addCurveToPoint(r3ControlPointView.dg_center(false), controlPoint1: r1ControlPointView.dg_center(false), controlPoint2: r2ControlPointView.dg_center(false)) bezierPath.addLineToPoint(CGPoint(x: width, y: 0.0)) bezierPath.closePath() return bezierPath.CGPath } 这个方法返回shapeLayer当前的CGPath。它用到了我们前面所定义和讨论的控制点。\n声明updateShapeLayer方法； func updateShapeLayer() { shapeLayer.path = currentPath() } This function will be called when we need shapeLayer to be updated. It is not a private func because we are going to use Selector() for CADisplayLink.\n这个方法会在shapeLayer需要更新的时候调用。\n声明layoutControlPoints方法； private func layoutControlPoints(baseHeight baseHeight: CGFloat, waveHeight: CGFloat, locationX: CGFloat) { let width = view.bounds.width let minLeftX = min((locationX - width / 2.0) * 0.28, 0.0) let maxRightX = max(width + (locationX - width / 2.0) * 0.28, width) let leftPartWidth = locationX - minLeftX let rightPartWidth = maxRightX - locationX l3ControlPointView.center = CGPoint(x: minLeftX, y: baseHeight) l2ControlPointView.center = CGPoint(x: minLeftX + leftPartWidth * 0.44, y: baseHeight) l1ControlPointView.center = CGPoint(x: minLeftX + leftPartWidth * 0.71, y: baseHeight + waveHeight * 0.64) cControlPointView.center = CGPoint(x: locationX , y: baseHeight + waveHeight * 1.36) r1ControlPointView.center = CGPoint(x: maxRightX - rightPartWidth * 0.71, y: baseHeight + waveHeight * 0.64) r2ControlPointView.center = CGPoint(x: maxRightX - (rightPartWidth * 0.44), y: baseHeight) r3ControlPointView.center = CGPoint(x: maxRightX, y: baseHeight) } 这一部分可能需要解释下这些变量的职责：\nbaseHeight - base的高度。 baseHeight + waveHeight = 我们所需要的总高度； waveHeight - 曲线的波浪高度，我们前面的maxWaveHeight为其设定一个最大值； locationX - 手指在视图中的位置的X坐标 width - 显而易见，我们视图的宽度； minLeftX - 定义l3ControlPointView的坐标X值的最小值，这个值可以是负值； maxRightX - 和minLeftX类似，定义r3ControlPointView的坐标X值的最大值； leftPartWidth - 定义 minLeftX 和 locationX之间的距离； rightPartWidth - 定义 locationX 和 maxRightX之间的距离； 你可能会问，为什么我们要使用这些值来设定我们的控制点呢？答案很简单，我使用了PaintCode，然后内部建立贝塞尔曲线，不断的试直到令我满意 的效果时，我就将这些值替换到程序中即可。\n更新我们的panGestureDidMove方法，这样当我们手指移动的时候所有的控制点也能移动。用以下的代码替换panGestureDidMove方法中的内容； func panGestureDidMove(gesture: UIPanGestureRecognizer) { if gesture.state == .Ended || gesture.state == .Failed || gesture.state == .Cancelled { } else { let additionalHeight = max(gesture.translationInView(view).y, 0) let waveHeight = min(additionalHeight * 0.6, maxWaveHeight) let baseHeight = minimalHeight + additionalHeight - waveHeight let locationX = gesture.locationInView(gesture.view).x layoutControlPoints(baseHeight: baseHeight, waveHeight: waveHeight, locationX: locationX) updateShapeLayer() } } 我们所做的就是计算wave高度，基础高度以及手指的位置，然后调用layoutControlPoints去布局各个控制点，然后调用updateShapeLayer来更新我们的shapeLayer的path。\nWhat we do is calculate wave height, base height, location of the finger and call our function: layoutControlPoints to layout control points and updateShapeLayer to update our shape layer path.\n在loadView方法尾部添加两行代码，使得我们在打开app的时候正确的显示； layoutControlPoints(baseHeight: minimalHeight, waveHeight: 0.0, locationX: view.bounds.width / 2.0) updateShapeLayer() 修改 shapeLayer.backgroundColor = UIColor(red: 57/255.0, green: 67/255.0, blue: 89/255.0, alpha: 1.0).CGColor 为\nshapeLayer.fillColor = UIColor(red: 57/255.0, green: 67/255.0, blue: 89/255.0, alpha: 1.0).CGColor 到这里，效果应该如下所示：\n最后一件要做的事情就是，把我们释放手指之后的回弹动画整合进去。 还是让我们一步一步来吧：\n定义displayLink变量； private var displayLink: CADisplayLink! 在loadView方法的末尾进行初始化：\ndisplayLink = CADisplayLink(target: self, selector: Selector(\u0026#34;updateShapeLayer\u0026#34;)) displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode) displayLink.paused = true 前面我们也提到过，我们的CADisplayLink对象会在每一帧都调用指定的方法（这里就是指定的updateShapeLayer），因此我们就可以在UIView的动画期间实时的更新shapeLayer的path了。\n定义animating变量； private var animating = false { didSet { view.userInteractionEnabled = !animating displayLink.paused = !animating } } 这个可以控制用户交互是否开启以及displayLink的停止和播放（动画播放期间你肯定不希望用户的交互直接就把动画效果给破坏了。）\nprivate func currentPath() -\u0026gt; CGPath { let width = view.bounds.width let bezierPath = UIBezierPath() bezierPath.moveToPoint(CGPoint(x: 0.0, y: 0.0)) bezierPath.addLineToPoint(CGPoint(x: 0.0, y: l3ControlPointView.dg_center(animating).y)) bezierPath.addCurveToPoint(l1ControlPointView.dg_center(animating), controlPoint1: l3ControlPointView.dg_center(animating), controlPoint2: l2ControlPointView.dg_center(animating)) bezierPath.addCurveToPoint(r1ControlPointView.dg_center(animating), controlPoint1: cControlPointView.dg_center(animating), controlPoint2: r1ControlPointView.dg_center(animating)) bezierPath.addCurveToPoint(r3ControlPointView.dg_center(animating), controlPoint1: r1ControlPointView.dg_center(animating), controlPoint2: r2ControlPointView.dg_center(animating)) bezierPath.addLineToPoint(CGPoint(x: width, y: 0.0)) bezierPath.closePath() return bezierPath.CGPath } 更新currentPath方法，将dg_center方法参数换为animating参数仅当动画需要的时候才提供layer的准确过渡值。\n最后一步就是需要更新panGestureDidMove方法中的if判断语句； if gesture.state == .Ended || gesture.state == .Failed || gesture.state == .Cancelled { let centerY = minimalHeight animating = true UIView.animateWithDuration(0.9, delay: 0.0, usingSpringWithDamping: 0.57, initialSpringVelocity: 0.0, options: [], animations: { () -\u0026gt; Void in self.l3ControlPointView.center.y = centerY self.l2ControlPointView.center.y = centerY self.l1ControlPointView.center.y = centerY self.cControlPointView.center.y = centerY self.r1ControlPointView.center.y = centerY self.r2ControlPointView.center.y = centerY self.r3ControlPointView.center.y = centerY }, completion: { _ in self.animating = false }) } else { let additionalHeight = max(gesture.translationInView(view).y, 0) let waveHeight = min(additionalHeight * 0.6, maxWaveHeight) let baseHeight = minimalHeight + additionalHeight - waveHeight let locationX = gesture.locationInView(gesture.view).x layoutControlPoints(baseHeight: baseHeight, waveHeight: waveHeight, locationX: locationX) updateShapeLayer() } 我们已经为UIView添加了Sprint动画，让我们的控制点能够通过很优雅的回弹效果回到它们的位置。你可以修改以上的变量值来使你的动画看起来更优美，更好看。\n让我们在设备上运行下看看能够让你震惊的效果：\n当然，我们可能不太想显示这些红色的点。你可以在loadView方法中移除那些设置这些控制点的frame 以及背景色的代码。\n再次运行代码效果如下：\n效果很完美，但是，在这个例子中我们还有一件事情没做，就是没有改变 shapeLayer 的高度，我们仅仅修改了 path 的高度。这个不够完美，需要修复它。我想这个对于你来说应该是不错的练习。尽情修改frame、path以及所有变量的值吧！\n这个Demo的源码在这里 DGElasticPullToRefresh的源码在这里\n","date":"2015-12-02","externalUrl":null,"permalink":"/post/elastic-view-animation-using-uibezierpath/","section":"Posts","summary":"原文：Elastic view animation, or how I built DGElasticPullToRefresh 原作者 @gontovnik\nDGElasticPullToRefresh 展示了如何实现一个弹性效果。效果如下图所示：\n开发环境： Xcode 7 Swift 2.0\n","title":"如何使用UIBezierPath实现一个弹性视图动画","type":"post"},{"content":"","date":"2015-09-14","externalUrl":null,"permalink":"/tags/category/","section":"Tags","summary":"","title":"Category","type":"tags"},{"content":"Objective-C中的分类（category）是一种编译时的手段，其允许我们通过给某个已知类添加方法来扩充该类的一种方式。当然这其中是有限制的，就是不能给已知类添加新的实例变量。\n如下代码展示：\n类MyClass为一个简单的类，其中有实例方法 -print\n#import \u0026lt;Foundation/Foundation.h\u0026gt; @interface MyClass : NSObject - (void)print; @end #import \u0026#34;MyClass.h\u0026#34; @implementation MyClass - (void)print { NSLog(@\u0026#34;MyClass...\u0026#34;); } @end 我们向为MyClass再添加一个方法Hello，那我们就可以用分类的方法实现，我们为其添加分类MyAddition1和MyAddition2，如下所示：\n#import \u0026#34;MyClass.h\u0026#34; @interface MyClass (MyAddition1) @property (nonatomic, copy) NSString *name; - (void)hello; @end #import \u0026#34;MyClass+MyAddition1.h\u0026#34; @implementation MyClass (MyAddition1) - (void)hello { NSLog(@\u0026#34;hello!!!\u0026#34;); } @end 其中MyAddition为分类的名称，而文件名字约定俗成用“类名+扩展名”的形式。这样，我们就能够为MyClass类添加hello方法了，如下调用：\n#import \u0026#34;MyClass+MyAddition1.h\u0026#34; int main() { //... MyClass my = [MyClass new]; [my hello]; //... } Category 的使用场景主要有以下几种：\n需求变更，需要为已知类添加方法； 将类的不同模块实现划分: a)具体可以将类的实现分开到不同的文件里面； b)将不同功能的分类文件交由不同的开发者实现； 想为Apple基础类库添加自己需要的方法，实际上和1相似； 但是分类的使用目前也有需要注意的地方：\nCategory可以访问原有类的实例变量，但不能添加实例变量； Category中实现和原有类中相同签名的方法时，会覆盖原有类的方法； 但是这两点均可以通过其他方式实现：\n为分类添加实例变量 # 为分类添加实例变量主要通过关联对象的方法。如下所示：\n#import \u0026#34;MyClass.h\u0026#34; @interface MyClass (MyAddition1) @property (nonatomic, copy) NSString *name; @end #import \u0026#34;MyClass+MyAddition1.h\u0026#34; #import \u0026lt;objc/objc-runtime.h\u0026gt; @implementation MyClass (MyAddition1) - (void)setName:(NSString *)name { objc_setAssociatedObject(self, \u0026#34;key\u0026#34;, name, OBJC_ASSOCIATION_COPY); } - (NSString *)name { NSString *name = objc_getAssociatedObject(self, \u0026#34;key\u0026#34;); return name; } @end 如果调用被分类覆盖掉的原方法 # 实际上，通过查看Objc runtime源码可以获知，分类方法并不是绝对意义上的覆盖原有类的方法，只是在调用的时候调用顺序导致而已，这涉及到Objc runtime的一些内容。\n在下载到的运行时源码中objc-runtime-new.mm文件中有attachCategoryMethods方法，其就是将类的分类方法添加到类的方法列表中去的：\n// objc-runtime-new.mm static void attachCategoryMethods(Class cls, category_list *cats, bool flushCaches) { if (!cats) return; if (PrintReplacedMethods) printReplacements(cls, cats); bool isMeta = cls-\u0026gt;isMetaClass(); method_list_t **mlists = (method_list_t **) _malloc_internal(cats-\u0026gt;count * sizeof(*mlists)); // Count backwards through cats to get newest categories first int mcount = 0; int i = cats-\u0026gt;count; BOOL fromBundle = NO; while (i--) { method_list_t *mlist = cat_method_list(cats-\u0026gt;list[i].cat, isMeta); if (mlist) { mlists[mcount++] = mlist; fromBundle |= cats-\u0026gt;list[i].fromBundle; } } attachMethodLists(cls, mlists, mcount, NO, fromBundle, flushCaches); _free_internal(mlists); } 那attachCategoryMethods的工作就是将所有分类的实例方法列表进行拼接，形成更大的mlists表，然后转交给attachMethodLists方法来执行。\nstatic void attachMethodLists(Class cls, method_list_t **addedLists, int addedCount, bool baseMethods, bool methodsFromBundle, bool flushCaches) { rwlock_assert_writing(\u0026amp;runtimeLock); // Don\u0026#39;t scan redundantly bool scanForCustomRR = !UseGC \u0026amp;\u0026amp; !cls-\u0026gt;hasCustomRR(); bool scanForCustomAWZ = !UseGC \u0026amp;\u0026amp; !cls-\u0026gt;hasCustomAWZ(); // There exist RR/AWZ special cases for some class\u0026#39;s base methods. // But this code should never need to scan base methods for RR/AWZ: // default RR/AWZ cannot be set before setInitialized(). // Therefore we need not handle any special cases here. if (baseMethods) { assert(!scanForCustomRR \u0026amp;\u0026amp; !scanForCustomAWZ); } // Method list array is nil-terminated. // Some elements of lists are nil; we must filter them out. method_list_t *oldBuf[2]; method_list_t **oldLists; int oldCount = 0; if (cls-\u0026gt;data()-\u0026gt;flags \u0026amp; RW_METHOD_ARRAY) { oldLists = cls-\u0026gt;data()-\u0026gt;method_lists; } else { oldBuf[0] = cls-\u0026gt;data()-\u0026gt;method_list; oldBuf[1] = nil; oldLists = oldBuf; } if (oldLists) { while (oldLists[oldCount]) oldCount++; } int newCount = oldCount; for (int i = 0; i \u0026lt; addedCount; i++) { if (addedLists[i]) newCount++; // only non-nil entries get added } method_list_t *newBuf[2]; method_list_t **newLists; if (newCount \u0026gt; 1) { newLists = (method_list_t **) _malloc_internal((1 + newCount) * sizeof(*newLists)); } else { newLists = newBuf; } // Add method lists to array. // Reallocate un-fixed method lists. // The new methods are PREPENDED to the method list array. newCount = 0; int i; for (i = 0; i \u0026lt; addedCount; i++) { method_list_t *mlist = addedLists[i]; if (!mlist) continue; // Fixup selectors if necessary if (!isMethodListFixedUp(mlist)) { mlist = fixupMethodList(mlist, methodsFromBundle, true/*sort*/); } // Scan for method implementations tracked by the class\u0026#39;s flags if (scanForCustomRR \u0026amp;\u0026amp; methodListImplementsRR(mlist)) { cls-\u0026gt;setHasCustomRR(); scanForCustomRR = false; } if (scanForCustomAWZ \u0026amp;\u0026amp; methodListImplementsAWZ(mlist)) { cls-\u0026gt;setHasCustomAWZ(); scanForCustomAWZ = false; } // Update method caches if (flushCaches) { cache_eraseMethods(cls, mlist); } // Fill method list array newLists[newCount++] = mlist; } // Copy old methods to the method list array for (i = 0; i \u0026lt; oldCount; i++) { newLists[newCount++] = oldLists[i]; } if (oldLists \u0026amp;\u0026amp; oldLists != oldBuf) free(oldLists); // nil-terminate newLists[newCount] = nil; if (newCount \u0026gt; 1) { assert(newLists != newBuf); cls-\u0026gt;data()-\u0026gt;method_lists = newLists; cls-\u0026gt;setInfo(RW_METHOD_ARRAY); } else { assert(newLists == newBuf); cls-\u0026gt;data()-\u0026gt;method_list = newLists[0]; assert(!(cls-\u0026gt;data()-\u0026gt;flags \u0026amp; RW_METHOD_ARRAY)); } } 注意上面代码中注释copy old methods to the method list array，可以获知该类原有的方法被追加到了新方法列表的后面，因此可以了解如果category和原来的都由methodA方法，那newLists中肯定存在两个methodA方法。而运行时在查找类方法的时候是在方法列表中按序查找的，一旦发现对应签名的方法则就返回，因此被后置的原有方法自然而然就被方法列表前面的同签名方法所覆盖了。\n当然，如果还有一个分类也覆盖了同样的方法，顺序又是什么样子呢？根据文件的编译顺序，有兴趣的童鞋可以尝试一下，关于编译顺序。。。\n明白了这种情况，如果我们需要调用类的被覆盖的方法，则通过查找类的实例方法列表中对应签名的方法，在方法列表前面的自然就是分类所对应的方法，而最后一个则是MyClass的原有方法了。\n#import \u0026lt;Foundation/Foundation.h\u0026gt; @interface MyClass : NSObject - (void)print; @end #import \u0026#34;MyClass.h\u0026#34; @implementation MyClass - (void)print { NSLog(@\u0026#34;MyClass\u0026#34;); } @end 为MyClass添加分类MyAddition1，代码如下：\n#import \u0026#34;MyClass.h\u0026#34; @interface MyClass (MyAddition1) - (void) print; @end #import \u0026#34;MyClass+MyAddition1.h\u0026#34; @implementation MyClass (MyAddition1) - (void)print { NSLog(@\u0026#34;MyAddition1\u0026#34;); } @end 然后再添加MyAddition2分类，\n#import \u0026#34;MyClass.h\u0026#34; @interface MyClass (MyAddition2) - (void) print; @end #import \u0026#34;MyClass+MyAddition2.h\u0026#34; @implementation MyClass (MyAddition2) - (void)print { NSLog(@\u0026#34;MyAddition2\u0026#34;); } @end 当我们将整个文件编译之后我们就可以获得MyClass所有的方法了，如下代码所示：\n#import \u0026#34;MyClass.h\u0026#34; #import \u0026lt;objc/objc-runtime.h\u0026gt; int main(int argc, const char * argv[]) { @autoreleasepool { Class cls = [MyClass class]; MyClass *my = [MyClass new]; [my print]; if(cls) { unsigned int methodCount; Method *methodList = class_copyMethodList(cls, \u0026amp;methodCount); IMP lastImp = NULL; SEL lastSel = NULL; for (NSInteger i = 0; i \u0026lt; methodCount; i++) { Method method = methodList[i]; NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method)) encoding:NSUTF8StringEncoding]; NSLog(@\u0026#34;methodName: %@\u0026#34;, methodName); if([methodName isEqualToString:@\u0026#34;print\u0026#34;]) { lastImp = method_getImplementation(method); lastSel = method_getName(method); } } typedef void(*fn)(id, SEL); if(lastImp != NULL) { fn f = (fn)(lastImp); f(my, lastSel); } free(methodList); } } return 0; } 代码内容就是将类所对应的方法列表取出，找到最后一个对应名称的方法，取得函数指针，然后传入参数执行。\n输出结果如下：\n2015-09-14 15:26:59.793 ObjMessage[88888:10428876] MyAddition2 2015-09-14 15:26:59.794 ObjMessage[88888:10428876] methodName: print 2015-09-14 15:26:59.795 ObjMessage[88888:10428876] methodName: print 2015-09-14 15:26:59.795 ObjMessage[88888:10428876] methodName: print 2015-09-14 15:30:38.941 ObjMessage[88888:10428876] MyClass 当我们使用print方法时，打印为MyAddition2中的实现，并且MyClass类方法列表中有三个方法，并且均为print，取出列表中最后一个print并调用打印结果为MyClass类原有类中的实现。此时就达到了我们的目的。 当然，你也可以调整下MyClass+MyAddition1.m文件和MyClass+MyAddition2.m文件的顺序验证下，结果MyClass默认输出为MyAddition1.\n","date":"2015-09-14","externalUrl":null,"permalink":"/post/objective-c%E4%B8%ADcategory%E7%9A%84%E4%B8%80%E7%82%B9%E4%B8%9C%E8%A5%BF/","section":"Posts","summary":"Objective-C中的分类（category）是一种编译时的手段，其允许我们通过给某个已知类添加方法来扩充该类的一种方式。当然这其中是有限制的，就是不能给已知类添加新的实例变量。\n","title":"Objective-C中Category的一点东西","type":"post"},{"content":"","date":"2015-09-07","externalUrl":null,"permalink":"/tags/runtime/","section":"Tags","summary":"","title":"Runtime","type":"tags"},{"content":"Objective-C 运行时对于刚刚踏入 Cocoa/Objective 世界的人是很容易忽 略的 Objective-C 语言的特性之一。原因就是尽管 Objective-C 是一门几个小时之内入门的语言，但是投身 Cocoa 的新手们会花费大量时间在 Cocoa 框架中，试图搞清楚他到底是怎么工作的。 我觉得每个开发者都应该对其有深入的了解，明白一些内部的实现细节，而不仅仅只知道代码 [target doMethodWith:var] 会被编译器转换成 objc_msgSend(target,@selector(doMethodWith:),var1); 而已。了解 Objective-C 运行时的原理有助于你对 Objective-C 语言有更深入的理解，清楚你得 App 是怎么运行的。我觉得这对无论是 Mac/iPhone 新手或者老手都会有所帮助。\n[TOC]\nObjective-C运行时是开源的 # Objective-C 运行时是开源的，你可以随时从 Apple 获取到。实际上查看 Objective-C 运行时源码是我搞清楚这个语言是怎么运作的首选方法，而不是去查看和它相关的苹果文档。你可以到这里 下载到运行时的源码（截止到译者翻译的时候，最新版本的文件是objc4-647.tar.gz）。\n动态 Vs. 静态 # Objective-C 是一门动态的面向对象语言，这意味着它可以将编译链接时决定的事情推迟到运行时进行。这就给了你很大的灵活性，你可以按照自己的需要重定向消息到适当的对象上，你甚至可以交换方法实现（译者注：method swizzling，方法调配，开发者常用此技术向原有实现中添加新功能）。而运行时可以使对象明白自己可以响应哪些消息，不能响应哪些消息（译者注：introspect 内省），并正确的派发消息。\n译者注： 内省（introspection）是面向对象语言和环境的一个强大特性，Objective-C和Cocoa在这个方面尤其的丰富。内省是对象揭示自己作为一个运行时对象的详细信息的一种能力。这些详细信息包括对象在继承树上的位置，对象是否遵循特定的协议，以及是否可以响应特定的消息。NSObject协议和类定义了很多内省方法，用于查询运行时信息，以便根据对象的特征进行识别。\n参考: Objective-C 的 Introspection\n我们将这些特征和C语言进行对比来看，在C中，你从main()函数开始，然后按顺序从上往下写你的代码逻辑或者执行函数方法。C结构体是无法将请求转发到其他目标对象来执行方法的。你很可能写了如下类似的代码：\n#include \u0026lt; stdio.h \u0026gt; int main(int argc, const char **argv[]) { printf(\u0026#34;Hello World!\u0026#34;); return 0; } 编译器解析、优化然后将优化后的代码转换成如下的汇编语言：\n.text .align 4,0x90 .globl _main _main: Leh_func_begin1: pushq %rbp Llabel1: movq %rsp, %rbp Llabel2: subq $16, %rsp Llabel3: movq %rsi, %rax movl %edi, %ecx movl %ecx, -8(%rbp) movq %rax, -16(%rbp) xorb %al, %al leaq LC(%rip), %rcx movq %rcx, %rdi call _printf movl $0, -4(%rbp) movl -4(%rbp), %eax addq $16, %rsp popq %rbp ret Leh_func_end1: .cstring LC: .asciz \u0026#34;Hello World!\u0026#34; 然后将其和C库链接生成可执行文件。相比之下，Objective-C语言整个过程和上面类似，不过代码的产生依赖Objective-C运行时的具体表现（译者注：运行时按照不同情况，生成不同的代码吧）。当我们刚接触Objective-C语言的时候，我们可能被告知像如下代码\n[self doSomethingWithVar:var1]; 会被转换成\nobjc_msgSend(self,@selector(doSomethingWithVar:),var1); 而除了这些，我们似乎就不清楚运行时还干了什么。\n什么是 Objective-C 运行时？ # Objective-C Runtime 是一个运行时库，主要是由C语言和汇编语言写成，为 C 语言添加面向对象的能力而创造了 Objective-C（译者注：正是 OC Runtime，才有 OC 这门语言）。这意味着它可以加载类信息，进行方法派发以及方法转发等等。Objective-C 运行时最重要的就是为Objective-C语言的面向对象特性的实现提供了所有的基础支撑。\nObjective-C 运行时术语 # 在我们进一步了解整个运行时之前，需要先了解一些接下来出现的术语。截至目前Mac和iPhone的开发者关心的有两个运行时：Modern Runtime \u0026amp; Legacy Runtime 。前者覆盖所有64位Mac OS X 的app和所有的iOS app，后者覆盖其余的（全部的Mac OS X 32位 App）。关于方法，这里有两种基本类型的方法，一种是实例方法（\u0026rsquo;-\u0026lsquo;开头， 例如-(void)doFoo，作用于对象实例），另一种是类方法（\u0026rsquo;+\u0026lsquo;开头，例如+(id)alloc）。方法就像C语言中的函数类似，一段代码完成一个小的任务，如下：\n- (NSString *)movieTitle { return @\u0026#34;Futurama: Into the Wild Green Yonder\u0026#34;; } Selector\nObjective-C中的Selector（选择子）是一个重要的C数据结构，用以标识你要一个对象执行的Objective-C方法。在运行时中，Selector的定义应该和下面这样类似：\ntypedef struct objc_selector *SEL; 用法就像下面这样：\nSEL aSel = @selector(movieTitle); Message\n[target getMovieTitleForObject:obj]; Objective-C中的方法由两个方括号[]组成，括号中间是你将要将消息发往的目标对象和你将要该对象执行的方法以及所需要发送的参数列表。Objective-C中的消息和C函数类似，但是又不同。你向一个对象发送消息并不意味着该对象就一定会执行它。这个对象会检查该消息的发送者，然后基于该发送者要么执行一个不同的方法或者将该消息转发给另外的不同的对象。\nClass 如果你看过Runtime中关于类的定义信息，你可能会遇到这样的定义：\ntypedef struct objc_class *Class; typedef struct objc_object { Class isa; } *id; 这其中有一些事情要注意。 每一个Objective-C类拥有一个结构体，每一个对象也有一个结构体。所有的对象都包含一个isa指针。所有的Objective-C运行时需要这个isa指针，用以检查一个对象具体的类型是什么，然后判别其是否能够响应你所派发过来的消息。 最后我们还注意到了id指针，这个id指针仅仅告诉我们其指向的是Objective-C对象，仅此而已。当你拥有一个id指针，你可以查询该对象的类型，然后查看该类型是否可以响应某个方法等等。还有就是当你知道了当前所指向的具体对象的具体类别，你就可以做出更具体的动作。\nBlocks\n你也可以在LLVM/Clang文档中对Blocks 的定义中发现和上面类似的东西。\nstruct Block_literal_1 { void *isa; // initialized to \u0026amp;_NSConcreteStackBlock or \u0026amp;_NSConcreteGlobalBlock int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 { unsigned long int reserved; // NULL unsigned long int size; // sizeof(struct Block_literal_1) // optional helper functions void (*copy_helper)(void *dst, void *src); void (*dispose_helper)(void *src); } *descriptor; // imported variables }; Blocks被设计成能够与Objective-C运行时兼容，因此它们可以被当做对象处理，并可以响应消息（像-retain, -release, -copy等）。\nIMP（Method Implementations）\ntypedef id (*IMP)(id self,SEL _cmd,...); IMP是编译器生成的函数指针，指向方法执行处。如果你刚接触Objective-C语言，你不需要直接和这些东西打交道，但是慢慢深入之后接触的就多了。后面我们会看到这也是Objective-C运行时唤醒方法的方式。\nObjective-C Classes\nObjective-C 的类内部有些什么东西呢？一个Objective-C的类的样子大体如下：\n@interface MyClass : NSObject { // vars NSInteger counter; } // methods -(void)doFoo; @end 但是运行时还会追加更多的内容以便跟踪（类每一时刻的状态）。\n#if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif 我们可以看到一个类中包含一个指向其父类的引用，该类的名字、实例变量、方法集合、缓存以及该类遵循的协议列表。运行时需要这些信息以便响应那些分发到该类或者类实例对象上得方法。\n因此类定义了对象而不是对象本身，那这又是如何实现的？ # 正如我前面说过的，Objective-C类本身也同样是对象（译者注：意味着你可以向一个类发送消息），运行时通过创建元类来处理它们。当你发送类似[NSObject alloc]的消息时，你实际上是向类对象发送了消息，而这个类对象需要是元类的实例，而元类本身又是根元类的一个实例。 当你说一个类继承自NSObject，那就意味着你的类指向NSObject作为其父类。而所有的元类指向根元类作为它们的父类，所有的元类都仅包含那些它能够响应的消息中的类方法。所以当你向一个类对象发送消息，例如[NSObject alloc]的时候，objc_msgSend()实际上会查看元类来确定该对象是否能够响应该消息，那如果找到了一个能响应该消息的方法，就在该对象上执行它。\n译者注：Objective-C类体系结构图如下所示： 为什么我们都要继承自Apple的类呢？ # 当你刚踏入Cocoa开发的时候，很多代码例子都告诉你这样做：先继承NSObject类然后再进行其他编码。你也乐在其中，确实享受到了很多继承自Apple类所提供的便利。但是你甚至可能都没有发现实际上你的类在和Objective-C运行时打交道。当你为我们的类实例化的时候，像这样：\nMyObject *object = [[MyObject alloc] init]; 第一个你要执行的消息就是+alloc。如果你[查看文档][6]，里面会讲到“一个新生实例的isa实例会被初始化为一个描述该类信息的数据结构，其余的实例变量的内存均被设置为空。”所以通过继承自Apple的类，我们不仅继承了一些很棒的属性，同时也继承了这些在内存上分配空间（大小是我们类的大小），创建对象的能力（就是创建运行时所期望的带有isa指针的数据结构）。\n类缓存（objc_cache* cache）是什么？ # 当Objective-C运行时通过一个对象的isa指针检查对象的时候，它会找到能够执行很多方法的类。然后你只需要调用其中很小一部分，所以每次运行时在进行一次查询动作时需要查找类分发表中所有的selectors这个动作是毫无意义的。这就是为什么类会由cache这个东西，当你查询一个类体系中的派发表的时候，一旦找到对应的selector时，就将该selector放到cache中。当objc_msgSend()方法在一个类中查询selector时，会先在cache中查找，这个理论的基础就是如果你曾经调用过一个类的消息，你有很大可能在之后还调用同样的方法。（译者注：CACHE的局部性原理）。所以按照这样考虑，如果我们先在有一个NSObject的子类MyObject如下：\nMyObject *obj = [[MyObject alloc] init]; @implementation MyObject -(id)init { if(self = [super init]) { [self setVarA:@”blah”]; } return self; } @end 具体发生了以下几点：\n[MyObject alloc]首先被执行，MyObject类没有实现该方法，因此在该类中没有找到+alloc方法，接着顺着superclass指针找到其父类NSObject； 我们询问NSObject类是否能够响应+alloc方法，而它能够响应。+alloc方法检查接受者类（也就是MyObject），分配该类大小的一块内存空间，然后初始化其isa指针指向MyObject类，此时我们拥有一个实例了，同时稍早我们将+alloc方法放置于NSObject的类缓存（cache）中； 到目前为止，我们都是在发送类方法，此刻我们需要向一个实例对象发送消息，这里简单的调用-init方法或者指定初始化方法（designated initializer），当然我们的类实现了该方法，因此我们将-(id)init方法放置于cache中； 接下来self = [super init]被调用，super是一个神奇的关键字（magic keyword），其指向类的父类，也即NSObject，我们调用NSObject的init方法。这样做的目的是为了确保面向对象编程的集成体系能够正确运行，在你正确初始化自身变量之前需要先初始化该类的所有父类的变量，如果你需要，你还可以覆写父类的方法。在该例中，对于NSObject类来说并没有多少特别重要的操作要进行，不过这并不是常态。有时候初始化中会做非常重要的事情，考虑以下代码： #import \u0026lt; Foundation/Foundation.h\u0026gt; @interface MyObject : NSObject { NSString *aString; } @property(retain) NSString *aString; @end @implementation MyObject - (id)init { if (self = [super init]) { [self setAString:nil]; } return self; } @synthesize aString; @end int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; id obj1 = [NSMutableArray alloc]; id obj2 = [[NSMutableArray alloc] init]; id obj3 = [NSArray alloc]; id obj4 = [[NSArray alloc] initWithObjects:@\u0026#34;Hello\u0026#34;,nil]; NSLog(@\u0026#34;obj1 class is %@\u0026#34;,NSStringFromClass([obj1 class])); NSLog(@\u0026#34;obj2 class is %@\u0026#34;,NSStringFromClass([obj2 class])); NSLog(@\u0026#34;obj3 class is %@\u0026#34;,NSStringFromClass([obj3 class])); NSLog(@\u0026#34;obj4 class is %@\u0026#34;,NSStringFromClass([obj4 class])); id obj5 = [MyObject alloc]; id obj6 = [[MyObject alloc] init]; NSLog(@\u0026#34;obj5 class is %@\u0026#34;,NSStringFromClass([obj5 class])); NSLog(@\u0026#34;obj6 class is %@\u0026#34;,NSStringFromClass([obj6 class])); [pool drain]; return 0; } 如果你是刚刚接触Cocoa，我让你猜以上代码打印结果是什么，你很可能会给出这样的结果：\nNSMutableArray NSMutableArray NSArray NSArray MyObject MyObject 但实际上是如下结果：\nobj1 class is __NSPlaceholderArray obj2 class is NSCFArray obj3 class is __NSPlaceholderArray obj4 class is NSCFArray obj5 class is MyObject obj6 class is MyObject 译者注：（本机XCode 7 beta6 运行结果如下：）\n2015-09-07 13:43:06.922 ObjMessage[5185:1441448] obj1 class is __NSPlaceholderArray 2015-09-07 13:43:11.201 ObjMessage[5185:1441448] obj2 class is __NSArrayM 2015-09-07 13:43:17.987 ObjMessage[5185:1441448] obj3 class is __NSPlaceholderArray 2015-09-07 13:43:18.503 ObjMessage[5185:1441448] obj4 class is __NSArrayI 2015-09-07 13:43:32.228 ObjMessage[5185:1441448] obj5 class is MyObject 2015-09-07 13:43:33.478 ObjMessage[5185:1441448] obj6 class is MyObject 原因就是Objective-C语言这里使用+alloc方法返回某个类的对象，但随后-init方法又可能返回另一个类的对象。\n那 objc_msgSend 方法都发生了什么呢？ # 实际上objc_msgSend()方法内部发生了许多事情。如下我们有这样的代码：\n[self printMessageWithString:@\u0026#34;Hello World!\u0026#34;]; 编译器会把其翻译成如下所示代码：\nobjc_msgSend(self, @selector(printMessageWithString:), @\u0026#34;Hello World!\u0026#34;); 我们通过目标对象的isa指针来查询该类或者其继承体系中的父类是否能够响应 @selector(printMessageWithString:)。假设我们在类的派发表或者它的cache中找到了该selector，然后我们通过该函数指针来执行该方法。因此我们可以了解objc_msgSend()方法永远不会返回，它从执行开始，通过指针查找到你的方法执行，然后是你的方法执行之后返回，因此看起来好像objc_msgSend()方法返回似的。Bill Bumgarner对此过程有更多的细节探索（Part 1, Part 2 \u0026amp; Part 3）。\n我这里总结下他所讲的，也就是你会在运行时代码中看到的：\n检查那些忽略掉的Selectors和Short Circut， 很明显如果我们运行在垃圾回收环境下，我们可以忽略掉针对-retain，-release等的调用；\n检查那些nil的目标。不像其他语言，Objective-C中向nil派发消息是合法的。当然你肯定也有很多理由希望这样。这里假设我们有一个非空的目标；\n接下来我们需要在该类中找到IMP，我们首先查找该类的缓存（cache），如果找到我们便通过缓存中的指针跳转到该函数执行处；\n如果缓存中没有找到该IMP，我们便紧接着查找类的派发表（dispatch table），如果找到同样跳转到函数执行处；\n如果类的派发表中也未找到我们就需要触发消息分发机制了，这意味着最终你得代码会被编译器转换成了C函数。因此一个如下方法：\n-(int)doComputeWithNum:(int)aNum 会被转换成如下：\nint aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum) Objective-C运行时通过触发指向这些方法的函数指针来调用你的方法。告诉你，你没法直接调用这些转换之后的方法，尽管Cocoa框架确实提供了能够获取这些指针的方法。\n//declare C function pointer int (computeNum *)(id,SEL,int); //methodForSelector is COCOA \u0026amp; not ObjC Runtime //gets the same function pointer objc_msgSend gets computeNum = (int (*)(id,SEL,int))[target methodForSelector:@selector(doComputeWithNum:)]; //execute the C function pointer returned by the runtime computeNum(obj,@selector(doComputeWithNum:),aNum); 通过这个方法，你可以获取到在运行时直接获取该方法并调用它。甚至在你确认一个指定的方法需要被执行的时候可以绕过运行时机制。这也是Objective-C运行时如何调用你的方法的，但还是使用objc_msgSend()方法为好。\nObjective-C 消息转发 # 在Objective-C中，向一个根本不知道怎么响应方法的对象发送方法是合法的（也可能是该语言内部设计哲理）。Apple这样做的其中一个原因就是模拟Objective-C语言原生不支持的多重继承。或者你也许想抽象化自己的设计，隐藏该消息响应背后的其他类或者对象。这对于运行时系统也是非常必要的。它的工作流程大体是这样：\n运行时在该类或者其继承体系中的缓存中和派发表中查找，然后查找失败；\nObjective-C运行时在所属对象的类上调用+ (BOOL) resolveInstanceMethod:(SEL)aSEL类方法，该类给予你一次机会来新增一个处理选择子aSEL的方法，然后告诉运行时你已经解决了该方法，消息转发机制会找到该方法。\n如下示例，你定义了一个函数：\nvoid fooMethod(id obj, SEL _cmd) { NSLog(@\u0026#34;Doing Foo\u0026#34;); } 你可以使用class_addMethod()方法类解决它：\n+(BOOL)resolveInstanceMethod:(SEL)aSEL { if(aSEL == @selector(doFoo:)) { class_addMethod([self class],aSEL,(IMP)fooMethod,\u0026#34;v@:\u0026#34;); return YES; } return [super resolveInstanceMethod]; } 其中方法class_addMethod()中的v@:标明了方法的返回类型以及其参数类型。你可以在运行时文档中[Type Encodings][10]分查看到详细的说明。\n如果2中+(BOOL)resolveInstanceMethod:(SEL)aSEL返回NO表示无法解析该方法的话，运行时接着调用- (id)forwardingTargetForSelector:(SEL) aSelector来给你再一次机会是否能够将该消息转发给其他接收者来处理。这要比之后运行完整的消息转发- (void)forwardInvocation:(NSInvocation *)anInvocation要好。你可以这样执行： - (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(mysteriousMethod:)) { return alternateObject; } return [super forwardingTargetForSelector:aSelector]; } 很明显，你肯定不想返回 self，否则会引起死循环。\n如果上一步没有找到合适的目标对象来执行上面的消息，接着运行时会尝试最后一步 - (void)forwardInvocation:(NSInvocation *)anInvocation。你可能没见过NSInvocation，它实际上是Objective-C语言中的消息类型。一旦你有一个NSInvocation，你基本上能够改变这个消息的任何东西，包括其目标、选择子以及参数。所以你可以这样做： - (void)forwardInvocation:(NSInvocation *)invocation { SEL invSEL = invocation.selector; if([altObject respondsToSelector:invSEL]) { [invocation invokeWithTarget:altObject]; } else { [self doesNotRecognizeSelector:invSEL]; } } 默认情况下，如果你继承自NSObject类，它所实现的- (void)forwardInvocation:(NSInvocation *)anInvocation内部仅仅简单的调用了-doesNotRecognizeSelector:方法，如果你想给自己最后一次机会做一些事情的话，你可以重载该方法。\nNon Fragile ivars (Modern Runtime) # 一个Modern Runtime新增加的概念就是Non Fragile ivars。当编译器编译我们的类时，编译器会生成一个变量布局来显示每次我们从什么位置去取我们的实例变量，其底层的实现细节是这样的，查看类成员变量和类对象指针指向位置的偏移，读取该变量大小的字节就可以将该变量读取出来。所以你得变量布局可能如下所示，左侧列标明字节偏移量：\n这里我们有NSObject类型的变量布局，然后我们继承NSObject来扩展它，并添加自己的变量，这在Apple发布新版本OSX SDK之前都运行良好。\n我们的代码就无法正常运行，我们自定义对象中的内容被擦出了，因为NSObject增加了两个成员变量，而MyObject类成员变量布局在编译时已经确定，有两个成员变量和基类的内存区域重叠。唯一能够阻止这个发生的就是Apple维持它之前的布局策略，但是一旦这样他们的框架就无法再往前发展了，因为它们的变量布局已经固化了。在这种情况下（也就是fragile ivars）你只能通过重新编译这些继承自Apple类的类来使得代码得以兼容。那在 non fragile ivars下会发生什么呢？\n在Non Fragile ivars下编译器虽然生成了和fragile ivars同样的布局，但是运行时会通过计算基类大小，动态调整MyObject类成员布局。结果如上图所示。\nObjective-C 关联对象 # 最近引入Mac OS X 10.6系统有一个特性称作“关联引用”。Objective-C不像其他语言，其原生不支持向对象动态添加变量。所以到目前为止，你都必须要费很大的劲，编译整个体系结构来假装自己向类中添加一个变量。不过在Mac OS X 10.6系统中，Objective-C 运行时原生支持（动态添加变量）。如果我们想向每一个已经存在的类中添加一个变量，例如向NSView类中添加，如下所示：\n#import \u0026lt; Cocoa/Cocoa.h\u0026gt; //Cocoa #include \u0026lt; objc/runtime.h\u0026gt; //objc runtime api’s @interface NSView (CustomAdditions) @property(retain) NSImage *customImage; @end @implementation NSView (CustomAdditions) static char img_key; //has a unique address (identifier) -(NSImage *)customImage { return objc_getAssociatedObject(self,\u0026amp;img_key); } -(void)setCustomImage:(NSImage *)image { objc_setAssociatedObject(self,\u0026amp;img_key,image, OBJC_ASSOCIATION_RETAIN); } @end 你可以在[runtime.h][11]，（译者注：最新版 [runtime.h][12]）文件中看到向objc_setAssociatedObject()传递的几个选项：\n/* Associative References */ /** * Policies related to associative references. * These are options to objc_setAssociatedObject() */ enum { OBJC_ASSOCIATION_ASSIGN = 0, /**\u0026lt; Specifies a weak reference to the associated object. */ OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**\u0026lt; Specifies a strong reference to the associated object. * The association is not made atomically. */ OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**\u0026lt; Specifies that the associated object is copied. * The association is not made atomically. */ OBJC_ASSOCIATION_RETAIN = 01401, /**\u0026lt; Specifies a strong reference to the associated object. * The association is made atomically. */ OBJC_ASSOCIATION_COPY = 01403 /**\u0026lt; Specifies that the associated object is copied. * The association is made atomically. */ }; 这些和你通过@property方式传递的选项相吻合。\n混合的虚表派生 # 如果你查看Modern runtime 代码，你会在objc-runtime-new.m（译者注： 最新版objc runtime源码为[objc-runtime-new.mm][14] 已经去掉了这个特性，译者发现从[objc4-551.1][15]版本开始就不支持了，不过读者还是可以借鉴下之前版本的实现方式。）中发现这个：\n/*********************************************************************** * vtable dispatch * * Every class gets a vtable pointer. The vtable is an array of IMPs. * The selectors represented in the vtable are the same for all classes * (i.e. no class has a bigger or smaller vtable). * Each vtable index has an associated trampoline which dispatches to * the IMP at that index for the receiver class\u0026#39;s vtable (after * checking for NULL). Dispatch fixup uses these trampolines instead * of objc_msgSend. * Fragility: The vtable size and list of selectors is chosen at launch * time. No compiler-generated code depends on any particular vtable * configuration, or even the use of vtable dispatch at all. * Memory size: If a class\u0026#39;s vtable is identical to its superclass\u0026#39;s * (i.e. the class overrides none of the vtable selectors), then * the class points directly to its superclass\u0026#39;s vtable. This means * selectors to be included in the vtable should be chosen so they are * (1) frequently called, but (2) not too frequently overridden. In * particular, -dealloc is a bad choice. * Forwarding: If a class doesn\u0026#39;t implement some vtable selector, that * selector\u0026#39;s IMP is set to objc_msgSend in that class\u0026#39;s vtable. * +initialize: Each class keeps the default vtable (which always * redirects to objc_msgSend) until its +initialize is completed. * Otherwise, the first message to a class could be a vtable dispatch, * and the vtable trampoline doesn\u0026#39;t include +initialize checking. * Changes: Categories, addMethod, and setImplementation all force vtable * reconstruction for the class and all of its subclasses, if the * vtable selectors are affected. **********************************************************************/ 这背后的原理就是，运行时试图去存储你最近调用过的选择子（selector）以便能够为你的App加速，因为其比objc_msgSend方法使用更少的指令。这个vTable存储你最近全局调用的16个选择子，实际上，在代码文件往下接着看你就会看到垃圾回收和非垃圾回收类型的App的默认选择子（selectors）。\nstatic const char * const defaultVtable[] = { \u0026#34;allocWithZone:\u0026#34;, \u0026#34;alloc\u0026#34;, \u0026#34;class\u0026#34;, \u0026#34;self\u0026#34;, \u0026#34;isKindOfClass:\u0026#34;, \u0026#34;respondsToSelector:\u0026#34;, \u0026#34;isFlipped\u0026#34;, \u0026#34;length\u0026#34;, \u0026#34;objectForKey:\u0026#34;, \u0026#34;count\u0026#34;, \u0026#34;objectAtIndex:\u0026#34;, \u0026#34;isEqualToString:\u0026#34;, \u0026#34;isEqual:\u0026#34;, \u0026#34;retain\u0026#34;, \u0026#34;release\u0026#34;, \u0026#34;autorelease\u0026#34;, }; static const char * const defaultVtableGC[] = { \u0026#34;allocWithZone:\u0026#34;, \u0026#34;alloc\u0026#34;, \u0026#34;class\u0026#34;, \u0026#34;self\u0026#34;, \u0026#34;isKindOfClass:\u0026#34;, \u0026#34;respondsToSelector:\u0026#34;, \u0026#34;isFlipped\u0026#34;, \u0026#34;length\u0026#34;, \u0026#34;objectForKey:\u0026#34;, \u0026#34;count\u0026#34;, \u0026#34;objectAtIndex:\u0026#34;, \u0026#34;isEqualToString:\u0026#34;, \u0026#34;isEqual:\u0026#34;, \u0026#34;hash\u0026#34;, \u0026#34;addObject:\u0026#34;, \u0026#34;countByEnumeratingWithState:objects:count:\u0026#34;, }; 因此你如何能知道你正在和它打交道呢？ # 当你进行调试的时候，你会在你的调试栈中看到稍后讲解到的某些方法的身影。你就把这些方法按照objc_msgSend()方法来对待就行，不过这些方法都是为了调试，具体有如下几个方法。\n当运行时正在将你所有调用的这些方法中的其中一个插入到虚表（vTable）中时，会调用objc_msgSend_fixup。\n而当objc_msgSend_fixedup发生时，表明你当前所调用的一个方法本应该存在于虚表中objc_msgSend_vtable[0-15]的位置，但却并不在\n你可能会看到objc_msgSend_vtable5类似的东西，其意味着你正在调用虚表中的一个方法。运行时可以根据需要动态调整虚表中的内容。因此你不应该期望这次代码循环调用的objc_msgSend_vtable10对应了-length方法，而之后每次代码循环还依然会这样。（因为vTable也在不断变化中）\n译者注：参考[objc explain]: objc_msgSend_vtable\n总结 # 我希望你们能够喜欢以上这些东西，这篇文章主要讲述了我和Des Moines Cocoaheads关于Objective-C runtime的谈话（我们的讨论估计能打包一箩筐）。Objective-C Runtime是一项很了不起的工程，它为我们Cocoa/Objective-C下制作的Apps注入能量，使得我们能够实现很多我们认为理所当然的特性。希望你能够看一看Apple官方文档对Objective-C运行时的讲解，这样能够使你更好的利用Objective-C运行时。谢谢。\n参考链接 # Objective-C Runtime Programming Guide Objective-C Runtime Reference\n","date":"2015-09-07","externalUrl":null,"permalink":"/post/understanding-the-objective-c-runtime/","section":"Posts","summary":"Objective-C 运行时对于刚刚踏入 Cocoa/Objective 世界的人是很容易忽 略的 Objective-C 语言的特性之一。原因就是尽管 Objective-C 是一门几个小时之内入门的语言，但是投身 Cocoa 的新手们会花费大量时间在 Cocoa 框架中，试图搞清楚他到底是怎么工作的。 我觉得每个开发者都应该对其有深入的了解，明白一些内部的实现细节，而不仅仅只知道代码 [target doMethodWith:var] 会被编译器转换成 objc_msgSend(target,@selector(doMethodWith:),var1); 而已。了解 Objective-C 运行时的原理有助于你对 Objective-C 语言有更深入的理解，清楚你得 App 是怎么运行的。我觉得这对无论是 Mac/iPhone 新手或者老手都会有所帮助。\n","title":"理解Objective-C运行时","type":"post"},{"content":"原文：How We Created Guillotine Menu Animation for iOS 原作者 @johnsundell\n你是否曾经有过这样的疑问？为什么app中几乎是清一色的侧边栏（sidebar），为什么不把它做成topBar或者bottomBar，甚至cornerBar呢？\n本文将要谈到的就是当前导航条动画的一个新趋势。\n动画很有趣，但更重要的是能发挥很大的作用，它们可以改变你思考问题的方式，使得你的产品更好用并提升的app整体的用户体验。接下来我们将要展示的是设计师Vitaly Rubtsov的一个非常棒的点子：\n“每个设计师都有那么一刻感到无聊。因为几乎所有（对动画）的完善修补、裁剪以及规格设定都给他们留下了很少发挥想象的余地。而每当这些时候，我就会驱使自己打开Adobe After Effects软件然后创作一些比较有趣的东西。\n当我开始想我要创建一个什么东西的时候，我突然有个想法，通常侧边栏都从屏幕左侧划出，同时将所有的内容都移动到右侧位置。这种传统侧边栏的实现方式太过无聊了。那如果我们将侧边栏变成上边栏会怎么样呢？它从页面的上方掉落然后以一种特别的方式呈现，听起来很棒不是吗？“\nVitaly设计的topBar动画由我们的iOS开发工程师Maksym Lazebnyi使用swift语言实现，并且开发者给了它一个很有趣的名字—— Guillotine Menu。\n我们是如何开发Guillotine Menu的？ # by Makssym Lazebnyi\n实际上，我们的iOS团队见到过很多实现这种动画效果的方法。我们选择了其中一种方式实现，这种方式允许开发者在Storyboard中以任何方式自定义菜单。\n为了实现我们的转场动画（transitioning animation），我们创建了一个UIStoryboardSegue的子类和一个自定义动画管理器（custom animation controller）。基本上这就是你实现该动画所需要做的全部工作，除非你想让它更炫酷。当然我们也确实想这样做，因此还创建了一些辅助的类。\n整体上，你需要三个类以及一个UIView的扩展类来创建这个动画，如下所示：\nGuillotineMenuSegue. 该类是继承自UIStoryboardSegue类。我们使用它来模态显示菜单，并实现由 GuillotineMenuTransitionAnimation类控制的呈现动画。 GuillotineMenuSegue允许你为菜单添加透明度，当然本文并没有做这个工作。\nGuillotineMenuTransitionAnimation. 该类是为了自定义呈现 GuillotineMenuViewController 类中视图的动画所用。\nGuillotineMenuViewController. 该类是一个UIViewController的子类，存放菜单视图所用。\n除此之外，我们还为UIView添加了扩展方法以便能为子视图添加约束来更好的适应父视图。\n接下来，我们逐一对每一个类进行阐述。\nGuillotineMenuSegue # 这个类中没有什么特别的地方，我只提及一些关键点。\n在该类重载的init方法中，我们检查目标视图控制器（destination view controller）是否遵守GuillotineAnimationProtocol 协议（该协议我们后面会讲到）。在重载的perform方法中我们将self设置成一个过渡动画的代理。\n在代理方法 animationControllerForPresentedController 中我们使用关联对象将GuillotineMenuSegue的实例对象（self）关联到具体的将要呈现的视图控制器中，这样当menu view controller呈现在屏幕上的时候，segue实例不会被销毁。（译者注：代码如下）\n- (id \u0026lt;UIViewControllerAnimatedTransitioning\u0026gt;)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { // 将segue示例self关联到将要呈现的试图控制器presented中，这样确保presented生命周期内segue实例不会被释放 objc_setAssociatedObject(presented, \u0026amp;key, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return [[GuillotineTransitionAnimation alloc] initWithMode:AnimationModePresentation]; } GuillotineMenuTransitionAnimation # 所有的动画处理逻辑都在这个类中实现。\n起初我们考虑使用 animateWithDuration usingSpringWithDamping \u0026amp; initialSprintVelocity方法，但是当我们认真考虑这个动画之后我们改变了主意。我们需要实现的菜单动画中，当菜单从上方掉落之后，需要和父视图的左侧边界发生碰撞，产生碰撞效果。而上面方法中的回弹效果会穿过左边界（spring through the border of the superview），因此我们放弃了这个实现方式，转而采用了UIDynamicAnimator。\n为了实现我们的动画，GuillotineMenuTransitionAnimation类必须遵从 UIViewControllerAnimatedTransitioning 协议，该协议中有两个代理方法：\ntransitionDuration 动画的过渡时间对我们影响不是太多，因此你可以返回任意的时间值。\nanimationDuration 当菜单开启和关闭均会调用该方法\n###我们如何计算动画的位置信息？\n我们需要了解动画中每一刻的精确位置。GuillotineMenuTransitionAnimation类需要GuillotineMenuViewController提供menu button中心的坐标来做为旋转动画的锚点（anchorPoint）。另外还需要GuillotineMenuViewController提供一些其他属性，因此我们创建一个协议GuillotineMenuViewController，视图控制器通过遵守该协议返回我们所需要的几个属性。 代码如下：\n@objc protocol GuillotineAnimationProtocol: NSObjectProtocol { func navigationBarHeight() -\u0026gt;CGFloat func anchorPoint() -\u0026gt;CGPoint func hostTitle() -\u0026gt;NSString } 译者注：Objective-C版本代码如下：\n@protocol GuillotineAnimationProtocol \u0026lt;NSObject\u0026gt; - (CGFloat) navigationBarHeight; - (CGPoint) anchorPoint; - (NSString*) hostTitle; @end 这三个方法的意义如下：\nnavigationBarHeight GuillotineMenuViewController开始显示动画的时候需要旋转90度，同时覆盖导航条（navigation bar）。我们需要将GuillotineMenuViewController中的视图位置设置成CGPoint(0，navigationBarHeight)；\nanchorPoint 提供我们动画的旋转轴心，这里是GuillotineMenuViewController中的menu button的中心位置；\nhostTitle 用来询问GuillotineMenuViewController获得主视图控制器的标题。\n我们如何实现菜单视图的掉落以及旋转？ # 为了实现掉落以及旋转的动画，我们使用UIDynamicAnimator并为其添加四种动力行为： （译者注：实际上实现UIKit动力学中的推动力、吸附、碰撞以及辅助行为，详见UIKit动力学）\nUIPushBehavior 为view添加一个拖拽的力，当我们需要呈现显示动画的时候，施加到view的底部，当我们需要呈现关闭菜单的动画时，施加到view的顶部；\nUIAttachmentBehavior 相当于一个钉子在menu button的中心点将整个view固定住。\nUICollisionBehavior 我们为view的父视图（superview）添加了一个边界，从视图中心位置到左下角位置的长度。用以实现GuillotineMenuViewController在掉落路径的末端模拟碰撞效果（译者注：当菜单视图关闭而回弹到上方时，同样需要添加boundary，不过此时是在顶部，屏幕水平方向菜单视图的中心位置到其右下角位置）；\nUIDynamicItemBehavior 实现菜单碰撞左边界之后的回弹效果。\n基本上，我们的动画是这样，首先使用CGAffineTransformRotate将GuillotineMenuViewController的view旋转正向90度，设置该view的边框位置为CGPoint(0, navigationBarHeight)。然后，我们将该view添加以上每一种需要使用的UIDynamicBehavior（UIPushBehavior、UIAttachmentBehavior、UICollisionBehavior以及UIDynamicItemBehavior）。\nUIDynamicAnimator会使得菜单的动画持续，一直到所有附加在其上的物理作用力达到平衡。\n我们通过代理协议UIDynamicAnimatorDelegate来告知视图控制器动画的完成情况。另外，我们还需要调用endAppearanceTransition()方法。\n这里有一个比较棘手的地方就是设置anchorPoint。为了使得动画正确呈现，锚点的位置到GuillotineMenuViewController中视图的左边界的距离必须和锚点到顶部导航条底部之间的距离相同。而且，当设备发生旋转也需要修改锚点位置。但是GuillotineMenuTransitionAnimation类调用代理方法anchorPoint()是在viewDidLayoutSubviews()用之前。\n因此我们将设备处于水平方向时的按钮位置进行了硬编码\n译者注： 设备布局一旦发生变化，例如设备进行了旋转，便会调用viewDidLayoutSubviews()方法，本身我们可以在该方法中动态调整按钮位置（也就是锚点位置），可是转场动画必须在该方法调用之前取到锚点位置，因此矛盾。作者就在代码里硬编码处理布局发生变化之后菜单按钮的位置了。\nUIViewExtension # 针对UIView的简单扩展，主要是针对子视图添加约束以更好的适应父视图。代码本身就很好能够说明功能了（self explanatory），这里就不叙述了。\n译者注： 针对Objective-C语言的类扩展和swift语言不同，在objc实现版本中，文件名字为UIView+ConstraintExtension.h和UIView+ConstraintExtension.m\nGuillotine Menu View Controller # 你可以继承该视图控制器或者进行任何的自定义甚至重写。唯一必须要记得是的遵守GuillotineAnimationProtocol协议。\n你如何才能定制该动画？ # 你可以用任何可能的方式来定制菜单视图！你只需要创建一个自定义的GuillotineMenuSegue，其中源视图控制器就是你的主视图控制器（host view controller），目标视图控制器就是你需要呈现的菜单视图控制器。\n实话讲，刚开始创建这个动画的时候我自认为这是一件很简单的事情，这个过程应该也没什么挑战。可是现在我们必须承认，对于iOS开发者而言这里面还有巨大的潜力可挖。另外，我们的动画还可以作为一个简单的动画视图来使用，或者作为一个带有自定义导航条的UINavigationViewController的子类来使用。接下来我们计划将不断更新这项工作，力图创造一个完整的带自定义转场动画效果的UINavigationViewController的子类。\n你可以在以下这几个位置找到我们的工程源码以及设计：\nGitHub Dribbble 译者注：Objective-C 版本实现可以参见GuillotineMenu-objc\n","date":"2015-09-01","externalUrl":null,"permalink":"/post/how-we-created-guillotine-menu-animation-for-ios/","section":"Posts","summary":"原文：How We Created Guillotine Menu Animation for iOS 原作者 @johnsundell\n你是否曾经有过这样的疑问？为什么app中几乎是清一色的侧边栏（sidebar），为什么不把它做成topBar或者bottomBar，甚至cornerBar呢？\n","title":"我们是如何创建iOS版的Guillotine菜单动画的","type":"post"},{"content":" 题目：28. Implement strStr() # Implement strStr().\nReturns the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.\n题意： # 该题目就是实现strstr()函数，该函数C语言原型如下：返回字符串str1在str2中的位置。\nextern char *strstr(char *str1, const char *str2); 解法： # 暴力求解法 # 最直接的实现方式就是暴力求解法，如下代码所示，\nclass Solution { public: int strStr(string haystack, string needle) { return bruth(haystack, needle); } private: int bruth(string text, string pattern) { int M = pattern.length(); int N = text.length(); if (M == 0) return 0; if (N == 0) return -1; for (int i = 0; i \u0026lt;= N - M; ++i) { int j = 0; for (; j \u0026lt; M; ++j) { if (text.at(i + j) != pattern.at(j)) break; } if (j == M) return i; } return -1; } } 在最坏的情况下，暴力子字符串查找算法在长度为N的文本中查找长度为M的模式字符串需要O(NM)次字符串比较。\n我们可以将上面的实现方法进行一定的修改，让大家能看到i指针的回退，如下代码所示，当i指针指向字符和j指向字符发生失配，i便会回退j个位置（此时j是失配前模式串的匹配项个数），同时j指向模式串的第一个位置。该段代码和上面代码其实是等效的。\nclass Solution { public: int strStr(string haystack, string needle) { return bruth(haystack, needle); } private: int bruth(string text, string pattern) { int M = pattern.length(); int N = text.length(); if (M == 0) return 0; if (N == 0) return -1; int i = 0; int j = 0; for (; i \u0026lt; N \u0026amp;\u0026amp; j \u0026lt; M; ++i) { if (text.at(i) == pattern.at(j)) j++; else {\ti -= j; j = 0; } } if (j == M) return i - M; else return -1; } } 那我们就可以很明显的看到，当模式串中j指向位置字符不匹配的时候，其实前面通过匹配我们已经获知了一部分文本的情况，因此如果能够利用这一部分已知的情况来加大模式串移动的有效性。KMP算法就是利用字符串匹配失效之前部分匹配的这个有用信息，通过保持文本指针不回退，仅有效移动模式字符串的位置来进行有效查找的。\nKMP算法 # KMP算法常用于在一个文本串S内查找一个模式串P 的出现位置，这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人同时独立发现，后取这3人的姓氏命名此算法. 大家可以参看july的Blog，讲解的是我目前看到最详细的了。\nKMP算法实现：\nclass Solution { public: int strStr(string haystack, string needle) { return kmp(haystack, needle); } private: // KMP int kmp(string text, string pattern) { int M = pattern.length(); int N = text.length(); if (M == 0) return 0; if (N == 0) return -1; int i = 0; int j = 0; int* next = new int[M]; calculateNext(pattern, next); for (; i \u0026lt; N \u0026amp;\u0026amp; j \u0026lt; M; ++i) { if ( j == -1 || text.at(i) == pattern.at(j) ) j++; else { i--; j = next[j]; }\t// i维持不变，j跳转位置由next数组决定 } delete[] next; if (j == M) return i - M; else return -1; } void calculateNext(string pattern, int* next) { if (pattern.length() == 0 || next == nullptr) return; next[0] = -1; int i = 0; int k = -1; while (i \u0026lt; pattern.length() - 1) { if (k == -1 || pattern.at(k) == pattern.at(i)) { ++i; ++k; next[i] = k; } else { k = next[k]; } } } } ","date":"2015-08-21","externalUrl":null,"permalink":"/post/leetcode-021-implementstrstr/","section":"Posts","summary":"题目：28. Implement strStr() # Implement strStr().\n","title":"[28] Implement strStr()","type":"post"},{"content":"","date":"2015-08-21","externalUrl":null,"permalink":"/tags/algorithm/","section":"Tags","summary":"","title":"Algorithm","type":"tags"},{"content":"","date":"2015-08-21","externalUrl":null,"permalink":"/categories/leetcode/","section":"Categories","summary":"","title":"LeetCode","type":"categories"},{"content":"","date":"2015-08-21","externalUrl":null,"permalink":"/tags/string/","section":"Tags","summary":"","title":"String","type":"tags"},{"content":" 题目：257. Binary Tree Paths # Given a binary tree, return all root-to-leaf paths. For example, given the following binary tree: 1 / 2 3 5 All root-to-leaf paths are: [\u0026ldquo;1-\u0026gt;2-\u0026gt;5\u0026rdquo;, \u0026ldquo;1-\u0026gt;3\u0026rdquo;]\n题意： # 难度不大，考察树的遍历\n解法： # C++版本实现方法：\n/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode(int x) : val(x), left(NULL), right(NULL) {} * }; */ class Solution { public: vector\u0026lt;string\u0026gt; binaryTreePaths(TreeNode* root) { vector\u0026lt;string\u0026gt; results; visitTreeNode(root, \u0026#34;\u0026#34;, results); return results; } private: void visitTreeNode(TreeNode* node, string path, vector\u0026lt;string\u0026gt;\u0026amp; result) { if(node == nullptr) return; if (!path.empty()) { path += \u0026#34;-\u0026gt;\u0026#34;; } path += int2Str(node-\u0026gt;val); if (node-\u0026gt;left == nullptr \u0026amp;\u0026amp; node-\u0026gt;right == nullptr) { result.push_back(path); } else { if (node-\u0026gt;left != nullptr) { visitTreeNode(node-\u0026gt;left, path, result); } if (node-\u0026gt;right != nullptr) { visitTreeNode(node-\u0026gt;right, path, result); } } } std::string int2Str(int nValue) { ostringstream ss; ss \u0026lt;\u0026lt; nValue; return ss.str(); } } ","date":"2015-08-18","externalUrl":null,"permalink":"/post/leetcode-019-binarytreepaths/","section":"Posts","summary":"题目：257. Binary Tree Paths # Given a binary tree, return all root-to-leaf paths. For example, given the following binary tree: 1 / 2 3 5 All root-to-leaf paths are: [“1-\u003e2-\u003e5”, “1-\u003e3”]\n","title":"[257] Binary Tree Paths","type":"post"},{"content":"","date":"2015-08-18","externalUrl":null,"permalink":"/tags/tree/","section":"Tags","summary":"","title":"Tree","type":"tags"},{"content":" 题目： # Given a string s consists of upper/lower-case alphabets and empty space characters \u0026rsquo; \u0026lsquo;, return the length of last word in the string. If the last word does not exist, return 0. Note: A word is defined as a character sequence consists of non-space characters only. For example, Given s = \u0026ldquo;Hello World\u0026rdquo;, return 5.\n题意： # 难度不大，考虑所有边界情况即可。\n解法： # C++版本实现方法1：\n// 从右往左依次遍历即可 class Solution { public: int lengthOfLastWord(string s) { int length = 0; for (int index = s.length() - 1; index \u0026gt;= 0; index--){ char c = s.at(index); if (c != \u0026#39; \u0026#39;){ length++; } else if (c == \u0026#39; \u0026#39; \u0026amp;\u0026amp; length != 0){ return length; } } return length; } } leetCode Oj系统评判结果如下图所示：\n;\nC++版本实现方法2：\n// 直接利用现有轮子STL class Solution { public: int lengthOfLastWord(string s) { auto left = find_if(s.rbegin(), s.rend(), ::isalpha); auto right = find_if_not(left, s.rend(), ::isalpha); return distance(left, right); } } leetCode Oj系统评判结果如下图所示：\n;\n","date":"2015-08-18","externalUrl":null,"permalink":"/post/leetcode-018-lengthoflastword/","section":"Posts","summary":"题目： # Given a string s consists of upper/lower-case alphabets and empty space characters ’ ‘, return the length of last word in the string. If the last word does not exist, return 0. Note: A word is defined as a character sequence consists of non-space characters only. For example, Given s = “Hello World”, return 5.\n","title":"[018] Length Of Last Word","type":"post"},{"content":"最近几天被iOS的推送部署给搞懵了，现在特地整理下和大家进行分享。\niOS远端推送机制 # APNS，全称为Apple Push Notification service，是苹果通知推送服务中最重要的一环。它是苹果通知推送服务器，为所有iOS设备以及OS X设备提供强大并且可靠的推送通知服务。每个注册通知服务的设备都会和该服务器进行长连接，从而实时获取推送通知。即使当前APP不在运行状态，当通知到达的时候也会有提示发生，最常见的就是短信服务。\n每一个App必须向APNs注册通知服务，APNs会返回给设备一个DeviceToken，该Token为APNs上针对该设备的唯一标示符。App需要将该DeviceToken返给自身的Server端保存后续使用，如下所示。\n当App开发者的server需要向特定设备推送通知时，就使用DeviceToken和固定格式数据（Push payload）发给APNs，然后APNs就会向DeviceToken指定的设备推送通知了，具体流程如下所示，单一推送\n或者多方通知，APNs都能一一对应，靠的就是之前我们提供给它的DeviceToken。\n本地推送证书配置 # 打开你mac的钥匙串访问，然后点击钥匙串访问\n随后它会弹出一个窗口 用户电子邮件信息 就填写你苹果开发者账号的名称即可（应该是一个邮件名称），点击保存到磁盘的选项，点击继续，点击存储，文件名为：CertificateSigningRequest.certSigningRequest。 然后我们打开苹果开发者中心 进入 Member Center 然后点击左侧列表中任意一项进入详情页面，\nAPP ID # 首先我们需要为我们要开发的APP建立身份信息，就是AppID，如图所示，点击左侧\n点击添加按钮进入注册页面，我们需要输入App Id的名字以及BundleID，其中BundleID不能有通配符，否则无法具备推送功能，然后在下面的APP Service中勾选Push Notification一项\n点击下一步，然后确认提交即可，大家注意到Push Notification一项为Configurable，这是因为我们还没有为该AppID生成推送证书，等推送证书生成完毕之后可以再回来查看该AppID 的状态。\nCertificates # 其次，我们需要生成开发者证书和推送证书，如下图所示，点击左侧Cerifications列表，选择添加进入下一页面，\n如果您的页面如图所示为灰色不可选，说明您已经拥有了开发者证书。就不需要再次生成了，如果可选就选择该选项，\n接下来进入以下界面，选择你之前添加的AppID，之后点击Continue即可，\n然后选择之前我们保存在本地的CSR文件CertificateSigningRequest.certSigningRequest，点击Generate就生成了开发者的证书。\n同理我们需要生成推送测试证书，生成流程和开发者证书类似，只是在证书类型页面，选择的证书类型换成了Apple Push Notification service SSL。\n当我们生成好推送证书之后再回头看我们之前创建的AppId，能够看Push Notifications一项已经为Enabled了。当然发布推送证书配置完毕之后，Distribution一项也显示为Enable。\nProvisioning Profiles # 第三步，需要生成Provisioning Profiles，该文件其实就是以上的证书、AppId以及设备信息的打包集合，我们只要在不同的场景下生成不同类型Provisioning Profiles即可，它会在后续打包ipa文件的时候被嵌入安装包内。\n首先我们选择左侧列表中的Provisioning Profiles中的All选项，选择添加\n之后选择生成类型，我们这里以开发类型为例，下面还有发布的两种类型，\n之后点击Continue，进入下一页面，同样选择我们之前创建的具有Push服务的AppId，\n接下来，选择上面生成的开发证书（一一对应的，如果你选择生成的是发布Provisioning Profiles，则会出现发布证书），\n紧接着，我们选择授权设备，即你需要进行开发的设备，该设备可以在左侧Devices列表中添加，需要提供设备的UUID，这里我们选择所有设备，点击Continue，\n最后一步，我们给Provisioning Profiles添加名称，\n点击Generate即生成我们所需要的Provisioning Profile。\n其实同理，我们可以生成发布版的开发者证书，推送证书以及对应的Provisioning Profiles。最后的文件我们都放到同一个文件夹里，如图所示，其中我把发布的两种（Ad Hoc 和 Distribution）都一起搞出来。\n其中Push.p12文件后续会提及~\n开发环境配置 # 我们将上一步生成的开发者证书ios_development.cer以及推送证书aps_development.cer在最初生成CSR文件的MAC机上安装，双击即可安装，同时会打开钥匙串页面，安装之后我们找到之前生成CSR文件时生成的专用密钥，名称就是我们之前生成CSR文件时填写的，选择该专用密钥，同时选中刚刚安装成功的推送证书，必须注意，同时选择，我们需要将专用密钥以及安装成功的推送证书同时导出成一个文件）右键菜单导出， 如图我们命名Push，点击存储， 接下来需要为证书添加密码，这个密码是需要提供给服务器的。 最后我们需要配置我们本地的开发环境，也就是XCode，第一步我们点击XCode的Preference打开XCode的首选项菜单， 在Account选项中添加我们的开发者账户，如果之前已经登录就会看到该账户信息，然后点击下方的View Details， 之后会显示该开发者账户的证书和Provisioning Profiles等信息，该信息会和你开发者账号里面显示的一致，如果不一致就点击刷新， 不久就会出现我们之前创建的Provisioning Profiles，接下来，我们在 Xcode中 Build Settings -\u0026gt; Code Signing中选择我们需要的Provisioning Profiles文件即可\n此时本地开发环境已经配置完毕。接下来就开始Coding，Coding，Coding。。。。\n远端推送通知的代码实现 # 首先我们需要注册推送通知服务并获取DeviceToken；\n- (void)initPushNotificationWithApp: (UIApplication*)application { // 注册通知服务 if([UIDevice currentDevice].systemVersion.floatValue \u0026lt; 8.0) { [application registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert)]; } else { // IOS8.0以上版本的注册推送方式和以往不同 UIUserNotificationSettings* settings = [UIUserNotificationSettings settingsForTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert) categories:nil]; [application registerUserNotificationSettings:settings]; [application registerForRemoteNotifications]; } } 如果注册成功，APNs会返回给你设备的token，iOS系统会把它传递给app delegate代理：\n// 如果注册成功，则会收到DeviceToken，我们需要将该Token发给服务器保存 - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { PRINT_FUNC NSString* tokenStr = [NSString stringWithFormat:@\u0026#34;%@\u0026#34;, deviceToken]; NSLog(@\u0026#34;deviceToken: %@\u0026#34;, tokenStr); if(tokenStr.length == 0) { NSLog(@\u0026#34;Device Token Invalid!\u0026#34;); } // 然后我们需要将该DeviceToken发给我们自己的服务器进行保存； [self sendDeviceToken:deviceToken]; } // 如果注册失败，会收到错误信息，包含错误原因 - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { PRINT_FUNC NSLog(@\u0026#34;***************************************\\n\u0026#34;); NSLog(@\u0026#34;Failed to Register The Notification!!!!\\n\u0026#34;); NSLog(@\u0026#34;error = %@\u0026#34;, error); NSLog(@\u0026#34;***************************************\\n\u0026#34;); } 之后我们就可以在AppDelegate中添加处理代码，当用户点击通知栏的通知或者处于运行状态时，App代码会执行- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo代理方法，如下所示：\n- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { PRINT_FUNC NSLog(@\u0026#34;收到推送通知: %@\u0026#34;, userInfo); // userInfo是一个字典数据类型，具体Key Value由客户端和服务器进行协商确定 NSString* orderId = [userInfo objectForKey:@\u0026#34;carryOrderId\u0026#34;]; NSLog(@\u0026#34;收到订单消息通知，订单号：%@\u0026#34;,orderId); // .... 其余逻辑，拿到具体关键信息之后进行下一步处理 } 还有一个方法\n/*! This delegate method offers an opportunity for applications with the \u0026#34;remote-notification\u0026#34; background mode to fetch appropriate new data in response to an incoming remote notification. You should call the fetchCompletionHandler as soon as you\u0026#39;re finished performing that operation, so the system can accurately estimate its power and data cost. This method will be invoked even if the application was launched or resumed because of the remote notification. The respective delegate methods will be invoked first. Note that this behavior is in contrast to application:didReceiveRemoteNotification:, which is not called in those cases, and which will not be invoked if this method is implemented. !*/ - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler; 我发现这两个方法在APP处于后台或者前台展示，也就是App存活期会同样调用，但当APP并未启动或者被后台销毁之后，用户点击通知象虽然都会调起App，但是前者这个方法就不会被触发，而后者依然会被触发。注释中也说明了该方法即使在其中或者休眠状态下都会由于远端通知而被调用而且会优先于上一个方法。而且后者可以让你和服务器进行一定的数据交互，比如订单状态变化了，我们在该方法中向服务器请求最新的订单信息等等。\n官方文档是这样描述这两个方法的，一目了然：\n// Tells the delegate that the running app received a remote notification. - application:didReceiveRemoteNotification: // Tells the app that a remote notification arrived that indicates there is data to be fetched. - application:didReceiveRemoteNotification:fetchCompletionHandler: 服务器端代码实现 # Apple官方APNs地址：\n测试地址 gateway.sandbox.push.apple.com:2195 正式发布地址 gateway.push.apple.com:2195 简单的通知数据格式，以二进制形式发送，网络字节序。 服务器端代码也可以自己使用原生的Socket写，这里我使用Javapns这个开源代码实现，比较简单，Payload数据格式也已经被封装，你只需要add，add，add。代码如下： 其中就需要用到我们之前生成的Push.p12\npackage testApplePush; import java.util.List; import javapns.Push; import javapns.notification.PushNotificationPayload; import javapns.notification.PushedNotifications; public class testApplePush { public static void main(String[] args) { try { PushNotificationPayload payload = new PushNotificationPayload(); payload.addAlert(\u0026#34;这是一条推送通知!\u0026#34;); // 通知主体内容 payload.addBadge(1); // 角标数字 payload.addSound(\u0026#34;default\u0026#34;); // 通知铃音 // 加入自定义信息 payload.addCustomDictionary(\u0026#34;carryOrderId\u0026#34;, \u0026#34;121212121212121212121212\u0026#34;); // 服务器端记录的DeviceToken String deviceToken = \u0026#34;************************************************\u0026#34;; PushedNotifications notifications = Push.payload(payload, // 自定义payload \u0026#34;Push.p12\u0026#34;,\t// 前面生成的证书 \u0026#34;111111111\u0026#34;, // 证书导出时的密码 false,\t// 是否发送到发布地址 deviceToken); // 客户端DeviceToken int numOfFailedNotifications = notifications.getFailedNotifications() .size(); int numOfSuccessfulNotificatios = notifications .getSuccessfulNotifications().size(); System.out.println(String.format( \u0026#34;Successful Send: %d, Failed Send: %d\u0026#34;, numOfSuccessfulNotificatios, numOfFailedNotifications)); } catch (Exception e) { e.printStackTrace(); } } } 当前这里只是发送一条，如果需要批量发送，貌似Javapns支持的不是太好。这里使用的证书就是上面我们导出的.p12格式证书（Windows平台使用没问题。有些教程说是Win系统不识别是不正确的），接下来还有一份PHP写的代码，供大家查阅。但是这其中需要将我们的p12格式证书转换成pem格式证书。具体教程如下：\n将我们之前生成的推送证书aps_developement.cer文件以及Push.p12文件放在同一文件夹下； 在Terminal中切换到该目录下，然后执行命令将aps_developement.cer文件转换成pem格式文件，之后会在本目录下生成PushCert.pem文件 openssl x509 -in aps_development.cer -inform der -out PushCert.pem 紧接着执行命令将Push.p12文件转换成pem格式文件，之后在本目录下生成PushKey.pem文件，其中会提示你先输入之前生成Push.p12文件的时候的密码，然后需要为新生成的证书文件添加密码，这个密码是要提供给服务端使用的； openssl pkcs12 -nocerts -out Pushkey.pem -in Push.p12 然后将这两个pem文件合成成一个pem文件，Push.pem cat PushCert.pem PushKey.pem \u0026gt; Push.pem 整个过程如下图所示： 证书生成完毕之后，我们就可以在代码中使用了。 如下为PHP写的推送通知服务。代码很简单，主要是注意其中证书为上一步生成的Push.pem，密码就是生成时输入的密码。\n\u0026lt;?php // DeviceToken 不包含空格 $deviceToken = \u0026#39;********************************************************\u0026#39;; // 证书密码 $passphrase = \u0026#39;1111111111\u0026#39;; // 通知主体内容 $alert = \u0026#39;这是一条推送通知!\u0026#39;; //////////////////////////////////////////////////////////////////////////////// $ctx = stream_context_create(); stream_context_set_option($ctx, \u0026#39;ssl\u0026#39;, \u0026#39;local_cert\u0026#39;, \u0026#39;Push.pem\u0026#39;);\t// ck.pem证书上有提及 stream_context_set_option($ctx, \u0026#39;ssl\u0026#39;, \u0026#39;passphrase\u0026#39;, $passphrase); // Open a connection to the APNS server $fp = stream_socket_client( \u0026#39;ssl://gateway.sandbox.push.apple.com:2195\u0026#39;, // 远端测试地址 $err,\t$errstr, 60, // 超时时间 STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx); if (!$fp) exit(\u0026#34;Failed to connect: $err $errstr\u0026#34; . PHP_EOL); echo \u0026#39;Connected to APNS\u0026#39; . PHP_EOL; // 建立字典数据 $body[\u0026#39;aps\u0026#39;] = array( \u0026#39;alert\u0026#39; =\u0026gt; $alert, \u0026#39;sound\u0026#39; =\u0026gt; \u0026#39;default\u0026#39;, \u0026#39;badge\u0026#39; =\u0026gt; 66 ); // 将字典数据转换成JSON $payload = json_encode($body); // 组织二进制数据格式，具体格式参照apple官方文档，本文中也有提及。 // Command + Token length + deviceToken + Payload length + payload $msg = chr(0) . pack(\u0026#39;n\u0026#39;, 32) . pack(\u0026#39;H*\u0026#39;, $deviceToken) . pack(\u0026#39;n\u0026#39;, strlen($payload)) . $payload; // 发送组成的数据给APNs $result = fwrite($fp, $msg, strlen($msg)); if (!$result) echo \u0026#39;Fail to delivery Notification\u0026#39; . PHP_EOL; else echo \u0026#39;Delivery Notification successfully\u0026#39; . PHP_EOL; fclose($fp); ?\u0026gt; 运行下下测试： 参考文献 # https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Introduction.html#//apple_ref/doc/uid/TP40008194-CH1-SW1 https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIApplicationDelegate_Protocol/ http://blog.csdn.net/shenjie12345678/article/details/41120637 ","date":"2015-07-30","externalUrl":null,"permalink":"/post/ios-push-notification/","section":"Posts","summary":"最近几天被iOS的推送部署给搞懵了，现在特地整理下和大家进行分享。\niOS远端推送机制 # APNS，全称为Apple Push Notification service，是苹果通知推送服务中最重要的一环。它是苹果通知推送服务器，为所有iOS设备以及OS X设备提供强大并且可靠的推送通知服务。每个注册通知服务的设备都会和该服务器进行长连接，从而实时获取推送通知。即使当前APP不在运行状态，当通知到达的时候也会有提示发生，最常见的就是短信服务。\n","title":"iOS 远端推送部署详解","type":"post"},{"content":"","date":"2015-07-30","externalUrl":null,"permalink":"/tags/push/","section":"Tags","summary":"","title":"Push","type":"tags"},{"content":"","date":"2015-05-07","externalUrl":null,"permalink":"/categories/data-structure/","section":"Categories","summary":"","title":"Data Structure","type":"categories"},{"content":"","date":"2015-05-07","externalUrl":null,"permalink":"/tags/linked-list/","section":"Tags","summary":"","title":"Linked List","type":"tags"},{"content":"关于有环单链表，即单链表中存在环路，该问题衍生出很多面试题，特在此汇总，方便查阅也帮助自己梳理下思路。\n如下图1所示为有环单链表，假设头结点为H， 环的入口点为A。\n关于有环单链表相关的问题： # 该单链表中是否真有环存在？ 如何求出环状的入口点？ 如何求出环状的长度？ 求解整条链表的长度？ 下面我们分别针对这几个问题进行分析和解答。\n判断一个单链表是否存在环 # 首先，关于第一个问题，如何确定一条链表中确实存在环，关于环状的检测主要有三种方法，链表环状检测主要有三种方法：外部记录法，内部记录法以及追赶法。\n内部标记法和外部标记法其实是一个道理，不过就是辅助变量一个是在链表节点内，一个是借助辅助数组或者hash或者AVL，红黑树等 把已经访问过的节点地址存起来，每次访问下一个节点的时候进行查询看是否已经出现过。这里不再赘述。主要看追赶法，也称快满指针法，而追赶法大家一定都已经烂熟于心了。\n追赶法主要利用最大公倍数原理，用2个游标，对链表进行访问，例如:pSlow， pFast。 pSlow访问每步向前进1个节点，而pFast则每次向前前进2个节点，如果有环则pSlow和pFast必会相遇，如果pFast最终指向了NULL，则说明该链表不存在环路。因为两个指针步子迈的不一样，因为被称作快慢指针。\n// Definition for singly - linked list. struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; bool isLoopList(ListNode *pHead){ if (nullptr == pHead || nullptr == pHead-\u0026gt;next){ return false; } ListNode *pSlow = pHead; ListNode *pFast = pHead; while (pFast \u0026amp;\u0026amp; pFast-\u0026gt;next){ pFast = pFast-\u0026gt;next-\u0026gt;next; pSlow = pSlow-\u0026gt;next; if (pFast == pSlow){ break; } } return !(nullptr == pFast || nullptr == pFast-\u0026gt;next); } 确定该有环单链表的环的入口 # 关于这个问题，首先我们需要证明当pSlow和pFast第一次相遇的时候，pSlow并未走完整个链表或者恰好到达环入口点。\n看上图（画的比较粗糙），假设 pSlow 到达环状入口点A的时候，pFast在环上某一点B，假设B逆时针方向离 A 点距离为 y ，并且整个环状的长度为R，我们知道y \u0026lt;= R。从A点开始，pSlow向前走y步，此时pFast从点B往前则走 2 * y 步 并与 pSlow 相遇于点 D，此时pSlow还需R - y 才能到达链表尾端，也即A点。因为y \u0026lt;= R，因此R - y \u0026gt;= 0。得证。\n此时我们假设相遇的时候pSlow走了s步，那pFast走2 * s步，而pFast多走的肯定是在环内绕圈，很自然我们有\n2 * s = s + n * R ; (n \u0026gt;= 1)\n有： s = n * R (n \u0026gt;= 1).\n设整个链表的长度为L， HA的长度为a ，第一次相遇点B与A的距离为x，则有\na + x = s = n * R;\na + x = (n - 1 + 1 ) * R = (n - 1) * R + R = (n - 1) * R + L - a;\n有 a = (n - 1) * R + (L - a - x);　有前面我们证明知，L - a - x 为我们所设变量y。 因此a = (n - 1) * R + y; (n \u0026gt;= 1).\n这样，我们就可以这样，相遇点设置一个指针，链表头部设置一个指针，这两个指针同时按照一步一个节点前进，第一次相遇的时候必定是相遇点指针走y + (n - 1)*R的时候，也就是入口点A的位置。得证，因此获取入口点的实现如下。\nListNode *loopJoint(ListNode *pHead){ if (nullptr == pHead || nullptr == pHead-\u0026gt;next){ return nullptr; } ListNode *pSlow = pHead; ListNode *pFast = pHead; while (pFast \u0026amp;\u0026amp; pFast-\u0026gt;next){ pFast = pFast-\u0026gt;next-\u0026gt;next; pSlow = pSlow-\u0026gt;next; if (pFast == pSlow){ break; } } if (nullptr == pFast || nullptr == pFast-\u0026gt;next){ return nullptr; } // 此时调整两个指针为普通指针，一次一步，并且其中一个指针从头部开始，第一次相遇点一定是环的入口点 pSlow = pHead; while (pFast != pSlow){ pFast = pFast-\u0026gt;next; pSlow = pSlow-\u0026gt;next; } return pSlow; } 求出该有环单链表中环的长度 # 有了以上的基础，环状的长度就很明显了，入口点已知，沿着环状走一圈即得。\n求出该有环单链表长度 # 同理，当R已知，而问题2中其中指向头部的指针到达环状入口点的时候HA已知，因此单链表的长度等于 L = R + a;\n","date":"2015-05-07","externalUrl":null,"permalink":"/post/%E5%85%B3%E4%BA%8E%E5%8D%95%E9%93%BE%E8%A1%A8%E7%9A%84%E9%82%A3%E4%BA%9B%E4%BA%8B%E5%84%BF/","section":"Posts","summary":"关于有环单链表，即单链表中存在环路，该问题衍生出很多面试题，特在此汇总，方便查阅也帮助自己梳理下思路。\n","title":"关于单链表的那些事儿","type":"post"},{"content":" 题目：Reverse Words in a String # Given an input string s, reverse the string word by word. For example, given s = \u0026ldquo;the sky is blue\u0026rdquo;, return \u0026ldquo;blue is sky the\u0026rdquo;.\n题意： # 题意很明确，将字符串中的单词进行翻转，形成新的字符串。但是这其中有几个问题我们需要思考（参考leetCode官方CleanCodeBook）：\nQ: What constitutes a word? A: A sequence of non-space characters constitutes a word.\nQ: Does tab or newline character count as space characters? A: Assume the input does not contain any tabs or newline characters.\nQ: Could the input string contain leading or trailing spaces? A: Yes. However, your reversed string should not contain leading or trailing spaces.\nQ: How about multiple spaces between two words? A: Reduce them to a single space in the reversed string.\n解法： # class Solution { public: void reverseWords(std::string \u0026amp;s) { std::string reversed(\u0026#34;\u0026#34;); int j = s.length(); for (int i = s.length() - 1; i \u0026gt;= 0; --i) { if(s.at(i) == \u0026#39; \u0026#39;) j = i; else if (i == 0 || s.at(i - 1) == \u0026#39; \u0026#39;) { if (reversed.length() != 0) reversed += \u0026#39; \u0026#39;; reversed += s.substr(i, j - i); } } s = reversed; } } ","date":"2015-05-01","externalUrl":null,"permalink":"/post/leetcode-002-reverse-words-in-a-string/","section":"Posts","summary":"题目：Reverse Words in a String # Given an input string s, reverse the string word by word. For example, given s = “the sky is blue”, return “blue is sky the”.\n","title":"[151] Reverse Words in a String","type":"post"},{"content":"","date":"2015-04-29","externalUrl":null,"permalink":"/tags/array/","section":"Tags","summary":"","title":"Array","type":"tags"},{"content":"本文主要包括 leetCode 题集里的两个题目，Two Sum1 和 Two Sum2\n题目1： 1. Two Sum 1 # Given an array of integers, find two numbers such that they add up to a specific target number.\nThe function twoSum should return indices of the two numbers such that they add up to the target, where index1 must be less than index2. Please note that your returned answers (both index1 and index2) are not zero-based.\nYou may assume that each input would have exactly one solution.\nInput: numbers={2, 7, 11, 15}, target=9 Output: index1=1, index2=2\n题意 # 题目中要求输入一个整形数组以及一个target，找出该整型数组中这样两个元素，使得这两个元素之和等于指定target的值。 题目中假设该两个元素必然存在，并且是只有一组（所以相对简单），返回的是这两个元素的index值（数组Index从1开始）。\n关键点 # 我们经常会使用空间换取时间的方法来获取较小的时间复杂度。因此增加必要的数据结构来减少时间复杂度。这道题目一样，我们增加一个map结构，key为数组元素值，value为其在数组中对应的index。每次遍历到数组元素便去map结构中查找对应的补数，如果存在，那就说明找到了。如果没存在就记录当前元素以及其index，直到结束。\n代码实现如下：\nclass Solution{ public: // O(n) runtime, O(n) space // We could reduce the runtime complexity of looking up a value to O(1) using a hash map that maps a value to its index. std::vector\u0026lt;int\u0026gt; twoSum(std::vector\u0026lt;int\u0026gt;\u0026amp; numbers, int target){ std::vector\u0026lt;int\u0026gt; vecRet; std::map\u0026lt;int, int\u0026gt; mapIndex; for (size_t i = 0; i \u0026lt; numbers.size(); ++i){ if (0 != mapIndex.count(target - numbers[i])){ int nIndex = mapIndex[target - numbers[i]]; // 当前存储的Index肯定比i要小，注意要排除i if (nIndex \u0026lt; i){ vecRet.push_back(nIndex + 1); vecRet.push_back(i + 1); return vecRet; } } else { mapIndex[numbers[i]] = i; } } return vecRet; } // twoSum } 题目2：167. Two Sum II - Input array is sorted # Given an array of integers, find two numbers such that they add up to a specific target number.\nThe function twoSum should return indices of the two numbers such that they add up to the target, where index1 must be less than index2. Please note that your returned answers (both index1 and index2) are not zero-based.\nYou may assume that each input would have exactly one solution.\nInput: numbers={2, 7, 11, 15}, target=9 Output: index1=1, index2=2\n题意 # 和上题中Two Sum1 比较，大家发现什么异同了没（没发现的童鞋面壁去），这道题目仅仅更改了输入条件，Input的数组为已经排序的数组，并且呈现升序。输入条件变严格，我们依然可以使用前一篇问丈夫中的方法进行。那这里既然为升序，我们能不能不使用前一种方法而又能够达到O（n）时间复杂度呢？ 答案是必然的。\n关键点 # 数组既然排序，元素之前的大小关系相对确定，存在的这一对元素必然处于相对固定的位置，我们可以使用两个游标，一个从头部遍历，一个从尾部遍历，两者所指元素之和如果偏大，调整后面的游标，相反则调整前面的游标。 由于两个int之和有可能出现INT_MAX的情况，所以如果输入类型给定int，则我们需要使用long long类型来表示才能避免出现溢出的情况。 实现代码如下：\nclass Solution{ public: std::vector\u0026lt;int\u0026gt; twoSum(std::vector\u0026lt;int\u0026gt; \u0026amp;numbers, int target){ std::vector\u0026lt;int\u0026gt; vecRet; int nLeft = 0; int nRight = numbers.size() - 1; while (nLeft \u0026lt; nRight){ // 小心两个int之和溢出，使用long long类型 long long int nAdd = numbers[nLeft] + numbers[nRight]; if (nAdd == target){ vecRet.push_back(nLeft + 1); vecRet.push_back(nRight + 1); return vecRet; } else if (nAdd \u0026gt; target){ nRight--; } else if (nAdd \u0026lt; target){ nLeft++; } } return vecRet; } } ","date":"2015-04-29","externalUrl":null,"permalink":"/post/leetcode-003-two-sum/","section":"Posts","summary":"本文主要包括 leetCode 题集里的两个题目，Two Sum1 和 Two Sum2\n题目1： 1. Two Sum 1 # Given an array of integers, find two numbers such that they add up to a specific target number.\n","title":"Two Sum","type":"post"},{"content":" 题目：Intersection of Two Linked lists # Write a program to find the node at which the intersection of two singly linked lists begins.\nFor example, the following two linked lists:\nA: a1 → a2 ↘ c1 → c2 → c3 ↗ B: b1 → b2 → b3 begin to intersect at node c1.\nNotes: 1. If the two linked lists have no intersection at all, return null. 2. The linked lists must retain their original structure after the function returns. 3. You may assume there are no cycles anywhere in the entire linked structure. 4. Your code should preferably run in O(n) time and use only O(1) memory\n题意： # 输入两个单链表，判断这两个单链表是否相交，返回相交点。这里有几种方法供大家参考。 本文中所有代码均为C++实现，其他语言代码稍后我会补上。\n解法1： # 《编程之美》一书中有讲到该问题，如何判断这两个链表相交，可以讲其中一个链表的头尾相连，然后从另一个链表的角度来判断时候有环状链表存在即可。相当于将问题转换成如何求有环单链表的环状入口点？详细方案见我的另一篇专门讲环状单链表的文章 《关于有环单链表的那些事儿》。需要注意的是，不能改变原有链表的结构，因此方法返回的时候需要恢复原状。\n1. 将链表B的尾节点指向其头节点HeadB，因此链表B形成环状，链表A成为有环单链表； 2. 此时问题转换成求链表A上环状入口点； 3. 恢复链表B的结构；\n代码如下：\n/** *\tDefinition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { if (nullptr == headA || nullptr == headB){ return nullptr; } ListNode *pNode = headB; while (pNode-\u0026gt;next){ pNode = pNode-\u0026gt;next; } ListNode *pRear = pNode; pNode-\u0026gt;next = headB; ListNode *pJoint = loopJoint(headA); pRear-\u0026gt;next = nullptr; // 恢复 return pJoint; } private: ListNode *loopJoint(ListNode *pHead){ if (nullptr == pHead || nullptr == pHead-\u0026gt;next){ return nullptr; } ListNode *pSlow = pHead; ListNode *pFast = pHead; while (pFast \u0026amp;\u0026amp; pFast-\u0026gt;next){ pFast = pFast-\u0026gt;next-\u0026gt;next; pSlow = pSlow-\u0026gt;next; if (pFast == pSlow){ break; } } if (nullptr == pFast || nullptr == pFast-\u0026gt;next){ return nullptr; } pSlow = pHead; while (pFast != pSlow){ pFast = pFast-\u0026gt;next; pSlow = pSlow-\u0026gt;next; } return pSlow; } } leetCode的OJ系统评判结果如下： 解法2： # 《剑指Offer》一书中提到的这个方法，如果两个没有环的链表相交于某个节点，那么在这个节点之后的所有节点都是两个链表所共有的。因此两个链表从交织点之前的部分长度差即为整条链表的长度差，我们只要让两条链表从离交织点相同距离的位置开始前进，经过O（n）步，一定能够找到交织点。 1. 遍历链表A，记录其长度len1，遍历链表B，记录其长度len2。 2. 按尾部对齐，如果两个链表的长度不相同，让长度更长的那个链表从头节点先遍历abs(len1-en2),这样两个链表指针指向对齐的位置。\n3. 然后两个链表齐头并进，当它们相等时，就是交集的节点。\n时间复杂度O(n+m)，空间复杂度O(1)\n代码如下：\n/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { if (nullptr == headA || nullptr == headB){ return nullptr; } int nLongLength = 0; int nShortLength = 0; ListNode *pLongList = headA; ListNode *pShortList = headB; while (pLongList){ ++nLongLength; pLongList = pLongList-\u0026gt;next; } while (pShortList){ ++nShortLength; pShortList = pShortList-\u0026gt;next; } if (nShortLength \u0026gt; nLongLength){ pLongList = headB; pShortList = headA; // 校正 nLongLength ^= nShortLength; nShortLength ^= nLongLength; nLongLength ^= nShortLength; } else{ pLongList = headA; pShortList = headB; } int offset = nLongLength - nShortLength; while (offset\u0026gt; 0){ pLongList = pLongList-\u0026gt;next; --offset; } while (pLongList != pShortList){ pLongList = pLongList-\u0026gt;next; pShortList = pShortList-\u0026gt;next; } return pLongList; } } leetCode的OJ系统评判结果如下所示： 解法3： # 还有一种解法，实际上还是利用步差来实现的，具体证明还米有搞懂，思考中。具体如下：\n1. 维护两个指针pA和pB，初始分别指向链表A和链表B。然后让它们分别遍历整个链表，每步一个节点。\n2. 当pA到达链表末尾时，让它指向B的头节点；类似的当pB到达链表末尾时，重新指向A的头节点。\n3. 当pA和pB相遇时也即为交织点。\n该算法的时间复杂度依然为O（m + n）,时间复杂度为O(1)。\n实现如下：\n/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { if (nullptr == headA || nullptr == headB){ return nullptr; } ListNode *nodeA = headA; ListNode *nodeB = headB; while (nodeA \u0026amp;\u0026amp; nodeB \u0026amp;\u0026amp; nodeA != nodeB){ nodeA = nodeA-\u0026gt;next; nodeB = nodeB-\u0026gt;next; if (nodeA == nodeB){ break; } if (nullptr == nodeA){ nodeA = headB; } if (nullptr == nodeB){ nodeB = headA; } } return nodeA; } } leetCode的OJ系统评判结果： ","date":"2015-04-29","externalUrl":null,"permalink":"/post/leetcode-003-intersection-of-two-linked-lists/","section":"Posts","summary":"题目：Intersection of Two Linked lists # Write a program to find the node at which the intersection of two singly linked lists begins.\n","title":"[160] Intersection of Two Linked Lists","type":"post"},{"content":"","date":"2015-04-29","externalUrl":null,"permalink":"/tags/linkedlist/","section":"Tags","summary":"","title":"Linkedlist","type":"tags"},{"content":"","date":"2015-04-29","externalUrl":null,"permalink":"/categories/c++/","section":"Categories","summary":"","title":"C++","type":"categories"},{"content":"","date":"2015-04-29","externalUrl":null,"permalink":"/tags/c++11/","section":"Tags","summary":"","title":"C++11","type":"tags"},{"content":"","date":"2015-04-29","externalUrl":null,"permalink":"/tags/lambda/","section":"Tags","summary":"","title":"Lambda","type":"tags"},{"content":"关于C++11的新特性，最近接触比较多的就是关于thread的部分，还有就是Lambda表达式，今天主要针对Lambda的用法进行一定的阐述和汇总（参考链接在文章下方，向大师致敬！），同时给自己梳理下知识点，加深印象。\n基本的Lambda表达式如下所示，该表达式计算一个整型数据的平方值并返回，并且该表达式能够直接使用，是不是特别方便？不再需要将类本身和函数定义分割开来。\nint result = [](int input){ return input * input; }(10); std::cout \u0026lt;\u0026lt; result \u0026lt;\u0026lt; std::endl; 如果你需要重用该段代码片段，可以将该函数保存为本地变量，如下所示：\nauto func = [](int input){ return input * input; }; std::cout \u0026lt;\u0026lt; func(10) \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; func(20) \u0026lt;\u0026lt; std::endl; 好了，现在我们需要写一个能够计算浮点类型的Lambda表达式怎么办？ 或者我们需要能够计算复数（complex number）怎么办？ 我们需要的就像下面这样：\n// int 的平方 std::cout \u0026lt;\u0026lt; func(10) \u0026lt;\u0026lt; std::endl; // double的平方 std::cout \u0026lt;\u0026lt; func(3.1415) \u0026lt;\u0026lt; std::endl; // 复数的平方 std::cout \u0026lt;\u0026lt; func(std::complex\u0026lt;double\u0026gt;(3, -2)) \u0026lt;\u0026lt; std::endl; 如何让代码复用起来？ 当然是 函数模板（function template）了。 如下：\ntemplate \u0026lt;typename T\u0026gt; T func(T param) { return param * param; } 但是函数模板并不是那篇文章所追求的，以上的这段代码被称作是 a named global function. 而在最新的通过的C++14标准中引入了 generalized lambda的概念。我们允许lambda表达式的传参类型为auto类型（看来C++是要强化类型自动推导啊，auto关键字能够使用的地方越来越多了。），如下我们能够使用更短，更优雅的代码实现以上需求。\nauto func = [](auto input){ return input * input; }; 完整代码如下：\n#include\u0026lt;iostream\u0026gt; #include\u0026lt;complex\u0026gt; int main() { // Store a generalized lambda, that squares a number, in a variable auto func = [](auto input) { return input * input; }; // Usage examples: // square of an int std::cout \u0026lt;\u0026lt; func(10) \u0026lt;\u0026lt; std::endl; // square of a double std::cout \u0026lt;\u0026lt; func(2.345) \u0026lt;\u0026lt; std::endl; // square of a complex number std::cout \u0026lt;\u0026lt; func(std::complex\u0026lt;double\u0026gt;(3, -2)) \u0026lt;\u0026lt; std::endl; return 0; } 其实lambda表达式和STL在一起使用能够发挥很大的作用，假设你要排序一个vector让其降序，使用generic lambda，我们可以这样写：\nstd::sort(V.begin(), V.end(), [](auto i, auto j) { return (i \u0026gt; j); }); 完整的代码如下，对一个包含10个整型数据的vector进行排序，使之降序。结合STL和Generic Lambda：\n#include\u0026lt;iostream\u0026gt; #include\u0026lt;vector\u0026gt; #include\u0026lt;numeric\u0026gt; #include\u0026lt;algorithm\u0026gt; int main() { std::vector\u0026lt;int\u0026gt; V(10); // Use std::iota to create a sequence of integers 0, 1, ... std::iota(V.begin(), V.end(), 1); // Print the unsorted data using std::for_each and a lambda std::cout \u0026lt;\u0026lt; \u0026#34;Original data\u0026#34; \u0026lt;\u0026lt; std::endl; std::for_each(V.begin(), V.end(), [](auto i) { std::cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34; \u0026#34;; }); std::cout \u0026lt;\u0026lt; std::endl; // Sort the data using std::sort and a lambda std::sort(V.begin(), V.end(), [](auto i, auto j) { return (i \u0026gt; j); }); // Print the sorted data using std::for_each and a lambda std::cout \u0026lt;\u0026lt; \u0026#34;Sorted data\u0026#34; \u0026lt;\u0026lt; std::endl; std::for_each(V.begin(), V.end(), [](auto i) { std::cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34; \u0026#34;; }); std::cout \u0026lt;\u0026lt; std::endl; return 0； } 其中关于lambda表达式前面的[], 作者只字未提，[]是捕获列表（capture list），我们可以将lambda表达式外围定义的变量捕获使得我们能够在lambda表达式内部使用。不通俗的说capture list指定了在可见域范围内lambda表达式的代码内可见的外部变量的列表，具体有以下几点：\n[a, \u0026amp;b] a变量传值捕获，b引用捕获，这和普通函数传参相类似； [this] 以值的方式捕获this指针 [\u0026amp;]以引用的方式捕获外部自动变量 [=] 以值的方式捕获外部自动变量 [] 不捕获任何外部变量， 以上所有代码均属于此种。 参考文章中作者在回答读者提出的为什么不讲解[]的原因是回答到：使用捕获列表会使你的代码变得bad~,因此他不鼓励。\n参考： # https://solarianprogrammer.com/2014/08/28/cpp-14-lambda-tutorial/\n","date":"2015-04-29","externalUrl":null,"permalink":"/post/%E5%85%B3%E4%BA%8Elambda%E7%9A%84%E4%B8%80%E7%82%B9%E6%A2%B3%E7%90%86/","section":"Posts","summary":"关于C++11的新特性，最近接触比较多的就是关于thread的部分，还有就是Lambda表达式，今天主要针对Lambda的用法进行一定的阐述和汇总（参考链接在文章下方，向大师致敬！），同时给自己梳理下知识点，加深印象。\n","title":"关于Lambda的一点梳理","type":"post"},{"content":" 题目： # Find the contiguous subarray within an array (containing at least one number) which has the largest product. For example, given the array [2,3,-2,4], the contiguous subarray [2,3] has the largest product = 6.\n题意： # 题目中给出一个（至少包含一个元素）整形数组，求一个子数组（元素连续），使得其元素之积最大。最直接了当的方法，当然是暴力穷举，但是其O(n^2)是无法通过 OJ 评判的。\nC语言实现如下：\nint maxProduct(int* nums, int numsSize) { int nMaxPro = nums[0]; int max_tmp = nums[0]; int min_tmp = nums[0]; for(int i = 1; i\u0026lt; numsSize; ++i){ int a = nums[i] * max_tmp; int b = nums[i] * min_tmp; max_tmp = (( a \u0026gt; b ? a : b ) \u0026gt; nums[i]) ? ( a \u0026gt; b ? a : b ) : nums[i]; min_tmp = (( a \u0026lt; b ? a : b ) \u0026lt; nums[i]) ? ( a \u0026lt; b ? a : b ) : nums[i]; nMaxPro = (nMaxPro \u0026gt; max_tmp) ? nMaxPro : max_tmp; } return nMaxPro; } C++实现如下：\nclass Solution { public: int maxProduct(vector\u0026lt;int\u0026gt;\u0026amp; nums) { int nMaxPro = nums[0]; int max_tmp = nums[0]; int min_tmp = nums[0]; for(int i = 1; i\u0026lt; nums.size(); ++i){ int a = nums[i] * max_tmp; int b = nums[i] * min_tmp; max_tmp = (( a \u0026gt; b ? a : b ) \u0026gt; nums[i]) ? ( a \u0026gt; b ? a : b ) : nums[i]; min_tmp = (( a \u0026lt; b ? a : b ) \u0026lt; nums[i]) ? ( a \u0026lt; b ? a : b ) : nums[i]; nMaxPro = (nMaxPro \u0026gt; max_tmp) ? nMaxPro : max_tmp; } return nMaxPro; } }; Java实现如下：\npublic class Solution { public int maxProduct(int[] nums) { int nMaxPro = nums[0]; int max_tmp = nums[0]; int min_tmp = nums[0]; for(int i = 1; i\u0026lt; nums.length; ++i){ int a = nums[i] * max_tmp; int b = nums[i] * min_tmp; max_tmp = (( a \u0026gt; b ? a : b ) \u0026gt; nums[i]) ? ( a \u0026gt; b ? a : b ) : nums[i]; min_tmp = (( a \u0026lt; b ? a : b ) \u0026lt; nums[i]) ? ( a \u0026lt; b ? a : b ) : nums[i]; nMaxPro = (nMaxPro \u0026gt; max_tmp) ? nMaxPro : max_tmp; } return nMaxPro; } } ","date":"2015-04-29","externalUrl":null,"permalink":"/post/leetcode-001-maximum-product-subarray/","section":"Posts","summary":"题目： # Find the contiguous subarray within an array (containing at least one number) which has the largest product. For example, given the array [2,3,-2,4], the contiguous subarray [2,3] has the largest product = 6.\n","title":"[152] Maximum Product Subarray","type":"post"},{"content":"","date":"2015-04-16","externalUrl":null,"permalink":"/categories/scala/","section":"Categories","summary":"","title":"Scala","type":"categories"},{"content":"因为这几天在学习Coursera公开课Principles of Reactive Programming ，因为之前木有木有注意到需要对至少一门函数式编程语言solid foundation，因此只能临时抱佛脚，恰好手头有一本七周七语言这本书一直木有看，借着这个课程的督促把这本书翻看下，当然内容仅包括Scala这一章，今天看到第二天的内容，重头戏是Collection。这篇blog主要记录关于这次自习题的解答。\n找 # 关于如何使用Scala文件的讨论 # 既然Scala可以使用Java中任意的对象（Objects），因此可以使用Java中针对文件的操作，例如java.io.File就是其中之一，示例如下：\nimport java.io._ object Test { def main(args: Array[String]) { val writer = new PrintWriter(new File(\u0026#34;test.txt\u0026#34;)) writer.write(\u0026#34;Hello Scala\u0026#34;) writer.close() } } 从本地读取文件，scala库自带文件读取，如下所示代码为一段示例，可以读取本地文件中得内容 读取文件内容：\nimport scala.io.Source object Test { def main(args: Array[String]) { println(\u0026#34;Following is the content read:\u0026#34; ) Source.fromFile(\u0026#34;test.txt\u0026#34; ).foreach{ print } } } 闭包(closure)和代码块有何不同 # 关于闭包的概念，只能从网路上查找资料加上自己理解写\n闭包（closure） # 闭包是可以包含自由（未绑定到特定对象）变量的代码块；这些变量不是在这个代码块内或者任何全局上下文中定义的，而是在定义代码块的环境中定义（局部变量）。“闭包” 一词来源于以下两者的结合：要执行的代码块（由于自由变量被包含在代码块中，这些自由变量以及它们引用的对象没有被释放）和为自由变量提供绑定的计算环境（作用域）。在 Scala、Scheme、Common Lisp、Smalltalk、Groovy、JavaScript、Ruby、 Python、Go、Lua、objective c 以及Java（Java8及以上）等语言中都能找到对闭包不同程度的支持。 —— 百度百科\n代码块（Block） # 而代码块是语法上由一些逻辑性语句组成的一个一个单元：\nif (Condition) { // one Block } else { // another Block } 做 # 使用foldLeft方法计算一个列表中所有字符串的长度 # 关于foldLeft和foldRight # foldLeft # Scala中 foldLeft 方法定义在GenTraversableOnce.scala文件中\n/** Applies a binary operator to a start value and all elements of this $coll, * going left to right. * * $willNotTerminateInf * $orderDependentFold * * @param z the start value. * @param op the binary operator. * @tparam B the result type of the binary operator. * @return the result of inserting `op` between consecutive elements of this $coll, * going left to right with the start value `z` on the left: * {{{ * op(...op(z, x_1), x_2, ..., x_n) * }}} * where `x,,1,,, ..., x,,n,,` are the elements of this $coll. */ def foldLeft[B](z: B)(op: (B, A) =\u0026gt; B): B 实现在TraversableOnce.scala里：\ndef foldLeft[B](z: B)(op: (B, A) =\u0026gt; B): B = { var result = z this foreach (x =\u0026gt; result = op(result, x)) result } 方法接受两个参数，z 和 op， 其中z是B类型，op是一个返回类型为B类型的方法。其实该方法还有一个版本需要用到:/操作符，如下所示：\ndef /:[B](z: B)(op: (B, A) =\u0026gt; B): B = foldLeft(z)(op) 该方法实际上是调用了foldLeft方法。举个例子：\nscala\u0026gt; val list= List(1, 2, 3) list: List[Int] = List(1, 2, 3) scala\u0026gt; list.foldLeft(0)((sum, value) =\u0026gt; sum + value) res6: Int = 6 foldLeft方法被传入一个初始值和一个代码块，这个代码块有两个参数sum 和 value，foldLeft将sum初始化为0，而value通过list遍历将list中每个元素累加到sum上，因此该行代码实现了对list中元素的累加。\nfoldRight # foldRight的实现如下：\ndef :\\[B](z: B)(op: (A, B) =\u0026gt; B): B = foldRight(z)(op) def foldRight[B](z: B)(op: (A, B) =\u0026gt; B): B = reversed.foldLeft(z)((x, y) =\u0026gt; op(y, x)) 我们看到foleRight内部还是调用的foldLeft方法，不过多了一个reversed，它其实是该trait定义的内部方法，实现如下：\n// @tparam A the element type of the collection ... // for internal use protected[this] def reversed = { var elems: List[A] = Nil self foreach (elems ::= _) elems } 看来foldRight使用list的逆序集合然后再进行foldLeft，需要注意的是foldRight方法中需要传入的block参数顺序发生了变化，前者是列表参数（自己理解，不知所以）。这样的话，foldRight相对foldLeft来讲效率肯定就差一点了。下面是一个例子：\nscala\u0026gt; val list = List(1, 2, 3, 4, 5) list: List[Int] = List(1, 2, 3, 4, 5) scala\u0026gt; list.foldRight(100)((elem, sum) =\u0026gt; sum - elem) res10: Int = 85 回归正题\n题目中要求计算一个列表中字符串的长度之和也就迎刃而解，关键是明白foldLeft方法的使用。\nscala\u0026gt; val numbers = List(\u0026#34;one\u0026#34;,\u0026#34;two\u0026#34;,\u0026#34;three\u0026#34;) numbers: List[String] = List(one, two, three) scala\u0026gt; val length = (0 /: numbers){(sum, elem) =\u0026gt; sum + elem.length} length: Int = 11 scala\u0026gt; val length2 = numbers.foldLeft(0)((sum, value)=\u0026gt; sum + value.length) length2: Int = 11 运算符 /: 和foldLeft方法均能实现需求（本质上一样一样的）\n编写一个Censor trait，包含一个可将Pucky和Beans替换为Shoot和Darn的方法。使用映射存储脏话和他们的替代品\nimport scala.collection.mutable.Map trait Censor { val curseWords = Map(\u0026#34;Pucky\u0026#34; -\u0026gt; \u0026#34;Shoot\u0026#34;, \u0026#34;Beans\u0026#34; -\u0026gt; \u0026#34;Darn\u0026#34;) def censor(s: String) = curseWords.foldLeft(s)((prev, curr) =\u0026gt; prev.replaceAll(curr._1, curr._2)) } class Text(words: String) extends Censor { def origin = words def transform = censor(words) } val text = new Text(\u0026#34;Pucky \u0026amp;\u0026amp; Beans\u0026#34;) println(\u0026#34;Original String: \u0026#34; + text.origin) println(\u0026#34;Replaced String: \u0026#34; + text.transform) 输出结果： $ scala Censor.trait Original String: Pucky \u0026amp;\u0026amp; Beans Replaced String: Shoot \u0026amp;\u0026amp; Darn 从一个文件中加载脏话或者它们的替代品\n通过针对scala对文件读写的讨论，就可以很轻松的写出这道题的答案，主要是需要将map中存储的映射关系存储下来进行读取。首先读取代码如下：\nimport scala.collection.mutable.Map trait fileRead { val curseWords = Map[String, String]() io.Source.fromFile(\u0026#34;./files/censor.txt\u0026#34;).getLines().foreach { (line) =\u0026gt; val subWords = line.split(\u0026#34;:\u0026#34;) curseWords += (subWords(0) -\u0026gt; subWords(1)) } println(\u0026#34;After Read, curseWords are\u0026#34;) curseWords.foreach(p =\u0026gt; println(\u0026#34;\u0026gt;\u0026gt;\u0026gt; key=\u0026#34; + p._1 + \u0026#34;, value=\u0026#34; + p._2 + \u0026#34; \u0026lt;\u0026lt;\u0026lt;\u0026#34;)) } class tryFile extends fileRead val fileObj = new tryFile 写入文件代码如下：\nimport scala.collection.mutable.Map import java.io._ trait fileWrite { val curseWords = Map(\u0026#34;Pucky\u0026#34;-\u0026gt;\u0026#34;Shoot\u0026#34;, \u0026#34;Beans\u0026#34;-\u0026gt;\u0026#34;Darn\u0026#34;) def writeMapToFile { println(\u0026#34;Start to write map to file\u0026#34;) val writer = new PrintWriter(new File(\u0026#34;./files/curseToWrite.txt\u0026#34;)) curseWords.foreach{ (elem) =\u0026gt; writer.write(elem._1 + \u0026#34;:\u0026#34; + elem._2) writer.write(\u0026#34;\\n\u0026#34;) } writer.close() println(\u0026#34;end to write map to file\u0026#34;) } } class tryFile extends fileWrite val fileObj = new tryFile fileObj.writeMapToFile That\u0026rsquo;s all! 关于闭包的示例晚上回家补上~~~ 工作鸟\n参考链接： # http://www.tutorialspoint.com/scala/scala_file_io.htm http://blog.csdn.net/oopsoom/article/details/23447317 http://zhangjunhd.github.io/2013/12/22/scala-note7-file.html ","date":"2015-04-16","externalUrl":null,"permalink":"/post/scala%E7%AC%AC%E4%BA%8C%E5%A4%A9/","section":"Posts","summary":"因为这几天在学习Coursera公开课Principles of Reactive Programming ，因为之前木有木有注意到需要对至少一门函数式编程语言solid foundation，因此只能临时抱佛脚，恰好手头有一本七周七语言这本书一直木有看，借着这个课程的督促把这本书翻看下，当然内容仅包括Scala这一章，今天看到第二天的内容，重头戏是Collection。这篇blog主要记录关于这次自习题的解答。\n","title":"scala Day2","type":"post"},{"content":"如果你不给自己烦恼，别人也永远不可能给你烦恼，因为你自己的内心，你放不下\n其实写博客一直在我的ToDoList上，不过放的时间有点久。我就是这样一个人，想干的太多，做到的却相当少。因此写这个博客的目的也是想督促自己前行，记录生活点滴的同时也能够让自己对生活有一些感悟。告诉自己，世界很大，千万不要局限在自己的小圈圈里无法自拔！\n","date":"2015-04-15","externalUrl":null,"permalink":"/post/myfirstpost/","section":"Posts","summary":"如果你不给自己烦恼，别人也永远不可能给你烦恼，因为你自己的内心，你放不下\n其实写博客一直在我的ToDoList上，不过放的时间有点久。我就是这样一个人，想干的太多，做到的却相当少。因此写这个博客的目的也是想督促自己前行，记录生活点滴的同时也能够让自己对生活有一些感悟。告诉自己，世界很大，千万不要局限在自己的小圈圈里无法自拔！\n","title":"博客之旅","type":"post"},{"content":"Hey, I\u0026rsquo;m Chen — an iOS / macOS developer based in China.\nI build apps with Swift and SwiftUI, and I enjoy digging into Apple platform internals, from CoreData and CloudKit to Xcode build systems and runtime internals.\nWhat I Do # Ship indie apps: Closet, Distill, EatWisely, SpendWisely, Structly Write about Swift, iOS, macOS, and developer tooling Occasionally translate great English-language iOS articles into Chinese Find Me # GitHub: hechen Stack Overflow: chen-he Email: hechen.dream@gmail.com ","externalUrl":null,"permalink":"/about/","section":"I make stuff","summary":"Hey, I’m Chen — an iOS / macOS developer based in China.\nI build apps with Swift and SwiftUI, and I enjoy digging into Apple platform internals, from CoreData and CloudKit to Xcode build systems and runtime internals.\nWhat I Do # Ship indie apps: Closet, Distill, EatWisely, SpendWisely, Structly Write about Swift, iOS, macOS, and developer tooling Occasionally translate great English-language iOS articles into Chinese Find Me # GitHub: hechen Stack Overflow: chen-he Email: hechen.dream@gmail.com ","title":"About","type":"page"},{"content":"A collection of apps I\u0026rsquo;ve designed and built for the Apple ecosystem.\n","externalUrl":null,"permalink":"/apps/","section":"Apps","summary":"A collection of apps I’ve designed and built for the Apple ecosystem.\n","title":"Apps","type":"apps"},{"content":"","externalUrl":null,"permalink":"/search/","section":"I make stuff","summary":"","title":"Search","type":"page"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]