Swift进阶黄金之路(二)

image-20200511230812677

Swift进阶黄金之路(一)

上期遗留一个问题:为什么 rethrows 一般用在参数中含有可以 throws 的方法的高阶函数中。

我们可以结合Swift的官方文档对rethrows再做一遍回顾:

A function or method can be declared with the rethrows keyword to indicate that it throws an error only if one of its function parameters throws an error. These functions and methods are known as rethrowing functions and rethrowing methods. Rethrowing functions and methods must have at least one throwing function parameter.

返回rethrows的函数要求至少有一个可抛出异常的函数式参数,而有以函数作为参数的函数就叫做高阶函数。

这期分两方面介绍Swift:特性修饰词和一些重要的Swift概念。

特性修饰词

在Swift语法中有很多@符号,这些@符号在Swift4之前的版本大多是兼容OC的特性,Swift4及之后则出现越来越多搭配@符号的新特性。以@开头的修饰词,在官网中叫Attributes,在SwiftGG的翻译中叫特性,我并没有找到这一类被@修饰的符号的统称,就暂且叫他们特性修饰词吧,如果有清楚的小伙伴可以告知我。

从Swift5的发布来看(@dynamicCallable,@State),之后将会有更多的特性修饰词出现,在他们出来之前,我们有必要先了解下现有的一些特性修饰词以及它们的作用。

参考:Swift Attributes

@available

@available: 可用来标识计算属性、函数、类、协议、结构体、枚举等类型的生命周期。(依赖于特定的平台版本 或 Swift 版本)。它的后面一般跟至少两个参数,参数之间以逗号隔开。其中第一个参数是固定的,代表着平台和语言,可选值有以下这几个:

  • iOS
  • iOSApplicationExtension
  • macOS
  • macOSApplicationExtension
  • watchOS
  • watchOSApplicationExtension
  • tvOS
  • tvOSApplicationExtension
  • swift

可以使用*指代支持所有这些平台。

有一个我们常用的例子,当需要关闭ScrollView的自动调整inset功能时:

1
2
3
4
5
6
// 指定该方法仅在iOS11及以上的系统设置
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
} else {
automaticallyAdjustsScrollViewInsets = false
}

还有一种用法是放在函数、结构体、枚举、类或者协议的前面,表示当前类型仅适用于某一平台:

1
2
3
4
5
6
7
8
9
10
11
12
@available(iOS 12.0, *)
func adjustDarkMode() {
/* code */
}
@available(iOS 12.0, *)
struct DarkModeConfig {
/* code */
}
@available(iOS 12.0, *)
protocol DarkModeTheme {
/* code */
}

版本和平台的限定可以写多个:

1
2
@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *)
public func applying(_ difference: CollectionDifference<Element>) -> ArraySlice<Element>?

注意:作为条件语句的available前面是#,作为标记位时是@

刚才说了,available后面参数至少要有两个,后面的可选参数这些:

  • deprecated:从指定平台标记为过期,可以指定版本号

  • obsoleted=版本号:从指定平台某个版本开始废弃(注意弃用的区别,deprecated是还可以继续使用,只不过是不推荐了,obsoleted是调用就会编译错误)该声明

  • message=信息内容:给出一些附加信息

  • unavailable:指定平台上是无效的

  • renamed=新名字:重命名声明

我们看几个例子,这个是Array里flatMap的函数说明:

1
2
@available(swift, deprecated: 4.1, renamed: "compactMap(_:)", message: "Please use compactMap(_:) for the case where closure returns an optional value")
public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]

它的含义是针对swift语言,该方式在swift4.1版本之后标记为过期,对应该函数的新名字为compactMap(_:),如果我们在4.1之上的版本使用该函数会收到编译器的警告,即⚠️Please use compactMap(_:) for the case where closure returns an optional value

在Realm库里,有一个销毁NotificationToken的方法,被标记为unavailable

1
2
3
4
extension RLMNotificationToken {
@available(*, unavailable, renamed: "invalidate()")
@nonobjc public func stop() { fatalError() }
}

标记为unavailable就不会被编译器联想到。这个主要是为升级用户的迁移做准备,从可用stop()的版本升上了,会红色报错,提示该方法不可用。因为有renamed,编译器会推荐你用invalidate(),点击fix就直接切换了。所以这两个标记参数常一起出现。

@discardableResult

带返回的函数如果没有处理返回值会被编译器警告⚠️。但有时我们就是不需要返回值的,这个时候我们可以让编译器忽略警告,就是在方法名前用@discardableResult声明一下。可以参考Alamofire中request的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@discardableResult
public func request(
_ url: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil)
-> DataRequest
{
return SessionManager.default.request(
url,
method: method,
parameters: parameters,
encoding: encoding,
headers: headers
)
}

@inlinable

这个关键词是可内联的声明,它来源于C语言中的inline。C中一般用于函数前,做内联函数,它的目的是防止当某一函数多次调用造成函数栈溢出的情况。因为声明为内联函数,会在编译时将该段函数调用用具体实现代替,这么做可以省去函数调用的时间。

内联函数常出现在系统库中,OC中的runtim中就有大量的inline使用:

1
2
3
4
5
6
7
8
static inline id autorelease(id obj)
{
ASSERT(obj);
ASSERT(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}

Swift中的@inlinable和C中的inline基本相同,它在标准库的定义中也广泛出现,可用于方法,计算属性,下标,便利构造方法或者deinit方法中。

例如Swift对Arraymap函数的定义:

1
@inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]

其实Array中声明的大部分函数前面都加了@inlinable,当应用某一处调用该方法时,编译器会将调用处用具体实现代码替换。

需要注意内联声明不能用于标记为private或者fileprivate的地方。

这很好理解,对私有方法的内联是没有意义的。内联的好处是运行时更快,因为它省略了从标准库调用map实现的步骤。但这个快也是有代价的,因为是编译时做替换,这增加了编译的开销,会相应的延长编译时间。

内联更多的是用于系统库的特性,目前我了解的Swift三方库中仅有CocoaLumberjack使用了@inlinable这个特性。

@warn_unqualified_access

通过命名我们可以推断出其大概含义:对“不合规”的访问进行警告。这是为了解决对于相同名称的函数,不同访问对象可能产生歧义的问题。

比如说,Swift 标准库中ArraySequence均实现了min()方法,而系统库中也定义了min(::),对于可能存在的二义性问题,我们可以借助于@warn_unqualified_access

1
2
3
4
5
6
7
8
extension Array where Self.Element : Comparable {
@warn_unqualified_access
@inlinable public func min() -> Element?
}
extension Sequence where Self.Element : Comparable {
@warn_unqualified_access
@inlinable public func min() -> Self.Element?
}

这个特性声明会由编译器在可能存在二义性的场景中对我们发出警告。这里有一个场景可以便于理解它的含义,我们自定义一个求Array中最小值的函数:

1
2
3
4
5
extension Array where Element: Comparable {
func minValue() -> Element? {
return min()
}
}

我们会收到编译器的警告:Use of 'min' treated as a reference to instance method in protocol 'Sequence', Use 'self.' to silence this warning。它告诉我们编译器推断我们当前使用的是Sequence中的min(),这与我们的想法是违背的。因为有这个@warn_unqualified_access限定,我们能及时的发现问题,并解决问题:self.min()

@objc

把这个特性用到任何可以在 Objective-C 中表示的声明上——例如,非内嵌类,协议,非泛型枚举(原始值类型只能是整数),类和协议的属性、方法(包括 setter 和 getter ),初始化器,反初始化器,下标。 objc 特性告诉编译器,这个声明在 Objective-C 代码中是可用的。

用 objc 特性标记的类必须继承自一个 Objective-C 中定义的类。如果你把 objc 用到类或协议中,它会隐式地应用于该类或协议中 Objective-C 兼容的成员上。如果一个类继承自另一个带 objc 特性标记或 Objective-C 中定义的类,编译器也会隐式地给这个类添加 objc 特性。标记为 objc 特性的协议不能继承自非 objc 特性的协议。

@objc还有一个用处是当你想在OC的代码中暴露一个不同的名字时,可以用这个特性,它可以用于类,函数,枚举,枚举成员,协议,getter,setter等。

1
2
3
4
5
6
7
8
// 当在OC代码中访问enabled的getter方法时,是通过isEnabled
class ExampleClass: NSObject {
@objc var enabled: Bool {
@objc(isEnabled) get {
// Return the appropriate value
}
}
}

这一特性还可以用于解决潜在的命名冲突问题,因为Swift有命名空间,常常不带前缀声明,而OC没有命名空间是需要带的,当在OC代码中引用Swift库,为了防止潜在的命名冲突,可以选择一个带前缀的名字供OC代码使用。

Charts作为一个在OC和Swift中都很常用的图标库,是需要较好的同时兼容两种语言的使用的,所以也可以看到里面有大量通过@objc标记对OC调用时的重命名代码:

1
2
3
4
5
@objc(ChartAnimator)
open class Animator: NSObject { }

@objc(ChartComponentBase)
open class ComponentBase: NSObject { }

@objcMembers

因为Swift中定义的方法默认是不能被OC调用的,除非我们手动添加@objc标识。但如果一个类的方法属性较多,这样会很麻烦,于是有了这样一个标识符@objcMembers,它可以让整个类的属性方法都隐式添加@objc,不光如此对于类的子类、扩展、子类的扩展都也隐式的添加@objc,当然对于OC不支持的类型,仍然无法被OC调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@objcMembers
class MyClass : NSObject {
func foo() { } // implicitly @objc

func bar() -> (Int, Int) // not @objc, because tuple returns
// aren't representable in Objective-C
}

extension MyClass {
func baz() { } // implicitly @objc
}

class MySubClass : MyClass {
func wibble() { } // implicitly @objc
}

extension MySubClass {
func wobble() { } // implicitly @objc
}

参考:Swift3、4中的@objc、@objcMembers和dynamic

@testable

@testable是用于测试模块访问主target的一个关键词。

因为测试模块和主工程是两个不同的target,在swift中,每个target代表着不同的module,不同module之间访问代码需要public和open级别的关键词支撑。但是主工程并不是对外模块,为了测试修改访问权限是不应该的,所以有了@testable关键词。使用如下:

1
2
3
4
5
6
import XCTest
@testable import Project

class ProjectTests: XCTestCase {
/* code */
}

这时测试模块就可以访问那些标记为internal或者public级别的类和成员了。

@frozen 和@unknown default

frozen意为冻结,是为Swift5的ABI稳定准备的一个字段,意味向编译器保证之后不会做出改变。为什么需要这么做以及这么做有什么好处,他们和ABI稳定是息息相关的,内容有点多就不放这里了,之后会单独出一篇文章介绍,这里只介绍这两个字段的含义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@frozen public enum ComparisonResult : Int {
case orderedAscending = -1
case orderedSame = 0
case orderedDescending = 1
}

@frozen public struct String {}

extension AVPlayerItem {
public enum Status : Int {
case unknown = 0
case readyToPlay = 1
case failed = 2
}
}

ComparisonResult这个枚举值被标记为@frozen即使保证之后该枚举值不会再变。注意到String作为结构体也被标记为@frozen,意为String结构体的属性及属性顺序将不再变化。其实我们常用的类型像IntFloatArrayDictionarySet等都已被“冻结”。需要说明的是冻结仅针对structenum这种值类型,因为他们在编译器就确定好了内存布局。对于class类型,不存在是否冻结的概念,可以想下为什么。

对于没有标记为frozen的枚举AVPlayerItem.Status,则认为该枚举值在之后的系统版本中可能变化。

对于可能变化的枚举,我们在列出所有case的时候还需要加上对@unknown default的判断,这一步会有编译器检查:

1
2
3
4
5
6
7
8
9
10
switch currentItem.status {
case .readyToPlay:
/* code */
case .failed:
/* code */
case .unknown:
/* code */
@unknown default:
fatalError("not supported")
}

@State、@Binding、@ObservedObject、@EnvironmentObject

这几个是SwiftUI中出现的特性修饰词,因为我对SwiftUI的了解不多,这里就不做解释了。附一篇文章供大家了解。

[译]理解 SwiftUI 里的属性装饰器@State, @Binding, @ObservedObject, @EnvironmentObject

几个重要关键词

lazy

lazy是懒加载的关键词,当我们仅需要在使用时进行初始化操作就可以选用该关键词。举个例子:

1
2
3
4
5
6
7
8
class Avatar {
lazy var smallImage: UIImage = self.largeImage.resizedTo(Avatar.defaultSmallSize)
var largeImage: UIImage

init(largeImage: UIImage) {
self.largeImage = largeImage
}
}

对于smallImage,我们声明了lazy,如果我们不去调用它是不会走后面的图片缩放计算的。但是如果没有lazy,因为是初始化方法,它会直接计算出smallImage的值。所以lazy很好的避免的不必要的计算。

另一个常用lazy的地方是对于UI属性的定义:

1
2
3
4
5
lazy var dayLabel: UILabel = {
let label = UILabel()
label.text = self.todayText()
return label
}()

这里使用的是一个闭包,当调用该属性时,执行闭包里面的内容,返回具体的label,完成初始化。

使用lazy你可能会发现它只能通过var初始而不能通过let,这是由 lazy 的具体实现细节决定的:它在没有值的情况下以某种方式被初始化,然后在被访问时改变自己的值,这就要求该属性是可变的。

另外我们可以在Sequences中使用lazy,在讲解它之前我们先看一个例子:

1
2
3
4
5
6
7
8
9
func increment(x: Int) -> Int {
print("Computing next value of \(x)")
return x+1
}

let array = Array(0..<1000)
let incArray = array.map(increment)
print("Result:")
print(incArray[0], incArray[4])

在执行print("Result:")之前,Computing next value of ...会被执行1000次,但实际上我们只需要0和4这两个index对应的值。

上面说了序列也可以使用lazy,使用的方式是:

1
2
3
4
5
6
7
let array = Array(0..<1000)
let incArray = array.lazy.map(increment)
print("Result:")
print(incArray[0], incArray[4])

// Result:
// 1 5

在执行print("Result:")之前,并不会打印任何东西,只打印了我们用到的1和5。就是说这里的lazy可以延迟到我们取值时才去计算map里的结果。

我们看下这个lazy的定义:

1
@inlinable public var lazy: LazySequence<Array<Element>> { get }

它返回一个LazySequence的结构体,这个结构体里面包含了Array,而map的计算在LazySequence里又重新定义了一下:

1
2
3
4
/// Returns a `LazyMapSequence` over this `Sequence`.  The elements of
/// the result are computed lazily, each time they are read, by
/// calling `transform` function on a base element.
@inlinable public func map<U>(_ transform: @escaping (Base.Element) -> U) -> LazyMapSequence<Base, U>

这里完成了lazy序列的实现。LazySequence类型的lazy只能被用于map、flatMap、compactMap这样的高阶函数中。

参考“懒”点儿好

纠错:参考文章中说:”这些类型(LazySequence)只能被用在 mapflatMapfilter这样的高阶函数中” 其实是没有filter的,因为filter是过滤函数,它需要完整遍历一遍序列才能完成过滤操作,是无法懒加载的,而且我查了LazySequence的定义,确实是没有filter函数的。

unowned weak

Swift开发过程中我们会经常跟闭包打交道,而用到闭包就不可避免的遇到循环引用问题。在Swift处理循环引用可以使用unownedweak这两个关键词。看下面两个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Dog {
var name: String
init (name: String ) {
self.name = name
}
deinit {
print("\(name) is deinitialized")
}
}

class Bone {
// weak 修饰词
weak var owner: Dog?
init(owner: Dog?) {
self.owner = owner
}
deinit {
print("bone is deinitialized" )
}
}

var lucky: Dog? = Dog(name: "Lucky")
var bone: Bone? = Bone(owner: lucky!)
lucky = nil
// Lucky is deinitialized

这里Dog和Bone是相互引用的关系,如果没有weak var owner: Dog?这里的weak声明,将不会打印Lucky is deinitialized。还有一种解决循环应用的方式是把weak替换为unowned关键词。

  • weak相当于oc里面的weak,弱引用,不会增加循环计数。主体对象释放时被weak修饰的属性也会被释放,所以weak修饰对象就是optional。
  • unowned相当于oc里面的unsafe_unretained,它不会增加引用计数,即使它的引用对象释放了,它仍然会保持对被已经释放了的对象的一个 “无效的” 引用,它不能是 Optional 值,也不会被指向 nil。如果此时为无效引用,再去尝试访问它就会crash。

这两者还有一个更常用的地方是在闭包里面:

1
2
3
4
5
lazy var someClosure: () -> Void = { [weak self] in
// 被weak修饰后self为optional,这里是判断self非空的操作
guard let self = self else { retrun }
self.doSomethings()
}

这里如果是unowned修饰self的话,就不需要用guard做解包操作了。但是我们不能为了省略解包的操作就用unowned,也不能为了安全起见全部weak,弄清楚两者的适用场景非常重要。

根据苹果的建议:

Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.

当闭包和它捕获的实例总是相互引用,并且总是同时释放时,即相同的生命周期,我们应该用unowned,除此之外的场景就用weak。

imgimg

参考:内存管理,WEAK 和 UNOWNED

Unowned 还是 Weak?生命周期和性能对比

KeyPath

KeyPath是键值路径,最开始是用于处理KVC和KVO问题,后来又做了更广泛的扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// KVC问题,支持struct、class
struct User {
let name: String
var age: Int
}

var user1 = User()
user1.name = "ferry"
user1.age = 18

//使用KVC取值
let path: KeyPath = \User.name
user1[keyPath: path] = "zhang"
let name = user1[keyPath: path]
print(name) //zhang

// KVO的实现还是仅限于继承自NSObject的类型
// playItem为AVPlayerItem对象
playItem.observe(\.status, changeHandler: { (_, change) in
/* code */
})

这个KeyPath的定义是这样的:

1
2
3
4
5
6
7
8
public class AnyKeyPath : Hashable, _AppendKeyPath {}

/// A partially type-erased key path, from a concrete root type to any
/// resulting value type.
public class PartialKeyPath<Root> : AnyKeyPath {}

/// A key path from a specific root type to a specific resulting value type.
public class KeyPath<Root, Value> : PartialKeyPath<Root> {}

定义一个KeyPath需要指定两个类型,根类型和对应的结果类型。对应上面示例中的path:

1
let path: KeyPath<User, String> = \User.name

根类型就是User,结果类型就是String。也可以不指定,因为编译器可以从\User.name推断出来。那为什么叫根类型的?可以注意到KeyPath遵循一个协议_AppendKeyPath,它里面定义了很多append的方法,KeyPath是多层可以追加的,就是如果属性是自定义的Address类型,形如:

1
2
3
4
struct Address {
var country: String = ""
}
let path: KeyPath<User, String> = \User.address.country

这里根类型为User,次级类型是Address,结果类型是String。所以path的类型依然是KeyPath

明白了这些我们可以用KeyPath做一些扩展:

1
2
3
4
5
6
7
8
9
extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return sorted { a, b in
return a[keyPath: keyPath] < b[keyPath: keyPath]
}
}
}
// users is Array<User>
let newUsers = users.sorted(by: \.age)

这个自定义sorted函数实现了通过传入keyPath进行升序排列功能。

参考:The power of key paths in Swift

some

some是Swift5.1新增的特性。它的用法就是修饰在一个 protocol 前面,默认场景下 protocol 是没有具体类型信息的,但是用 some 修饰后,编译器会让 protocol 的实例类型对外透明。

可以通过一个例子理解这段话的含义,当我们尝试定义一个遵循Equatable协议的value时:

1
2
3
4
5
6
7
8
// Protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
var value: Equatable {
return 1
}

var value: Int {
return 1
}

编译器提示我们Equatable只能被用来做泛型的约束,它不是一个具体的类型,这里我们需要使用一个遵循Equatable的具体类型(Int)进行定义。但有时我们并不想指定具体的类型,这时就可以在协议名前加上some,让编译器自己去推断value的类型:

1
2
3
var value: some Equatable {
return 1
}

在SwiftUI里some随处可见:

1
2
3
4
5
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}

这里使用some就是因为View是一个协议,而不是具体类型。

当我们尝试欺骗编译器,每次随机返回不同的Equatable类型:

1
2
3
4
5
6
7
var value: some Equatable {
if Bool.random() {
return 1
} else {
return "1"
}
}

聪明的编译器是会发现的,并警告我们Function declares an opaque return type, but the return statements in its body do not have matching underlying types

参考:SwiftUI 的一些初步探索 (一)

IMG_2555

如何通过静态分析提高iOS代码质量


随着项目的扩大,依靠人工codereview来保证项目的质量,越来越不现实,这时就有必要借助于一种自动化的代码审查工具:程序静态分析

程序静态分析(Program Static Analysis)是指在不运行代码的方式下,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否满足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术。(来自百度百科)

词法分析,语法分析等工作是由编译器进行的,所以对iOS项目为了完成静态分析,我们需要借助于编译器。对于OC语言的静态分析可以完全通过Clang,对于Swift的静态分析除了Clange还需要借助于SourceKit

Swift语言对应的静态分析工具是SwiftLint,OC语言对应的静态分析工具有Infer和OCLitn。以下会是对各个静态分析工具的安装和使用做一个介绍。

SwiftLint


对于Swift项目的静态分析可以使用SwiftLint。SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具。它的实现是 Hook 了 Clang 和 SourceKit 从而能够使用 AST 来表示源代码文件的更多精确结果。Clange我们了解了,那SourceKit是干什么用的?

SourceKit包含在Swift项目的主仓库,它是一套工具集,支持Swift的大多数源代码操作特性:源代码解析、语法突出显示、排版、自动完成、跨语言头生成等工作。

安装

安装有两种方式,任选其一:

方式一:通过Homebrew

1
$ brew install swiftlint

这种是全局安装,各个应用都可以使用。

方式二:通过CocoaPods

1
pod 'SwiftLint', :configurations => ['Debug']

这种方式相当于把SwiftLint作为一个三方库集成进了项目,因为它只是调试工具,所以我们应该将其指定为仅Debug环境下生效。

集成进Xcode

我们需要在项目中的Build Phases,添加一个Run Script Phase。如果是通过homebrew安装的,你的脚本应该是这样的。

1
2
3
4
5
if which swiftlint >/dev/null; then
swiftlint
else
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi

如果是通过cocoapods安装的,你得脚本应该是这样的:

1
"${PODS_ROOT}/SwiftLint/swiftlint"

运行SwiftLint

键入CMD + B编译项目,在编译完后会运行我们刚才加入的脚本,之后我们就能看到项目中大片的警告信息。有时候build信息并不能填入项目代码中,我们可以在编译的log日志里查看。

定制

SwiftLint规则太多了,如果我们不想执行某一规则,或者想要滤掉对Pods库的分析,我们可以对SwfitLint进行配置。

在项目根目录新建一个.swiftlint.yml文件,然后填入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
disabled_rules: # rule identifiers to exclude from running
- colon
- trailing_whitespace
- vertical_whitespace
- function_body_length
opt_in_rules: # some rules are only opt-in
- empty_count
# Find all the available rules by running:
# swiftlint rules
included: # paths to include during linting. `--path` is ignored if present.
- Source
excluded: # paths to ignore during linting. Takes precedence over `included`.
- Carthage
- Pods
- Source/ExcludedFolder
- Source/ExcludedFile.swift
- Source/*/ExcludedFile.swift # Exclude files with a wildcard
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
- explicit_self

# configurable rules can be customized from this configuration file
# binary rules can set their severity level
force_cast: warning # implicitly
force_try:
severity: warning # explicitly
# rules that have both warning and error levels, can set just the warning level
# implicitly
line_length: 110
# they can set both implicitly with an array
type_body_length:
- 300 # warning
- 400 # error
# or they can set both explicitly
file_length:
warning: 500
error: 1200
# naming rules can set warnings/errors for min_length and max_length
# additionally they can set excluded names
type_name:
min_length: 4 # only warning
max_length: # warning and error
warning: 40
error: 50
excluded: iPhone # excluded via string
allowed_symbols: ["_"] # these are allowed in type names
identifier_name:
min_length: # only min_length
error: 4 # only error
excluded: # excluded via string array
- id
- URL
- GlobalAPIKey
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)

一条rules提示如下,其对应的rules名就是function_body_length

1
! Function Body Length Violation: Function body should span 40 lines or less excluding comments and whitespace: currently spans 43 lines (function_body_length)

disabled_rules下填入我们不想遵循的规则。

excluded设置我们想跳过检查的目录,Carthage、Pod、SubModule这些一般可以过滤掉。

其他的一些像是文件长度(file_length),类型名长度(type_name),我们可以通过设置具体的数值来调节。

另外SwiftLint也支持自定义规则,我们可以根据自己的需求,定义自己的rule

生成报告

如果我们想将此次分析生成一份报告,也是可以的(该命令是通过homebrew安装的swiftlint):

1
2
# reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)
$ swiftlint lint --reporter html > swiftlint.html

xcodebuild

xcodebuild是xcode内置的编译命令,我们可以用它来编译打包我们的iOS项目,接下来介绍的Infer和OCLint都是基于xcodebuild的编译产物进行分析的,所以有必要简单介绍一下它。

一般编译一个项目,我们需要指定项目名,configuration,scheme,sdk等信息以下是几个简单的命令及说明。

1
2
3
4
5
6
# 不带pod的项目,target名为TargetName,在Debug下,指定模拟器sdk环境进行编译
xcodebuild -target TargetName -configuration Debug -sdk iphonesimulator
# 带pod的项目,workspace名为TargetName.xcworkspace,在Release下,scheme为TargetName,指定真机环境进行编译。不指定模拟器环境会验证证书
xcodebuild -workspace WorkspaceName.xcworkspace -scheme SchemeName Release
# 清楚项目的编译产物
xcodebuild -workspace WorkspaceName.xcworkspace -scheme SchemeName Release clean

之后对xcodebuild命令的使用都需要将这些参数替换为自己项目的参数。

Infer


Infer是Facebook开发的针对C、OC、Java语言的静态分析工具,它同时支持对iOS和Android应用的分析。对于Facebook内部的应用像是 Messenger、Instagram 和其他一些应用均是有它进行静态分析的。它主要检测隐含的问题,主要包括以下几条:

  • 资源泄露,内存泄露
  • 变量和参数的非空检测
  • 循环引用
  • 过早的nil操作

暂不支持自定义规则。

安装及使用

1
$ brew install infer

运行infer

1
2
3
$ cd projectDir
# 跳过对Pods的分析
$ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator

我们会得到一个infer-out的文件夹,里面是各种代码分析的文件,有txt,json等文件格式,当这样不方便查看,我们可以将其转成html格式:

1
$ infer explore --html

点击trace,我们会看到该问题代码的上下文。

因为Infer默认是增量编译,只会分析变动的代码,如果我们想整体编译的话,需要clean一下项目:

1
$ xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator clean

再次运行Infer去编译。

1
$ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator

Infer的大致原理

Infer的静态分析主要分两个阶段:

1、捕获阶段

Infer 捕获编译命令,将文件翻译成 Infer 内部的中间语言。

这种翻译和编译类似,Infer 从编译过程获取信息,并进行翻译。这就是我们调用 Infer 时带上一个编译命令的原因了,比如: infer -- clang -c file.c, infer -- javac File.java。结果就是文件照常编译,同时被 Infer 翻译成中间语言,留作第二阶段处理。特别注意的就是,如果没有文件被编译,那么也没有任何文件会被分析。

Infer 把中间文件存储在结果文件夹中,一般来说,这个文件夹会在运行 infer 的目录下创建,命名是 infer-out/

2、分析阶段

在分析阶段,Infer 分析 infer-out/ 下的所有文件。分析时,会单独分析每个方法和函数。

在分析一个函数的时候,如果发现错误,将会停止分析,但这不影响其他函数的继续分析。

所以你在检查问题的时候,修复输出的错误之后,需要继续运行 Infer 进行检查,知道确认所有问题都已经修复。

错误除了会显示在标准输出之外,还会输出到文件 infer-out/bug.txt 中,我们过滤这些问题,仅显示最有可能存在的。

在结果文件夹中(infer-out),同时还有一个 csv 文件 report.csv,这里包含了所有 Infer 产生的信息,包括:错误,警告和信息。

OCLint

OCLint是基于Clange Tooling编写的库,它支持扩展,检测的范围比Infer要大。不光是隐藏bug,一些代码规范性的问题,例如命名和函数复杂度也均在检测范围之内。

安装OCLint

OCLint一般通过Homebrew安装

1
2
$ brew tap oclint/formulae   
$ brew install oclint

通过Hombrew安装的版本为0.13。

1
2
3
4
5
6
7
8
9
10
$ oclint --version
LLVM (http://llvm.org/):
LLVM version 5.0.0svn-r313528
Optimized build.
Default target: x86_64-apple-darwin19.0.0
Host CPU: skylake

OCLint (http://oclint.org/):
OCLint version 0.13.
Built Sep 18 2017 (08:58:40).

我分别用Xcode11在两个项目上运行过OCLint,一个实例项目可以正常运行,另一个复杂的项目却运行失败,报如下错误:

1
2
3
4
1 error generated
1 error generated
...
oclint: error: cannot open report output file ..../onlintReport.html

我并不清楚原因,如果你想试试0.13能否使用的话,直接跳到安装xcpretty。如果你也遇到了这个问题,可以回来安装oclint0.15版本。

OCLint0.15

我在oclint issuse #547这里找到了这个问题和对应的解决方案。

我们需要更新oclint至0.15版本。brew上的最新版本是0.13,github上的最新版本是0.15。我下载github上的release0.15版本,但是这个包并不是编译过的,不清楚是不是官方自己搞错了,只能手动编译了。因为编译要下载llvm和clange,这两个包较大,所以我将编译过后的包直接传到了这里CodeChecker

如果不关心编译过程,可以下载编译好的包,跳到设置环境变量那一步。

编译OCLint

1、安装CMakeNinja这两个编译工具

1
$ brew install cmake ninja

2、clone OCLint项目

1
$ git clone https://github.com/oclint/oclint

3、进入oclint-scripts目录,执行make命令

1
$ ./make

成功之后会出现build文件夹,里面有个oclint-release就是编译成功的oclint工具。

设置oclint工具的环境变量

设置环境变量的目的是为了我们能够快捷访问。然后我们需要配置PATH环境变量,注意OCLint_PATH的路径为你存放oclint-release的路径。将其添加到.zshrc,或者.bash_profile文件末尾:

1
2
OCLint_PATH=/Users/zhangferry/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH

执行source .zshrc,刷新环境变量,然后验证oclint是否安装成功:

1
2
3
4
$ oclint --version
OCLint (http://oclint.org/):
OCLint version 0.15.
Built May 19 2020 (11:48:49).

出现这个介绍就说明我们已经完成了安装。

安装xcpretty

xcpretty是一个格式化xcodebuild输出内容的脚本工具,oclint的解析依赖于它的输出。它的安装方式为:

1
$ gem install xcpretty

OCLint的使用

在使用OCLint之前还需要一些准备工作,需要将编译项COMPILER_INDEX_STORE_ENABLE设置为NO。

  • 将 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 设置为 NO
  • 在 podfile 中 target ‘target’ do 前面添加下面的脚本,将各个pod的编译配置也改为此选项
1
2
3
4
5
6
7
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO"
end
end
end

使用方式

1、进入项目根目录,运行如下脚本:

1
$ xcodebuild -workspace ProjectName.xcworkspace -scheme ProjectScheme -configuration Debug -sdk iphonesimulator | xcpretty -r json-compilation-database -o compile_commands.json

会将xcodebuild编译过程中的一些信息记录成一个文件compile_commands.json,如果我们在项目根目录看到了该文件,且里面是有内容的,证明我们完成了第一步。

2、我们将这个json文件转成方便查看的html,过滤掉对Pods文件的分析,为了防止行数上限,我们加上行数的限制:

1
$ oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html -rc LONG_LINE=9999 -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999

最终会产生一个oclintReport.html文件。

OCLint支持自定义规则,因为其本身规则已经很丰富了,自定义规则的需求应该很小,也就没有尝试。

封装脚本

OCLint跟Infer一样都是通过运行几个脚本语言进行执行的,我们可以将这几个命令封装成一个脚本文件,以OCLint为例,Infer也类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash
# mark sure you had install the oclint and xcpretty

# You need to replace these values with your own project configuration
workspace_name="WorkSpaceName.xcworkspace"
scheme_name="SchemeName"

# remove history
rm compile_commands.json
rm oclint_result.xml
# clean project
# -sdk iphonesimulator means run simulator
xcodebuild -workspace $workspace_name -scheme $scheme_name -configuration Debug -sdk iphonesimulator clean || (echo "command failed"; exit 1);

# export compile_commands.json
xcodebuild -workspace $workspace_name -scheme $scheme_name -configuration Debug -sdk iphonesimulator \
| xcpretty -r json-compilation-database -o compile_commands.json \
|| (echo "command failed"; exit 1);

# export report html
# you can run `oclint -help` to see all USAGE
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \
-disable-rule ShortVariableName \
-rc LONG_LINE=1000 \
|| (echo "command failed"; exit 1);

open -a "/Applications/Safari.app" oclintReport.html

oclint-json-compilation-database命令的几个参数说明:

-e
需要忽略分析的文件,这些文件的警告不会出现在报告中

-rc
需要覆盖的规则的阀值,这里可以自定义项目的阀值,默认阀值

-enable-rule
支持的规则,默认是oclint提供的都支持,可以组合-disable-rule来过滤掉一些规则
规则列表

-disable-rule
需要忽略的规则,根据项目需求设置

在Xcode中使用OCLint

因为OCLint提供了xcode格式的输出样式,所以我们可以将它作为一个脚本放在Xcode中。

1、在项目的 TARGETS 下面,点击下方的 “+” ,选择 cross-platform 下面的 Aggregate。输入名字,这里命名为 OCLint

new_target

2、选中该Target,进入Build Phases,添加Run Script,写入下面脚本:

1
2
3
4
5
6
# Type a script or drag a script file from your workspace to insert its path.
# 内置变量
cd ${SRCROOT}
xcodebuild clean
xcodebuild | xcpretty -r json-compilation-database
oclint-json-compilation-database -e Pods -- -report-type xcode

可以看出该脚本跟上面的脚本一样,只不过 将oclint-json-compilation-database命令的-report-typehtml改为了xcode。而OCLint作为一个target本身就运行在特定的环境下,所以xcodebuild可以省去配置参数。

3、通过CMD + B我们编译一下项目,执行脚本任务,会得到能够定位到代码的warning信息:

xcode_warning

总结

以下是对这几种静态分析方案的对比,我们可以根据需求选择适合自己的静态分析方案。

SwiftLint Infer OCLint
支持语言 Swift C、C++、OC、Java C、C++、OC
易用性 简单 较简单 较简单
能否集成进Xcode 可以 不能集成进xcode 可以
自带规则丰富度 较多,包含代码规范 相对较少,主要检测潜在问题 较多,包含代码规范
规则扩展性 可以 不可以 可以

参考

OCLint 实现 Code Review - 给你的代码提提质量

Using OCLint in Xcode

Infer 的工作机制

LLVM & Clang 入门

Swift进阶黄金之路

image-20200511230812677

Swift进阶黄金之路(二)

这篇是对一文鉴定是Swift的王者,还是青铜文章中问题的解答。这些问题仅仅是表层概念,属于知识点,在我看来即使都很清楚也并不能代表上了王者,如果非要用段位类比的话,黄金还是合理的😄。

Swift是一门上手容易,但是精通较难的语言。即使下面这些内容都不清楚也不妨碍你开发业务需求,但是了解之后它能够帮助我们写出更加Swifty的代码。

iOS开发图片格式选择

图片是如何显示的

在讲解如何选择图片格式之前,我感觉有必要先了解下,图片是如何展示的。如果我们要展示一张图片,一般步骤是这样的:

1
2
3
4
5
/// Assets.xcassets中的图片,不需要后缀
let image = UIImage(named: "icon")
let imageView = UIImageView(frame: rect)
imageView.image = image
view.addSubview(imageView)

运行程序,我们就可以在指定位置看到这个icon。看似简单的代码背后隐藏了很多细节工作。一张图片的展示,从代码执行到展示出来大致经历了这些步骤:

快手iOS面经

背景

过完年来北京之后,有准备看看机会,也是想了解下市场行情。简历没有投太多,只定向投了头条教育部门、抖音、快手、阿里,这些公司。头条和阿里的简历都没过,肯定是亮点太少吧。只有快手简历过了,快手是三轮技术面+一轮HR面,前两轮技术都比较顺利,到第三轮却栽了,很痛心o(╥﹏╥)o。目前就不考虑换工作了,等下半年再说了,接下来的时间再好好精炼一下。

快手是视频面试,不支持周末,但是可以选择晚上时间,我这几次都是定在了晚上九点。视频面试是通过牛客网进行的,以下是我还记得下来的各轮面试题,对于一些iOS基础知识就不做解答了。

Runtime内存模型探究

Objective-C是一种通用、高级、面向对象的编程语言。它扩展了标准的ANSI C编程语言,将Smalltalk式的消息传递机制加入到ANSI C中。

可以这么理解,Objective-C = C + Runtime,Runtime是将C语言具有面向对象功能的推动剂,是iOS开发中的核心概念。我们可以在苹果开源的 runtime(当前的最新版本objc4-779.1.tar.gz)中可以发现一些 Objective-C 对象模型的实现细节。

VSCode配置Python版本

刚学习Python没多久,对VSCode也不是很熟悉,在一次解问题的过程中,加深了对这两者的印象,于是记录一下。

环境:Mac OS 10.15,VSCode 1.41.1

问题定位

我在尝试用VSCode写python爬虫,遇到一个问题ImportError: No module named requests

通过问题描述很容易知道问题,这是因为导入了requests库,但是VSCode却没有找到这个库。

2019年总结

好像随着年龄的增长,对时间的感知就变得越来越弱了。总感觉2019年还是一个很新鲜的年份,但它却已经走完了,2020年好像很遥远的样子,但它已经到来了。来个总结吧,2019年,这一年有太多的酸甜苦辣。从工作、生活和规划三个方面展开说吧。

工作

年初原项目组有一位很厉害的开发离职了,当时是我俩共同负责一个项目。他开发经验比我多,而且工作态度和习惯都很好,是我见过的未数不多让我称赞的程序员。曾经一起工作的将近一年多时间里,我都以他为学习榜样。对于他的离职,我纠结了很长一段时间,自认这是对我对公司的一大损失。

后来很长的时间里,一直都没有招到合适的人,我便一个人扛起了项目。很多之前应该是两个人一起商量的问题都需要自己一个人去思考解决方式,渐渐的我不断磨练自己的独立思考能力。大半年下来,突然感觉自己被逼着成长了许多,从一个崇拜别人,向别人学习的角色变成了一个不惧怕任何技术难题,能够帮助别人解决困难的角色。

2019年学会的第一个最重要的道理,很多时候有一个可以依赖的对象,是会减弱自己探索和尝试的动力,而开发很多时候对技能的理解和掌握,乃至个人进步,都是要依靠自己的探索获取的

OC项目转Swift指南

运行环境:Xcode 11.1 Swift5.0

最近参与的一个项目需要从Objective-C(以下简称OC)转到Swift,期间遇到了一些坑,于是有了这篇总结性的文档。如果你也有将OC项目Swift化的需求,可以作为参考。

OC转Swift有一个大前提就是你要对Swift有一定的了解,熟悉Swift语法,最好是完整看过一遍官方的Language Guide

转换的过程分自动化和手动转译,鉴于自动化工具的识别率不能让人满意,大部分情况都是需要手动转换的。

【译】iOS 架构模式--浅析MVC, MVP, MVVM 和 VIPER

作者:Bohdan Orlov
原文地址:https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52

在iOS开发过程中你是否对MVC的使用感觉很别扭?你是否对转向MVVM有疑惑?你听说过VIPER,但不清楚这个东西是否值得一试。

接着读下去,你会找到上面这些问题的答案。如果读完仍不能解惑,欢迎到评论区捶我。

接下来你将在iOS环境下构建关于架构模式的知识体系。我们将简要构建一些经典的例子,并在理论和实践上进行比较他们的不同。如果你需要更多关于任何一个特定的细节,请关注我。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×