一月份
,在微信小程序正式上线后,开始开发酷爱生活小程序
,针对前端的技术又学了一波。
二、三月份
,好酷科技正式被幸福西饼收购,随后更名为幸福早餐,由于种种原因决定离职。
离职后不久被同事介绍去一家 O2O 公司,算得上是一个驻场兼职吧,期间顺利拿到驾照。
五月份
,老婆为我生了一对娃,这是 2017 年最大的收获,虽然很忙,但很开心,孩子出生后的那种感觉真的很奇妙。
六月份
,开源一个第三方库 InputKit,并支持 Swift 版本。
七、八月份
左右,离开这家 O2O 公司。参与《iOS 成长之路 3期·WWDC17 内参》 ,并在八月底又开源了一个第三方库 SakuraKit。
十月底,来到现在的公司,脸萌
年底,为了带两个娃出行方便,买了辆国产新能源。
SakuraKit,是⼀一个轻量级的、专门用于 App 主题变更、⽪肤切换的开源库,采用函数式、链式的编码方式,简单实⽤、方便理解、利于维护。
InputKit,是⼀个轻量级的,专⻔⽤于做输入限制的第三⽅库。
2017 看的书籍较少。
Objective-C 继续深入了解底层,Swift 则时刻处于学习并进行观望。
书籍:《Swift 4.0 beta》、《iOS 与 OS X 多线程和内存管理》、《经济学原理》、《HACKERS & PAINTERS》等,其中 经济学原理还未读完。
推广个人微信公众号、博客,每月写一篇 Blog 作为输出。
😀
在脸萌大家庭,最大的收获,无疑是又认识了一大波同事。在技术上,开始用 Swift 进行实战,之前都是看官方的 Guide,从 2.x 看到 3.x,再 4.x ,现在终于有机会进行实战了。
2017 忙碌而充实的过了一年,收获颇多。2018 技术学习仍有待加强。
欢迎关注微信公众号
]]>本文主要闲聊一些 Objective-C 和 Swift 混编项目带来的一些潜规则,希望能帮到对此感到疑惑的朋友。下面我们开始进入主题:
官方 Guide 上只是简单叙述(Using Swift with Cocoa and Objective-C),即 Swift 编译器会在我们使用 Objective-C 的 API 时自动的将其转成 Swift 风格的 API(说白了就是会对一些方法名、枚举名等等做一些有规则的删减,即重命名)。
在 Swift 中引用 Objective-C 单例时,如果单例方法包含于类名,则会出现编译错误,下面我们来看几个例子。
1 |
|
TXLoginManager
类有一个单例方法命名为 manager,在 Swift 中引用 manager 方法时,会出现编译错误:
说白了,manager 方法已经废了。。。
在 Example 1 的基础上,我们把单例方法的命名改一改:
1 |
|
单例方法命名改成 shareInstance 后,编译通过了。至此,至少问题已经解决了,现在我们再简单看看是什么原因?为何 manager 方法无法引用,而 shareInstance 却可以引用呢?
在 Example 1 的基础上,把 manager 单例方法名称改为 shareManager :
1 |
|
我们可以发现在 Swift 中引用时,shareManager 方法名被重命名为 share :
至此,我们可以得出一个简单的命名潜规则:在 Swift 中引用 Objective-C 单例时,如果单例方法包含于类名,则会出现编译错误,准确的说,应该是如果单例方法的名称正好是该类名驼峰命名的后缀,那么在 Swift 中引用该单例方法时,会出现编译错误。
为何在 Swift 中引用 Objective-C 类的 API 会出现这种问题呢?官方 Guide 上时这样描述的:
The Swift compiler automatically imports Objective-C code as conventional Swift code. It imports Objective-C class factory methods as Swift initializers, and Objective-C enumeration cases truncated names.
因为 Swift 编译器在使用 Objective-C 的代码时会自动的将其转成 Swift 风格的代码,就是会对一些方法名、枚举名等等做一些有规则的删减。
There may be edge cases in your code that are not automatically handled. If you need to change the name imported by Swift of an Objective-C method, enumeration case, or option set value, you can use the NS_SWIFT_NAME macro to customize how a declaration is imported.
根据官方 Guide,上述的这种 case 属于 特殊的情况。那如何解决这种问题呢,Swift 提供了一个宏,专门处理我们遇到的这种 case —— NS_SWIFT_NAME
1 |
|
这样,manager 该单例方法,当我们在 Swift 中引用时,会被重命名为 shareInstance。
1 |
|
有时候,我们在 Swift 中引用 Objective-C 中某个类的 API 时,方法名是可能会被重命名的,下面我们直接看例子。
1 |
|
当该类的类方法返回自身类型的实例对象时,上述的方法会被重命名。应该这样引用:
1 | // 方式一: |
通过上述实践,我们可以发现类方法中的 manager 前缀会被删掉,而且变成了 Swift 中的 init 方法。如果该类的类方法不返回自身类型的实例对象呢?
1 |
|
通过实践可以发现,在 Swift 中是可以这样引用的:
1 |
|
这种方式的引用同我们一般的方法引用是一致的,无异同。
实例方法的重命名规则与类方法有点相似,此处就不再详述了,感兴趣的朋友可以自己实践一下。(当然方法的重命名我们一般都可以通过 NS_SWIFT_NAME
来指定)
本文主要翻译今年 The Swift Programming Language (Swift 4) 中新出的章节 -《Memory Safety》。在 Swift 4 中,内存安全访问进行很大的优化(《What’s New in Swift 4 ?》)。
默认情况下,Swift 会克服代码层面上的一些不安全的行为,如:确保一个变量被初始化完后才能被访问、确保变量在销毁后不会被访问等等安全操作。
Swift 也会确保在多路访问内存中同一区域时不会冲突(独占访问该区域)。通常情况下,我们完全无需考虑内存访问冲突的问题,因为 Swift 是自动管理内存的。然而,在码代码的时候,了解那些地方可能发生内存访问冲突是非常重要的。通常情况下,如果你的代码有内存访问冲突,那么 Xcode 会提示编译错误或者运行时错误。
本文不会介绍什么是内存访问冲突。详见 The Swift Programming Language (Swift 4)。如果你写的是并发或者多线程的程序,内存冲突访问与单线程是非常相似的一个问题。本文主要讨论单线程上的内存冲突访问。如果想检测多线程是否存在内存访问冲突,你可以看看这篇文档。
我们可以把访问分为两种:即时和长期(instantaneous & long-term)
重叠访问主要带有 in-out 参数的函数(或方法)以及结构体中带有 mutating 关键字的方法。我们下面来看看例子。
一个函数对其 in-out 参数具有长期的访问权限,如下代码:
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4).” iBooks”.
1 |
|
在上述代码中,stepSize
是一个全局变量,而且被作为一个 in-out 参数传给 increment(_:)
方法。冲突的原因在于 number
和 stepSize
引用的是内存中同一区域,并且同时进行读写访问,因此导致访问冲突。
我们可以采用复制 stepSize
的方式解决该问题:
1 |
|
在结构体中,带有 mutating 关键字的方法调用期间对 self 具有写入权限。
1 |
|
上述代码是 Ok 的,即时写入权限在时间上是重叠的,但是是分别访问 oscar
的 health 和 maria
的 health,因此在 shareHealth(with:)
方法中并没有发生内存访问冲突。
然而,如果你把 oscar
作为参数传给 shareHealth(with:)
,那么就会产生内存访问冲突:
1 |
|
很显然,shareHealth(with:)
方法中的 self
和 teammate
同时指向内存中同一区域,即同时对 oscar
的 health
进行读写访问,因此导致访问冲突。
像结构体、元组、枚举这些类型都是由各个值组成的,如:结构体的各种属性、元组的各种元素等等。因为它们都是值类型,这意味着对其中一个属性的读写访问就是对整个值进行读写访问。代码如下:
1 |
|
上述代码不难理解,因为元祖是值类型,上述 balance(_:_:)
发生内存访问冲突,即同时访问 playerInformation。
下面我们再看一下结构体,其中 holly
是一个全局变量
1 |
|
上述代码会报这样一个错误:Simultaneous accesses to 0x10**580, but modification requires exclusive access。其实就是内存访问冲突了,Swift 4 中也针对这块做了优化处理,感兴趣的同学可以查阅我之前写的一篇文章《[WWDC17] What’s New in Swift 4 ?》。
在实践中,上述代码中的 holly
一般是个局部变量而非全局变量,编译器可以保证对结构体的存储属性进行重叠访问是安全的,代码如下:
1 |
|
上述代码运行是 Ok 的,有时候,限制结构体的各属性进行重叠访问是没有必要的,这也就是为什么 someFunction()
没有发生冲突访问的原因。内存访问安全虽应当得到保证,但是独占访问比内存安全访问要求更加严格,从上述代码可看出,即时违背了独占访问的原则,内存安全也能得到保证。一般情况下,编译器会在如下条件下保证对结构体的存储属性进行安全的重叠访问:
感兴趣的同学可以查阅这里 The Swift Programming Language (Swift 4)。
欢迎关注微信公众号
]]>本文主要简单谈谈并收集一些关于 iOS 11 & iPhone X 的适配及设计指南。
众所周知,iPhone X 屏幕与其他的 iPhone 设备均不同,苹果称 iPhone X 的屏幕为超级视网膜显示屏。
Don’t mask or call special attention to key display features. Don’t attempt to hide the device’s rounded corners, sensor housing, or indicator for accessing the Home screen by placing black bars at the top and bottom of the screen. Don’t use visual adornments like brackets, bezels, shapes, or instructional text to call special attention to these areas either.
注意:根据 Human Interface Guidelines for iPhone X 规范,不要试图去隐藏屏幕的圆角、刘海等。前段时间 GitHub 上小火了一个 Swift 库 NotchKit,专门用于隐藏 iPhone X 的刘海。笔者建议先别急着集成公司项目,这种做法可能会被苹果拒绝(违反了 HIG 条例) ,不过小伙们可以集成至个人项目提交审核试试。
竖屏情况下并且无导航栏时,上下安全边距分别为 44pt/34pt,即安全区域宽高为 375pt/734pt。
横屏情况下并且无导航栏时,上下安全边距分别为 0pt/21pt,左右安全边距为 44pt/44pt,即安全区域宽高为 724pt/354pt。
在 iOS 11 中导航栏有个新特性 —— 大标题,直接上代码:
1 | if #available(iOS 11.0, *) { |
当没有开启大标题且有导航栏时,上下安全边距分别为 88pt/34pt,即安全区域宽高为 375pt/690pt。
开启大标题时,上下安全边距分别为 140pt/34pt,即安全区域宽高为 375pt/638pt。
不管有没有开启大标题,横盘状态下一样,上下安全边距分别为 32pt/21pt,左右安全边距为 44pt/44pt,即安全区域宽高为 724pt/322pt。
在 iOS 11 中必须支持 When In Use
授权模式(NSLocationWhenInUseUsageDescription),在 iOS 11 中,为了避免开发者只提供请求 Always
授权模式这种情况,加入此限制,如果不提供When In Use
授权模式,那么 Always
相关授权模式也无法正常使用。
如果要支持老版本,即 iOS 11 以下系统版本,那么建议在 info.plist 中配置所有的 Key(即使 NSLocationAlwaysUsageDescription 在 iOS 11及以上版本不再使用):
NSLocationAlwaysAndWhenInUseUsageDescription 为 iOS 11 中新引入的一个 Key。
欢迎关注微信公众号
]]>目前市场上很多 App 都有主题变更、皮肤切换的功能。随着项目代码量的不断增长,业务不断完善,功能性代码逐渐趋于模块化,尤其是在多人协作开发同一个项目时,模块解耦尤为重要,同时,公共基础库的功能性代码使用越简单越好。
前段时间在维护旧项目时,收到 App 主题变更、皮肤切换的需求,其包括 App 中各种图标、色值、文字、字体等都包括在内,都需实现主题化。主要用于:
由于老项目代码比较混乱,功能模块耦合严重以及开发时间等综合因素,在实现 App 主题变更、皮肤切换的功能的同时,想要在尽量不修改旧代码的基础上增加新的功能是比较麻烦的。
由于没有合适的第三方库,于是自己手撸了一个库 SakuraKit,并开源,希望能帮到需要的朋友。
下面我们开始介绍 SakuraKit 及快速入门。
SakuraKit,是一个轻量级的、专门用于 App 主题变更、皮肤切换的开源库(灵感源自 SwiftTheme、DKNightVersion等),采用函数式 + 链式的编码方式,简单实用、方便理解、利于维护。
在体验前,我们先来看看效果图:
下面以 UIButton
为例,介绍如何使用 SakuraKit 进行主题化:
1 |
|
上述代码是给一个 button 的背景色(backgroundColor
)以及标题颜色(titleColor
)进行主题化。其中 Home.buttonBackgroundColor
与 Home.buttonTitleColor
属配置文件中的 KeyPath
,配置文件的功能有点类似语言本地化文件(Localizable.strings)。后文会重点介绍如何设置配置文件。
到此为止,我们已经实现了 button 按钮主题化功能,如果你想切换主题,可以调用如下 API:
1 |
|
其中 name
参数代表主题的名称,type
参数代表主题类型(目前有两种:沙盒和本地)。
现在我们再具体的介绍一下如何使用 SakuraKit。
做过 App 语言本地化的童鞋,应该比较熟悉 Localizable.strings 文件配置,同理,我们在使用 SakuraKit 对 App 进行主题化时,也需要进行类似的配置。目前支持 .json 和 .plist 两种文件格式。
下面我们以 .json 文件格式做示例:
1 | { |
在上述体验代码中,我们看到这样的字符串:Home.buttonBackgroundColor
和 Home.buttonTitleColor
,这其实就是配置文件中字典的 KeyPath,通过 KeyPath
可以取得不同主题下的值,如:色值、图片名称、文字、字体大小等等。
注意事项:
本地主题,即用户无需下载的主题,在 App Bundle 中。除了 App 本身自带的默认主题外,SakuraKit 还能够为 App 新增多种本地主题。
配置步骤如下:
新建 .json 配置文件,比如新建一个名叫 typewriter 的主题,因此配置文件命名为 typewriter.json。
配置一套切图,并且命名与已有的主题要做区分。
完成上述步骤后,在 AppDelegate 中 -application:application didFinishLaunchingWithOptions:launchOptions API 注册所有本地主题:
1 |
|
调用切换主题 API 即可切换至该指定主题:
1 |
|
远程主题(资源压缩包.zip),即用户通过网络下载的主题,后台可动态配置。同本地主题一致,分为两部分:配置文件 + 切图。当配置文件和切图都弄好后,将文件夹打包成zip文件,传给后台即可。主题数据格式如下(仅供参考):
1 |
|
sakuraName 是切换主题时用的名称,而 url 是该主题的下载地址。(注:如果 sakuraName 字段传空,那么主题的名称将默认为下载的压缩包名称)
当远程主题下载完毕后,可以这样切换主题:
1 |
|
值得一提的是,SakuraKit 提供了一些主题下载的简单接口,支持多种主题同时下载等操作,并且支持 Block 和 Delegate 两种方式的回调,同时用户还可自定义下载操作。
下面我们来依次介绍一下主题下载。
我们直接来介绍 API :
1 |
|
其中 sakuraModel
模型数据遵守了 TXSakuraDownloadProtocol
协议,具体使用详见 SakuraDemo_OC,在 DownloadSakuraController
控制器演示了该操作。
直接调用 API 实现主题下载:
1 |
|
如果针对步骤一的下载操作需要回调,那么可以选择性的再实现以下方法:
1 |
|
具体使用详见 SakuraDemo_OC,在 AppDelegate
中演示了该操作。
除了上述自带的下载操作外,SakuraKit 还提供了自定义下载操作相关的 API :
1 |
|
1.为何每个主题都有自己配置文件?
答: 由于每个主题,除了切图的命名是是一致的外,不同的主题背景色、字体大小可能不一样,因此,每个主题都要有自己的配置文件,除非只对切图进行本地化。
2.为何主题名称与配置文件名称一致?
答: 这只是一个约定,SakuraKit 会通过主题名称找到该主题在本地或者在沙盒中的路径,使得主题名称与配置文件名称一致,可以减少不必要的工作量。
3.本地与沙盒主题有什么区别?
答: 在本地主题称为 mainBundle 主题,远程主题称为 Sandbox 主题。
关于 SakuraKit 具体使用,详见 Demo。
GitHub 项目地址:https://github.com/tingxins/SakuraKit
有什么问题或者更好的建议,GitHub 上直接提 issue 或者 PR。感谢支持
Demo 素材来源:网易云音乐等第三方 App,如有不妥之处,请及时联系并予以删除,谢谢。
欢迎关注微信公众号
]]>本文主要是笔者小结 WWDC2017 中 《What’s New in Swift》的 Session ,其中也掺杂了些《What’s New in Foundation》,仅作记录。
下面步入主题。
在 Swift 4 中,private 修饰的属性可以在 Extension 中访问了,再也不要用 fileprivate 修饰属性了😎。
下面我们来区分 Swift 3 与 Swift 4 中的区别。
Swift 3:
Swift 4:
在 Swift 3 中,有些童鞋使用代理时,无法同时继承类和协议
Swift 4 中,针对此处进行了改进,直接上 WWDC17 示例代码:
1 |
|
在 Swift 4 中新增了一种 Key-Path 表达式,该表达式可用于 KVC & KVO 中的 APIs,格式如下:
1 |
|
示例代码如下:
1 |
|
如果在上下文中,能隐含的推断出其类型,那么 Key-Path 表达式中的 Type Name 可以省略,即
1 |
|
如:
1 | @objcMembers class SomeClass: NSObject { |
Excerpt From: Apple Inc. “Using Swift with Cocoa and Objective-C (Swift 4 beta).
我们以下面这段 JSON 为例,来看 Swift 4 中针对 JSON 进行解析的新方法
1 |
|
首先,我们要遵循 Codable 协议:
1 |
|
使用 JSONDecoder 进行解析:
1 |
|
本节主要展示 JSONEncoder 编码,直接上代码:
1 |
|
在 Swift 4 中,修复了字形群集长度计算的一些问题,如 emoji 表情。关于字形群集或者 Unicode 编码概念生疏的童鞋可以看笔者之前写的两篇文章 《字符编码(一)》、《Swift3.0 中 Strings/Characters 闲聊》。下面我们来看看 WWDC17 上的的示例:
1 |
|
在之前 family.count 会等于 4(\u{200D} 是一个零宽度的 joiner)。
笔者在 Xcode 9 beta1 上运行,选择 Swift 编译语言版本时,测试结果无效,只有在 Xcode 8 测试时 family.count = 4。
在 Swift 2 中,String 的集合这一特性被遗弃,在 Swift 3 中,String 也没有遵守集合的相关协议(如:RangeReplaceableCollection, BidirectionalCollection),因此自 Swift 2 起,String 不是一个集合,而是把这一特性赋予给了 String 的一个属性 –> characters (A view of the string’s contents as a collection of characters.),该属性是 String.CharacterView 类型,并且遵守 RangeReplaceableCollection 协议。
1 |
|
因此我们在遍历或者操作 String 时,经常会这么写:
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 3.1).” iBooks. https://itunes.apple.com/us/book/the-swift-programming-language-swift-3-1/id881256329?mt=11
1 |
|
.characters.····。
但,直至 Swift 4,String 又开始遵循集合相关协议,从此可以这么写了:
1 |
|
当然在 Swift 4 中又出现了一个新的结构体 Substring,Substring 无法直接赋值给 String 的。
关于 Substring 与 String 之间的转换可以这么写:
1 |
|
如果字符串需要跨多行,可以这么写:
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4).”.
1 |
|
没错,三对引号。
如果字符串本身包含三个连续的 ‘“””‘ 引号时,可以采用反斜杠进行转义处理(\),如:
1 |
|
在 Swift 3 中,区间运算符只有两种:闭区间运算符(Closed Range Operator)、半闭区间运算符(Half-Open Range Operator)。在 Swift 4 中,又新增了一种更加简单方便的区间运算符–>单面区间(One-Sided Ranges)。
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4).
你可以这样写:
1 |
|
当然也和结合半闭区间运算符,可以这么写:
1 |
|
判断区间是否包含,可以这么写:(for语句中要注意死循环哈)
1 | let range = ...5 |
WWDC17 示例代码:
在 Swift 3 中,假设我们要为 Sequence 扩展一个方法,要这么写:
1 |
|
但在 Swift 4 中, 针对 Sequence 做了一些小改进,使我们代码更加轻便,看起来更加清爽:
1 | extension Sequence where Element: Equatable { |
这是怎么实现的呢?因为在 Swift 4 中,我们在声明一个 associatedtype 的 placeholder 时,我们可以使用 where 语句了。
下面我们来对比一下 Swift 3 与 Swift 4 中 Sequence 的区别:
在 Swift 3 中 Sequence 协议是这么写的:
在 Swift 4 中进行改进后,是这么写的:
对比看完后,想必读者一目了然。
下面针对 associatedtype 中使用 where 语句,我们再来看个例子:
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4).
1 |
|
如果在 Swift 3 下写,Xcode 会出现这样的编译错误:
有了上面这些特性后,我们在使用 Swift 4 时,可以省略一些冗余约束,这里直接上 WWDC17 的示例代码:
在 Swift 3 中,是这样写的:
在 Swift 4 中,现在我们可以这么写:
在 Swift 4 中,现在支持泛型下标了,直接上代码:
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4).
1 |
|
上述代码我们为 Container 添加了下标取值能力,在这个泛型下标中有 3 个约束:
Swift 3 中,NSNumber 转换有个 Bug,如:
1 |
|
Swift 4 中已修复:
现在可变集合增加了一个方法,我们可以直接使用 swapAt 方法,而非 swap 。
1 |
|
欢迎关注微信公众号
]]>在 Objective-C 项目中,不少开发者们可能会写或者曾看到过这样的代码:
1 |
|
??把自己的代理设置为自己??这种做法到底妥不妥呢?
本文将采用自问自答、通俗易懂的方式讨论 self.delegate = self
这种做法是否妥当,以及这种做法将会带来的问题,或者说致命的问题。
首先,我们先回顾一下 Delegate 的出现的原因是什么呢?再反思一下,我们为何会这么写呢?以及出现的场景有哪些?
笔者觉得 Delegate 模式其实就是 NSProxy 设计模式的一种衍生版,它们共同的特点可以理解为都是传递对象的消息,主要区别如下:
Delegate 无非就是把 A 的消息传递给代理对象 B,self.delegate = self
直接把代理对象设置为自己,这样省去了引入第三方代理,这种做法大部分情况是为了图个方便,一般出现在使用第三方闭源代码以及系统类(如:UITextField等)的情况下,因为我们无法获知内部消息是如何传递的,只能通过代理对象获知消息。
self.delegate = self
这种做法笔者并不推荐,因为它可能会带来一些安全隐患(特别是在依赖第三方库非常多的项目中),后文会做说明。
本文以系统类 UITextField 的子类为例展开讨论。
在项目中我们经常会用到 UITextField 类或者其子类,有时候为了图其方便会把 UITextField 的 delegate 设置为自己(self.delegate = self
),然而在使用 UITextField 控件时,发现程序不响应了,过了几秒后程序出现闪退现象。
既然 Bug 来了,那当然就是找 Bug,于是我们开始排查原因(先撇开调用栈信息):
self.delegate = self
代码注释掉,然后重新运行程序,发现问题得到解决。self.delegate = self
导致的?于是新建工程,写了一份一模一样的代码(注:TXLimitedTextField 继承自 UITextField):
1 |
|
运行新建的工程后,发现没有这问题。于是在 TXLimitedTextField.m 文件中再实现自己的代理方法:
1 |
|
运行工程,使用 TXLimitedTextField 控件,发现还是没有这问题。
What‘s fuck?? 原项目代码有毒??
进行全局断点后,重新再次运行项目,发现调用栈无限递归,直到栈溢出,最后导致程序崩溃。
下面我们来看是什么原因导致的。
self.delegate = self
(self 指 TXLimitedTextField 实例)调用栈无限递归?于是,我们针对 TXLimitedTextField 类查找一下整个项目中有没有这种代码:
1 |
|
首先,这种写法一定要避免,尤其是在使用系统类或者第三方闭源框架时应特别注意,因为你并不知道其实现代码是如何写的。
如果整个项目中没有这种代码,检查一下是否存在 UITextField 运行时相关代码或者第三方框架,比如:BlocksKit等等。下面笔者举个具体的例子:
这段时间在维护一个旧项目,最近发现项目出现上述的问题,仔细排查后发现项目中用到了 BlocksKit,其中有一个 Category(UITextField + BlocksKit),其中针对 UITextField 的 delegate 进行动态调剂,把 delegate 替换成 A2DynamicUITextFieldDelegate(父类为 A2DynamicDelegate,根类为 NSProxy 类)的实例,NSProxy 类主要用于消息转发的(不熟悉的,请查阅官方文档)。
断点至自定义的 UITextField 中的 -respondsToSelector: 方法以及 A2DynamicDelegate 中的如下方法:
1 |
|
发现程序一直在这四个方法中循环执行,直到栈溢出,最终致使程序崩溃。相信大家遇到的问题都与此类似,下面笔者将以此例进行具体分析并究其原因。
找到了程序的崩溃点后,通过 NSLog 输出上述方法中的选择器 selector,发现是 -keyboardInputChangedSelection: 方法,于是设置条件断点([NSStringFromSelector(aSelector) isEqualToString:@”keyboardInputChangedSelection:”])如图所示:
进入断点调试后,发现一个有意思的事,如图所示:
这说明,在 UITextField 中,伪代码如下:
1 |
|
看到这个方法后,读者应该发现 -keyboardInputChangedSelection: 方法与本节开头所提 -doSomething: 方法结构是一模一样的?只是方法名不同而已。
此时,细心的读者可能会产生一个疑惑,如果如上所述,那么上文提到新建的工程(TXLimitedTextField 类,如果写了 self.delegate = self
)也应该会出现无限递归(死循环)才对啊?
然而事实上却没发生死循环。
笔者通过断点调试,发现 TXLimitedTextField 同样会调用 -keyboardInputChangedSelection:,断点截图同上,但不会出现死循环,最终导致程序崩溃的现象,笔者猜测分析,UITextField 类应该针对 self.delegate = self
做了一些特殊的处理,具体什么处理,就得问苹果爸爸了。可以肯定的是,在没有任何方法调剂的情况下,即 “self.delegate == self”,是不会出现死循环的问题的。
但是,此处存在方法调剂,即 BlocksKit 动态替换了 UITextField 类中 delegate(在其子类亦生效),因此 delegate 其实是 A2DynamicDelegate 实例,为了帮助读者理解,笔者简单画了一张图:
当点击 UITextField 控件时调用栈如下(省略部分):
通过上文主要以 UITextField 为例进行讨论分析,那么这种问题应当如何解决?
self.delegate = self
。至于在 BlocksKit 中具体怎么处理该问题,在笔者的一个开源项目中进行了实践,在使用 InputKit 过程中,即使 self.delegate = self
,也不会出现上述死循环的问题,本文不做详述,详见InputKit.
笔者遇到上述的问题,其实属消息转发间接导致,可以说是第三方框架 BlocksKit 的一个 Bug,作者已经有一年多未更新该框架了哈(可能有由于 Swift 不再推荐使用 runtime 相关的方法,特别是消息转发相关 API,作者无力更新 Objective-C,哈哈)。
欢迎关注微信公众号
]]>最近接手了两个 O2O 的老项目,其中的 Bug 也不言而喻,单看项目中的布局就有 n 种不同的方式,有用纯代码的,有用 Masonry 的,有用 VFL 的,也有用 Xib 的,更有用代码约束等等等,🐮。不扯远了,回归正题。
由于这两个项目是 O2O 项目,因此针对输入组件的限制相比其他类型的项目要多一些,比如商品价格输入(如:保留3位整数,2位小数等)、买家留言字数限制、不能输入中文、不能输入英文、只能输入数字等等限制。
于是输入限制 InputKit 诞生了!本文主要简单介绍 InputKit 的使用及相关注意事项。
InputKit 是一个轻量级的,专门用于做输入限制的第三方库,灵感源自 BlocksKit,在项目中,主要为了解决三个问题:
所谓解耦,即在开发项目中工程师不需要仅仅只为做个输入限制,就在项目中到处写 UITextFieldDelegate 协议中的方法,如:
1 |
|
只需继承 InputKit 中的类即可,然后设置相关的限制属性即可,无需设置 delegate。以 TXLimitedTextFieldTypePrice 类型为例,如:
Objective-C
1 |
|
Swift
1 |
|
如果想设置 textField 的 delegate 也可以(即 textField.delegate = self),不会影响其限制功能,就像使用普通的 UITextField 一样,毫无差异,非常方便。
Demo 截图:
文章开头提到过,需求即针对商品价格输入(如:保留3位整数,2位小数等)、买家留言字数限制、不能输入中文、不能输入英文、只能输入数字等等做限制。
如果针对上述的部分需求做定制键盘,是完全没必要的,因为工作量增多且并不能从源头解决问题,比如:用户使用粘贴功能、使用键盘提示文本等等,导致定制的键盘也是白搭。因此 InputKit 从源头解决该问题,针对用户的输入进行筛选并限制。比如我们只能让用户输入中文:
Objective-C
1 |
|
(Swift 代码略)
关于上述的正则表达式,在 InputKit 中的 TXMatchConst.h 头文件中提供了一些常用的,比如:只能输入数字、中文、字母等等,欢迎大家在 GitHub 上 PR。(注意:此处的正则表达式限制的是输入源头,而非结果!不然会导致用户无法输入。体会一下哈)。
Demo 截图:
在没使用 InputKit 之前,有时候,运行到程序的某处,点击输入框,程序莫名其妙的卡死,过会儿就闪退了。相信不少人遇到过,后来发现是 self.delegate = self(self 即输入框对象) 导致的。注释后,发现没问题,打开后,程序又闪退,后来发现原来是 self.delegate = self 引起的死循环,因此不得不注释该句代码。
上述的这些问题,如:在项目中 UITextFieldDelegate 协议方法遍地都是,以及一不小心使用了 self.delegate = self 时,还会出现死循环等等,InputKit 都解决了。
使用 InputKit 后,self.delegate = self 程序不再卡死。(晚点会再发一篇软文针对 self.delegate = self 的问题进行剖析)。
至此,需求、Bug 均已解决。👀
GitHub 项目及 Demo 地址:https://github.com/tingxins/InputKit。有什么问题或者更好的建议,直接提 issue 或者 PR。
欢迎关注微信公众号
]]>现在写文章拖延症特别严重啊 (😂)……
本文我们将复习一下 Objective-C 中的一些关于类的知识。
在开发过程中,类与对象相信大家再熟悉不过了,有时我们也会接触一个比较陌生的概念,元类(metaclass),甚至在回头来想时,发现类与对象是什么都开始犯糊涂了,本文主要探讨这三者之间的关系以及在消息转发中各自扮演的角色,希望读者看完本文能有所收获。如有不妥的地方还望大家及时帮忙纠正。
在面向对象编程语言中,类是一个非常重要的概念,理解了它,能更好的造轮子、能更好的面向对象编程、能写出模块化的代码、更能提高代码的可读性及后期维护性。
类
是数据及行为的封装体,在 Objective-C 中,在数据上,类
定义了内存分配大小、内存布局以及成员变量数据类型等,在行为上,类
定义了实例方法等。我们可以简单的把类
比作为某个产品的设计稿。
通过查阅 Apple 官方开源的 objc 源码(官方最新版—>传送门),得知类的数据结构(其中字段本文不做解释,可自行 Google),如下:
1 |
|
看完这小段代码,细心的同学会发现,objc_class 继承自 objc_object,没错,类其实也是一个对象,既然类是一个对象,那么它一定是某个类的实例。先不讨论此问题,我们先来谈谈对象。
对象一定是某个类
具体的一个实例,可以简单理解为类
的“值”,可直接使用,就像洗衣机(对象
)一样可以直接用来洗衣服(数据
),但洗衣机的设计稿(类
)却不能。对象具有动态性,它有自己的生命周期,对象在生命周期结束时会调用 dealloc 方法。
在 Objective-C 中,含有一个 isa 指针并且可以正确指向某个类的数据结构,都可以视作为一个对象,其中 isa 指针指向当前对象所属的类。通过查阅 Apple 官方开源的 objc 源码(官方最新版—>传送门),得知类的数据结构(其中字段本文不做解释,可自行 Google),如下:
1 |
|
每当要向某个对象发送一个消息时,都会通过 isa 指针找到该对象所属的类(因为类定义了对象的行为),然后再遍历其方法缓存表或者方法列表(行为),通过 SEL 找到后取出方法(Method)中的 IMP 函数入口指针,并执行该函数,如果找不到该方法,则进入消息转发等等,此处本文不做概述。
讨论完对象之后,我们再来回顾上文遗留的一个问题:类既然是一个对象,那么这个对象的类是什么呢?
接下来我们将讨论此问题。
元类是类对象的类。简单的说类描述的是对象,那么元类描述的就是类。同理,元类定义了类的行为(类方法),每当要向某个类发送一个消息时,都会通过 isa 指针找到该类所属的元类,然后遍历其方法缓存表或者方法列表,通过 SEL 找到后取出方法中的 IMP 函数入口指针,并执行该方法,否则进行消息转发阶段等等。
说到元类,那么定会有人问,元类的类是什么?元类也是一个对象,元类的类是根元类,根元类在继承体系中是根类的元类,那么根类的元类是属于哪个类呢?根元类的类就是自己(即 isa 指针指向自己),根元类的父类 superClass 指针指向 根类。
Greg Parker (@gparker) 给出了一张图,使得整个结构清晰明了:
如果上述都理解了,那么下面这段代码看懂就没问题了。 -(IMP)methodForSelector:(SEL)aSelector 是 NSObject 类的一个实例方法,以下两种调用方式都是正确的:
为了验证本文的一些说法,下面我们写个测试代码进行验证,输出结果本文不做分析,有兴趣的读者可以对照上文自行分析:
1 |
|
运行上述代码后,输出结果如下:
欢迎关注微信公众号
]]>在开发过程中,有时候会遇到这样一些问题,比如:
那么在上述的这些场景下应如何发送网络请求?发同步请求 or 异步请求?请求嵌套?······
本文将简单探究开发过程中网络请求同步的问题以及相关注意点。
我们都知道 NSURLConnection 中有一个同步请求的 API :
1 |
|
针对上述的第一种情况 A,该 API 可满足要求。如果同步请求阻塞主线程的时间过长,存在被 watchdog kill 的可能。想避免这种情况,建议在子线程中调用此 API。(感兴趣的同学可以看看,关于 watchdog timeout crashes/Understanding and Analyzing Application Crash Reports)
同步请求相对异步请求而言存在一些缺陷,如:
很遗憾,NSURLConnection 目前已被苹果全面弃用,并且 AFNetworking 在 3.x 中已经移除此类 API,因此同步请求不建议采用此种方式。
信号量机制,我们可以简单理解为资源管理分配的一种抽象方式。在 GCD 中,提供了以下这么几个函数,可用于请求同步等处理,模拟同步请求:
value 可以理解为资源数量,以 value = 0 为例,调用 dispatch_semaphore_wait 操作成功后,当资源数量 value 等于 0 时,就会阻塞当前线程(反之,value 就会减 1),直到有 dispatch_semaphore_signal 通知信号发出,当 value 大于 0 时,当前线程就会被唤醒继续执行其他操作。
下面我们展示一段代码来模拟同步请求:
Objective-C:
1 | // 1.创建信号量 |
Swift:
1 | func sendSynchronousDataTask(with url: URL) -> (Data?, URLResponse?, Error?) { |
在 iOS 系统中,如果应用不能及时的响应用户界面交互事件(如启动、暂停、恢复和终止),watchdog 就会杀死程序并生成一个 watchdog 超时崩溃报告,据官方说法,watchdog timeout 时间并没有明文规定,但一般会少于网络请求超时时间。
In order to keep the user interface responsive, iOS includes a watchdog mechanism. If your application fails to respond to certain user interface events (launch, suspend, resume, terminate) in time, the watchdog will kill your application and generate a watchdog timeout crash report. The amount of time the watchdog gives you is not formally documented, but it’s always less than a network timeout.
这里有一个奇怪的现象,经测试,笔者采用信号量机制一直阻塞主线程时并没有被 watchdog kill,但 NSURLConnection 中的同步请求方法 + sendSynchronousRequest:returningResponse:error: 在慢速网络下与其说 crash 了,不如说被 watchdog kill 了。不扯远了,开始下一个话题 —— dispatch_group_t
继续本文话题,回顾文章开头提到的问题,如果针对单个请求进行同步处理,那么使用同步请求即可,上述两种方式都可以。如果在某些界面需请求多个接口,且各个接口返回的数据之间或者整体存在依赖关系,那怎么办呢?虽然采用嵌套请求的方式能解决此问题,但存在很多问题,如:其中一个请求失败会导致后续请求无法正常进行、多个请求在时间上没有复用,即无并发性。
A dispatch group is a mechanism for monitoring a set of blocks. Your application can monitor the blocks in the group synchronously or asynchronously depending on your needs. By extension, a group can be useful for synchronizing for code that depends on the completion of other tasks.
针对这种情形,即某个操作依赖于其他几个任务的完成时,我们可采用 dispatch_group。主要使用如下两个函数:
以上这两个函数必须配对使用,否则 dispatch_group_notify 不会触发。贴一段代码 + 一张效果图(源码):
1 | // 创建 dispatch 组 |
对于熟悉 dispatch_group 的同学来说,可能会想,为何不用 dispatch_group_async?对于网络请求而言,请求发出时它就已经执行完毕,也就是 block 中还有个 completeHandler 的情况下,dispatch_group_async 并不会等待网络请求的回调,所以不符合我们要求。
通过本文简单探究,展示了如何采用信号量机制模拟同步请求,在开发过程中,我们应尽量避免发送同步请求;并且在某个操作依赖于其他几个任务的完成时,采用 dispatch_group_async or dispatch_group_enter/dispatch_group_leave 来实现同步等处理。如果是进行网络请求同步,应采用后者。当然,如果感兴趣,我们可以在第三方网络库的基础上封装一层自己网络库。(相关源码)
欢迎关注微信公众号
]]>希尔排序算法其本质就是插入排序,是直接插入排序算法的一种改进,因 [D.L shell]
(https://en.wikipedia.org/wiki/Donald_Shell) 于 1959 年提出而得名,通常我们也称希尔排序为缩小增量排序,所谓增量
,即将待排序的序列按该增量分割一个或多个子序列,所谓缩小
,即当以某个增量分成的所有子序列都排序完后,增量会逐渐缩小(ps:最后一定会缩小到1)。如:先以3为增量,则将待排序的序列下标1、4、7···分成一组,将下标为2、5、8···分成另一组···,当以3为增量分割的所有子序列都排序好后(默认递增),再以1为增量分割该序列(ps:其实就是对基本有序的序列进行直接插入排序),最后完成整个希尔排序。
希尔排序算法依然可采用嵌套 for 循环的方式实现,本文将只采用递归的方法实现该算法。对于 for 循环的方式,感兴趣的童鞋可以参考笔者之前写的一篇文章哥德巴赫猜想。下面我们开始进入正题。
我们以下面10个数字组成的序列来做分析:
13, 12, 2, 22, 16, 11, 10, 1, 21, 15
首先我们要明白希尔排序算法的执行时间依赖于增量序列,关于增量序列,要注意两点:
针对上面给出的序列,我们以5、3、1为增量序列,下面模拟排序,以增量5为例,下面是每个子序列完成排序后的结果:
13 12 2 22 16 11 10 1 21 15
11
12 2 22 16 13
10 1 21 15 //11 与 13 互换
10
2 22 16 13 12
1 21 15 //10 与 12 互换1
22 16 13 12 2
21 15 //1 与 2 互换21
16 13 12 2 22
15 //21 与 22 互换15
13 12 2 22 16
//15 与 16 互换此时以5为增量分割的五个子序列都已排序完成:
然后在对上面得出的结果以增量3进行分割,重复相同的操作,最后在以1为增量进行分割(即进行一趟直接插入排序),从而完成希尔排序。
采用递归方式,上文也提到过,希尔排序本质就是一种插入排序,是直接插入排序的一种改进。通过上述分析,我们可以划分如下几个步骤:
针对这几个步骤,我们来把握一个临界值:
通过上述分析,想必大部分读者已经有思路,现在我们上代码。程序主要分四个模块:
专门处理特定子序列排序递归函数(采用直接插入排序)
1 |
|
处理所有子序列排序的递归函数
1 |
|
处理增量序列的递归函数
1 |
|
主函数模块
1 | int main() { |
希尔排序的复杂度分析较为复杂,笔者高数较水,此处不做分析,感兴趣的同学可以参考:传送门。源码地址
欢迎关注微信公众号
]]>哥德巴赫猜想是(Goldbach’s Conjecture)是数论中存在最久的未解问题之一,
是一个伟大的世界性的数学猜想,其基本思想可以陈述为:
任何一个大于2的偶数,都能表示成两个素数之和。
如:
4 = 2 + 2
6 = 3 + 3
96= 23 + 73
本文将采用两种不同的算法来求出给定范围 n 内的哥德巴赫数字,并对比其时间复杂度,得出更优算法。
根据哥德巴赫猜想,我们可以得出如下信息:
思路A与之前见过的很多想法一样,简单粗暴,采用嵌套 for 循环。思路如下:
Show the (garbage) code!
我们把思路A实现的程序分成两个功能模块:
判断是否为素数模块 int isPrime(int i),返回 1 即为素数。
1 |
|
主程序模块:针对 [4, n] 之间的正偶数进行数值拆分,然后再用isPrime函数进行筛选,如果k,j都为素数,即满足哥德巴赫猜想,输出该数字。
1 | do { |
递归算法,也是我业余时间自己写的一个,递归路径类似鱼骨头,基本思路如下:
这里笔者画了一张抽象的鱼骨头图,帮助读者理解:
思路B实现的程序主要分成三个功能模块,为了区分思路A,判断素数的模块也采用递归的形式:
判断是否为素数 int isPrime(int i),返回 1 即为素数。
1 |
|
递归模块
参数 current
: 代表分裂初始值,参数 flag
: 代表是否深入遍历,此处用于控制重复遍历的情况,如:original=10 时,second=8 时,两次会都会重复遍历 6/4/2,因此加入flag进行限制,只进行一次深入遍历!!
1 |
|
主程序模块
1 |
|
时间复杂度说白了就是算法中基本操作的执行次数,更通俗的说法,就是最深层循环内的语句。基本操作的重复执行次数是和算法的执行时间成正比的。下面我们来粗略计算一下上述算法的时间复杂度。
在程序 A 中,与下面的代码相同,采用嵌套三层 for 循环的方式进行遍历:
1
2
3
4
5
6
7
8
9
for (int i = 1; i <= n; i ++) { // 第一层循环
for (int j = 1; j <= i; j ++) { // 第二层循环
for (int k = 1; k <= j; k ++) { // 第三层循环
count ++;
printf("%d*%d*%d\n", i, j, k);
}
}
}
下面我们来剖析一下基本操作:
第三层 for 循环采用排列组合来计算,举个例子,当 n = 3 时,有 10 次基本操作,我们把执行路径格式定义成 ijk,如下:
1 |
|
以上时间复杂度只是笔者通过简单粗略的分析得出,仅供参考。通过上述分析,我们发现算法A与算法B时间复杂度是一样的,感兴趣的童鞋可以自己计算上述两种算法的时间复杂度。笔者通过测试发现,相同的问题规模,随着 n 的增大,算法B的时间复杂度要远小于算法A。如:n = 100 时,算法B遍历次数是 6380 次左右,算法A遍历次数高达 15569 次(论算法糟糕的可怕性…)。源码地址
欢迎关注微信公众号
]]>本篇文章主要浅析字符串\字符在 Swift 和 Objective-C 之间的区别及其简单用法。如有不妥的地方还望大家及时帮忙纠正。
在 swift 语言中空字符串初始化方式常用的有两种:
1 | // 方式一: |
在开发过程中,我们应该如何用正确的方式来对字符串进行判空处理呢?
1 |
|
首先我们来回忆一下,在 Objective-C 中字符串是怎么计算长度的?我想大家都应该知道。来看看苹果是怎么说的:
A string object is implemented as an array of Unicode characters (in other words, a text string). An immutable string is a text string that is defined when it is created and subsequently cannot be changed. To create and manage an immutable string, use the NSString class. To construct and manage a string that can be changed after it has been created, use NSMutableString.
A string object presents itself as an array of Unicode characters. You can determine how many characters it contains with the length method and can retrieve a specific character with the characterAtIndex: method.
看完这段话,想必大家都明白 NSString 是怎么实现的,以及如何获取其长度。通过 length 方法即可,那么 length 方法是如何实现的呢?苹果官方是这样说的:length 方法利用的是 UTF-16 表示的十六位编码单元数字为单位进行计算的(The number of UTF-16 code units in the receiver.)。UTF-16是什么?(感兴趣的童鞋可以看一下我之前写的一篇文章,字符编码(一)),此处不再详述。
在 Swift 中,字符和字符串都是基于 Unicode 标量建立的,采用21位二进制进行编码,共17个平面(除了基本多文种平面中的 UTF-16 代理对码位外,即U+D800至U+DFFF的编码空间),也就是说编码范围是U+0000-U+D7FFF 或者 U+E000-U+10FFFF。
A Unicode scalar is any Unicode code point in the range U+0000 to U+D7FF inclusive or U+E000 to U+10FFFF inclusive. Unicode scalars do not include the Unicode surrogate pair code points, which are the code points in the range U+D800 to U+DFFF inclusive.”
因此在 Swift 中,我们可直接采用 Unicode 标量的形式来表示字符或字符串,如:
1 |
|
在 Swift 中,每一个 Character 类型实例都代表单个可扩展的字形群集——即由一个或多个 Unicode 标量的序列组成的一个可读字符。
汉字 “听” 拼音为 tīng,以字母 ī 为例,用两种方式表示。第一种,可以直接用单个 Unicode 标量 ī (LATIN SMALL LETTER I WITH MACRON) 来表示,即 U+012B,该字形群集中包含一个 Unicode 标量。第二种,可以采用两个 Unicode 标量来表示,一个拉丁字母 i (LATIN SMALL LETTER I) 加上一个音调符(元音,COMBINING MACRON ACCENT)的标量,即 U+0069 U+0304,这样,当字母 i 被 Unicode 文字渲染系统时就会转换成 ī,该字形群集中包含两个 Unicode 标量。
1 |
|
这两种情况中,字母 ī 即代表了 Swift 中单个 Character 类型实例,也代表了一个可扩展的字形群集。想了解更多关于可扩展的字形群集,可参考此链接。
我们已经简单了解了可扩展的字形群集,现在我们再来看看 Swift 字符串中一些有意思的事。
Swift 中 String 类型,说白了就是 Character 类型实例的集合,在开发过程中,我们一般采用两种方式来求字符串的长度,第一种是转成 Objective-C 中的 NSString 类型,通过 length 方法来获取其长度,第二种是通过字符串属性 characters.count 的方式获得。本小节主要讨论第二种,本文会在结尾针对这两种方式进行比较。
在 Swift 中,细心的同学或许已经发现 tingPD 与 tingPS 字符串的字符数量是一样的:
1 |
|
下面我们来解决此疑惑,笔者已在前文说过,Swift 中 String\Character 都是基于 Unicode 标量建立的,且 String 是 Character 的集合(即包含关系),而 String 属性 characters.count 其实就是计算 Character 的数量,那么 character 是怎么定义的呢,或者说什么才算是一个 character?此时又引出了一个概念——字形群集界限(Grapheme Cluster Boundaries),而”什么才算是一个 character?“这个问题就是字形群集界限给出的答案,想深入了解的同学请看:传送门。从用户感观(user-perceived)角度讲,不管是字符 ī(U+012B) 或者是 i(U+0069) 再加上一个音调符(U+0304),这两种表示最终的结果都是组成一个相同的可读的字符,因此 tingPD 与 tingPS 字符串中的字符数量是一样的。
通过上文的简单解释,可以得出两个结论:
一个字符串拼接一个字符时,不一定会更改字符串的数量,即 characters.count 的值。
在没有获取到字形群集界限的时候,无法计算出该字符串的字符数量,因此必须遍历字符串中全部的 Unicode 标量以获取字形群集界限,进而确定字符串的字符数量。
下面在看一个例子,相信大家都已明白输出结果的原因:
1 |
|
首先 .length 是 Objective-C 中字符串长度计算方法,而 .characters.count 可以说是 Swift 中字符串长度计算方法,由于 Swift 中 String 类型可以转成 Objective-C 中的 NSString 类型,因此在 Swift 开发过程中可能有如下两种写法:
1 |
|
从上述结果可看出,.length 方法得到的字符串长度为5,而 .characters.count 等于4,可能读者会有点懵,同一个字符串怎么计算的长度不一致?其实 .length 与 .characters.count 的计算原理在上文已经做了解释,本小节就简单总结一下:
.length 与 .characters.count 返回值不总是相同的,.length 方法是采用 UTF-16 表示的编码单元为单位进行计算并返回的,即字母 i(U+0069) 、音调符(U+0304)会当做两个字符,因而长度为2。.character.count 的值是通过字形群集界限来确定字符数量的,如还不理解请查看上文。(PS:其实这里也是 Swift 中采用索引的方式访问字符串的原因)
欢迎关注微信公众号
]]>最近在看书的时候突然纠结于Unicode相关字符编码,查了一些资料,并写了这篇文章,顺带做下笔记,希望能帮到一些人。文章如果有写的不妥的或者不正确的地方还请大家纠正。
ASCII(“阿斯柯”) 是国际上普遍采用的一种字符编码系统,由8位二进制进行编码,最高位恒为0,因此可以定义128个字符,其中包括10个十进制数字、52个英文大小写字母(A~Z, a~z)等。
UTF(Unicode Transformation Format, Unicode字符集转换格式),UTF-7、UTF-8、UTF-16、UTF-32、GB18030…只是Unicode的一种实现方式,即怎样将 Unicode 定义的数字转换成程序数据。
UTF-8 编码,以8位无符号整数为单位进行编码,是针对Unicode的可变长字符编码,UTF-8 是 ASCII 编码的父集,也就是说,UTF-8 与 ASCII 编码兼容,如:对于0x000000-0x00007F之间的字符,即前128个字符,UTF-8 编码与 ASCII 编码完全相同。这使得原来处理 ASCII 码字符的软件无须或只须做少部分修改,即可继续使用,UTF-8 编码应用广泛,基本所有互联网协议都支持 UTF-8 编码,是目前编码方式中优先采用的方式之一。
关于Unicode 与 UTF-8 编码之间的转换关系,如下表所示:
在基本多文种平面中约定00D800-00DFFF这范围用于UTF-16扩展标识辅助平面(即低位两个字节),在UTF-16 中会详细介绍。
举个例子,汉字“听”的 Unicode 编码是U+542C,转成UTF-8,步骤如下:
从Unicode 2.0开始,Unicode采用了与ISO 10646-1相同的字库和字码;ISO也承诺,ISO 10646将不会替超出U+10FFFF的UCS-4编码赋值,以使得两者保持一致。2003年11月 UTF-8 被 RFC 3629重新规范,只能使用原来Unicode定义的区域,U+0000到U+10FFFF。如果以上都能理解,那么下表就非常好理解了(摘自Wikipedia):
之前有较多的人在微博上私信我@tingxins,关于这表格,疑惑颇多,因此在此处进行补充并简单解释一下,希望能帮到读者。C0,C1非常好理解,不再详述。我们来看看F5-FF的头字节,为什么是非法的?我们可以以 U+10FFFF 为例,转UTF-8编码后,可以得出头字节二进制流为11110100,即F4,基于 RFC 3629 规范,因此可得出大于F4头字节的可以理解成非法的或者不可能出现的编码,就7或8字节序列的头字节而言,更是违反了早期UTF-8编码不可超过6字节序列的规范。(更新于 2017-2-18)
UTF-8 小结
现在我们已经知道了UTF-8的含义,以及其编码原理,下面我们来探究一下 UTF-16 编码方式。
UTF-16 编码,以16位无符号整数为单位进行编码。上文中所提及到的“基本多文种平面”的编码空间中保留了一块区域(从U+D800到U+DFFF),该区域不映射Unicode字符,UTF-16就是利用保留下来的0xD800-0xDFFF编码空间来对U+10000到U+10FFFF(即辅助平面)进行字符映射的。
在 UTF-16 编码中,从U+0000至U+D7FF以及从U+E000至U+FFFF的编码空间的映射关系同 Unicode,相对应于ISO通用字符集中的USC-2。从U+10000到U+10FFFF的编码空间,UTF-16用一对16比特长的码元(即32bit,4Bytes)进行编码,熟称代理对(Surrogate Pair).
0xD800-0xDFFF编码空间分成两部分(即上述所说的代理对):
UTF-16 辅助平面编码方式比较巧妙,从U+10000到U+10FFFF,共计FFFFF个,即2^(20)个,至少需要20位来表示,我们再来看代理对,先看高半区,从U+D800到U+DBFF,共计3FF个,即2^(10)个,同理低半区也是2^(10)个,正好为2^(20)个代理对,这也是“基本多语言平面”中保留不对应于Unicode字符的2048个码位的原因。下面我们来看一张表:
举个例子,古意大利字母”𐌀”的Unicode编码为U+10300,转成UTF-16,步骤如下:
关于Unicode 与 UTF-16 编码之间的转换关系,如下表所示:
由上表可看出,UTF-16无法兼容ASCII编码。
UTF-16 存储形式
想必读者现在有这样一个疑惑,UTF-16 是以16位无符号整数位单位进行编码,即每个字符占用两个字节,如:在Mac和Window上,对字节顺序的理解是不一样的,这时就出现了一个问题,同一字节流可能会被解释为不同内容,以字符“心“为例,该字符十六进制编码为U+5FC3,按两个字节进行拆分:5F和C3,在Mac上读取时是从低字节开始,那么在Mac OS会认为此U+5FC3编码为U+C35F,显示字符为”썟”,而在Windows上从高字节开始读取,则编码为U+5FC3的字符为“心”。为了解决该问题,字节顺序标记(Byte-Order Mark, BOM)诞生,字符U+FEFF如果出现在字节流的开头,则用来标识该字节流的字节序,是高位在前还是低位在前,反之同理。这两种字节序在计算机我们通常称大端和小端,下面我们来继续探究一下。
大端存储(Big Endian, 简称BE):一个字中的高位字节放在内存中这个字区域的低地址。小端存储(Little Endian, 简称LE):即一个字中的低位字节放在内存中这个字区域的低地址处。
还是以古意大利字母”𐌀”为例,我们刚已计算出其UTF-16编码为U+D800DF00,如果采用大端存储,编码存储的序列为D800 DF00,采用小端存储,则为00D8 00DF。这个两个存储模式的区别在于字中字节的存储顺序不同,而字的存储顺序是相同的。再看几个例子(摘自Wikipedia):
UTF-16 小结
[^1]: UCS即ISO 10646的通用字符集(Universal Character Set, 简称UCS),UCS-2我们可以简单理解为UTF-16,同样使用16位的编码空间。
欢迎关注微信公众号
]]>作为一名技术从事者(iOS 工程师),不断的学习与积累是必不可少的,但学会沉淀、学会输出也是非常重要的。于是,2016 年我又重新开启了 blog 之旅。
二月份
,发布了15年写的一个项目 听心字典,旨在为广大国内外朋友提供一个学习中文的工具(个人项目)。
三月份
,发布了一个智能硬件相关的项目(公司项目)。
四月份
,发布了一个项目 iRepeater,旨在为大学生及英语爱好者提供一个学习英语口语的工具(个人项目)。
五月份
,由于个人职业规划等因素,裸辞,离开 A 公司。离职前当了一次面试官,替公司招一名 iOS 工程师,当时简历简直是投爆我的邮箱(印象深刻,五六百份简历),也开始知道现在行业的状况。同时自己也开始去外边进行多次面试,五月底选择来到现在这个团队。
七月份
,前公司的同事发布了我之前主导的一个项目。当前公司也发布了一个新的项目。
十月份
,开始搭建个人博客,并在月底开源了一个 iOS 跑马灯小类库 TXScrollLabelView。
…
在 A 公司,出过差去合作公司和他家小伙伴进行结对编程;也学会了采用发邮件的形式与人沟通,很多事会在邮件说的非常直(为了项目进度,毕竟对事不对人);也适应了独立开发项目;在公司的KPI月考核制度上也拿过好几次奖金。公司同事都蛮不错的,但公司气氛有点严肃,但由于个人职业规划原因,离开公司。
由于之前属于独立开发,在现在这个团队(一个逗比团队😀),工作气氛活跃,无疑提高了自己的团队协作能力,对自己的行业了解的更深,开始学会与他人分享技术的乐趣,一起做开源项目,技术上以及解决问题的能力都有一定的提升。
主要成果:AppStore 上架5个项目、建立个人博客、发布开源项目。
语言:汇编、Swift、CSS、H5、JS、Objective-C。都懂点皮毛。
书籍:《AVFoudation 开发秘籍》、《iOS Auto Layout 开发秘籍》、《Swift 2.2》、《Effective Objective-C 2.0》、《Rework》、《成功、动机与目标》…
工具:WebStorm、Sublime Text、PostMan、MWeb、Reveal…
在 2017 有待加强!
😀
年轻的时候,需要不断的学习与积累,提高自己解决问题的能力。扩展自己的视野,熟话说,技多不压身,多多涉猎几门技术,但要做一个 T 字人。
在技术的世界里追求一种自由。(PS:文笔较差,有待提升)
欢迎关注微信公众号
]]>前段时间在开发一个广播的功能,网上也自己找了一些库,没有发现非常好用的,于是自己抽时间写了一个,发布一天收获六十多个 star
,这里首先感谢大家在微博上的转发,使得 TXScrollLabelView
被更多需要的人知道,同时非常感谢大家的吐槽及建议,使之诞生 TXScrollLabelView
v1.1.1 版本,目前已支持 CocoaPods
,后续会增加 Carthage
。Github 地址: TXScrollLabelView
1 | pod search TXScrollLabelView |
TXScrollLabelView
是一个能够快速接入自定义标签滚动视图,可以做促销栏、头条栏、广播栏、广告栏等等展示,效果图:
现在 TXScrollLabelView
支持4种滚动类型:
TXScrollLabelViewTypeLeftRight
:从右向左单行滚动
TXScrollLabelViewTypeUpDown
:从下至上多行滚动
TXScrollLabelViewTypeFlipRepeat
:从下至上单行循环滚动
TXScrollLabelViewTypeFlipNoRepeat
:从下至上单行依次滚动
前几天 GitHub
有人提出 scrollVelocity
针对相关类型失效问题,现在已经全部解决,以上四种类型 scrollVelocity
全部支持啦。后期会持续增加更多的功能,满足更多的需求。
目前支持两种方式集成 TXScrollLabelView
:
使用 cocoaPods
platform :ios, '7.0'pod 'TXScrollLabelView'
手动
Clone
或者 DownloadZip
至本地,然后手动拖拽 TXScrollLabelView
文件夹中的文件至项目中,使用的时候 #import "TXScrollLabelView.h“
即可。
1 | //1.获取滚动的内容 |
更多请详见Demo:https://github.com/tingxins/TXScrollLabelView/tree/master/TXScrollLabelViewDemo
欢迎关注微信公众号
]]>本篇文章中我们主要谈谈NSTimer
\CADisplayLink
在使用过程中牵扯到内存泄漏的相关问题及解决思路(文章末尾会附上Demo),有时候我们在不知情的情况容易入坑,最关键你还不知道自己掉坑了,闲话不多说,让我们开始进入正题。
我们先来回顾一下NSRunLoop
对NSTimer
\CADisplayLink
的影响。(为了方便,以下统称定时器)
大家都知道定时器的运行需要结合一个NSRunLoop
(有疑惑的同学可以查看Xcode Document,此处不细说),同时NSRunLoop
对该定时器会有一个强引用,这也是为什么我们不对NSRunLoop
中的定时器进行强引的原因(如:self.timer = timer, 此代码可省略)。
由于NSRunLoop
对定时器有着牵引,那么问题就来了,那么定时器怎样才能被释放掉呢(先不考虑使用removeFromRunLoop:),此时- invalidate
函数的作用就来了,我们来看看官方就此函数的介绍:
Removes the object from all runloop modes (releasing the receiver if it has been implicitly retained) and releases the ‘target’ object.
据官方介绍可知,- invalidate
做了两件事,首先是把本身(定时器)从NSRunLoop
中移除,然后就是释放对‘target
’对象的强引用。从而解决定时器带来的内存泄漏问题。
看到这里我们可能会有点懵逼,先上一个图(为了方便讲解,途中箭头指向谁就代表强引谁):
此处我们必须明确,在开发中,如果创建定时器只是简单的计时,不做其他引用,那么timer对象与myClock对象循环引用的问题就可以避免(即省略self.timer = timer,前文已经提到过,不再阐述),即图中箭头5
可避免。
虽然孤岛问题已经避免了,但还是存在问题,因为myClock对象被UIViewController以及timer引用(timer直接被NSRunLoop强引用着),当UIViewController控制器被UIWindow释放后,myClock不会被销毁,从而导致内存泄漏。
讲到这里,有些人可能会说对timer对象发送一个invalidate
消息,这样NSRunLoop即不会对timer进行强引,同时timer也会释放对myClock对象的强引,这样不就解决了吗?没错,内存泄漏是解决了。
但是,这并不是我们想要的结果,在开发中我们可能会遇到某些需求,只有在myClock对象要被释放时才去释放timer(此处要注意释放的先后顺序及释放条件),如果提前向timer发送了invalidate
消息,那么myClock对象可能会因为timer被提前释放而导致数据错了,就像闹钟失去了秒针
一样,就无法正常工作了。所以我们要做的是在向myClock对象发送dealloc
消息前在给timer发送invalidate
消息,从而避免本末倒置的问题。这种情况就像一个死循环(因为如果不给timer发送invalidate
消息,myClock对象根本不会被销毁,dealloc方法根本不会执行),那么该怎么做呢?
现在我们已经知道内存泄漏在哪了,也知道原因是什么,那么如何解决,或者说怎样优雅的解决这问题呢?方式有很多.
a.NSTimer Target
为了解决timer与myClock之间类似死锁的问题,我们会将定时器中的‘target
’对象替换成定时器自己,采用分类实现。
#import "NSTimer+TXTimerTarget.h"@implementation NSTimer (TXTimerTarget)+ (NSTimer *)tx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeat:(BOOL)yesOrNo block:(void (^)(NSTimer *))block{ return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(startTimer:) userInfo:[block copy] repeats:yesOrNo];}+ (void)startTimer:(NSTimer *)timer { void (^block)(NSTimer *timer) = timer.userInfo; if (block) { block(timer); }}@end
b.NSTimer Proxy
这种方式就是创建一个NSProxy
子类TXTimerProxy
(不太清楚NSProxy的同学可以去查一下相关资料哈),TXTimerProxy
的作用是什么呢?就是什么也不做,可以说只会重载消息转发机制,如果创建一个TXTimerProxy
对象将其作为timer的‘target
’,专门用于转发timer消息至myClock对象,那么问题是不是就解决了呢?答案:是的。
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.25 target:[TXTimerProxy timerProxyWithTarget:self] selector:@selector(startTimer) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];self.timer = timer;
实现详情文章末尾会附上Demo,感兴趣的同学可以去看看哈,有什么问题可以直接问,互相交流。
c.NSTimer Block
还有一种方式就是采用Block,iOS 10增加的API。
+ scheduledTimerWithTimeInterval:repeats:block:
The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
有点类似a方式,此处不再详述。
//NSTimer Block(解决self内存泄漏) 模拟器会崩溃//API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.25 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"TXNSTimerBlockController timer start");}];[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];self.timer = timer;
此处以NSTimer举例,CADisplayLink不再详述(方式都是一样)。
以上纯属个人看法和观点,如有不妥或不对之处,请指出,互相交流,欢迎一起讨论,谢谢^_^。
TXTimerLeaksDemo链接:https://github.com/tingxins/TXTimerLeaksDemo
欢迎关注微信公众号
]]>首先,过度的创建NSDateFormatter
用于NSDate
与NSString
之间转换,会导致App卡顿,打开Profile工具查一下性能,你会发现这种操作占CPU比例是非常高的。据官方说法,创建NSDateFormatter
代价是比较高的,如果你使用的非常频繁,那么建议你缓存起来,缓存NSDateFormatter
一定能提高效率。
Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, it is typically more efficient to cache a single instance than to create and dispose of multiple instances. One approach is to use a static variable
即只有在UI
需要使用转换结果时在进行转换。
根据NSDateFormatter
线程安全性,不同的iOS系统版本内存缓存如下:
如果直接采用静态变量进行存储,那么可能就会存在线程安全问题,在iOS 7之前,NSDateFormatter
是非线程安全的,因此可能就会有两条或以上的线程同时访问同一个日期格式化对象,从而导致App崩溃。
1 | + (NSDateFormatter *)cachedDateFormatter { |
在iOS 7、macOS 10.9及以上系统版本,NSDateFormatter
都是线程安全
的,因此我们无需担心日期格式化对象在使用过程中被另外一条线程给修改,为了提高性能,我们还可以在上述代码块中进行简化(除去冗余部分)。
1 | static NSDateFormatter *cachedDateFormatter = nil; |
如果缓存了日期格式化或者是其他依赖于current locale
的对象,那么我们应该监听NSCurrentLocaleDidChangeNotification
通知,当current locale
变化时及时更新被缓存的日期格式化对象。
In theory you could use an auto-updating locale (autoupdatingCurrentLocale) to create a locale that automatically accounts for changes in the user’s locale settings. In practice this currently does not work with date formatters.
Apple Threading Programming Guide
如果时间日期格式是固定的,我们可以采用C语言中的strptime函数,这样更加简单高效。
1 | - (NSDate *) easyDateFormatter{ |
欢迎关注微信公众号
]]>