最详尽的 Swift 代码规范指南

1. 代码格式

  • 1.1 使用四个空格进行缩进。
  • 1.2 每行最多160个字符,这样可以避免一行过长。 (Xcode->Preferences->Text Editing->Page guide at column: 设置成160即可)
  • 1.3 确保每个文件结尾都有空白行。
  • 1.4 确保每行都不以空白字符作为结尾 (Xcode->Preferences->Text Editing->Automatically trim trailing whitespace + Including whitespace-only lines).
  • 1.5 左大括号不用另起一行。
class SomeClass {
    func someMethod() {
        if x == y {
            /* ... */
        } else if x == z {
            /* ... */
        } else {
            /* ... */
        }
    }

    /* ... */
}
  • 1.6 当在写一个变量类型,一个字典里的主键,一个函数的参数,遵从一个协议,或一个父类,不用在分号前添加空格。
// 指定类型
let pirateViewController: PirateViewController

// 字典语法(注意这里是向左对齐而不是分号对齐)
let ninjaDictionary: [String: AnyObject] = [
    "fightLikeDairyFarmer": false,
    "disgusting": true
]

// 声明函数
func myFunction<T, U: SomeProtocol where T.RelatedType == U>(firstArgument: U, secondArgument: T) {
    /* ... */
}

// 调用函数
someFunction(someArgument: "Kitten")

// 父类
class PirateViewController: UIViewController {
    /* ... */
}

// 协议
extension PirateViewController: UITableViewDataSource {
    /* ... */
}
  • 1.7 基本来说,要在逗号后面加空格。
let myArray = [1, 2, 3, 4, 5]
  • 1.8 二元运算符(+, ==, 或->)的前后都需要添加空格,左小括号后面和右小括号前面不需要空格。
let myValue = 20 + (30 / 2) * 3
if 1 + 1 == 3 {
    fatalError("The universe is broken.")
}
func pancake() -> Pancake {
    /* ... */
}
  • 1.9  遵守Xcode内置的缩进格式( 如果已经遵守,按下CTRL-i 组合键文件格式没有变化)。当声明的一个函数需要跨多行时,推荐使用Xcode默认的格式,目前Xcode 版本是 7.3。
// Xcode针对跨多行函数声明缩进
func myFunctionWithManyParameters(parameterOne: String,
                                  parameterTwo: String,
                                  parameterThree: String) {
    // Xcode会自动缩进
    print("\(parameterOne) \(parameterTwo) \(parameterThree)")
}

// Xcode针对多行 if 语句的缩进
if myFirstVariable > (mySecondVariable + myThirdVariable)
    && myFourthVariable == .SomeEnumValue {

    // Xcode会自动缩进
    print("Hello, World!")
}
  • 1.10 当调用的函数有多个参数时,每一个参数另起一行,并比函数名多一个缩进。
someFunctionWithManyArguments(
    firstArgument: "Hello, I am a string",
    secondArgument: resultFromSomeFunction()
    thirdArgument: someOtherLocalVariable)
  • 1.11 当遇到需要处理的数组或字典内容较多需要多行显示时,需把 [ 和 ] 类似于方法体里的括号, 方法体里的闭包也要做类似处理。
someFunctionWithABunchOfArguments(
    someStringArgument: "hello I am a string",
    someArrayArgument: [
        "dadada daaaa daaaa dadada daaaa daaaa dadada daaaa daaaa",
        "string one is crazy - what is it thinking?"
    ],
    someDictionaryArgument: [
        "dictionary key 1": "some value 1, but also some more text here",
        "dictionary key 2": "some value 2"
    ],
    someClosure: { parameter1 in
        print(parameter1)
    })
  • 1.12 应尽量避免出现多行断言,可使用本地变量或其他策略。
// 推荐
let firstCondition = x == firstReallyReallyLongPredicateFunction()
let secondCondition = y == secondReallyReallyLongPredicateFunction()
let thirdCondition = z == thirdReallyReallyLongPredicateFunction()
if firstCondition && secondCondition && thirdCondition {
    // 你要干什么
}

// 不推荐
if x == firstReallyReallyLongPredicateFunction()
    && y == secondReallyReallyLongPredicateFunction()
    && z == thirdReallyReallyLongPredicateFunction() {
    // 你要干什么
}

2. 命名

  • 2.1 在Swift中不用如Objective-C式 一样添加前缀 (如使用 GuybrushThreepwoode 而不是 LIGuybrushThreepwood)。
  • 2.2 使用帕斯卡拼写法(又名大骆驼拼写法,首字母大写)为类型命名 (如 structenumclasstypedefassociatedtype 等)。
  • 2.3 使用小骆驼拼写法 (首字母小写) 为函数,方法,变量,常量,参数等命名。
  • 2.4 首字母缩略词在命名中一般来说都是全部大写,例外的情形是如果首字母缩略词是一个命名的开始部分,而这个命名需要小写字母作为开头,这种情形下首字母缩略词全部小写。
// "HTML" 是变量名的开头, 需要全部小写 "html"
let htmlBodyContent: String = "<p>Hello, World!</p>"
// 推荐使用 ID 而不是 Id
let profileID: Int = 1
// 推荐使用 URLFinder 而不是 UrlFinder
class URLFinder {
    /* ... */
}
  • 2.5 使用前缀 k + 大骆驼命名法 为所有非单例的静态常量命名。
class MyClassName {
    // 基元常量使用 k 作为前缀
    static let kSomeConstantHeight: CGFloat = 80.0

    // 非基元常量也是用 k 作为前缀
    static let kDeleteButtonColor = UIColor.redColor()

    // 对于单例不要使用k作为前缀
    static let sharedInstance = MyClassName()

    /* ... */
}
  • 2.6 对于泛型和关联类型,可以使用单个大写字母,也可是遵从大骆驼命名方式并能描述泛型的单词。如果这个单词和要实现的协议或继承的父类有冲突,可以为相关类型或泛型名字添加 Type 作为后缀。
class SomeClass<T> { /* ... */ }
class SomeClass<Model> { /* ... */ }
protocol Modelable {
    associatedtype Model
}
protocol Sequence {
    associatedtype IteratorType: Iterator
}
  • 2.7 命名应该具有描述性 和 清晰的。
// 推荐
class RoundAnimatingButton: UIButton { /* ... */ }

// 不推荐
class CustomButton: UIButton { /* ... */ }
  • 2.8 不要缩写,简写命名,或用单个字母命名。
// 推荐
class RoundAnimatingButton: UIButton {
    let animationDuration: NSTimeInterval

    func startAnimating() {
        let firstSubview = subviews.first
    }

}

// 不推荐
class RoundAnimating: UIButton {
    let aniDur: NSTimeInterval

    func srtAnmating() {
        let v = subviews.first
    }
}
  • 2.9 如果原有命名不能明显表明类型,则属性命名内要包括类型信息。
// 推荐
class ConnectionTableViewCell: UITableViewCell {
    let personImageView: UIImageView

    let animationDuration: NSTimeInterval

    // 作为属性名的firstName,很明显是字符串类型,所以不用在命名里不用包含String
    let firstName: String

    // 虽然不推荐, 这里用 Controller 代替 ViewController 也可以。
    let popupController: UIViewController
    let popupViewController: UIViewController

    // 如果需要使用UIViewController的子类,如TableViewController, CollectionViewController, SplitViewController, 等,需要在命名里标名类型。
    let popupTableViewController: UITableViewController

    // 当使用outlets时, 确保命名中标注类型。
    @IBOutlet weak var submitButton: UIButton!
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var nameLabel: UILabel!

}

// 不推荐
class ConnectionTableViewCell: UITableViewCell {
    // 这个不是 UIImage, 不应该以Image 为结尾命名。
    // 建议使用 personImageView
    let personImage: UIImageView

    // 这个不是String,应该命名为 textLabel
    let text: UILabel

    // animation 不能清晰表达出时间间隔
    // 建议使用 animationDuration 或 animationTimeInterval
    let animation: NSTimeInterval

    // transition 不能清晰表达出是String
    // 建议使用 transitionText 或 transitionString
    let transition: String

    // 这个是ViewController,不是View
    let popupView: UIViewController

    // 由于不建议使用缩写,这里建议使用 ViewController替换 VC
    let popupVC: UIViewController

    // 技术上讲这个变量是 UIViewController, 但应该表达出这个变量是TableViewController
    let popupViewController: UITableViewController

    // 为了保持一致性,建议把类型放到变量的结尾,而不是开始,如submitButton
    @IBOutlet weak var btnSubmit: UIButton!
    @IBOutlet weak var buttonSubmit: UIButton!

    // 在使用outlets 时,变量名内应包含类型名。
    // 这里建议使用 firstNameLabel
    @IBOutlet weak var firstName: UILabel!
}
  • 2.10 当给函数参数命名时,要确保函数能理解每个参数的目的。
  • 2.11 根据苹果接口设计指导文档, 如果协议描述的是协议做的事应该命名为名词(如Collection) ,如果描述的是行为,需添加后缀 able 或 ing (如Equatable 和 ProgressReporting)。 如果上述两者都不能满足需求,可以添加Protocol作为后缀,例子见下面。
// 这个协议描述的是协议能做的事,应该命名为名词。
protocol TableViewSectionProvider {
    func rowHeight(atRow row: Int) -> CGFloat
    var numberOfRows: Int { get }
    /* ... */
}

// 这个协议表达的是行为, 以able最为后缀
protocol Loggable {
    func logCurrentState()
    /* ... */
}

// 因为已经定义类InputTextView,如果依然需要定义相关协议,可以添加Protocol作为后缀。
protocol InputTextViewProtocol {
    func sendTrackingEvent()
    func inputText() -> String
    /* ... */
}

3. 代码风格

3.1 综合

  • 3.1.1 尽可能的多使用let,少使用var。
  • 3.1.2 当需要遍历一个集合并变形成另一个集合时,推荐使用函数 mapfilter 和 reduce。
// 推荐
let stringOfInts = [1, 2, 3].flatMap { String($0) }
// ["1", "2", "3"]

// 不推荐
var stringOfInts: [String] = []
for integer in [1, 2, 3] {
    stringOfInts.append(String(integer))
}

// 推荐
let evenNumbers = [4, 8, 15, 16, 23, 42].filter { $0 % 2 == 0 }
// [4, 8, 16, 42]

// 不推荐
var evenNumbers: [Int] = []
for integer in [4, 8, 15, 16, 23, 42] {
    if integer % 2 == 0 {
        evenNumbers(integer)
    }
}
  • 3.1.3 如果变量类型可以依靠推断得出,不建议声明变量时指明类型。
  • 3.1.4 如果一个函数有多个返回值,推荐使用 元组 而不是 inout 参数, 如果你见到一个元组多次,建议使用typealias ,而如果返回的元组有三个或多于三个以上的元素,建议使用结构体或类。
func pirateName() -> (firstName: String, lastName: String) {
    return ("Guybrush", "Threepwood")
}

let name = pirateName()
let firstName = name.firstName
let lastName = name.lastName
  • 3.1.5 当使用委托和协议时,请注意避免出现循环引用,基本上是在定义属性的时候使用 weak 修饰。
  • 3.1.6 在闭包里使用 self 的时候要注意出现循环引用,使用捕获列表可以避免这一点。
myFunctionWithClosure() { [weak self] (error) -> Void in
    // 方案 1

    self?.doSomething()

    // 或方案 2

    guard let strongSelf = self else {
        return
    }

    strongSelf.doSomething()
}
  • 3.1.7 Switch 模块中不用显式使用break。
  • 3.1.8 断言流程控制的时候不要使用小括号。
// 推荐
if x == y {
    /* ... */
}

// 不推荐
if (x == y) {
    /* ... */
}
  • 3.1.9 在写枚举类型的时候,尽量简写。
// 推荐
imageView.setImageWithURL(url, type: .person)

// 不推荐
imageView.setImageWithURL(url, type: AsyncImageView.Type.person)
3.1.10 在使用类方法的时候不用简写,因为类方法不如 枚举 类型一样,可以根据轻易地推导出上下文。
// 推荐
imageView.backgroundColor = UIColor.whiteColor()

// 不推荐
imageView.backgroundColor = .whiteColor()
  • 3.1.11 不建议使用用self.修饰除非需要。
  • 3.1.12 在新写一个方法的时候,需要衡量这个方法是否将来会被重写,如果不是,请用 final 关键词修饰,这样阻止方法被重写。一般来说,final 方法可以优化编译速度,在合适的时候可以大胆使用它。但需要注意的是,在一个公开发布的代码库中使用 final 和本地项目中使用 final 的影响差别很大的。
  • 3.1.13 在使用一些语句如 else,catch等紧随代码块的关键词的时候,确保代码块和关键词在同一行。下面 if/else 和 do/catch 的例子.
if someBoolean {
    // 你想要什么
} else {
    // 你不想做什么
}

do {
    let fileContents = try readFile("filename.txt")
} catch {
    print(error)
}

3.2 访问控制修饰符

  • 3.2.1 如果需要把访问修饰符放到第一个位置。
// 推荐
private static let kMyPrivateNumber: Int

// 不推荐
static private let kMyPrivateNumber: Int
  • 3.2.2 访问修饰符不应单独另起一行,应和访问修饰符描述的对象保持在同一行。
// 推荐
public class Pirate {
    /* ... */
}

// 不推荐
public
class Pirate {
    /* ... */
}
  • 3.2.3  默认的访问控制修饰符是 internal, 如果需要使用internal 可以省略不写。
  • 3.2.4 当一个变量需要被单元测试 访问时,需要声明为 internal 类型来使用@testable import {ModuleName}。 如果一个变量实际上是private 类型,而因为单元测试需要被声明为 internal 类型,确定添加合适的注释文档来解释为什么这么做。这里添加注释推荐使用 - warning: 标记语法。
/**
 这个变量是private 名字
 - warning: 定义为 internal 而不是 private 为了 `@testable`.
 */
let pirateName = "LeChuck"

3.3 自定义操作符

不推荐使用自定义操作符,如果需要创建函数来替代。

在重写操作符之前,请慎重考虑是否有充分的理由一定要在全局范围内创建新的操作符,而不是使用其他策略。

你可以重载现有的操作符来支持新的类型(特别是 ==),但是新定义的必须保留操作符的原来含义,比如 == 必须用来测试是否相等并返回布尔值。

3.4 Switch 语句 和 枚举

  • 3.4.1 在使用 Switch 语句时,如果选项是有限集合时,不要使用default,相反地,把一些不用的选项放到底部,并用 break 关键词 阻止其执行。
  • 3.4.2 因为Swift 中的 switch 选项默认是包含break的, 如果不需要不用使用 break 关键词。
  • 3.4.3 case 语句 应和 switch 语句左对齐,并在 标准的 default 上面。
  • 3.4.4 当定义的选项有关联值时,确保关联值有恰当的名称,而不只是类型。(如. 使用 case Hunger(hungerLevel: Int) 而不是 case Hunger(Int)).
enum Problem {
    case attitude
    case hair
    case hunger(hungerLevel: Int)
}

func handleProblem(problem: Problem) {
    switch problem {
    case .attitude:
        print("At least I don't have a hair problem.")
    case .hair:
        print("Your barber didn't know when to stop.")
    case .hunger(let hungerLevel):
        print("The hunger level is \(hungerLevel).")
    }
}
  • 3.4.5 推荐尽可能使用fall through。
  • 3.4.6 如果default 的选项不应该触发,可以抛出错误 或 断言类似的做法。
func handleDigit(digit: Int) throws {
    case 0, 1, 2, 3, 4, 5, 6, 7, 8, 9:
        print("Yes, \(digit) is a digit!")
    default:
        throw Error(message: "The given number was not a digit.")
}

3.5 可选类型

  • 3.5.1 唯一使用隐式拆包可选型(implicitly unwrapped optionals)的场景是结合@IBOutlets,在其他场景使用 非可选类型 和 常规可选类型,即使有的场景你确定有的变量使用的时候永远不会为 nil, 但这样做可以保持一致性和程序更加健壮。
  • 3.5.2 不要使用 as! 或 try!。
  • 3.5.3 如果对于一个变量你不打算声明为可选类型,但当需要检查变量值是否为 nil,推荐用当前值和 nil 直接比较,而不推荐使用 if let 语法。
// 推荐
if someOptional != nil {
    // 你要做什么
}

// 不推荐
if let _ = someOptional {
    // 你要做什么
}
  • 3.5.4 不要使用 unowned,unowned 和 weak 变量基本上等价,并且都是隐式拆包( unowned 在引用计数上有少许性能优化),由于不推荐使用隐式拆包,也不推荐使用unowned 变量。
// 推荐
weak var parentViewController: UIViewController?

// 不推荐
weak var parentViewController: UIViewController!
unowned var parentViewController: UIViewController
  • 3.5.5 当拆包取值时,使用和被拆包取值变量相同的名称。
guard let myVariable = myVariable else {
    return
}

3.6 协议

在实现协议的时候,有两种方式来组织你的代码:

  1. 使用 // MARK: 注释来分割协议实现和其他代码。
  2. 使用 extension 在 类/结构体已有代码外,但在同一个文件内。

请注意 extension 内的代码不能被子类重写,这也意味着测试很难进行。 如果这是经常发生的情况,为了代码一致性最好统一使用第一种办法。否则使用第二种办法,其可以代码分割更清晰。

使用而第二种方法的时候,使用  // MARK:  依然可以让代码在 Xcode 可读性更强。

3.7 属性

  • 3.7.1 对于只读属性,计算后(Computed)属性, 提供 getter 而不是 get {}。
var computedProperty: String {
    if someBool {
        return "I'm a mighty pirate!"
    }
    return "I'm selling these fine leather jackets."
}
  • 3.7.2 对于属性相关方法 get {}set {}willSet, 和 didSet, 确保缩进相关代码块。
  • 3.7.3 对于willSet/didSet 和 set 中的旧值和新值虽然可以自定义名称,但推荐使用默认标准名称 newValue/oldValue。
var computedProperty: String {
    get {
        if someBool {
            return "I'm a mighty pirate!"
        }
        return "I'm selling these fine leather jackets."
    }
    set {
        computedProperty = newValue
    }
    willSet {
        print("will set to \(newValue)")
    }
    didSet {
        print("did set from \(oldValue) to \(newValue)")
    }
}
  • 3.7.4 在创建类常量的时候,使用 static 关键词修饰。
class MyTableViewCell: UITableViewCell {
    static let kReuseIdentifier = String(MyTableViewCell)
    static let kCellHeight: CGFloat = 80.0
}
  • 3.7.5 声明单例属性可以通过下面方式进行:
class PirateManager {
    static let sharedInstance = PirateManager()

    /* ... */
}

3.8 闭包

  • 3.8.1 如果参数的类型很明显,可以在函数名里可以省略参数类型, 但明确声明类型也是允许的。 代码的可读性有时候是添加详细的信息,而有时候部分重复,根据你的判断力做出选择吧,但前后要保持一致性。
// 省略类型
doSomethingWithClosure() { response in
    print(response)
}

// 明确指出类型
doSomethingWithClosure() { response: NSURLResponse in
    print(response)
}

// map 语句使用简写
[1, 2, 3].flatMap { String($0) }
  • 3.8.2 如果使用捕捉列表 或 有具体的非 Void返回类型,参数列表应该在小括号内, 否则小括号可以省略。
// 因为使用捕捉列表,小括号不能省略。
doSomethingWithClosure() { [weak self] (response: NSURLResponse) in
    self?.handleResponse(response)
}

// 因为返回类型,小括号不能省略。
doSomethingWithClosure() { (response: NSURLResponse) -> String in
    return String(response)
}
  • 3.8.3 如果闭包是变量类型,不需把变量值放在括号中,除非需要,如变量类型是可选类型(Optional?), 或当前闭包在另一个闭包内。确保闭包里的所以参数放在小括号中,这样()表示没有参数,Void 表示不需要返回值。
let completionBlock: (success: Bool) -> Void = {
    print("Success? \(success)")
}

let completionBlock: () -> Void = {
    print("Completed!")
}

let completionBlock: (() -> Void)? = nil

3.9 数组

  • 3.9.1 基本上不要通过下标直接访问数组内容,如果可能使用如 .first 或 .last, 因为这些方法是非强制类型并不会崩溃。 推荐尽可能使用 for item in items 而不是 for i in 0..<items.count 遍历数组。 如果需要通过下标访问数组内容,在使用前要做边界检查。
  • 3.9.2 不要使用 += 或 + 操作符给数组添加新元素,使用性能较好的.append() 或.appendContentsOf()  ,如果需要声明数组基于其他的数组并保持不可变类型, 使用 let myNewArray = [arr1, arr2].flatten(),而不是let myNewArray = arr1 + arr2 。

3.10 错误处理

假设一个函数 myFunction 返回类型声明为 String,但是总有可能函数会遇到error,有一种解决方案是返回类型声明为 String?, 当遇到错误的时候返回 nil。

例子:

func readFile(withFilename filename: String) -> String? {
    guard let file = openFile(filename) else {
        return nil
    }

    let fileContents = file.read()
    file.close()
    return fileContents
}

func printSomeFile() {
    let filename = "somefile.txt"
    guard let fileContents = readFile(filename) else {
        print("不能打开 \(filename).")
        return
    }
    print(fileContents)
}

实际上如果预知失败的原因,我们应该使用Swift 中的 try/catch 。

定义 错误对象 结构体如下:

struct Error: ErrorType {
    public let file: StaticString
    public let function: StaticString
    public let line: UInt
    public let message: String

    public init(message: String, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) {
        self.file = file
        self.function = function
        self.line = line
        self.message = message
    }
}

使用案例:

func readFile(withFilename filename: String) throws -> String {
    guard let file = openFile(filename) else {
        throw Error(message: “打不开的文件名称 \(filename).")
    }

    let fileContents = file.read()
    file.close()
    return fileContents
}

func printSomeFile() {
    do {
        let fileContents = try readFile(filename)
        print(fileContents)
    } catch {
        print(error)
    }
}

其实项目中还是有一些场景更适合声明为可选类型,而不是错误捕捉和处理,比如在获取远端数据过程中遇到错误,nil作为返回结果是合理的,也就是声明返回可选类型比错误处理更合理。

整体上说,如果一个方法有可能失败,并且使用可选类型作为返回类型会导致错误原因湮没,不妨考虑抛出错误而不是吃掉它。

3.11 使用 guard 语句

  • 3.11.1 总体上,我们推荐使用提前返回的策略,而不是 if 语句的嵌套。使用 guard 语句可以改善代码的可读性。
// 推荐
func eatDoughnut(atIndex index: Int) {
    guard index >= 0 && index < doughnuts else {
        // 如果 index 超出允许范围,提前返回。
        return
    }

    let doughnut = doughnuts[index]
    eat(doughnut)
}

// 不推荐
func eatDoughnuts(atIndex index: Int) {
    if index >= 0 && index < donuts.count {
        let doughnut = doughnuts[index]
        eat(doughnut)
    }
}
  • 3.11.2 在解析可选类型时,推荐使用 guard 语句,而不是 if 语句,因为 guard 语句可以减少不必要的嵌套缩进。
// 推荐
guard let monkeyIsland = monkeyIsland else {
    return
}
bookVacation(onIsland: monkeyIsland)
bragAboutVacation(onIsland: monkeyIsland)

// 不推荐
if let monkeyIsland = monkeyIsland {
    bookVacation(onIsland: monkeyIsland)
    bragAboutVacation(onIsland: monkeyIsland)
}

// 禁止
if monkeyIsland == nil {
    return
}
bookVacation(onIsland: monkeyIsland!)
bragAboutVacation(onIsland: monkeyIsland!)
  • 3.11.3 当解析可选类型需要决定在 if 语句 和 guard 语句之间做选择时,最重要的判断标准是是否让代码可读性更强,实际项目中会面临更多的情景,如依赖 2 个不同的布尔值,复杂的逻辑语句会涉及多次比较等,大体上说,根据你的判断力让代码保持一致性和更强可读性, 如果你不确定 if 语句 和 guard 语句哪一个可读性更强,建议使用 guard 。
// if 语句更有可读性
if operationFailed {
    return
}

// guard 语句这里有更好的可读性
guard isSuccessful else {
    return
}

// 双重否定不易被理解 - 不要这么做
guard !operationFailed else {
    return
}
  • 3.11.4  如果需要在2个状态间做出选择,建议使用if 语句,而不是使用 guard 语句。
// 推荐
if isFriendly {
    print("你好, 远路来的朋友!")
} else {
    print(“穷小子,哪儿来的?")
}

// 不推荐
guard isFriendly else {
    print("穷小子,哪儿来的?")
    return
}

print("你好, 远路来的朋友!")
  • 3.11.5  你只应该在在失败情形下退出当前上下文的场景下使用 guard 语句,下面的例子可以解释 if 语句有时候比 guard 语句更合适 – 我们有两个不相关的条件,不应该相互阻塞。
if let monkeyIsland = monkeyIsland {
    bookVacation(onIsland: monkeyIsland)
}

if let woodchuck = woodchuck where canChuckWood(woodchuck) {
    woodchuck.chuckWood()
}
  • 3.11.6 我们会经常遇到使用 guard 语句拆包多个可选值,如果所有拆包失败的错误处理都一致可以把拆包组合到一起 (如 returnbreakcontinue,throw 等).
// 组合在一起因为可能立即返回
guard let thingOne = thingOne,
    let thingTwo = thingTwo,
    let thingThree = thingThree else {
    return
}

// 使用独立的语句 因为每个场景返回不同的错误
guard let thingOne = thingOne else {
    throw Error(message: "Unwrapping thingOne failed.")
}

guard let thingTwo = thingTwo else {
    throw Error(message: "Unwrapping thingTwo failed.")
}

guard let thingThree = thingThree else {
    throw Error(message: "Unwrapping thingThree failed.")
}

4. 文档/注释

4.1 文档

如果一个函数比 O(1) 复杂度高,你需要考虑为函数添加注释,因为函数签名(方法名和参数列表) 并不是那么的一目了然,这里推荐比较流行的插件 VVDocumenter. 不论出于何种原因,如果有任何奇淫巧计不易理解的代码,都需要添加注释,对于复杂的 类/结构体/枚举/协议/属性 都需要添加注释。所有公开的 函数/类/变量/枚举/协议/属性/常数 也都需要添加文档,特别是 函数声明(包括名称和参数列表) 不是那么清晰的时候。

写文档时,确保参照苹果文档中提及的标记语法合集。

在注释文档完成后,你应检查格式是否正确。

规则:

  • 4.1.1 一行不要超过160个字符 (和代码长度限制雷同).
  • 4.1.2 即使文档注释只有一行,也要使用模块化格式 (/** */).
  • 4.1.3 注释模块中的空行不要使用 * 来占位。
  • 4.1.4 确定使用新的 – parameter 格式,而不是就得 Use the new -:param: 格式,另外注意 parameter 是小写的。
  • 4.1.5 如果需要给一个方法的 参数/返回值/抛出异常 添加注释,务必给所有的添加注释,即使会看起来有部分重复,否则注释会看起来不完整,有时候如果只有一个参数值得添加注释,可以在方法注释里重点描述。
  • 4.1.6 对于负责的类,在描述类的使用方法时可以添加一些合适的例子,请注意Swift注释是支持 MarkDown 语法的。
/**
 ## 功能列表

 这个类提供下一下很赞的功能,如下:

 - 功能 1
 - 功能 2
 - 功能 3

 ## 例子

 这是一个代码块使用四个空格作为缩进的例子。

     let myAwesomeThing = MyAwesomeClass()
     myAwesomeThing.makeMoney()

 ## 警告

 使用的时候总注意以下几点

 1. 第一点
 2. 第二点
 3. 第三点
 */
class MyAwesomeClass {
    /* ... */
}
  • 4.1.8 在写文档注释时,尽量保持简洁。

4.2 其他注释原则

  • 4.2.1  // 后面要保留空格。
  • 4.2.2 注释必须要另起一行。
  • 4.2.3 使用注释 // MARK: - xoxo 时, 下面一行保留为空行。
class Pirate {

    // MARK: - 实例属性

    private let pirateName: String

    // MARK: - 初始化

    init() {
        /* ... */
    }

}

那些年提交 AppStore 审核踩过的坑

感谢@云峰小罗投递   原文链接

做iOS开发近5年了,每次提交版本时不可谓不小心翼翼,如履薄冰,但是还是难免踩到了一些坑。苹果的官方文档(AppStore审核条款)这里就不罗列了,太冗长繁琐了,而且大部分是一般app都不会触碰的到的,今天我主要想以自己的亲身经历,跟大家回顾一下这些年我提交AppStore审核时踩过的坑,并且针对如何避免给出一些tips供大家参考。大神请忽略,专家请轻拍。

1、未遵守苹果iOS APP数据储存指导方针。

如果你的App有离线数据下载功能,尤其需要关注这一点。因为离线数据一般占用存储空间比较大,可以被重新下载和重建,但是用户往往希望系统存储空间紧时也依然能够妥妥的存在着,不会被IOS系统自动清理掉。所以不能放在/Library/Caches 目录下(该目录在系统空间紧张时可能会被iOS系统清除)。 那就只能放在主目录/Documents  或 主目录/Library/自定义文件夹下,这样才不会被iOS系统自动清理掉。但是这些数据可能会很大,如果放在 主目录/Documents  或 主目录/Library/自定义的文件夹下,会被iCoud自动同步,那么用户需要为了同步消耗不少流量,苹果可能会因此拒绝你的应用上架。所以需要在程序中给自定义的目录设置“do not backup”属性。

关于数据存储需要注意的点,总结在下面:

关键数据

内容:用户创建的数据文件,无法在删除后自动重新创建

路径:主目录/Documents

管理:iOS系统即时遇到存储空间不足的情况下,也不会清除,同时会备份到iTunes或iCloud中

缓存数据

内容:可用于离线环境,可被重复下载重复生成,即使在离线时缺失,应用本身也可以正常运行

路径:主目录/Library/Caches

管理:在存储空间不足的情况下,会清空, 并且不会被自动备份到iTunes和iCloud中

临时数据

内容:应用运行时,为完成某个内部操作临时生成的文件

路径:主目录/tmp

管理:随时可能被iOS系统清除,且不会自动备份到iTunes和iCloud,尽量在文件不再使用时,应用自己清空,避免对用户设备空间的浪费

离线数据

内容:与缓存数据类似,可以被重新下载和重建,但是用户往往希望这些数据即使在存储紧张时也不会被系统自动删除

目录:主目录/Documents  或 主目录/Library/自定义的文件夹

管理:与关键数据类似,即使在存储空间不足的情况下也不会被清除,应用自己应该清除已经不再使用的文件,以免浪费用户设备空间 。需要设置”不备份到iCoud” ,否则会审核不过。

2、未提供测试账号

如果你的App有部分功能需要登录才能使用,那么你需要再提交审核时,勾选演示账户,并提供对应信息,如下图:

测试账号填写

现在很多app为了更方便快捷,防止用户忘记密码,都采用手机号+验证码的方式,这样的话就没有办法给苹果提供演示账户了,除非账户系统后台做修改提供支持。这种情况,就不需要勾选演示账户了,但是要在备注信息里跟苹果好好解释一下,说我们也是为了提升用户体验的,所以对账户系统做了改进,用户有手机就能登录,不需要注册啥的,如下图。如果你啥也不说的话,那就乖乖等着被拒吧。

测试账号说明

3、跟相关硬件配合使用的app,未提供演示视频。

这里指的硬件是不需要MFi认证的,通过BLE(低功耗蓝牙)或者WiFi连接的硬件。直接在备注里提供相关功能的演示视频即可,如下图。

硬件连接演示视频

演示视频需要把完整的连接过程操作以及连接硬件之后跟硬件相关的功能演示都包含在内。从截图可以看到我的“裤宝”演示视频我是直接放在优酷上了。所以并不像传闻中那样,需要翻墙放到YouTube上,直接放优酷土豆或者百度网盘都行。也不需要用英文,用中文即可。

4、跟相关硬件配合使用的app,未提供PPID.(Product Plan ID )

如果你的App是需要跟通过MFi认证的硬件进行交互,即使用了EA框架(ExternalAccessory.framework),配置了协议字符串(Supported external accessory protocols),那么你需要在备注信息里提供PPID。

ppid说明

很多时候,我们的App可以同时适配很多型号的硬件,每个型号的硬件对应的PPID不一样。如果AppStore提交审核通过之后,又新增了一款型号硬件支持怎么办呢?是否需要单独发一个版本,把对应的PPID增加上去了? 答案是不需要,因为App支持的PPID列表信息是放在备注信息里面的,往列表中新增PPID并不需要修改到二进制文件信息,苹果在这里也比较人性化,可以在不提交新版本的情况下增加PPID信息。

5、使用了后台定位服务,但是没有具体说明原因

之前使用后台定位功能的app都是只需要在在Info.plist中配置 Required background modes -App registers for location updates 即可.但是从2016年的某个时候开始苹果突然要求如果App要使用定位功能,除了程序里做配置,还需要在界面上显式告诉用户你的后台定位是用来干啥的,否则你就会收到类似下面的邮件。

1.1 – Apps using background location services must provide a reason that clarifies the purpose of the use, using mechanisms described in the Human Interface Guidelines.

要修改也可以简单,根据你的app需要在info.plist中配置,NSLocationAlwaysUsageDescription或者NSLocationWhenInUseUsageDescription字段说明。如下图

定位目的说明

6、上传的屏幕快照跟App具体使用截屏相差太远

AppStore提供的屏幕快照功能是为了用户在未下载时可以直观的了解这个App的功能、界面大概是什么样的。所以苹果也允许开发者对屏幕截屏做一些加工美化,并不一定要是原始截屏。但是这里有个限度,就是不能相差太远,具体尺度苹果没有给出量化标准。  公司项目中有个大版本上线了一个比较大的新功能,为了突出宣传这个功能,设计师就重新设计了一套非常Q版的功能演示截图。结果上传后被苹果告知,屏幕快照不符合App本身的功能。

以上这些是本人在AppStore审核时亲自踩过的一些坑,当然还有很多坑,我和我的团队注意到了所以努力避免了,但是个人认为也是非常需要注意的,我简单列在下面供大家参考。

使用未公开的API被发现

使用和系统接近的图标

界面太丑 或者交互太过复杂

不稳定,容易崩溃

跟应用市场上其他App太过雷同

App内有检测更新

出现第三方操作系统的名字或图标

测试不充分,某些App声明支持的操作系统版本有兼容性问题

tips

我们说了这么多踩过的坑,或者差点踩过的坑,无非就是想在以后App开发中尽可能的避免。这里介绍本人的一些经验总结,供大家参考。

1、预防在先

对产品经理规划的功能,首先需要判断是否在技术上可以实现,或者说在不使用非公开API的前提下实现。因为很多时候,即使你通过函数名动态拼接等技术手段在提交审核时躲过API扫描,但是也难免被苹果从功能上发现或者被竞争对手举报。然后对交互设计和UI效果图需要有自己的判断,界面不能太丑,交互不能太复杂,不能使用跟系统太过雷同的Icon。

2、发版前过checklist

每个项目都需要沉淀发版前的checklist,把之前踩过的坑进行备忘,也可以通过网络资讯等手段了解最近时间被拒的一些主要原因,把可能跟自己APP相关的部分进行备注,然后在发版前逐条检查一遍。

3、预提交AppStore审核

如果也预防了,发版前也过了checklist,但是有时候还是难免百密一疏有所遗漏,特别是新功能较多的版本。这里我要重点推荐的就是预提交AppStore审核。项目的版本都是有发版周期的,一般在发版前一周左右App版本基本稳定,只是还需要修改一些bug并回归测试。这个时候完全可以先提交一个版本到AppStore去审核,反正版本号是用不完的,只要不占用产品经理定的版本号就行。预提交审核有什么好处呢?

(1)可以帮助暴露潜在的问题。

这个版本可能开发了一些新功能,然后有些地方可能没有考虑到审核相关的风险。如果等待项目都要结束正式发版时才暴露出来,就追悔莫及了。

(2)在迫不得已的情况下,可以试探一下苹果的界限。

苹果审核条款其实很多时候是没有一个量化标准的,比如屏幕快照不能跟App具体使用时的截屏相差太远,拿到UI设计师给到屏幕快照时,我们有时候也没有办法确定到底是否真的符合苹果的规范,但是没有关系,我们先提交一个版本试一试就知道了;还有再比如前段时间,苹果要求6月1号以后提交的App都要支持IPV6-Only的网络。但是由于历史原因,项目中有个功能用的是第三方的SDK,他们没有办法在我们发版前提供新的支持IPV6的版本。然后我看网上也有人分享说苹果对这个要求并不是非常严格,只需要在iOS9下主要功能能支持IPV6就行了。当然作为项目负责人,肯定也不能说直接把这个功能砍掉不要了,亦或轻信网友所言忽视风险。怎么办呢?赶紧先预提交一个版本试一下再做决定。结果是确实可以通过审核,所以最终版本没有砍掉这个功能,保证了产品的完整性上线了。

4、关于AppStore加急审核

如果经过前面的努力,你还是被拒了,或者App的发布要赶上某个时间运营节点,但是由于各种原因导致预留给App审核的时间太少了。这个时候你需要使用到苹果的加急审核通道。

你在百度里搜索iOS加急审核,你会发现有很多宣称可以帮你快速审核的人,24小时通过审核,审核通过后付款,不通过不要钱。如果你不知道苹果有官方的加急审核功能,你就很容易被这些空手套白狼的人所骗,而且收费都是5000RMB起步。那我真的很想对你说,找我吧,给你友情价打5折。

苹果的加急审核如何使用呢? 在iTunesconnect页面,点击右上角的“?”图标,在弹出菜单中选择“联系我们”,

联系我们

然后在Contact Us页面,选择“App Review” —> “App Store Review” —>” Request Expedited Review”,

加急审核选项

最后在表格里填写相关信息,其中最重要的写你需要加急审核的原因。一般是写要赶某个重大节日运营节点,或者紧急修复某个严重的闪退问题,然后注明闪退现象复现的详细步骤,就可以了。

关于具体加急审核有没有次数限制,次数是跟App相关还是跟开发账号相关,苹果并没有官方的说明。但是可以肯定的是,网上传闻一年只有两次加急审核的机会是不正确的。不过为了让好钢用在刀刃上,还是慎用这个功能,以防到时真的有需要加急审核时却得不到响应。

从今年上半年开始,app审核时间大大缩短了,一般都不需要用到这个功能了。百度CarLife 最近几个版本都是3天就通过审核了,尤其是最新的支持EAP连接的版本V2.1.0,一个晚上就审核通过了。

毛主席告诉我们“与天奋斗,其乐无穷!与地奋斗,其乐无穷!与人奋斗,其乐无穷!”,但是作为iOS开发者,跟苹果奋斗,还是小心谨慎为好。最后提一句, 如果你知道你的app存在某个审核风险,但是通过了苹果审核,那么不要存在侥幸心理,请尽快修改。因为毕竟苹果是人工审核,这个版本过了可能是审核人员心情好,并不代表下个版本审核时心情也这么好。

其实想想最近的广电总局手游审查新政,对AppStore的审核规则也就没有啥可以抱怨的了。

如何实现1080P延迟低于500ms的实时超清直播传输技术

最近由于公司业务关系,需要一个在公网上能实时互动超清视频的架构和技术方案。众所周知,视频直播用 CDN + RTMP 就可以满足绝大部分视频直播业务,我们也接触了和测试了几家 CDN 提供的方案,单人直播没有问题,一旦涉及到多人互动延迟非常大,无法进行正常的互动交谈。对于我们做在线教育的企业来说没有互动的直播是毫无意义的,所以我们决定自己来构建一个超清晰(1080P)实时视频的传输方案。

先来解释下什么是实时视频,实时视频就是视频图像从产生到消费完成整个过程人感觉不到延迟,只要符合这个要求的视频业务都可以称为实时视频。关于视频的实时性归纳为三个等级:

  • 伪实时:视频消费延迟超过 3 秒,单向观看实时,通用架构是 CDN + RTMP + HLS,现在基本上所有的直播都是这类技术。
  • 准实时: 视频消费延迟 1 ~ 3 秒,能进行双方互动但互动有障碍。有些直播网站通过 TCP/UDP + FLV 已经实现了这类技术,YY 直播属于这类技术。
  • 真实时:视频消费延迟 < 1秒,平均 500 毫秒。这类技术是真正的实时技术,人和人交谈没有明显延迟感。QQ、微信、Skype 和 WebRTC 等都已经实现了这类技术。

市面上大部分真实时视频都是 480P 或者 480P 以下的实时传输方案,用于在线教育和线上教学有一定困难,而且有时候流畅度是个很大的问题。在实现超清晰实时视频我们做了大量尝试性的研究和探索,在这里会把大部分细节分享出来。

要实时就要缩短延迟,要缩短延迟就要知道延迟是怎么产生的,视频从产生、编码、传输到最后播放消费,各个环节都会产生延迟,总体归纳为下图:

( 点击图片可以全屏缩放)

成像延迟,一般的技术是毫无为力的,涉及到 CCD 相关的硬件,现在市面上最好的 CCD,一秒钟 50 帧,成像延迟也在 20 毫秒左右,一般的 CCD 只有 20 ~ 25 帧左右,成像延迟 40 ~ 50 毫秒。

编码延迟,和编码器有关系,在接下来的小结介绍,一般优化的空间比较小。

我们着重针对网络延迟和播放缓冲延迟来进行设计,在介绍整个技术细节之前先来了解下视频编码和网络传输相关的知识和特点。

一、视频编码那些事

我们知道从 CCD 采集到的图像格式一般的 RGB 格式的(BMP),这种格式的存储空间非常大,它是用三个字节描述一个像素的颜色值,如果是 1080P 分辨率的图像空间:1920 x 1080 x 3 = 6MB,就算转换成 JPG 也有近 200KB,如果是每秒 12 帧用 JPG 也需要近 2.4MB/S 的带宽,这带宽在公网上传输是无法接受的。

视频编码器就是为了解决这个问题的,它会根据前后图像的变化做运动检测,通过各种压缩把变化的发送到对方,1080P 进行过  H.264 编码后带宽也就在 200KB/S ~ 300KB/S 左右。在我们的技术方案里面我们采用 H.264 作为默认编码器(也在研究 H.265)。

1.1 H.264 编码

前面提到视频编码器会根据图像的前后变化进行选择性压缩,因为刚开始接收端是没有收到任何图像,那么编码器在开始压缩的视频时需要做个全量压缩,这个全量压缩在 H.264 中 I 帧,后面的视频图像根据这个I帧来做增量压缩,这些增量压缩帧叫做 P 帧,H.264 为了防止丢包和减小带宽还引入一种双向预测编码的 B 帧,B 帧以前面的 I 或 P 帧和后面的 P 帧为参考帧。H.264 为了防止中间 P 帧丢失视频图像会一直错误它引入分组序列(GOP)编码,也就是隔一段时间发一个全量 I 帧,上一个 I 帧与下一个 I 帧之间为一个分组 GOP。它们之间的关系如下图:

PS:在实时视频当中最好不要加入 B 帧,因为 B 帧是双向预测,需要根据后面的视频帧来编码,这会增大编解码延迟。

1.2 马赛克、卡顿和秒开

前面提到如果 GOP 分组中的P帧丢失会造成解码端的图像发生错误,其实这个错误表现出来的就是马赛克。因为中间连续的运动信息丢失了,H.264 在解码的时候会根据前面的参考帧来补齐,但是补齐的并不是真正的运动变化后的数据,这样就会出现颜色色差的问题,这就是所谓的马赛克现象,如图:

这种现象不是我们想看到的。为了避免这类问题的发生,一般如果发现 P 帧或者 I 帧丢失,就不显示本 GOP 内的所有帧,直到下一个 I 帧来后重新刷新图像。但是 I 帧是按照帧周期来的,需要一个比较长的时间周期,如果在下一个 I 帧来之前不显示后来的图像,那么视频就静止不动了,这就是出现了所谓的卡顿现象。如果连续丢失的视频帧太多造成解码器无帧可解,也会造成严重的卡顿现象。视频解码端的卡顿现象和马赛克现象都是因为丢帧引起的,最好的办法就是让帧尽量不丢

知道 H.264 的原理和分组编码技术后所谓的秒开技术就比较简单了,只要发送方从最近一个 GOP 的 I 帧开发发送给接收方,接收方就可以正常解码完成的图像并立即显示。但这会在视频连接开始的时候多发一些帧数据造成播放延迟,只要在接收端播放的时候尽量让过期的帧数据只解码不显示,直到当前视频帧在播放时间范围之内即可。

1.3 编码延迟与码率

前面四个延迟里面我们提到了编码延迟,编码延迟就是从 CCD 出来的 RGB 数据经过 H.264 编码器编码后出来的帧数据过程的时间。我们在一个 8 核 CPU 的普通客户机测试了最新版本 X.264 的各个分辨率的延迟,数据如下:


从上面可以看出,超清视频的编码延迟会达到 50ms,解决编码延迟的问题只能去优化编码器内核让编码的运算更快,我们也正在进行方面的工作。

在 1080P 分辨率下,视频编码码率会达到 300KB/S,单个 I 帧数据大小达到 80KB,单个 P 帧可以达到 30KB,这对网络实时传输造成严峻的挑战。

二、网络传输质量因素

实时互动视频一个关键的环节就是网络传输技术,不管是早期 VoIP,还是现阶段流行的视频直播,其主要手段是通过 TCP/IP 协议来进行通信。但是 IP 网络本来就是不可靠的传输网络,在这样的网络传输视频很容易造成卡顿现象和延迟。先来看看 IP 网络传输的几个影响网络传输质量关键因素。

2.1 TCP 和 UDP

对直播有过了解的人都会认为做视频传输首选的就是 TCP + RTMP,其实这是比较片面的。在大规模实时多媒体传输网络中,TCP 和 RTMP 都不占优势。TCP 是个拥塞公平传输的协议,它的拥塞控制都是为了保证网络的公平性而不是快速到达,我们知道,TCP 层只有顺序到对应的报文才会提示应用层读数据,如果中间有报文乱序或者丢包都会在 TCP 做等待,所以 TCP 的发送窗口缓冲和重发机制在网络不稳定的情况下会造成延迟不可控,而且传输链路层级越多延迟会越大。

关于 TCP 的原理:

http://coolshell.cn/articles/11564.html

关于 TCP 重发延迟:

http://weibo.com/p/1001603821691477346388

在实时传输中使用 UDP 更加合理,UDP 避免了 TCP 繁重的三次握手、四次挥手和各种繁杂的传输特性,只需要在 UDP 上做一层简单的链路 QoS 监测和报文重发机制,实时性会比 TCP 好,这一点从 RTP 和 DDCP 协议可以证明这一点,我们正式参考了这两个协议来设计自己的通信协议。

2.2 延迟

要评估一个网络通信质量的好坏和延迟一个重要的因素就是 Round-Trip Time(网络往返延迟),也就是 RTT。评估两端之间的 RTT 方法很简单,大致如下:

  1. 发送端方一个带本地时间戳 T1 的 ping 报文到接收端;
  2. 接收端收到 ping 报文,以 ping 中的时间戳 T1 构建一个携带 T1 的 pong 报文发往发送端;
  3. 发送端接收到接收端发了的 pong 时,获取本地的时间戳 T2,用 T2 – T1 就是本次评测的 RTT。

示意图如下:

( 点击图片可以全屏缩放)

上面步骤的探测周期可以设为 1 秒一次。为了防止网络突发延迟增大,我们采用了借鉴了 TCP 的 RTT 遗忘衰减的算法来计算,假设原来的 RTT 值为 rtt,本次探测的 RTT 值为 keep_rtt。那么新的 RTT 为:

new_rtt = (7 * rtt + keep_rtt) / 8

可能每次探测出来的 keep_rtt 会不一样,我们需要会计算一个 RTT 的修正值 rtt_var,算法如下:

new_rtt_var = (rtt_var * 3 + abs(rtt – keep_rtt)) / 4 

rtt_var 其实就是网络抖动的时间差值。

如果 RTT 太大,表示网络延迟很大。我们在端到端之间的网络路径同时保持多条并且实时探测其网络状态,如果 RTT 超出延迟范围会进行传输路径切换(本地网络拥塞除外)。

2.3 抖动和乱序

UDP 除了延迟外,还会出现网络抖动。什么是抖动呢?举个例子,假如我们每秒发送 10 帧视频帧,发送方与接收方的延迟为 50MS,每帧数据用一个 UDP 报文来承载,那么发送方发送数据的频率是 100ms 一个数据报文,表示第一个报文发送时刻 0ms, T2 表示第二个报文发送时刻 100ms . . .,如果是理想状态下接收方接收到的报文的时刻依次是(50ms, 150ms, 250ms, 350ms….),但由于传输的原因接收方收到的报文的相对时刻可能是(50ms, 120ms, 240ms, 360ms ….),接收方实际接收报文的时刻和理想状态时刻的差值就是抖动。如下示意图:


( 点击图片可以全屏缩放)

我们知道视频必须按照严格是时间戳来播放,否则的就会出现视频动作加快或者放慢的现象,如果我们按照接收到视频数据就立即播放,那么这种加快和放慢的现象会非常频繁和明显。也就是说网络抖动会严重影响视频播放的质量,一般为了解决这个问题会设计一个视频播放缓冲区,通过缓冲接收到的视频帧,再按视频帧内部的时间戳来播放既可以了。

UDP 除了小范围的抖动以外,还是出现大范围的乱序现象,就是后发的报文先于先发的报文到达接收方。乱序会造成视频帧顺序错乱,一般解决的这个问题会在视频播放缓冲区里做一个先后排序功能让先发送的报文先进行播放。

播放缓冲区的设计非常讲究,如果缓冲过多帧数据会造成不必要的延迟,如果缓冲帧数据过少,会因为抖动和乱序问题造成播放无数据可以播的情况发生,会引起一定程度的卡顿。关于播放缓冲区内部的设计细节我们在后面的小节中详细介绍。

2.4 丢包

UDP 在传输过程还会出现丢包,丢失的原因有多种,例如:网络出口不足、中间网络路由拥堵、socket 收发缓冲区太小、硬件问题、传输损耗问题等等。在基于 UDP 视频传输过程中,丢包是非常频繁发生的事情,丢包会造成视频解码器丢帧,从而引起视频播放卡顿。这也是大部分视频直播用 TCP 和 RTMP 的原因,因为 TCP 底层有自己的重传机制,可以保证在网络正常的情况下视频在传输过程不丢。基于 UDP 丢包补偿方式一般有以下几种:

报文冗余

报文冗余很好理解,就是一个报文在发送的时候发送 2 次或者多次。这个做的好处是简单而且延迟小,坏处就是需要额外 N 倍(N 取决于发送的次数)的带宽。

FEC

Forward Error Correction,即向前纠错算法,常用的算法有纠删码技术(EC),在分布式存储系统中比较常见。最简单的就是 A B 两个报文进行 XOR(与或操作)得到 C,同时把这三个报文发往接收端,如果接收端只收到 AC,通过 A 和 C 的 XOR 操作就可以得到 B 操作。这种方法相对增加的额外带宽比较小,也能防止一定的丢包,延迟也比较小,通常用于实时语音传输上。对于  1080P 300KB/S 码率的超清晰视频,哪怕是增加 20% 的额外带宽都是不可接受的,所以视频传输不太建议采用 FEC 机制。

丢包重传

丢包重传有两种方式,一种是 push 方式,一种是 pull 方式。Push 方式是发送方没有收到接收方的收包确认进行周期性重传,TCP 用的是 push 方式。pull 方式是接收方发现报文丢失后发送一个重传请求给发送方,让发送方重传丢失的报文。丢包重传是按需重传,比较适合视频传输的应用场景,不会增加太对额外的带宽,但一旦丢包会引来至少一个 RTT 的延迟。

2.5 MTU 和最大 UDP

IP 网定义单个 IP 报文最大的大小,常用 MTU 情况如下:

超通道 65535

16Mb/s 令牌环 179144

Mb/s 令牌环 4464

FDDI 4352

以太网 1500

IEEE 802.3/802.2 1492

X.25 576

点对点(低时延)296

红色的是 Internet 使用的上网方式,其中 X.25 是个比较老的上网方式,主要是利用 ISDN 或者电话线上网的设备,也不排除有些家用路由器沿用 X.25 标准来设计。所以我们必须清晰知道每个用户端的 MTU 多大,简单的办法就是在初始化阶段用各种大小的 UDP 报文来探测 MTU 的大小。MTU 的大小会影响到我们视频帧分片的大小,视频帧分片的大小其实就是单个 UDP 报文最大承载的数据大小。

分片大小 = MTU – IP 头大小 – UDP 头大小 – 协议头大小;

IP 头大小 = 20 字节, UDP 头大小 = 8 字节。

为了适应网络路由器小包优先的特性,我们如果得到的分片大小超过 800 时,会直接默认成 800 大小的分片。

三、传输模型

我们根据视频编码和网络传输得到特性对 1080P 超清视频的实时传输设计了一个自己的传输模型,这个模型包括一个根据网络状态自动码率的编解码器对象、一个网络发送模块、一个网络接收模块和一个 UDP 可靠到达的协议模型。各个模块的关系示意图如下:


( 点击图片可以全屏缩放)

3.1 通信协议

先来看通信协议,我们定义的通信协议分为三个阶段:接入协商阶段、传输阶段、断开阶段。

接入协商阶段:

主要是发送端发起一个视频传输接入请求,携带本地的视频的当前状态、起始帧序号、时间戳和 MTU 大小等,接收方在收到这个请求后,根据请求中视频信息初始化本地的接收通道,并对本地 MTU 和发送端 MTU 进行比较取两者中较小的回送给发送方, 让发送方按协商后的 MTU 来分片。示意图如下:

( 点击图片可以全屏缩放)

传输阶段:

传输阶段有几个协议,一个测试量 RTT 的 PING/PONG 协议、携带视频帧分片的数据协议、数据反馈协议和发送端同步纠正协议。其中数据反馈协议是由接收反馈给发送方的,携带接收方已经接收到连续帧的报文 ID、帧 ID 和请求重传的报文 ID 序列。同步纠正协议是由发送端主动丢弃发送窗口缓冲区中的报文后要求接收方同步到当前发送窗口位置,防止在发送主动丢弃帧数据后接收方一直要求发送方重发丢弃的数据。示意图如下:

( 点击图片可以全屏缩放)

断开阶段:

就一个断开请求和一个断开确认,发送方和接收方都可以发起断开请求。

3.2 发送

发送主要包括视频帧分片算法、发送窗口缓冲区、拥塞判断算法、过期帧丢弃算法和重传。先一个个来介绍。

帧分片

前面我们提到 MTU 和视频帧大小,在 1080P 下大部分视频帧的大小都大于 UDP 的 MTU 大小,那么就需要对帧进行分片,分片的方法很简单,按照先连接过程协商后的 MTU 大小来确定分片大小(确定分片大小的算法在 MTU 小节已经介绍过),然后将 帧数据按照分片大小切分成若干份,每一份分片以 segment 报文形式发往接收方。

重传

重传比较简单,我们采用 pull 方式来实现重传,当接收方发生丢包,如果丢包的时刻 T1 + rtt_var< 接收方当前的时刻 T2,就认为是丢包了,这个时候就会把所有满足这个条件丢失的报文 ID 构建一个 segment ack 反馈给发送方,发送方收到这个反馈根据 ID 到重发窗口缓冲区中查找对应的报文重发即可。

为什么要间隔一个 rtt_var 才认为是丢包了?因为报文是有可能乱序到达,所有要等待一个抖动周期后认为丢失的报文还没有来才确认是报文丢失了,如果检测到丢包立即发送反馈要求重传,有可能会让发送端多发数据,造成带宽让费和网络拥塞。

发送窗口缓冲区

发送窗口缓冲区保存这所有正在发送且没有得到发送方连续 ID 确认的报文。当接收方反馈最新的连续报文 ID,发送窗口缓冲就会删除所有小于最新反馈连续的报文 ID,发送窗口缓冲区缓冲的报文都是为了重发而存在。这里解释下接收方反馈的连续的报文 ID,举个例子,假如发送方发送了 1. 2. 3. 4. 5,接收方收到 1.2. 4. 5。这个时候最小连续 ID = 2,如果后面又来了 3,那么接收方最小连续 ID = 5。

拥塞判断

我们把当前时间戳记为 curr_T,把发送窗口缓冲区中最老的报文的时间戳记为 oldest_T,它们之间的间隔记为 delay,那么

delay = curr_T – oldest_T

在编码器请求发送模块发送新的视频帧时,如果 delay > 拥塞阈值 Tn,我们就认为网络拥塞了,这个时候会根据最近 20 秒接收端确认收到的数据大小计算一个带宽值,并把这个带宽值反馈给编码器,编码器收到反馈后,会根据带宽调整编码码率。如果多次发生要求降低码率的反馈,我们会缩小图像的分辨率来保证视频的流畅性和实时性。Tn 的值可以通过 rtt 和 rtt_var 来确定。

但是网络可能阶段性拥塞,过后却恢复正常,我们设计了一个定时器来定时检查发送方的重发报文数量和 delay,如果发现恢复正常,会逐步增大编码器编码码率,让视频恢复到指定的分辨率和清晰度。

过期帧丢弃

在网络拥塞时可能发送窗口缓冲区中有很多报文正在发送,为了缓解拥塞和减少延迟我们会对整个缓冲区做检查,如果有超过一定阈值时间的 H.264 GOP 分组存在,我们会将这个 GOP 所有帧的报文从窗口缓冲区移除。并将它下一个 GOP 分组的 I 的帧 ID 和报文 ID 通过 wnd sync 协议同步到接收端上,接收端接收到这个协议,会将最新连续 ID 设置成同步过来的 ID。这里必须要说明的是如果频繁出现过期帧丢弃的动作会造成卡顿,说明当前网络不适合传输高分辨率视频,可以直接将视频设成更小的分辨率

3.3 接收

接收主要包括丢包管理、播放缓冲区、缓冲时间评估和播放控制,都是围绕播放缓冲区来实现的,一个个来介绍。

丢包管理

丢包管理包括丢包检测和丢失报文 ID 管理两部分。丢包检测过程大致是这样的,假设播放缓冲区的最大报文 ID 为 max_id,网络上新收到的报文 ID 为 new_id,如果 max_id + 1 < new_id,那么可能发生丢包,就会将 [max_id + 1, new_id -1] 区间中所有的 ID 和当前时刻作为 K/V 对加入到丢包管理器当中。如果 new_id < max_id,那么就将丢包管理中的 new_id 对应的 K/V 对删除,表示丢失的报文已经收到。当收包反馈条件满足时,会扫描整个丢包管理,将达到请求重传的丢包 ID 加入到 segment ack 反馈消息中并发往发送方请求重传,如果 ID 被请求了重传,会将当前时刻设置为 K/V 对中,增加对应报文的重传计数器 count,这个扫描过程会统计对包管理器中单个重发最多报文的重发次数 resend_count。

缓冲时间评估

在前面的抖动与乱序小节中我们提到播放端有个缓冲区,这个缓冲区过大时延迟就大,缓冲区过小时又会出现卡顿现象,我们针对这个问题设计了一个缓冲时间评估的算法。缓冲区评估先会算出一个 cache timer,cache timer 是通过扫描对包管理得到的 resend count 和 rtt 得到的,我们知道从请求重传报文到接收方收到重传的报文的时间间隔是一个 RTT 周期,所以 cache timer 的计算方式如下。

cache timer = (2 * resend_count+ 1) * (rtt + rtt_var) / 2

有可能 cache timer 计算出来很小(小于视频帧之间间隔时间 frame timer),那么 cache timer = frame timer,也就是说网络再好,缓冲区缓冲区至少 1 帧视频的数据,否则缓冲区是毫无意义的。

如果单位时间内没有丢包重传发生,那么 cache timer 会做适当的缩小,这样做的好处是当网络间歇性波动造成 cache timer 很大,恢复正常后 cache timer 也能恢复到相对小位置,缩减不必要的缓冲区延迟。

播放缓冲区

我们设计的播放缓冲区是按帧 ID 为索引的有序循环数组,数组内部的单元是视频帧的具体信息:帧 ID、分片数、帧类型等。缓冲区有两个状态:waiting 和 playing,waiting 状态表示缓冲区处于缓冲状态,不能进行视频播放直到缓冲区中的帧数据达到一定的阈值。Playing 状态表示缓冲区进入播放状态,播放模块可以从中取出帧进行解码播放。我们来介绍下这两个状态的切换关系:

  1. 当缓冲区创建时会被初始化成 waiting 状态。
  2. 当缓冲区中缓冲的最新帧与最老帧的时间戳间隔 > cache timer 时,进入 playing 状态并更当前时刻设成播放绝对时间戳 play ts。
  3. 当缓冲区处于 playing 状态且缓冲区是没有任何帧数据,进入 waiting 状态直到触发第 2 步。

播放缓冲区的目的就是防止抖动和应对丢包重传,让视频流能按照采集时的频率进行播放,播放缓冲区的设计极其复杂,需要考虑的因素很多,实现的时候需要慎重。

播放控制

接收端最后一个环节就是播放控制,播放控制就是从缓冲区中拿出有效的视频帧进行解码播放。但是怎么拿?什么时候拿?我们知道视频是按照视频帧从发送端携带过来的相对时间戳来做播放,我们每一帧视频都有一个相对时间戳 TS,根据帧与帧之间的 TS 的差值就可以知道上一帧和下一帧播放的时间间隔,假如上一帧播放的绝对时间戳为 prev_play_ts,相对时间戳为 prev_ts,当前系统时间戳为 curr_play_ts,当前缓冲区中最小序号帧的相对时间戳为  frame_ts,只要满足:

Prev_play_ts + (frame_ts – prev_ts) < curr_play_ts 且这一帧数据是所有的报文都收齐了

这两个条件就可以进行解码播放,取出帧数据后将 Prev_play_ts = cur_play_ts,但更新 prev_ts 有些讲究,为了防止缓冲延迟问题我们做了特殊处理。

如果 frame_ts + cache timer < 缓冲区中最大帧的 ts,表明缓冲的时延太长,则 prev_ts = 缓冲区中最大帧的 ts – cache timer。 否则 prev_ts = frame_ts。

四、测量

再好的模型也需要有合理的测量方式来验证,在多媒体这种具有时效性的传输领域尤其如此。一般在实验室环境我们采用 netem 来进行模拟公网的各种情况进行测试,如果在模拟环境已经达到一个比较理想的状态后会组织相关人员在公网上进行测试。下面来介绍怎么来测试我们整个传输模型的。

4.1 netem 模拟测试

Netem 是 Linux 内核提供的一个网络模拟工具,可以设置延迟、丢包、抖动、乱序和包损坏等,基本能模拟公网大部分网络情况。

关于 netem 可以访问它的官网:

https://wiki.linuxfoundation.org/networking/netem

我们在实验环境搭建了一个基于服务器和客户端模式的测试环境,下面是测试环境的拓扑关系图:

我们利用 Linux 来做一个路由器,服务器和收发端都连接到这个路由器上,服务器负责客户端的登记、数据转发、数据缓冲等,相当于一个简易的流媒体服务器。Sender 负责媒体编码和发送,receiver 负责接收和媒体播放。为了测试延迟,我们把 sender 和 receiver 运行在同一个 PC 机器上,在 sender 从 CCD 获取到 RGB 图像时打一个时间戳,并把这个时间戳记录在这一帧数据的报文发往 server 和 receiver,receiver 收到并解码显示这帧数据时,通过记录的时间戳可以得到整个过程的延迟。我们的测试用例是用 1080P 码率为 300KB/S 视频流,在 router 用 netem 上模拟了以下几种网络状态:

  1. 环路延迟 10m,无丢包,无抖动,无乱序
  2. 环路延迟 30ms,丢包 0.5%,抖动 5ms, 2% 乱序
  3. 环路延迟 60ms,丢包 1%,抖动 20ms, 3% 乱序,0.1% 包损坏
  4. 环路延迟 100ms,丢包 4%,抖动 50ms, 4% 乱序,0.1% 包损坏
  5. 环路延迟 200ms,丢包 10%,抖动 70ms, 5% 乱序,0.1% 包损坏
  6. 环路延迟 300ms,丢包 15%,抖动 100ms, 5% 乱序,0.1% 包损坏

因为传输机制采用的是可靠到达,那么检验传输机制有效的参数就是视频延迟,我们统计 2 分钟周期内最大延迟,以下是各种情况的延迟曲线图:

从上图可以看出,如果网络控制在环路延迟在 200ms 丢包在 10% 以下,可以让视频延迟在 500ms 毫秒以下,这并不是一个对网络质量要求很苛刻的条件。所以我们在后台的媒体服务部署时,尽量让客户端到媒体服务器之间的网络满足这个条件,如果网路环路延迟在 300ms 丢包 15% 时,依然可以做到小于 1 秒的延迟,基本能满足双向互动交流。

4.2 公网测试

公网测试相对比较简单,我们将 Server 部署到 UCloud 云上,发送端用的是上海电信 100M 公司宽带,接收端用的是河北联通 20M 小区宽带,环路延迟在 60ms 左右。总体测试下来 1080P 在接收端观看视频流畅自然,无抖动,无卡顿,延迟统计平均在 180ms 左右。

五、坑

在整个 1080P 超清视频的传输技术实现过程中,我们遇到过比较多的坑。大致如下:

Socket 缓冲区问题

我们前期开发阶段都是使用 socket 默认的缓冲区大小,由于 1080P 图像帧的数据非常巨大(关键帧超过 80KB),我们发现在在内网测试没有设置丢包的网络环境发现接收端有严重的丢包,经查证是 socket 收发缓冲区太小造成丢包的,后来我们把 socket 缓冲区设置到 128KB 大小,问题解决了。

H.264 B 帧延迟问题

前期我们为了节省传输带宽和防丢包开了 B 帧编码,由于 B 帧是前后双向预测编码的,会在编码期滞后几个帧间隔时间,引起了超过 100ms 的编码延时,后来我们为了实时性干脆把 B 帧编码选项去掉。

Push 方式丢包重传

在设计阶段我们曾经使用发送端主动 push 方式来解决丢包重传问题,在测试过程发现在丢包频繁发生的情况下至少增加了 20% 的带宽消耗,而且容易带来延迟和网络拥塞。后来几经论证用现在的 pull 模式来进行丢包重传。

Segment 内存问题

在设计阶段我们对每个视频缓冲区中的帧信息都是动态分配内存对象的,由于 1080P 在传输过程中每秒会发送 400 – 500 个 UDP 报文,在 PC 端长时间运行容易出现内存碎片,在服务器端出现莫名其妙的 clib 假内存泄露和并发问题。我们实现了一个 memory slab 管理频繁申请和释放内存的问题。

音频和视频数据传输问题

在早期的设计之中我们借鉴了 FLV 的方式将音频和视频数据用同一套传输算法传输,好处就是容易实现,但在网络波动的情况下容易引起声音卡顿,也无法根据音频的特性优化传输。后来我们把音频独立出来,针对音频的特性设计了一套低延迟高质量的音频传输体系,定点对音频进行传输优化。

后续的工作是重点放在媒体器多点分布、多点并发传输、P2P 分发算法的探索上,尽量减少延迟和服务带宽成本,让传输变的更高效和更低廉。

Q&A

提问:在优化到 500ms 方案中,哪一块是最关键的?

袁荣喜:主要是丢包重传 拥塞和播放缓冲这三者之间的协调工作最为关键,要兼顾延迟控制和视频流畅性。

提问:多方视频和单方有哪些区别,用到了 CDN 推流吗?

袁荣喜:我们公司是做在线教育的,很多场景需要老师和学生交谈,用 CDN 推流方式延迟很大,我们这个视频主要是解决多方通信之间交谈延迟的问题。我们现在观看放也有用 CDN 推流,但只是单纯的观看。我们也在研发基于 UDP 的观看端分发协议,目前这部分工作还没有完成。

Android架构系列-基于MVP创建适合自己的架构

0 Android架构系列文章

该系列文章会不断更新Android项目开发中一些好的架构和小技巧

系列一 Android架构系列-基于MVP创建适合自己的架构
系列二 Android架构系列-如何优美的写Intent
系列三 Android架构系列-开发规范
系列四 Android架构系列-封装自己的okhttp

1 为什么选择MVP

MVP架构是当前比较成熟的Android架构,还有其他架构比如最初始的MVC和MVVM。MVC相对于较为落后,MVVM使用DataBind,普及性不如MVP。所以最终决定自己设计的框架是基于MVP思想进行总结的框架。

选择MVP框架的原因之一也是google官方的示例中MVP sample已经是完成,证明google官方对于MVP的承认度。

官方项目地址:
https://github.com/googlesamples/android-architecture

一个较为详细的官方项目源码解析的文章:
http://www.infoq.com/cn/articles/android-official-mvp-architecture-sample-project-analysis

2 MVP简介

具体的MVP架构相关文章网上已经非常多了,具体的可以自行查找。MVP的存在主要是由于普通MVC架构会导致项目中activity过于臃肿,当项目越来越大后,代码可读性大大降低。

MVP的思想是将activity作为view层,只负责与xml的渲染和监听事件,具体处理数据逻辑放到一个新定义的Present层。减少了activity负责的事情。并且可以强迫开发者养成分模块功能开发的思想。开发前设计好功能模块,而不是像以前一样写流水账一样写代码。从头写到尾。

MVP

3 我的总结

自己根据MVP的思想和一些好的源码总结了一套适合字的框架。真正的架构是依赖义务存在的,所以建议大家能总结出适合自己项目的代码。

3.1 目录分配

在目录分配上决定采用根据功能模块进行划分,而不是所有activty在一个目录的方法。类似google的例子:

目录

原因几点:

  1. 功能模块划分更为清晰,对于以后代码阅读和新人接手更好
  2. 适用于模块化开发,比如以后又是一个新项目,老项目的登录模块、用户模块、论坛模块等待可以整个复制出来重用
  3. 虽然网上提过按照多个模块划分可能会有公用的页面。我认为复制一份也没什么,不会造成很大的冗余代码,并且对于页面来说万一以后某个模块页面有自定义修改不会对其他影响。(毕竟页面的灵活性要求很高,不适合架构抽出来通用的)
  4. 需要抽出来的独立于功能模块的应该是common_util 和 common_widget,分别是通用工具层和通用自定义控件层

具体例子分配如下:

Sample目录
  • GloabApp 全局Application
  • RootAct 启动页面
  • Base目录 基础activity fragment存放
  • util目录 通用工具
  • mywidget 通用自定义控件
  • SampleModule Sample功能模块。里面包含独立的MVP的接口

3.2 Model层

Model层中又可以分为Api层和Cache层。

3.2.1 Api层

主要是网络获取数据信息等接口。

使用了自己二次封装过的Retrofit+Okhttp+Gson组合。详细可以参见文章:http://www.jianshu.com/p/283d1a7a0aff

示例SampleApi:

public class SampleApi extends BaseApi {

    private static final String mBaseUrl = "http://192.168.3.1/";

    private ApiStore mApiStore;

    public SampleApi() {
        super(mBaseUrl);
        mApiStore = mRetrofit.create(ApiStore.class);
    }

    /**
     * 获取xxx数据
     * @param uid
     * @param callback
     */
    public void getSampleInfo(String uid, ApiCallback<GetSampleInfoRet> callback) {
        Call<GetSampleInfoRet> call = ((ApiStore)mApiStore).getSampleInfo(uid);
        call.enqueue(new RetrofitCallback<GetSampleInfoRet>(callback));
    }

    public interface ApiStore {
        @FormUrlEncoded
        @POST("test_retrofit.php")
        Call<GetSampleInfoRet> getSampleInfo(@Field("uid") String uid);
    }
}

3.2.2 Cache层

本地缓存部分数据。

使用了ASimpleCache缓存开源代码。详细可以参见文章:http://www.jianshu.com/p/25c107ed7348

示例SampleCache:

public class SampleCache  extends BaseCache {

    private final String KEY_NEWEST_SAMPLE_INFO = "sample_newest_info";

    public SampleCache(Context context) {
        super(context);
    }

    /**
     * 保存sample信息
     * @param serializable
     */
    public void saveNewestSample(Serializable serializable) {
        mCache.put(KEY_NEWEST_SAMPLE_INFO, serializable);
    }

    /**
     * 获取sample信息
     * @return
     */
    public SampleInfo getNewestSampleInfo() {
        return (SampleInfo) mCache.getAsObject(KEY_NEWEST_SAMPLE_INFO);
    }

    /**
     * 移除缓存
     */
    public void removeNewestSampleInfo() {
        mCache.remove(KEY_NEWEST_SAMPLE_INFO);
    }
}

3.3 Data层

实体化数据类。

3.4 Presenter层

Presenter层又可以分为Contract协议接口,和具体的Presenter处理

3.4.1 Contract层

负责约定view层和presenter层的接口,view和presenter实现相应接口,最终达到解耦的目的。

SampleContract示例:

public interface SampleContract {

    interface View {
        void showSample(SampleInfo sampleInfo);     //显示sample

        void errorGetSample(String msg);    //显示错误信息
    }

    interface Presenter {
        void getNewestSample(); //获取当前最新的xxx
    }
}

3.4.2 Presenter层

负责从model层获取数据、整理数据、行为处理等。处理后调用view显示数据。

SamplePresenter示例:

public class SamplePresenter extends BasePresenter implements SampleContract.Presenter {

    private SampleContract.View mView;
    private SampleApi mApi;
    private SampleCache mCache;

    public SamplePresenter(SampleContract.View view) {
        mView = view;

        mApi = new SampleApi();
        mCache = new SampleCache(GlobalApp.getInstance().getContext());
    }

    @Override
    public void getNewestSample() {
        //先从缓存获取
        SampleInfo sampleInfo = mCache.getNewestSampleInfo();

        if(sampleInfo == null) {
            //从网络获取
            mApi.getSampleInfo("uid", new BaseApi.ApiCallback<GetSampleInfoRet>() {
                @Override
                public void onSuccess(GetSampleInfoRet ret) {
                    //缓存
                    mCache.saveNewestSample(ret.data);

                    //页面显示
                    mView.showSample(ret.data);
                }

                @Override
                public void onError(int err_code, String err_msg) {
                    //服务端返回错误码
                    mView.errorGetSample(err_msg);
                }

                @Override
                public void onFailure() {
                    //网络请求或者解析错误
                    mView.errorGetSample("服务器请求错误");
                }
            });
        } else {
            mView.showSample(sampleInfo);
        }
    }
}

3.5 view层

即平时所说的activity、fragment等。继承自SampleContract的view接口,只负责UI相关显示刷新等。由于拉出了presenter层,view层的代码变得极为清晰

SampleActivity示例:

public class SampleActivity extends BaseActivity implements SampleContract.View {

    @BindView(R.id.txtName)
    TextView txtName;

    @BindView(R.id.imgAvatar)
    ImageView imgAvatar;

    private SampleContract.Presenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sample);
        ButterKnife.bind(this);

        mPresenter = new SamplePresenter(this);
    }

    @Override
    public void showSample(SampleInfo sampleInfo) {
        txtName.setText(sampleInfo.sample_name);
        Glide.with(this)
                .load(sampleInfo.avatar)
                .into(imgAvatar);
    }

    @Override
    public void errorGetSample(String msg) {
        //错误信息
    }

}

4 总结

以上代码Github地址:
https://github.com/tsy12321/BaseAndroidProject

注:该项目会做成一个基础的项目框架,包含各种封装好的工具,底层库和MVP架构,还在不断更新中,欢迎关注提Issue!

 

文/Tsy远(简书作者)
原文链接:http://www.jianshu.com/p/2ca7767df08c
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

九个Console命令,让js调试更简单

一、显示信息的命令

   1: <!DOCTYPE html>
   2: <html>
   3: <head>
   4:     <title>常用console命令</title>
   5:     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
   6: </head>
   7: <body>
   8:     <script type="text/javascript">
   9:         console.log('hello');
  10:         console.info('信息');
  11:         console.error('错误');
  12:         console.warn('警告');
  13:     </script>
  14: </body>
  15: </html>

最常用的就是console.log了。

二:占位符

console上述的集中度支持printf的占位符格式,支持的占位符有:字符(%s)、整数(%d或%i)、浮点数(%f)和对象(%o)

   1: <script type="text/javascript">
   2:         console.log("%d年%d月%d日",2011,3,26);
   3: </script>

效果:
image

三、信息分组

   1: <!DOCTYPE html>
   2: <html>
   3: <head>
   4:     <title>常用console命令</title>
   5:     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
   6: </head>
   7: <body>
   8:     <script type="text/javascript">
   9:         console.group("第一组信息");
  10:
  11:         console.log("第一组第一条:我的博客(http://www.ido321.com)");
  12:
  13:         console.log("第一组第二条:CSDN(http://blog.csdn.net/u011043843)");
  14:
  15:       console.groupEnd();
  16:
  17:       console.group("第二组信息");
  18:
  19:         console.log("第二组第一条:程序爱好者QQ群: 259280570");
  20:
  21:         console.log("第二组第二条:欢迎你加入");
  22:
  23:       console.groupEnd();
  24:     </script>
  25: </body>
  26: </html>

效果:
image

四、查看对象的信息

console.dir()可以显示一个对象所有的属性和方法。

   1: <script type="text/javascript">
   2:         var info = {
   3:             blog:"http://www.ido321.com",
   4:             QQGroup:259280570,
   5:             message:"程序爱好者欢迎你的加入"
   6:         };
   7:         console.dir(info);
   8: </script>

效果:
image

五、显示某个节点的内容

console.dirxml()用来显示网页的某个节点(node)所包含的html/xml代码。

   1: <!DOCTYPE html>
   2: <html>
   3: <head>
   4:     <title>常用console命令</title>
   5:     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
   6: </head>
   7: <body>
   8:     <div id="info">
   9:         <h3>我的博客:www.ido321.com</h3>
  10:         <p>程序爱好者:259280570,欢迎你的加入</p>
  11:     </div>
  12:     <script type="text/javascript">
  13:         var info = document.getElementById('info');
  14:         console.dirxml(info);
  15:     </script>
  16: </body>
  17: </html>

效果:
image

六、判断变量是否是真

console.assert()用来判断一个表达式或变量是否为真。如果结果为否,则在控制台输出一条相应信息,并且抛出一个异常。

   1: <script type="text/javascript">
   2:       var result = 1;
   3:       console.assert( result );
   4:       var year = 2014;
   5:       console.assert(year == 2018 );
   6: </script>

1是非0值,是真;而第二个判断是假,在控制台显示错误信息
image

七、追踪函数的调用轨迹。

console.trace()用来追踪函数的调用轨迹。

   1: <script type="text/javascript">
   2: /*函数是如何被调用的,在其中加入console.trace()方法就可以了*/
   3:   function add(a,b){
   4:         console.trace();
   5:     return a+b;
   6:   }
   7:   var x = add3(1,1);
   8:   function add3(a,b){return add2(a,b);}
   9:   function add2(a,b){return add1(a,b);}
  10:   function add1(a,b){return add(a,b);}
  11: </script>

控制台输出信息:
image

八、计时功能

console.time()和console.timeEnd(),用来显示代码的运行时间。

   1: <script type="text/javascript">
   2:   console.time("控制台计时器一");
   3:   for(var i=0;i<1000;i++){
   4:     for(var j=0;j<1000;j++){}
   5:   }
   6:   console.timeEnd("控制台计时器一");
   7: </script>

运行时间是38.84ms
image

九、console.profile()的性能分析

性能分析(Profiler)就是分析程序各个部分的运行时间,找出瓶颈所在,使用的方法是console.profile()。

   1: <script type="text/javascript">
   2:       function All(){
   3:             alert(11);
   4:          for(var i=0;i<10;i++){
   5:                 funcA(1000);
   6:              }
   7:         funcB(10000);
   8:       }
   9:
  10:       function funcA(count){
  11:         for(var i=0;i<count;i++){}
  12:       }
  13:
  14:       function funcB(count){
  15:         for(var i=0;i<count;i++){}
  16:       }
  17:
  18:       console.profile('性能分析器');
  19:       All();
  20:       console.profileEnd();
  21:     </script>

输出如图:
image

后台管理UI的选择

最近要做一个企业的OA系统,以前一直使用EasyUI,一切都好,但感觉有点土了,想换成现在流行的Bootstrap为基础的后台UI风格,想满足的条件应该达到如下几个:

1、美观、大方、简洁

2、兼容IE8、不考虑兼容IE6/IE7,因为现在还有很多公司在使用Win7系统,系统内置了IE8

3、能通过选项卡打开多个页面,不想做单页,iframe也没关系

4、性能好,不要太笨重

5、最好以Bootstrap为基础

6、还希望在以后别的系统中能够复用。

一次次反复纠结的选择开始了,给大家介绍下我考虑过的UI,也给大家一个参考。

一、EasyUI

easyui是一种基于jQuery的用户界面插件集合。

easyui为创建现代化,互动,JavaScript应用程序,提供必要的功能。

使用easyui你不需要写很多代码,你只需要通过编写一些简单HTML标记,就可以定义用户界面。

easyui是个完美支持HTML5网页的完整框架。

easyui节省您网页开发的时间和规模。

easyui很简单但功能强大的。

优点:轻量、功能强大、免费、兼容性好、帮助详细、使用的人多生态好

缺点:非响应式布局、某些系统看起来有点土(客户与老板的感觉、确实与最新的那些UI有差距)

获得:上网搜索、网盘搜索大把被搭建好了基础功能的框架。下载

下载后大家可以替换成最新的1.5版的easyui

官网:http://www.jeasyui.com/,有免费版,有商业版,商业版收费,帮助非常详尽

资源:http://www.jeasyui.net/,easy是国外的产品,这个网站类似官网的中文版

二、DWZ JUI

特点:DWZ富客户端框架(jQuery RIA framework), 是中国人自己开发的基于jQuery实现的Ajax RIA开源框架. 设计目标是简单实用,快速开发,降低ajax开发成本。

官网:http://jui.org/

下载:https://github.com/dwzteam/

三、HUI

H-ui前端框架是在bootstrap的思想基础上基于 HTML、CSS、JAVASCRIPT开发的轻量级web前端框架,开源免费,简单灵活,兼容性好,满足大多数中国网站。分了前端UI与后端UI。

官网:http://www.h-ui.net/H-ui.admin.shtml 后台,http://www.h-ui.net/ 前台

下载:https://github.com/jackying/

缺点:感觉用的人少,名气小,资料不全,配套组件不多,但国人的产品符合国人的口味。

四、BUI

BUI她是基于jQuery,兼容KISSY的UI类库,专致于解决后台系统的框架方案,BUI提供了丰富的DPL含有强大的控件库对业务做了精细的分析。

官网:http://www.builive.com/

下载:https://github.com/dxq613/bui

感觉也比较冷、与HUI有点类似的优点整体框架符合我的要求,但风格有种说不出的感觉。

五、Ace Admin

响应式Bootstrap网站后台管理系统模板ace admin,非常不错的轻量级易用的admin后台管理系统,基于Bootstrap3,拥有强大的功能组件以及UI组件,基本能满足后台管理系统的需求,而且能根据不同设备适配显示,而且还有四个主题可以切换。以前收费,好像最新版不再收费了。

下载:https://github.com/bopoda/ace

官网:http://ace.jeka.by/

感觉比较全,功能强大,组件多,美观,只是用了很多不同的插件,兼容性不错。

兼容的浏览器:

  • Internet Explorer 10
  • Internet Explorer 11
  • Internet Explorer 8
  • Internet Explorer 9
  • Latest Chrome
  • Latest Firefox
  • Latest Opera
  • Latest Safari

使用的插件:

View Code

使用到的插件并没有分开存放,使用起来会麻烦一些。

另外该插件也被很多人简化、修改成选项卡+iframe风格了。

六、Metronic

Metronic 是一套精美的响应式后台管理模板,基于强大的 Twitter Bootstrap 框架实现。Metronic 拥有简洁优雅的 Metro UI 风格界面,6 种颜色可选,76 个模板页面,包括图表、表格、地图、消息中心、监控面板等后台管理项目所需的各种组件。

页面规范、精致、细腻、美观大方;功能强大、非常全;在所有我看到过的基于Bootstrap的网站模版中,Metronic是我认为最优秀的,其外观之友好、功能之全面让人惊叹。Metronic 是一个自适应的HTML模版,提供后台管理模版和前端内容网页模版两种风格。

优点:

支持HTML5 和 CSS3
自适应,基于响应式 Twitter Bootstrap框架,同时面向桌面电脑、平板、手机等终端。
整合AngularJS 框架。
可自定义管理面板,包括灵活的布局、主题、导航菜单、侧边栏等。
提供了部分电子商务模块:CMS, CRM, SAAS。
多风格,提供了3个前端风格,7个后端管理面板风格。
简洁扁平风格设计。
700多个网页模版,1500多个UI小组件,100多个表单,80多个jQuery插件。
提供说明文档。

缺点:

太大了,真的不知道从那里开始

对IE的兼容不好,虽然官方声称支持IE8,但我测试结果不支持

收费,今天的价格是$28

七、H+ UI

官网的介绍:H+是一个完全响应式,基于Bootstrap3.3.6最新版本开发的扁平化主题,她采用了主流的左右两栏式布局,使用了Html5+CSS3等现代技术,她提供了诸多的强大的可以重新组合的UI组件,并集成了最新的jQuery版本(v2.1.4),当然,也集成了很多功能强大,用途广泛的jQuery插件,她可以用于所有的Web应用程序,如网站管理后台,网站会员中心,CMS,CRM,OA等等,当然,您也可以对她进行深度定制,以做出更强系统。

官网:http://www.zi-han.net/theme/hplus/

与Metronic非常像,插件非常多,收费998人民币。

八、Admin LTE

AdminLTE 是一个基于Bootstrap 3.x的免费主题,它是一个完全响应式管理模板。高度可定制的,易于使用。适合从小型移动设备到大的台式机很多的屏幕分辨率。

下载:https://github.com/almasaeed2010/AdminLTE (目前star 11652+)

预览: http://almsaeedstudio.com/preview/

官网:Free Bootstrap Admin Template

浏览器支持:
IE 9+
Firefox (latest)
Chrome (latest)
Safari (latest)
Opera (latest)

插件:

View Code

特点:

  • 响应式布局,支持多种设备
  • 打印增强
  • 丰富可排序的面板组件
  • 18个插件与3个自定义插件
  • 轻量、快速
  • 兼容主流浏览器,IE8不兼容
  • 支持Glyphicons, Fontawesome和Ion图标

整体感觉与Metronic类似、功能强大,UI精致,被许多公司使用。

评论中感谢网友(dotNetDR_醉丶千秋)推荐,确定是值得关注的一个UI。

九、其它UI

十、总结

没有形式就没有内容、UI重要,特别是当客户与老板不懂太多关于代码、功能、性能的时候。

上面的UI你也许可以通过各种途径获得,但商业应用请慎重。

想来想去还是拿不定主意,不过有点想法:

1、使用HUI和bootstrap

2、使用EasyUI的框架,内容页使用HUI+BootStrap,iframe选项卡

3、从各个功能强大的页面中拿一些插件过来

前端学习路径

作者:余博伦
链接:https://zhuanlan.zhihu.com/p/21935921
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

什么是前端工程师?

总而言之前端工程师就是运用HTML/CSS/JavaScript等Web技术,在工作中配合设计师实现用户界面,和后端工程师进行数据对接,完成Web应用开发的职位。

开发工具

设计软件

前端工程师最首要的任务就是把设计师的设计图切好并翻译成代码,所以我们要学习一些设计软件的基础操作和切图方法。

编辑器

工欲善其事,必先利其器。可以用的编辑器和IDE有很多,在这里我只推荐最棒的两个。

代码管理

不光要学会写代码,也要学会管理你的代码。在工作中你可能会遇到需要自己部署代码的情况;不停地修改迭代重构,当然也需要你掌握版本控制软件。

测试工具

预览和调试必不可少,编写前端代码的大部分时间都是在编辑器和浏览器之间切来切去。

  • Chrome Dev Tools 谷歌浏览器开发工具,想要预览调试你的前端页面必须在这里啦

基础知识

中级知识

高级知识

服务器端

技能图谱

在线资源

在线教程

在线书籍

推荐书目

iOS 学习资料整理

这份学习资料是为 iOS 初学者所准备的, 旨在帮助 iOS 初学者们快速找到适合自己的学习资料, 节省他们搜索资料的时间, 使他们更好的规划好自己的 iOS 学习路线, 更快的入门, 更准确的定位的目前所处的位置.

该文档会持续更新, 同时也欢迎更多具有丰富经验的 iOS 开发者将自己的常用的一些工具, 学习资料, 学习心得等分享上来, 我将定期筛选合并, 文档尚有一些不完善之处, 也请不吝指出, 感谢您对 iOS 所做的贡献, 让我们一起把国内的 iOS 做得更好, 谢谢.

如果您有任何意见或建议也可以通过邮件或微博@李锦发联系我
我的微信:lijinfa894330982

本文章由 The EST Group 成员 @Aufree 整理而成, 转载请注明出处.

视频教程(英文)

视频 简介
Developing iOS 7 Apps for iPhone and iPad 斯坦福开放教程之一, 课程主要讲解了一些 iOS 开发工具和 API 以及 iOS SDK 的使用, 属于 iOS 基础视频
iPad and iPhone Application Development 该课程的讲师 Paul Hegarty 是斯坦福大学软件工程学教授, 视频内容讲解得深入, 权威, 深受好评
Advanced iPhone Development – Fall 2010 iOS 开发的进阶课程, 开始涉及到 Core Animation, Core Data, OpenGL 等框架的应用
iOS Dev Center 苹果官方提供的 iOS 学习视频
Lynda Lynda 上面 iOS 和 Objective-C 的学习资料比较多, 从初级到高级的都有, 覆盖面比较广, 无论 iOS 走到哪个层次, 都可以在上面挑到适合自己的课程
Code School CodeSchool 上面的 iOS 不多, 不过质量都不错, 一些课程也挺有趣的
Udemy Udemy 帮助初学者规划了视频学习路线, 从新手到高级分的比较详细
Developing iOS 8 Apps with Swift 斯坦福白胡子老爷爷的 iOS8 和 Swift 课程, 已经翻译完成 GitHub
Developing iOS 9 Apps with Swift 斯坦福白胡子老爷爷最新的 iOS9 和 Swift 课程, 现在 GitHub 正在翻译

视频教程(中文)

视频 简介
iOS 7 应用开发 斯坦福白胡子老爷爷的系列视频, 所有视频皆完成翻译, 视频较新, 翻译质量也很高
iPhone 开发教程 2010 年冬 全部视频翻译完毕, 较为深入的讲解 iPhone 开发, 视频适合给有一定 Objective-C 基础的人观看
使用 Swift 开发 iOS8 App 实战 慕课网的视频, 主要讲 Swift 的一些基本使用, 并在讲解的过程中做了几个小 APP, 最后还讲了 Sketch 制作分享按钮
征战 Objective-C 视频还未完结, 讲了一些 C 和 Objective-C 的基本语法, 适合零基础的人观看
Developing iOS 8 Apps with Swift GitHub 上正在翻译的斯坦福最新的 iOS8 课程, 目前正在翻译, 未完结

书籍

书籍名称 推荐理由
Objective-C Programming 内容不多, 却都是精华, 有了一点 C 语言基础可以快速阅读此书, 大概一天时间就可以看完, 看完后对 iOS 开发能够有个基本的印象, 该书的官方论坛有各个章节习题的解答.
iOS Programming 这本书在 Quora 上被评为 iOS 入门最佳书籍, 具体评价可见豆瓣下方该书籍的评论
Cocoa Design Patterns 适合打算深入了解 Cocoa 的人看
Learn Objective-C 短小精练, 适合有编程基础的人在半小时内对 Objective-C 有个一定了解
Programming with Objective-C 看完 Learn Objective-C 可以接着看这个官方对 Objective-C 更为深入的介绍
Objective-C 基础教程 该书作者 Scott Knaster 是 Mac 开发界的传奇人物, 目前在 Google 出过多数书籍都广受许多程序员好评, 此书适合从初级跳到中级的 iOS 开发者阅读
iOS 开发进阶 该书作者唐巧是国内 iOS 开发界的名人, 曾参与多个知名软件的开发, 目前该书尚在预售中, 书本内容由浅入深, 将读者一步一步引入到 iOS 中去, 同样适合初级跳到中级的 iOS 开发者阅读
Programming in Objective-C 这本书在亚马逊上面深受欢迎, 有关 Objective-C 的东西讲得非常详细
iOS 测试指南 该书作者是豆瓣的员工, 书中写的多数内容都是作者在平时的工作实践当中提炼出来的测试经验, 重点讲述了各个测试阶段的具体实践方法, 并且通过持续集成串联了各个测试阶段的活动。
Objective-C 编程之道 解析 iOS 的开山之作, 详细介绍了 MVC 在 Cocoa Touch 上的运作过程, 该书适用于 iOS 中级开发者阅读
Objective-C 高级编程 本书主要介绍 iOS 与 OS X 多线程和内存管理, 深入破析了苹果官方公布的源代码, 告诉你一些苹果公司官方文档中不会出现的知识, 适合中级以上 iOS 开发人员阅读
Effective Objective C 2.0 书里写了编写高质量 iOS 与 OS X 代码的 52 个有效方法, 适合 iOS 开发的进阶使用
Swift Fundamentals 估计将来这本书会成为 Swift 的经典入门书籍, 它的 Stars 数说明了一切
The Swift Programming Language 中文版 90 后开发者梁杰组织翻译的 Swift 编程语言中文版

博客

博客地址 博主信息
OneV’s Den 王巍(喵神), 现居日本, 就职于 LINE, 知名 iOS 开发者, 写的文章大多深入浅出, 内容广泛, 目前在维护的Swifter 也值得收藏
唐巧的技术博客 唐巧, 国内知名 iOS 开发者, 现就职于猿题库, 博客推出的 iOS 移动开发周报很受欢迎, 更新频繁
txx’s blog 90 后 iOS 开发者, 人称虾神, 文章内容讲解大多浅白易懂, 很值得看
破船之家 博主也是 iOS 大神一个, 经常更新一些 iOS 教程, 文章的质量都很高, 非常值得看
NSHipster NSHipster 的中文网站, 主要对 NSHipster 的英文网站进行翻译, 博文出自 Mattt 大神之手, 文章大都写得很深入, 详细, 每周一更
Limboy 无网不剩 李忠, 知乎前员工, 目前在负责花瓣 iOS 开发, 不少文章里面有介绍博主个人的学习方法, 让读者在学到技术的同时也掌握学习的技巧
念茜的博客 iOS 圈的女神人物, 写的关于安全问题的文章都值得一看, 由于新博客刚开通不久, 目前文章较少, 可以去看下她以前的博客
iOS技术周报 吴发伟, 天猫资深软件开发工程师, iOS 技术周报每周一更, 推送一些 iOS 技巧, 代码库, 设计等资讯.
iWangKe.me 王轲, IndieBros Studio 创始人, 优秀的 iOS 开发工程师, 写的文章深入浅出, 很多问题分析透彻, 非常有条理性
叶孤城 叶孤城, 优秀 iOS 开发工程师, 发表的文章都有很多干货, 对源码解析类文章写得浅显易懂, 并时常总结一些 iOS 开发技巧, 值得一读
Kevin Blog 周楷雯, 秒视创始人, 知名 iOS 工程师, 做出了 PNChartWaver 这样的好项目, 在博客中也有谈到具体的实现过程
IMTX 图拉鼎, 知名 Apple 平台开发者, 曾经的 Ubuntu 平台开发者, 文章有不少干货, 大多讲解技术实现和学习经验
更多 唐巧收集的中文 iOS/Mac 开发博客列表, 更新频繁, 值得收藏

文章

标题 内容简介
Learn Objective C: The Path to iPhone Development Udemy 写的文章, 说明了一些学习 Objective-C 的前提条件, Objective-C 的发展历史, 学习方法以及学习资源
I Want to Write iOS Apps. Where Do I Start? 主要对 iOS 的开发环境进行了介绍, 并且涉及到了 Swift 的学习, iOS 上架的注意事项, iOS 的设计, 测试, 代码托管等, 讲解较为广泛, 同时也给出不少学习资源
How to become a professional iOS developer 文章写的很有条理, 文中多次强调了版本控制系统的重要性, 主要内容是对学习 iOS 开发到就职, 给出了自己的建议
Learning iOS Programming 作者总结了一些自己学习 iOS 的血的教训, 最后给出了一些不错学习建议
Become an iOS Developer 作者列举了一些学习 iOS 的方法以及常用的库, 以及自学 iOS 的一些建议
iOS 开发如何提高 唐巧写的一篇文章, 主要是对 iOS 技术的提高做的一个总结, 文中不少资源, 工具, 学习方法
自学 iOS 开发的一些经验 文章从入门到进阶到高级, 分为三个阶段, 有条理的讲出了 iOS 的整个学习过程中开发者可能遇到的问题, 并给出了解决办法, 奉献了不少好工具, 资源还有珍贵的学习经验
如何从 0 开始学 iOS 开发 作者给出了学习 iOS 的流程, 并给出一些不错的学习资源
如果我可以重新学习 iOS 开发 作者在文中给出了学习的一些建议, 也谈到了自己的学习方法
iOS 开发学习路径的一些建议 文中谈到了英语的重要性, 以及写博客, 看源代码的好处
iOS 开发入门 作者分享了自己学习 iOS 的经验和资源
Mac 和 iOS 开发资源汇总 破船之家发布的资源汇总
CocoaPods 使用教程 文章讲解了 CocoaPods 的基本使用, 并且配上 AFNetworking 做出了一个小 Demo, 值得一看
iOS 开发路线简述 作者简单介绍了一下自己 iOS 开发的感受,也是他学习 iOS 开发的一个体系架构.
iOS 干货文章、blog 作者收集的 iOS 干货文章、blog.

相关网站

网站 简介
tutsplus 不定时更新一些 iOS 教程
WWDC 苹果官方每年一度的 WWDC 视频, 可以了解历年有关 iOS 发布的内容
ASCIIwwdc WWDC 的文字版
Awesome Swift 该网站收集了很多关于 Swift 的学习资料, 新闻
Appcoda 经常发布一些 iOS 编程教程, 更新比较频繁, 想了解更多可以查看该网站的 About 界面
NSHipster NSHipster is a journal of the overlooked bits in Objective-C, Swift, and Cocoa. Updated weekly.
Think and Build Some tutorials about Core Graphic and Core Animation.
Tutorials 大把的 Objective-C, Swift, iOS 教程, 且全部免费, Raywenderlich 真是业界良心, 赞!

社区

社区 简介
CocoaChina 全球最大苹果开发者中文社区
code4app 经常更新一些很不错的 iOS 代码片段和一些 iOS 资源
objc 定期发布一些有关 Objective-C 的高质量的文章
objc中国 喵神组织的对 objc.io 的翻译网站, 旨在推进国内技术圈整体水平, 翻译质量非常高
DevDiv 发布一些 iOS 的最新资讯及教程
Cocos2d-x Cocos2d-x 论坛
iPhone Dev SDK 国外较有名的 iOS 开发者论坛
Learn Cocoa and iOS Development Forum Learn Cocoa on the MacBeginning iOS 7 Development 这两本书籍的官方论坛, 用户活跃度较高
Apple Developer Forums 苹果官方的开发者论坛
Swiftist Swift 中文社区

工具/插件

工具/插件 简介
CocoaPods 开发 OS X 和 iOS 应用程序的一个第三方库的依赖管理工具, 本身是 Ruby 的一个 Gem, 极大的简化了 Objective-C 的开发流程
Alcatraz Alcatraz 是一款管理 Xcode 插件、模版以及颜色配置的工具
XcodeColors 使 Xcode 调试控制台色彩更丰富
xctool Facebook 开源的一个 iOS 编译和测试的工具
XToDo 一款注释辅助插件,主要用于收集并列出项目中的TODO, FIXME, ???, !!!
KSImageNamed-Xcode 自动补全图片命名的一款插件
VVDocumenter 一个自动生成代码注释的工具
ImageOptim 用于压缩图片一款工具
fastlane 开发流程工具,将开发过程流程化,极大提高开发效率
iOS 必备的 75 个工具 其中包含了非常多好用的工具, 涉及到设计, 分析, 部署等, 总结的十分详细, 有中文翻译
更多 唐巧总结的一些图形应用工具, 命令行工具, Xcode 插件, 并介绍了一点基础的用法

指南/教程

网址 简介
App Store Review Guidelines iOS 应用商店审核指南, 有中文翻译版
Swift 语言指南 有很多丰富的 Swift 学习资料, 学习 Swift 有这份资料可以省下很多力气
苹果 Xcode 帮助文档阅读指南 Tinyfool 推出的一篇对于帮助新手阅读官方文档的指南
Get started with your iOS developer pragram 苹果写的一篇入门指南, 粗略讲解了 iOS 程序从开发到上架的整个流程
Teamtreehouse 文章主要讲解 Objective-C 的一些语法, 文章内容有趣且通俗易懂
A map for iOS development 一张 iOS 开发地图, 做得很赞, 看完对 iOS 开发流程有一定的认知
Start Developing iOS Apps Today 苹果官方给出的 iOS 入门教程, 看过之后能够做一个 To-Do 小程序
Ry’s Objective-C Tutorial 讲解 Objective-C 的教程, 图文并茂, 适合新手阅读
Objective-C Style Guide Ray Wenderlich 推出的 Objective-C 风格指南
iOS8 Day-by-Day 每日一个 iOS8 的小教程, 所有的 DEMO 都可以在其 GitHub上面的找到相关代码
iOS9 Day-by-Day 每日一个 iOS9 的小教程, 所有的 DEMO 都可以在其 GitHub上面的找到相关代码

邮件订阅

  • iOS Dev Weekly (每周一期,内容多为这一星期里值得关注的 GitHub 项目、文章、工具等)
  • iOS Design Weekly (Tips, news and inspiration delivered each week)

文档

Awesome 系列

Raywenderlich 系列 (以下书籍目前均已更新至xcode7.0和swift2.0)

书籍名称 简介
Swift Apprentice 非常棒的 Swift 入门书籍,同时也提及了函数式编程,泛型,面向协议编程等话题
The iOS Apprentice 从零构建 4 个不同类型的 App,深入浅出各种 iOS 开发的技术,配合上面那本 Swift Apprentice 效果更佳
tvOS Apprentice 开发 Apple TV 应用的入门教程
iOS 9 by Tutorials iOS9.0 的新特性的介绍与实践
watchOS 2 by Tutorials 开发 Apple Watch 应用的入门教程
Core Data by Tutorials 深入浅出 Core Data
iOS Animations by Tutorials 深入浅出 iOS 动画
2D iOS & tvOS Games by Tutorials iOS 与 tvOS 2D 游戏开发入门教程
3d-ios-games-by-tutorials iOS 3D 游戏开发教程

知乎上的讨论

Quora 上的讨论

国内知名的程序员开发日报

将ASP.NET Core应用程序部署至生产环境中(CentOS7)

这段时间在使用Rabbit RPC重构公司的一套系统(微信相关),而最近相关检验(逻辑测试、压力测试)已经完成,接近部署至线上生产环境从而捣鼓了ASP.NET Core应用程序在CentOS上的部署方案,今天就跟大家分享一下如何将ASP.NET Core应用程序以生产的标准部署在CentOS上。

环境说明

服务器系统:CentOS 7.2.1511

相关工具:Xshel、Xftp

服务器软件软件:.netcore、nginx、supervisor、policycoreutils-python

准备你的ASP.NET Core应用程序

首先将你的应用程序以便携的模式进行发布。

ps:这边我使用一个空的Web项目来进行演示,因为本篇主要介绍生产环境的部署,与应用无关。

命令为:dotnet publish –c release

具体的可以看:拥抱.NET Core,如何开发跨平台的应用并部署至Ubuntu运行,这篇博文介绍了以便携与自宿主方式发布web应用。

image

确保这份发布应用可以在windows上运行,以减少后续的问题。

image

为什么不用自宿主的方式进行部署?

自宿主的发布方式进行部署会简单很多,为什么生产环境要使用便携的方式进行发布呢?

原因1:性能比便携式的低(主)。

原因2:微软给出的建议(次)。

口说无凭,有图有真相。

image

image

参考地址:https://docs.microsoft.com/zh-cn/dotnet/articles/core/app-types

so,既然是用于生产环境的,当然我们要追求更高的性能。

安装CentOS7

这个就不细说了,网上教程很多,这边我使用了Hyper-V来虚拟化了CentOS7。

安装.NET Core SDK for CentOS7。

sudo yum install libunwind libicu(安装libicu依赖)

image

curl -sSL -o dotnet.tar.gz https://go.microsoft.com/fwlink/?LinkID=809131(下载sdk压缩包)

sudo mkdir -p /opt/dotnet && sudo tar zxf dotnet.tar.gz -C /opt/dotnet(解压缩)

sudo ln -s /opt/dotnet/dotnet /usr/local/bin(创建链接)

image

输入 dotnet –info 来查看是否安装成功

image

如果可以执行则表明.NET Core SDK安装成功。

参考资料:https://www.microsoft.com/net/core#centos

部署ASP.NET Core应用程序

上传之前发布的文件夹至/home/wwwroot/。

这边我使用了Xftp进行文件的上传。

image

image

检查是否能够运行

命令:dotnet /home/wwwroot/WebApplication1/WebApplication1.dll

image

如果出现这些信息则表示成功运行。

这时候我们是无法访问到这个页面的,这时候我们需要部署一个web容器来进行转发。

配置Nginx

安装Nginx

curl -o  nginx.rpm http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm

image

rpm -ivh nginx.rpm

yum install nginx

image

安装成功!

输入:systemctl start nginx 来启动nginx。

输入:systemctl enable nginx 来设置nginx的开机启动(linux宕机、重启会自动运行nginx不需要连上去输入命令)。

配置防火墙

命令:firewall-cmd –zone=public –add-port=80/tcp –permanent(开放80端口)

命令:systemctl restart firewalld(重启防火墙以使配置即时生效)

测试nginx是否可以访问。

image

配置nginx对ASP.NET Core应用的转发

修改 /etc/nginx/conf.d/default.conf 文件。

将文件内容替换为

server {
listen 80;
location / {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}

上传至CentOS进行覆盖。

执行:nginx –s reload 使其即时生效

运行ASP.NET Core应用程序

image

命令:dotnet /home/wwwroot/WebApplication1/WebApplication1.dll

这时候再次尝试访问。

image

想哭的心都有。。。经过后续了解,这个问题是由于SELinux保护机制所导致,我们需要将nginx添加至SELinux的白名单。

接下来我们通过一些命令解决这个问题。。

yum install policycoreutils-python

sudo cat /var/log/audit/audit.log | grep nginx | grep denied | audit2allow -M mynginx

sudo semodule -i mynginx.pp

image

再次尝试访问。

image

至此基本完成了部署。

配置守护服务(Supervisor)

目前存在三个问题

问题1:ASP.NET Core应用程序运行在shell之中,如果关闭shell则会发现ASP.NET Core应用被关闭,从而导致应用无法访问,这种情况当然是我们不想遇到的,而且生产环境对这种情况是零容忍的。

问题2:如果ASP.NET Core进程意外终止那么需要人为连进shell进行再次启动,往往这种操作都不够及时。

问题3:如果服务器宕机或需要重启我们则还是需要连入shell进行启动。

为了解决这个问题,我们需要有一个程序来监听ASP.NET Core 应用程序的状况。在应用程序停止运行的时候立即重新启动。这边我们用到了Supervisor这个工具,Supervisor使用Python开发的。

安装Supervisor

yum install python-setuptools

easy_install supervisor

配置Supervisor

mkdir /etc/supervisor

echo_supervisord_conf > /etc/supervisor/supervisord.conf

修改supervisord.conf文件,将文件尾部的配置

image

修改为

image

ps:如果服务已启动,修改配置文件可用“supervisorctl reload”命令来使其生效

配置对ASP.NET Core应用的守护

创建一个 WebApplication1.conf文件,内容大致如下

[program:WebApplication1]
command=dotnet WebApplication1.dll ; 运行程序的命令
directory=/home/wwwroot/WebApplication1/ ; 命令执行的目录
autorestart=true ; 程序意外退出是否自动重启
stderr_logfile=/var/log/WebApplication1.err.log ; 错误日志文件
stdout_logfile=/var/log/WebApplication1.out.log ; 输出日志文件
environment=ASPNETCORE_ENVIRONMENT=Production ; 进程环境变量
user=root ; 进程执行的用户身份
stopsignal=INT

将文件拷贝至:“/etc/supervisor/conf.d/WebApplication1.conf”下

运行supervisord,查看是否生效

supervisord -c /etc/supervisor/supervisord.conf

ps -ef | grep WebApplication1

image

如果存在dotnet WebApplication1.dll 进程则代表运行成功,这时候在使用浏览器进行访问。

image

至此关于ASP.NET Core应用程序的守护即配置完成。

配置Supervisor开机启动

新建一个“supervisord.service”文件

# dservice for systemd (CentOS 7.0+)
# by ET-CS (https://github.com/ET-CS)
[Unit]
Description=Supervisor daemon

[Service]
Type=forking
ExecStart=/usr/bin/supervisord -c /etc/supervisor/supervisord.conf
ExecStop=/usr/bin/supervisorctl shutdown
ExecReload=/usr/bin/supervisorctl reload
KillMode=process
Restart=on-failure
RestartSec=42s

[Install]
WantedBy=multi-user.target

将文件拷贝至:“/usr/lib/systemd/system/supervisord.service”

执行命令:systemctl enable supervisord

image

执行命令:systemctl is-enabled supervisord #来验证是否为开机启动

image

测试

GIF

 


如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,转载文章之后须在文章页面明显位置给出作者和原文连接,谢谢。

基于ReactNative实现的博客园手机客户端

去年九月,facebook发布了react-native,将web端的javaScript和react技术扩展到了IOS和Android的原生应用开发。用一句大白话来说,就是利用相同的核心代码,就可以搞出androidapp,iosapp,以及后台应用程序。同时,得益于它的热更新能力,软件更新不再需要用户下载新的安装包,就像传统的web网页一样,服务器有修改,终端可即时接触到最新内容。多端技术统一,热更新,原生的体验,真正拥有了这些,才发现当前普遍的移动端开发有多么蛋疼。就像桌面时代的cs结构程序开发很大程度上已经被bs结构所取代一样,移动端的这一进程,会进行的更快。

出于学习及实践的目的,这次用react-native构建了一个博客园的手机客户端,因为没有ios的开发环境(穷),所以当前仅仅支持安卓平台(>=android 4.1.6),但适配ios的话,预估至多也就20%的工作量。

实现功能

  • 首页 & 排行 & 新闻等列表查看
  • 博文详情 & 新闻详情查看
  • 热门博主查看及博主检索
  • 博主详情及博主博文列表
  • 博文评论 & 新闻评论查看
  • 博主及新闻离线收藏及查看
  • 设置 & 关于
  • CodePush代码热更新

由于博客园官方开放接口所限,而我又不倾向于通过非正规手段实现目的,以下列举一些很重要但并未实现的功能:

  • 用户登录
  • 发表评论
  • 博文发表
  • 博文分类别查看
  • 评论消息通知等。

页面截图

this is a picture of gif, may die

this is a picture of gif, may die

home page

detail page

author page

comment page

offline page

search page

下载入口

可扫码直接下载体验:

search page

或访问以下链接下载:
http://fir.im/togayther

存在的问题

  • 详情页面HTML解析组件仍然存在一些性能和细节问题,对于一些长博客的渲染会耗费比较长的时间。
  • 博主详情、博文详情等接口会出现偶尔不会返回数据的问题。
  • 接口返回的数据格式为xml,对于前端的解析不够友好。我个人搭建了一个php的中间层。所以客户端请求的接口地址为:123.56.135.166。
  • 当前app引用的图标为自己创作,因为找了很久也没有找到博客园相关的app图标资源。不知道这样会不会有什么问题。
  • 站内链接应用内跳转查看(官方博文详情接口调用需要传入博文id,但很多博文都自定义了链接,这个还需要再斟酌一下)。
  • 一些性能问题。
  • IOS适配的问题,看接下来我的时间安排吧。
  • 其它一些交互及功能完善。

源码地址

https://github.com/togayther/react-native-cnblogs
有任何问题,可在博文下方留言,或提交issue。

一点后话

在可预知的未来,构建移动端产品的工具及生产力,一定会伴随着科学技术的发展,变得越来越简单和统一。你很难想像时代的进步造福了全人类,但IT从业人员却仍然苦逼的为了兼容各大平台而感觉身体被掏空。就像现在很多原生开发人员开始抱怨工作没有前几年那么好找,其实一定程度上,缘于很多公司的技术选型发生了变化,更加倾向于以一种轻便统一的方式构建业务应用,react-native 当前在业内的热度也印证了这一点。作为技术人员,应该时刻关注行业动态,扩展视野,更新自己的技术栈,才能保证自己的竞争力。在这里祝各位园友工作顺利,也祝博客园紧跟移动互联网浪潮,越来越好!