中国·芜湖 80后·理工男 软件开发工程师 移动应用开发专业教师 技术方向:iOS、Android、Java
协议 协议为方法、属性、以及其他特定的任务需求或功能定义一个大致的框架。协议可被类、结构体、或枚举类型采纳以提供所需功能的具体实现。满足了协议中需求的任意类型都叫做遵循了该协议。 协议的定义 协议的定义方式与类,结构体,枚举的定义都非常相似 protocol SomeProtocol { // 属性 // 方法 } 协议中的属性 不可以有默认值 必须设置是{get}或者{get set} ,注意:get与set之间是没有逗号的 必须设置为var 协议中的方法 可以定义普通方法也可以是mutating方法 方法不能有方法体 方法的参数不可以有默认值 protocol Pet { // 定义属性 var name: String {get set} var age: Int {get} // 定义方法 func feed(food: String) mutating func shout(sound :String); } 协议的遵守格式(实现协议) 格式 class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol { // 类的内容 // 实现协议中的方法 } 实现协议中的属性 此时属性可以设置默认值 协议中属性为可读可写的,可以直接声明为var类型就可以 协议中属性为可读属性,可以直接声明为let类型就可以,也可以声明为var类型 实现协议中的方法 可以为方法中的参数设置默认值 在结构体中,如果需要改变自身的值,需要在方法前面加mutating关键字。在协议的方法中添加mutating关键字,如果结构体来遵守协议,需要有mutating这个关键字,如果是类来遵守协议,mutating关键字就不需要了。 class Cat : Pet{ //属性可以设置默认值 //可读可写用var var name: String = "Dingding" //只读可以用let let age: Int = 10 func feed(food: String) { print("feed\(food)") } //方法参数可以有默认值 //类实现mutating方法要修改属性值时必须加不用加mutating func shout(sound: String = "miaomiao") { self.name = "Caicai" print("shout\(sound)") } } struct Dog : Pet{ var name: String var age: Int func feed(food: String) { print("feed\(food)") } //结构体实现mutating方法要修改属性值时必须加mutating mutating func shout(sound: String) { self.name = "Guaiguai" print("shout\(sound)") } } 协议之间的继承 protocol CrazySportProtocol { func jumping() } //继承协议 protocol SportProtocol : CrazySportProtocol { func playBasketball() func playFootball() } //此时的类必须所有方法都实现 class Person:SportProtocol{ func playBasketball() { print("playBasketball") } func playFootball() { print("playFootball") } func jumping() { print("jumping") } } 协议中方法的可选 方法一 定义协议 @objc protocol SportProtocol2 { // 该方法可选 @objc optional func playBasketball() func playFootball() } 遵守协议 class Person : SportProtocol2 { var name : String? var age : Int = 0 // 实现协议中的方法,此时可以不实现协议中的可选方法,当然也可以实现 func playBasketball() { print("人在打篮球") } } 方法二 扩展协议 protocol SomeProtocol { func requiredFunc() func optionalFunc() } extension SomeProtocol { func optionalFunc() { print("optionalFunc扩展中实现") } } class SomeClass: SomeProtocol { func requiredFunc() { print("requiredFunc具体类中实现") } } 协议的运用——代理模式 //1.定义一个协议,规定需要完成的任务 protocol BuyTicketProtocol { func buyTicket() } //2.让具体的类或者结构体实现协议,将任务具体化 class Assist : BuyTicketProtocol{ func buyTicket() { print("助手去买票") } } class HN : BuyTicketProtocol{ func buyTicket() { print("黄牛去买票") } } //3.委托方申明一个属性(遵循协议),然后在真正需要完成任务的时候调用属性来完成 class Person { // 1.定义协议属性 var delegate : BuyTicketProtocol // 2.自定义构造函数 init (delegate : BuyTicketProtocol) { self.delegate = delegate } // 3.行为 func goToBeijing() { print("找代理买票") delegate.buyTicket() print("拿到票去北京") } } let p = Person(delegate: HN()) p.goToBeijing() let p2 = Person(delegate: Assist()) p2.goToBeijing()
扩展 为现有的类、结构体、枚举类型、协议添加了新功能。扩展和 Objective-C 中的分类类似。 扩展可以: 添加计算属性; 定义方法; 提供新的构造函数; 使现有的类型遵循某协议 Swift中使用extension关键字实现扩展 语法 extension SomeType { // new functionality to add to SomeType goes here } 扩展可以使已有的类型遵循一个或多个协议。在这种情况下,协议名的书写方式与类或结构体完全一样: extension SomeType: SomeProtocol, AnotherProtocol { // implementation of protocol requirements goes here } 扩展计算属性 扩展可以向已有的类型添加计算实例属性和计算类型属性。 extension Double { var km: Double { return self * 1_000.0 } var m: Double { return self } var cm: Double { return self / 100.0 } var mm: Double { return self / 1_000.0 } var ft: Double { return self / 3.28084 } } 25.0.km 25.0.m 25.0.cm 25.0.mm 3.0.ft 扩展构造函数 扩展可向已有的类型添加新的初始化器 extension CGRect{ init(center: CGPoint, size: CGSize) { let originX = center.x - size.width * 0.5 let originY = center.y - size.height * 0.5 self.init(x: originX, y: originY, width: size.width, height: size.height) } } let centerRect = CGRect(center: CGPoint(x: 100.0, y: 100.0), size: CGSize(width: 100.0, height: 100.0)) 扩展方法 扩展可以为已有的类型添加新的实例方法和类型方法。 extension Int { func repetitions(task: () -> Void) { for _ in 0..<self { task() } } } 3.repetitions { print("Hello!") } 扩展mutating方法 扩展结构体,如果要修改里面的值,必须在方法面前加上mutating extension Int { mutating func square() { self = self * self } } //必须用变量,因为它的值要被修改 var someInt = 3 someInt.square() 使现有的类型遵循某协议 class ViewController : UIViewController{ } extension ViewController : UITableViewDataSource{ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 10 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return UITableViewCell() } } 面向协议编程 针对某个需要实现的功能,可以使用协议定义出接口,然后利用协议扩展提供默认的实现。需要这个功能,只需要声明遵守了这个协议即可,遵守某个协议的对象调用协议声明的方法时,如果类本身没有提供实现,协议扩展提供的默认实现会被调用。 案例一 protocol Eat { func eat() } class Person: Eat { func eat() { print("吃饭了") } } var p = Person() p.eat() 改进 extension Eatable { func eat() { print("吃饭了") } } class Person: Eatable { } var p = Person() p.eat() 再次修改 class Person: Eatable { func eat() { print("人要吃饭了") } } var p = Person() p.eat() 案例二 protocol Coder { var haveFun:Bool {get set} var ownMoney:Bool {get set} } protocol Swifter { var codingLevel:Int {get set} } struct CoderA : Coder { var name:String var haveFun: Bool var ownMoney: Bool } struct CoderB : Coder, Swifter { var name:String var haveFun: Bool = true var ownMoney: Bool = true var codingLevel: Int = 3 } struct CoderC : Coder, Swifter{ var name:String var haveFun: Bool = true var ownMoney: Bool = true var codingLevel: Int = 5 } 可以发现CoderB与CoderC有冗余 改进 protocol Coder { var haveFun:Bool {get set} var ownMoney:Bool {get set} } protocol Swifter { var codingLevel:Int {get set} } //where限定条件 extension Coder where Self:Swifter { var haveFun:Bool { return true} var ownMoney:Bool { return true} } struct CoderA : Coder { var name:String var haveFun: Bool var ownMoney: Bool } struct CoderB : Coder, Swifter { var name:String var haveFun: Bool var ownMoney: Bool var codingLevel: Int = 3 } struct CoderC : Coder, Swifter{ var name:String var haveFun: Bool var ownMoney: Bool var codingLevel: Int = 5 }
类的属性介绍 Swift中属性有多种 存储属性:存储实例的常量和变量 计算属性:通过某种方式计算出来的属性 类属性:与整个类自身相关的属性 存储属性 存储属性是最简单的属性,它作为类实例的一部分,用于存储常量和变量 可以给存储属性提供一个默认值,也可以在初始化方法中对其进行初始化 下面是存储属性的写法 age和name都是存储属性,用来记录该学生的年龄和姓名 chineseScore和mathScore也是存储属性,用来记录该学生的语文分数和数学分数 class Student { // 存储属性 var age : Int = 0 var name : String? var chineseScore : Double = 0.0 var mathScore : Double = 0.0 } // 创建学生对象 let stu = Student() // 给存储属性赋值 stu.age = 10 stu.name = "why" stu.chineseScore = 89.0 stu.mathScore = 98.0 计算属性 计算属性并不存储实际的值,而是提供一个getter和一个可选的setter来间接获取和设置其它属性 计算属性一般只提供getter方法 如果只提供getter,而不提供setter,则该计算属性为只读属性,并且可以省略get{} 下面是计算属性的写法 averageScore是计算属性,通过chineseScore和mathScore计算而来的属性 在setter方法中有一个newValue变量,是系统指定分配的 class Student { // 存储属性 var age : Int = 0 var name : String? var chineseScore : Double = 0.0 var mathScore : Double = 0.0 // 计算属性 var averageScore : Double { get { return (chineseScore + mathScore) / 2 } // 死循环,在内部又会调用set方法 // newValue是系统分配的变量名,内部存储着新值 set { self.averageScore = newValue } } } // 获取计算属性的值 print(stu.averageScore) 类属性 类属性是与类相关联的,而不是与类的实例相关联 所有的类和实例都共有一份类属性.因此在某一处修改之后,该类属性就会被修改 类属性的设置和修改,需要通过类来完成 下面是类属性的写法 类属性可以使用static来修饰 courseCount是类属性,用来记录学生有多少门课程 class Student { // 存储属性 var age : Int = 0 var name : String? var chineseScore : Double = 0.0 var mathScore : Double = 0.0 // 计算属性 var averageScore : Double { return (chineseScore + mathScore) / 2 } // 类属性 static var englishScore:Double = 95.5 } // 设置类属性的值 Student.englishScore = 90.5 // 取出类属性的值 print(Student.englishScore) 懒加载属性 苹果的设计思想:希望所有的对象在使用时才真正加载到内存中 在OC中我们可以重写get方法来进行懒加载 Swift用lazy关键字可以用于定义某一个属性懒加载 lazy的作用是只会赋值一次 懒加载的本质是,在第一次使用的时候执行闭包,将闭包的返回值赋值给属性 懒加载的使用 格式 lazy var 变量: 类型 = { 创建变量代码 }() 案例 lazy var teacher:[String] = { ()->[String] in return ["Mr Zhang", "Mr Li", "Mr Yang"] }() student.teacher 几种属性的区别 存储属性 —— 实实在在存储常量和变量的 计算属性 —— 依赖于存储属性,通过计算得出来,它提供getter和setter方法间接访问和设置值 类属性 —— 本质是一个全局属性,在类里限定了其作用域,用关键字static修饰 懒加载属性 —— 用关键字lazy修饰,必须进行初始化,非实例属性懒加载时在大括号{}后面要加上() 全局属性 —— 类外面的属性,作用域全局 总结: 存储属性,最先被初始化 构造方法,仅次于存储属性调用,可以在这里对存储属性进行赋值 懒加载属性、类属性、全局属性都是在第一次使用的时候初始化一次,以后调用都不再初始化 当懒加载属性是基于一个存储属性计算的时候,切勿使用懒加载属性,采用计算属性 监听属性的改变 在OC中我们可以重写set方法来监听属性的改变 Swift中可以通过属性观察者来监听和响应属性值的变化 通常是监听存储属性和类属性的改变.(对于计算属性,不需要定义属性观察者,因为可以在计算属性的setter中直接观察并响应这种值的变化) 定义观察者 willSet:在属性值被存储之前设置。此时新属性值作为一个常量参数被传入。该参数名默认为newValue,可以自定义 didSet:在新属性值被存储后立即调用。与willSet相同,此时传入的是属性的旧值,默认参数名为oldValue,可以自定义 willSet与didSet只有在属性改变时才会调用,在初始化时不会去调用这些监听方法 class Student { var name:String?{ // 属性即将改变,还未改变时会调用的方法 // 可以给newValue自定义名称 willSet(new){ print("willSet---") // 在该方法中有一个默认的系统属性newValue,用于存储新值 if let new = new { print(new) } } // 属性值已经改变了,会调用的方法 didSet{ print("didSet---") // 在该方法中有一个默认的系统属性oldValue,用于存储旧值 if let oldValue = oldValue { print(oldValue) } } } } var student = Student() // 在赋值时,监听该属性的改变 student.name = "Zhangsan" student.name = "Lisi" 属性继承与重写 属性继承:子类可以继承父类的属性,包括存储属性、计算属性和类型属性,还可以继承父类的属性观察器。 属性重写 无论继承的是存储属性还是计算属性,子类都可以通过提供getter和setter对属性进行重写 可以将一个继承的属性重写为一个读写属性 不可以将继承来的读写属性重写为只读属性 如果重写时提供了setter方法,一定要提供getter方法 属性观察器重写 无论父类有没有为该属性添加属性观察器,子类都可以添加属性观察器 如果父类已经添加了属性观察器,当属性发生变化时,父类与子类都会得到通知 属性观察器不能用于计算属性,只能用于存储属性,因为计算属性在setter里就可以获取到属性的变化 实例方法 实例方法属于特定类实例、结构体实例或者枚举实例的函数 这里的方法其实就是函数,只不过放在类、结构体和枚举中时称之为方法 self 属性 每一个类的实例都隐含一个叫做 self的属性,可以使用 self访问当前类中的属性和方法 class Student { var str = "Hello, playground" func say(info:String) { print(info) } func eat(food:String){ //self指向的是当前类的实例 self.say(info: food) print("吃\(food)") } } var stu = Student() stu.eat(food: "米饭") 值类型在实例方法中修改属性和调用方法 值类型默认情况下,不能在实例方法中修改属性 不能用self调用其他的函数 可以在函数前放一个mutating关键字来实现 struct Student { var str = "Hello, playground" mutating func say(info:String) { print(info) self.str = "Hello World" print(self.str) } mutating func eat(food:String){ self.say(info: food) print("吃\(food)") } } 类型方法 在函数前使用 static关键字(能在类、结构体中使用) 在函数前使用 class关键字(只能在类中使用) static func play(){ print("play") } class func study(){ print("study") } class 和 static 总结 class 和 static 相同点 可以修饰方法,static 修饰的方法叫做静态方法,class修饰的叫做类方法 都可以修饰计算属性 class 和 static 不同点 class 不能修饰存储属性,static 可以修饰存储属性,static修饰的存储属性称为静态变量(常量) class 修饰的计算属性可以被重写,static 修饰的不能被重写 static 修饰的静态方法不能被重写,class 修饰的类方法可以被重写 class 修饰的类方法被重写时,可以使用static 让方法变为静态方法 class 修饰的计算属性被重写时,可以使用static 让其变为静态属性,但它的子类就不能被重写了 class 只能在类中使用,但是static 可以在类,结构体,或者枚举中使用
重要说明 本文中提到的构造函数,在很多书中有其他的说法,如构造器,构造方法,初始化,初始函数等 本文中提到的析构函数,在很多书中有其他的说法,如反构造器,析构方法,反初始化,反初始函数等 构造函数的介绍 构造函数用于初始化一个类的实例(创建对象) 默认情况下载创建一个类时,必然会调用一个构造函数 即便是没有编写任何构造函数,编译器也会提供一个默认的构造函数 如果是继承自NSObject,可以对父类的构造函数进行重写 默认构造函数 在创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值,如果不是在定义时初始化值,可以在构造函数中赋值 构造函数就像一个没有形式参数的实例方法,使用 init 关键字来写 class Person { var name:String var age:Int var sex:String //1.构造函数没有func修饰 //2.构造函数默认完成调用 不能手动调用 init() { print("被调用") name = "Zhangsan" age = 10 sex = "male" } } var p = Person() p.age p.name p.sex 自定义构造函数 希望在创建一个对象时手动给属性赋值(属性的值是在外面传进去的) 可以自定义构造函数 自定义构造函数和默认构造函数可以同时存在 //定义类 class Person { var name:String var age:Int var sex:String //默认构造函数 init() { print("被调用") name = "Zhangsan" age = 10 sex = "male" } //自定义构造函数 init(name:String, age:Int, sex:String) { self.name = name self.age = age self.sex = sex } } var p = Person() p.age p.name p.sex var p2 = Person(name: "Wangli", age: 11, sex: "female") p2.age p2.name p2.sex 结构体类型的成员构造函数 如果结构体类型中没有定义任何自定义构造函数,它会自动获得一个成员构造函数 //定义了一个名为 Size 有两个属性分别是 width 和 height 的结构体,这两个属性通过分配默认值 0.0 ,从而被推断为 Double 类型 struct Size { var width = 0.0, height = 0.0 } //Size 结构体自动接收一个 init(width:heght:) 构造函数 let twoByTwo = Size(width: 2.0, height: 2.0) 值类型的构造函数委托 构造函数可以调用其他构造函数来执行部分实例的初始化。这个过程,就是所谓的构造函数委托 构造函数委托对于值类型和类类型是不同的。 值类型(结构体和枚举)不支持继承,所以它们的构造函数委托的过程相对简单 注意如果为值类型定义了自定义构造函数,就不能访问默认构造函数或者是成员构造函数 struct Size { var width = 0.0, height = 0.0 init() { //构造函数委托 self.init(width: 2.0, height: 2.0) } init(width:Double, height:Double) { self.width = width self.height = height } } //要么不要写任何构造函数,要么全写所有的构造函数,否则下面第二种调用方式会有问题,参考上面第四条 var size = Size() size.width size.height var size2 = Size(width: 1.2, height: 1.2) size2.width size2.height 类的继承和初始化 所有类的存储属性——包括从它的父类继承的所有属性都必须在初始化期间分配初始值。 Swift 为类类型定义了两种构造函数以确保所有的存储属性接收一个初始值,它们就是指定构造函数(Designated Initializer)和便捷构造函数(Convenience Initializer) 指定构造函数是类的主要构造函数。指定构造函数可以初始化所有类引用的属性并且调用合适的父类构造函数来继续这个初始化过程给父类链 一个类通常只有一个指定构造函数并且每个类至少得有一个指定构造函数 便捷构造函数是次要的,可以在相同的类里定义一个便捷构造函数来调用一个指定构造函数给指定构造函数设置默认形式参数 //类的指定构造函数 init(parameters) { statements } //便捷构造函数有着相同的书写方式,但是要用 convenience 修饰符放到 init 关键字前,用空格隔开 convenience init(parameters) { statements } 类类型的构造函数委托 为了简化指定和便捷构造函数之间的调用关系,Swift 在构造函数之间的委托调用有下面的三个规则: 规则 1——指定构造函数必须从它的直系父类调用指定构造函数 规则 2——便捷构造函数必须从相同的类里调用另一个构造函数(可以是指定也可以是便捷) 规则 3——便捷构造函数最终必须调用一个指定构造函数 简单记忆的这些规则的方法如下: 指定构造函数必须总是向上委托。 便捷构造函数必须总是横向委托。 类类型的构造函数委托 class Car{ var speed:Double //Designated Initializer init(speed:Double) { self.speed = speed } convenience init(){ self.init(speed: 60.0) } } class Bus : Car { var wheels : Int init(wheels: Int) { self.wheels = wheels //由于子类继承了父类中的存储属性 所以必须借助父类的指定构造函数来初始化继承的那个存储属性的值 //一定要在子类的属性初始化完毕以后调用 super.init(speed: 120.0) } convenience init(){ self.init(wheels: 6) } } 构造函数的继承与重写 在Swift中,子类的构造函数有两种来源,首先是自己拥有的构造函数,其次是从父类中继承过来的构造函数。但是,并不是所有父类构造函数都能够被子类继承。子类继承父类的构造函数是有条件的,遵守以下2个规则: 规则1——如果子类没有定义任何指定构造函数,它会自动继承父类所有指定构造函数 规则2——如果子类提供了所有父类指定构造函数的实现(通过规则1继承来的或者提供自定义实现的),那么它会自动继承所有父类便捷初始化器 如果一个子类中任意的构造器和父类的便利构造器一模一样, 不算重写 class Person { var name: String! var weight: Double // 普通自定义构造函数 init(name: String) { self.name = name self.weight = 0.0 } // 定义指定构造函数 init(name: String, weight: Double) { self.name = name self.weight = weight } // 定义便利构造函数 convenience init(n name: String, w weight: Double) { // 便利构造函数必须调用同类中的指定构造函数 self.init(name: name, weight: weight) } convenience init(showStr: String) { self.init(name: "", weight: 0.0) print(showStr) } } class Man: Person { var sex: String = "男" override init(name: String) { super.init(name: name) self.name = name self.weight = 0.0 } override init(name: String, weight: Double) { self.sex = "女" // 子类的指定构造函数中必须调用父类的构造函数 // 重写的时候,必须将调用父类的构造函数语句放在调用父类属性的前面 super.init(name: name, weight: weight) self.name = name self.weight = weight } // 定义构造函数与父类的便利构造函数一样, 这里不算重写 convenience init(showStr: String) { self.init(name: "", weight: 0.0) print(showStr) } } var manA = Man(name: "ZhangSan", weight: 62.0) var manB = Man(showStr: "Hello Swift") 可失败的构造函数 定义类、结构体或枚举初始化时可以失败 失败可能由以下几种方式触发,包括给初始化传入无效的形式参数值,或缺少某种外部所需的资源,又或是其他阻止初始化的情况 为了处理这种可能,在类、结构体或枚举中定义一个或多个可失败的构造函数。通过在 init 关键字后面添加问号init? 可失败的构造函数里面应该有一个 return nil 的语句(虽然没有也不报错) 通过可失败的构造函数构造出来的实例是一个可选型 struct Animal { let species: String init?(species: String) { // 返回一个nil if species.isEmpty { return nil } self.species = species } } //返回的类型是当前类型的可选型 let cat = Animal(species: "CAT") if let cat = cat { cat.species } let dog = Animal(species: "") if let dog = dog { dog.species } 必要构造函数 在类的构造函数前添加required 修饰符来表明表明它是一个必要构造函数 当子类重写父类的必要构造函数时,必须在子类的构造函数前也要添加 required 修饰符以确保当其它类继承该子类时,该构造函数同为必要构造函数 在重写父类的必要构造函数时,不需要添加 override 修饰符 class SomeClass { required init() { } } class SomeSubclass: SomeClass { required init() { } } 析构函数 Swift 会自动释放不再需要的实例以释放资源 Swift 通过自动引用计数(ARC)处理实例的内存管理 当引用计数为0时,系统会自动调用析构函数(不可以手动调用) 通常在析构函数中释放一些资源(如移除通知等操作) 析构函数的写法 //后面连()都没有 deinit { // 执行析构过程 } 示例练习 class Person { var name : String var age : Int init(name : String, age : Int) { print("Person-init") self.name = name self.age = age } deinit { print("Person-deinit") } } var person : Person? = Person(name: "Zhangsan", age: 18) person = nil
类介绍 Swift虽然推荐面向协议编程,但其也是一门面向对象开发的语言 面向对象的基础是类,类产生了对象(类的实例) Swift中用class关键字定义类 定义语法 class 类名 { // 定义属性和方法 } 举例 class Person { var name = "Zhangsan" var age = 10 var sex = "man" func say(){ print("人会说话") } } 解读 定义了一个名叫 Person的类 这个类有三个变量存储属性和一个方法 类的实例 结构体一样,最简单的是在名称后面加一个(),但默认没有成员构造函数 var p1 = Person() 访问属性和方法 可以用.来访问一个实例的属性和方法 访问时如果使用了赋值语句就是设置属性 //普通构造函数 var p1 = Person() //访问 p1.age //设置 p1.age = 30 p1.name p1.sex p1.say() 类是引用类型 不同于值类型,引用类型被赋值到一个常量,变量或者本身被传递到一个函数的时候它是不会被拷贝的,而使用的是同一个对某个实例的引用。 var p2 = p1 p2.name = "Lisi" p2.age = 20 p2.sex = "woman" //对p2的修改会影响原来的p1 p1.age p1.name p1.sex 特征运算符 因为类是引用类型,可能有很多常量和变量都是引用到了同一个类的实例。有时候需要找出两个常量或者变量是否引用自同一个实例,Swift提供了两个特征运算符来检查两个常量或者变量是否引用相同的实例。 相同于 ( ===) 不同于( !==) //特征运算符 let p3 = Person() p1 === p2 //true p1 === p3 //false p1 !== p3 //true 继承 一个类可以从另一个类继承方法、属性和其他的特性 当一个类从另一个类继承的时,继承的类就是子类,被继承的就是父类 继承的目的是为了代码复用 Do Not Repeat Yourself 案例 // 定义一个父类 class Vehicle { var currentSpeed = 0.0 func makeNoise() { print("Ba Ba") } } // 定义一个子类 class Bicycle: Vehicle { var hasBasket = false } // 定义一个子类 class Car: Vehicle { var banner = "皖B12345" } var vehicle = Vehicle() vehicle.makeNoise() let bicycle = Bicycle() bicycle.makeNoise() var car = Car() car.makeNoise() 重写(覆写) 所谓重写就是子类可以对继承自父类实例方法、类型方法、实例属性,类型属性进行覆盖 重写需要在前面加上 override 关键字 override 关键字执行时Swift 编译器会检查重写的类的父类(或者父类的父类)是否有与之匹配的声明来提供重写 class Car: Vehicle { var banner = "皖B12345" override func makeNoise() { print("Di Di") } } var car = Car() car.makeNoise() 防止被重写 可以通过final标记阻止这种行为:final var , final func , final class func class Vehicle { var currentSpeed = 0.0 func makeNoise() { print("Ba Ba") } //不能被子类重写 final func run() { print("Run Run") } } 类与结构体的对比 相同点 定义属性 定义方法 定义构造函数(init函数) 可以被扩展 遵循协议 类有而结构体没有的额外功能 继承 类型转换(子类 as 父类) 析构函数 引用计数 开发中类与结构体的使用场景 类和结构体都可以用来定义自定义的数据类型,结构体实例总是通过值来传递,而类实例总是通过引用来传递。 按照通用准则,当符合以下一条或多条情形时应考虑结构体,其他情况下,使用类。大部分的自定义的数据结构应该是类,而不是结构体。 要描述的数据类型中只有少量的简单数据类型的属性 要描述的数据类型在数据传递时要以复制的方式进行 要描述的数据类型中所有的属性在进行传递时需要以复制的方式进行 不需要继承另一个数据类型 比如: 定义几何形状的大小,封装了一个 width属性和 height属性,两者为 Double类型; 定义一定范围的路径,封装了一个 start属性和 length属性,两者为 Int类型; 定义三维坐标系的一个点,封装了 x , y 和 z属性,它们是 Double类型。
结构体的介绍 概念介绍 结构体(struct)是由一系列具有相同类型或不同类型的数据构成的数据集合 结构体是值类型 结构体既可以定义属性又可以定义方法 定义语法 struct 结构体名称 { // 属性和方法 } 举例 struct Person { var name = "Zhangsan" var age = 10 var sex = "man" func say(){ print("人会说话") } } 解读 定义了一个名叫 Person的结构体 这个结构体拥有两个存储属性 name、 age和 sex 这个结构体拥有一个方法say 结构体实例 实例化结构体最简单的是在结构体名字后面写上(),任何属性都被初始化为它们的默认值 var p1 = Person() 所有的结构体都有一个自动生成的成员构造函数来实例化结构体,可以使用它来初始化所有的成员属性 var p2= Person(name: "Lisi", age: 20, sex: "woman") 访问属性和方法 可以用.来访问一个结构体实例的属性和方法 访问时如果使用了赋值语句就是设置属性 //访问 p2.age //设置 p2.age = 30 //通过.调用结构体中的属性和方法 p1.name p1.age p1.sex p1.say() 结构体是值类型 值类型是一种当它被赋值给一个常量或者变量,或者被传递给函数时会被拷贝的类型 Swift 中的结构体(包括枚举)是值类型,它在代码传递中总是会被拷贝 //值类型拷贝 var p3 = p2 //此时改变p3并不会改变p2的值 p3.name = "Wangwu" p3.age = 30 p2.age //20 p2.name //Lisi p3.age //30 p3.name //Wangwu 常用的结构体 CGRect /* Rectangles. */ public struct CGRect { public var origin: CGPoint public var size: CGSize public init() public init(origin: CGPoint, size: CGSize) } CGSize /* Sizes. */ public struct CGSize { public var width: CGFloat public var height: CGFloat public init() public init(width: CGFloat, height: CGFloat) } CGPoint /* Points. */ public struct CGPoint { public var x: CGFloat public var y: CGFloat public init() public init(x: CGFloat, y: CGFloat) } 字符串,数组和字典的赋值与拷贝行为 Swift 中的 String , Array 和 Dictionary类型是作为结构体来实现的,这意味着String , Array 和 Dictionary在它们被赋值到一个新的常量或者变量,或它们本身被传递到一个函数或方法中的时候,其实是传递了拷贝。 OC中的 NSString, NSArray和 NSDictionary,它们是作为类来实现的,所以NSString , NSArray 和 NSDictionary实例总是作为一个引用而不是拷贝来赋值和传递。
枚举介绍 概念介绍 枚举定义了一个通用类型的一组相关的值,可以在代码中以一个安全的方式来使用这些值 Swift中的枚举是一等类型, 它可以像类和结构体一样增加属性和方法 在 C/OC 语言中枚举指定相关名称为一组整型值,而Swift 中的枚举更加灵活 不必给每一个枚举成员提供一个值 Swift 中的枚举可以提供的值类型有:字符串,字符,整型值,浮点值等 枚举类型的语法 使用enum关键词并且把它们的整个定义放在一对大括号内 enum SomeEnumeration { // enumeration definition goes here } enum Method{ case Add case Sub case Mul case Div } let selectMethod = Method.Sub switch (selectMethod){ case Method.Add: print("加法") case Method.Sub: print("减法") case Method.Mul: print("除法") case Method.Div: print("乘法") default: print("都不是") } 枚举的定义 定义方式一 case关键词表明新的一行成员值将被定义 不像 C 和 Objective-C 一样,Swift 的枚举成员在被创建时不会被赋予一个默认的整数值 下面的例子中,North,South,East和West不是隐式的等于0,1,2和3 enum CompassPoint { case North case South case East case West } 定义方式二:多个成员值可以出现在同一行上 enum CompassPoint { case North, South, East,West } 枚举赋值 枚举类型赋值可以是字符串/字符/整型/浮点型 如果有给枚举类型赋值,则必须在枚举类型后面明确说明具体的类型 enum CompassPoint : Int { case North = 1 case South = 2 case East = 3 case West = 4 } enum CompassPoint : Double { case North = 1.0 case South = 2.0 case East = 3.0 case West = 4.0 } enum CompassPoint : String { case North = "North" case South = "South" case East = "East" case West = "West" } 枚举类型推断 前面的例子中,在使用枚举的时候,是通过枚举.值的形式来访问的,其实由于Swift的类型推断非常强大,如果枚举类型确定了,在访问值的时候可以用.值的形式来访问 enum Method { case Add case Sub case Mul case Div func method(){ } } //已经明确a是一个Method类型 后面访问可以简写 let a:Method = .Add let selectMethod = Method.Sub switch (selectMethod){ case .Add: print("加法") case .Sub: print("减法") case .Mul: print("除法") case .Div: print("乘法") default: print("都不是") } 枚举的原始值 C/OC中枚举的本质就是整数,所以C/OC中的枚举是有原始值的,默认是从0开始,而Swift中的枚举默认是没有原始值的, 但是可以在定义时告诉系统让枚举有原始值 注意: 原始值区分大小写 通过rawValue可以获取原始值 通过rawValue返回的枚举是一个可选型,因为原始值对应的枚举值不一定存在 如果想指定第一个元素的原始值之后,后面的元素的原始值能够默认+1 , 枚举一定是 Int 类型 enum Planet:Int { case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune } let planet:Planet = .Mars //打印4 print(planet.rawValue) //p是一个可选型 let p = Planet(rawValue: 3) if let p = p { switch p { case .Mercury: print("Mercury") case .Venus: print("Venus") case .Earth: print("Earth") case .Mars: print("Mars") case .Jupiter: print("Jupiter") case .Saturn: print("Saturn") case .Uranus: print("Uranus") case .Neptune: print("Neptune") } }
闭包引入 计算1个数的平方 函数写法 func square(param:Int) -> Int{ return param * param } square(param:3) 闭包写法 let squareCloure = { (param:Int) -> Int in return param * param } squareCloure(3) 闭包含义 闭包是可以被传递和引用的一个独立模块 闭包能够捕获和存储定义在其上下文中的任何常量和变量,即闭合并包裹那些常量和变量,因此被称为“闭包” 闭包符合如下三种形式中的一种: 全局函数是一个有名字但不会捕获任何值的闭包 内嵌函数是一个有名字且能从其上层函数捕获值的闭包(函数中的嵌套函数知识点) 闭包表达式是一个轻量级语法,可以捕获其上下文中常量或变量值的没有名字的闭包 闭包和函数一样也是引用类型 简单案例 案例一 let demo= { print("Swift 闭包实例。") } demo() 案例二 let divide = {(val1: Int, val2: Int) -> Int in return val1 / val2 } let result = divide(200, 20) print (result) 闭包表达式 闭包表达式语法有如下的一般形式: { (parameters) -> (return type) in statements } 闭包表达式由一对{}开始与结束 由in关键字将闭包分割成两部分:参数与返回值、闭包体 闭包参数与函数参数的区别 形式参数不能提供默认值,其他和函数一样 闭包主要知识点 参数名称缩写 Swift 提供了参数名称的缩写功能,直接通过 $0,$1,$2来顺序调用闭包的参数 在闭包表达式中使用参数名称缩写,可以在闭包参数列表中省略对其定义 参数类型可以通过函数类型进行推断 在单行闭包的时候,return 关键字可以省略 参数名称省略以后,in 关键字也可以被省略 //从数组中筛选指出合适的数据组成新的数组 func getList(score:[Int], con:(Int)->Bool) -> [Int]{ var newScore:[Int] = [Int]() for item in score { if con(item) { newScore.append(item) } } return newScore } let newAarray = getList(score: [75,60,95,45,85], con:{(s:Int)->Bool in return s>80}) print(newAarray) 第一种简写: 省略 ->与返回类型(根据后面表达式可以推断返回值是一个Bool) let newAarray = getList(score: [75,60,95,45,85], con:{(s:Int) in return s>80}) 第二种简写:省略参数类型和括号(根据函数的参数可推断传进来的必然是Int) let newAarray = getList(score: [75,60,95,45,85], con:{s in return s>80}) 第三种简写:省略return关键字 let newAarray = getList(score: [75,60,95,45,85], con:{s in s>80}) 第四种简写:参数名称缩写,省略参数声明和in,改为$0 let newAarray = getList(score: [75,60,95,45,85], con:{$0>80}) 捕获 闭包可以从上下文环境中捕获常量、变量,并在自己的作用域内使用 Swift最简单的闭包形式是嵌套函数,也就是定义在其他函数的函数体内的函数,嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。 func makeIncrementor(forIncrement amount: Int) -> () -> Int { var runningTotal = 0 func incrementor() -> Int { runningTotal += amount return runningTotal } return incrementor } let incrementByTen = makeIncrementor(forIncrement: 10) // 返回的值为10 print(incrementByTen()) // 返回的值为20 print(incrementByTen()) // 返回的值为30 print(incrementByTen()) 尾随闭包 尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用 闭包是函数的最后一个参数 调用时,函数的 ) 可以前置到倒数第二个参数末尾,后面的参数直接使用 { // 执行代码 },形式参数标签也随之省略 将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性 //尾随闭包 func doSomething(info:String, clousre:(String)->Void){ clousre(info) } //不使用尾随闭包进行函数调用 doSomething(info: "Hello", clousre: { s in print(s) }) //使用尾随闭包进行函数调用 doSomething(info: "World") { s in print(s) } 逃逸闭包 闭包作为一个参数传递给一个函数 传入函数的闭包如果在函数执行结束之后才会被调用,那么这个闭包就叫做逃逸闭包 声明一个接受闭包作为形式参数的函数时,可以在形式参数的类型之前写上@escaping 来明确闭包是允许逃逸的 逃逸闭包会在函数结束后才执行 举例 //逃逸闭包:闭包可以超出函数的范围来调用 //存放没有参数、没有返回值的闭包 var closureArray :[()->Void] = [()->Void]() //定义一个函数,接收一个非逃逸闭包为参数 func nonEscapeClosure(closure:()->Void){ closure() } //定义一个函数,接收一个逃逸闭包为参数,将闭包并存储到一个数组里面去,并没有调用 func escapeClosure(closure: @escaping ()->Void){ print("函数开始") closureArray.append(closure) print("函数结束") } var x = 10 //打印10 print(x) nonEscapeClosure { x = 100 } //打印100 因为闭包在函数里面执行了 print(x) escapeClosure { x = 200 } //打印100 因为闭包逃逸了 没有在函数里面执行 print(x) closureArray.first?() //打印200 在函数外面调用了闭包 print(x) //尾随闭包常用于异步回调 自动闭包 一种自动创建的闭包,用于包装函数参数的表达式 不接受任何参数,被调用时会返回被包装在其中的表达式的值 在形式参数的类型之前加上@autoclosure关键字标识这是一个逃逸闭包 //自动闭包 func printIfTrue(predicate:@autoclosure ()->Bool){ if predicate() { print("is true") } else{ print("is false") } } //直接进行调用了,Swift 将会把 2 > 1 这个表达式自动转换为 () -> Bool。这样我们就得到了一个写法简单、表意清楚的表达式。 printIfTrue(predicate: 2>1) printIfTrue(predicate: 2<1) 闭包的循环引用 class NetworkTools: NSObject { /// 完成回调属性 var finishedCallBack: (()->())? /// 加载数据 /// - parameter finished: 完成回调 func loadData(finished: () -> ()) { self.finishedCallBack = finished working() } func working() { finishedCallBack?() } deinit { print("网络工具 88") } class ViewController: UIViewController { var tools: NetworkTools? override func viewDidLoad() { super.viewDidLoad() tools = NetworkTools() tools?.loadData() { print("加载数据完成,更新界面:", NSThread.currentThread()) weakSelf!.view.backgroundColor = UIColor.redColor() } } /// 与 OC 中的 dealloc 类似,注意此函数没有() deinit { print("控制器 88") } } Swift中解决循环引用的方式 方案一: 使用weak,对当前控制器使用弱引用 但是因为self可能有值也可能没有值,因此weakSelf是一个可选类型,在真正使用时可以对其强制解包(该处强制解包没有问题,因为控制器一定存在,否则无法调用所在函数) // 解决方案一: weak var weakSelf = self tools.loadData { print("加载数据完成,更新界面:", NSThread.currentThread()) weakSelf!.view.backgroundColor = UIColor.redColor() } 方案二: 和方案一类型,只是书写方式更加简单 可以写在闭包中,并且在闭包中用到的self都是弱引用 tools.loadData {[weak self] () -> () in print("加载数据完成,更新界面:", NSThread.currentThread()) self!.view.backgroundColor = UIColor.redColor() } 方案三: 使用关键字unowned 从行为上来说 unowned 更像OC中的 unsafe_unretained unowned 表示:即使它原来引用的对象被释放了,仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不能是 Optional 值,也不会被指向 nil tools.loadData {[unowned self] () -> () in print("加载数据完成,更新界面:", NSThread.currentThread()) self.view.backgroundColor = UIColor.redColor() }
函数的介绍 函数相当于OC中的方法 函数的格式如下 func 函数名(参数列表) -> 返回值类型 { 代码块 return 返回值 } func是关键字,多个参数列表之间可以用逗号,分隔,也可以没有参数 使用箭头->指向返回值类型 如果函数没有返回值,返回值为Void 并且-> 返回值类型部分可以省略 常见的函数类型 没有参数,没有返回值 func about() -> Void { print("iPhone Xs Max") } // 调用函数 about() // 简单写法 // 如果没有返回值,Void可以写成() func about1() -> () { print("iPhone Xs Max") } about1() // 如果没有返回值,后面的内容可以都不写 func about2() { print("iPhone Xs Max") } about2() 有参数,没有返回值 func call(phoneNum : String) { print("打电话给\(phoneNum)") } call("18888888888") 没有参数,有返回值 func readMessage() -> String { return "吃饭了吗?" } var str = readMessage() print(str) 有参数,有返回值 func sum(num1 : Int, num2 : Int) -> Int { return num1 + num2 } var result = sum(num1: 20, num2: 30) print(result) 返回值为复杂类型 import Foundation func triple(info:String) -> (name:String, age:Int) { let infos = info.components(separatedBy: ",") return (infos[0], Int(infos[1])!) } let p:(name:String, age:Int) = triple(info: "zhangsan,20") p.name p.age 函数的使用注意 注意一: 函数的参数虽然没有用var和let修饰,但它是常量,不能在函数内修改 func say(message:String) { //报错:Cannot assign to value: 'message' is a 'let' constant message = "Hello Swift" print("说话内容:\(message)") } 注意二: 每一个函数的形式参数都包含形式参数标签和形式参数名两部分 形式参数标签用在调用函数的时候 形式参数名用在函数的实现当中 在调用函数的时候每一个形式参数前边都要写形式参数标签 默认情况下,形式参数使用它们的形式参数名作为形式参数标签 如果不想要形式参数标签,可以在参数名称前加 _ func minus(num1 a :Int,num2 b:Int) -> Int { return a - b } minus(num1: 5, num2: 2) func multi(_ a :Int,_ b:Int) -> Int { return a * b } multi(2, 3) 注意三: 默认参数 某些情况,如果没有传入具体的参数,可以使用默认参数 func makecoffee(type :String = "卡布奇诺") -> String { return "制作一杯\(type)咖啡。" } let coffee1 = makecoffee(type: "拿铁") let coffee2 = makecoffee() 注意四: 可变参数 Swift中函数的参数个数可以变化,可接受不确定数量的参数 参数必须具有相同的类型 可以通过在参数类型名后面加入... 的方式来指示可变参数 func total(numbers:Int...) -> Int { var sum = 0 for i in numbers { sum += i } return sum } total() total(numbers:10) total(numbers:10,20) total(numbers:10,20,30) 注意五: 引用类型(指针的传递) 默认情况下,函数的参数是值传递.如果想改变外面的变量,则需要传递变量的地址 Swift提供的inout关键字可以实现 func swapInt(a: inout Int, b: inout Int) { let tmp = a a = b b = tmp } var a = 10 var b = 20 print("a=\(a), b=\(b)") swapInt(a: &a, b: &b) print("a=\(a), b=\(b)") 注意六: 函数的嵌套使用 Swift中函数可以嵌套使用 即函数中包含函数,但是不推荐该写法 // 函数的嵌套 let value = 55 func test() { func demo() { print("demo \(value)") } print("test") demo() } demo() // 错误 test() // 执行函数会先打印'test',再打印'demo' 函数的类型 函数类型的概念 函数是引用类型 每个函数都有属于自己的类型,由函数的参数类型和返回类型组成 有了函数类型以后,就可以把函数类型像Int、Double、Array来用 下面的例子中定义了两个函数:addTwoInts 和 multiplyTwoInts,这两个函数都传入两个 Int 类型参数,返回一个Int类型值,因此这两个函数的类型是 (Int, Int) -> Int // 定义两个函数 func addTwoInts(a : Int, b : Int) -> Int { return a + b } func multiplyTwoInt(a : Int, b : Int) -> Int { return a * b } 抽取两个函数的类型,并且使用 // 定义函数的类型 var mathFunction : (Int, Int) -> Int = addTwoInts // 使用函数的名称 mathFunction(10, 20) // 给函数类型变量赋值其他值 mathFunction = multiplyTwoInt // 使用函数的名称 mathFunction(10, 20) 函数作为函数的参数 // 将函数的类型作为函数的参数 func printResult(a : Int, b : Int, calculateMethod : (Int, Int) -> Int) { print(calculateMethod(a, b)) } printResult(a: 10, b: 20, calculateMethod: addTwoInts) printResult(a: 10, b: 20, calculateMethod: multiplyTwoInt) 函数作为函数的返回值 //定义两个函数 func addTwoInts(a : Int, b : Int) -> Int { return a + b } func multiplyTwoInt(a : Int, b : Int) -> Int { return a * b } //函数作为返回值 func getResult(a:Int) -> (Int, Int)->Int{ if a > 10 { return addTwoInts } else{ return multiplyTwoInt } } //调用返回的函数 getResult(a: 2)(10,20) getResult(a: 12)(10,20)
Any、AnyObject Any是一个空协议集合的别名,它表示没有实现任何协议,因此它可以是任何类型,包括类实例与结构体实例。可以表示任何类型,包括函数类型。 AnyObject是一个成员为空的协议,任何对象都实现了这个协议。可以表示任何类类型的实例。 类型转化符号 is : 使用类型检查操作符 ( is )来检查一个实例是否属于一个特定的子类。如果实例是该子类类型,类型检查操作符返回 true ,否则返回 false 。 as : 类型转换操作符( as? 或 as! )进行向下类型转换至其子类类型。 如果不确定向下转换类型是否能够成功,使用条件形式的类型转换操作符 ( as? ) 如果确定向下转换类型会成功时,使用强制形式的类型转换操作符( as! ) 例子 // 1.定义数组 let array : [Any] = [12, "zhangsan"] // 2.取出数组中的第一个和最后一个元素 let objcFirst = array.first! let objcLast = array.last! // 3.判断第一个元素是否是一个Int类型 if objcFirst is Int { print("是Int类型") } else { print("非Int类型") } // 4.转成真正的类型来使用 // 4.1.as? 将Any转成可选类型,通过判断可选类型是否有值,来决定是否转化成功了 let name = objcLast as? String print(name) // 结果:Optional("zhangsan") // 4.2.as! 将Any转成具体的类型,如果不是该类型,那么程序会崩溃 let name2 = objcLast as! String print(name2) // 结果:zhangsan
可选类型(Optional)的介绍 说明: 可选类型是Swift中比较晦涩难懂的一个知识点 先了解定义,多利用Xcode的提示来使用 随着学习的深入,慢慢理解其中的原理和好处 概念: 在OC开发中,如果一个变量暂时不使用,可以赋值为0(基本属性类型)或者赋值为nil(对象类型) 在Swift中,nil也是一个特殊的类型,与Int,String一样,又因为Swift是强类型语言,因此不能直接将nil赋值给其他类型的数据 开发中碰到nil在所难免.因此推出了可选类型 可选类型的取值: nil 有值 定义可选类型 定义一个可选类型有两种写法 最基本的写法 语法糖(常用) // 写法一:定义可选类型 let name : Optional<String> = nil // 写法二:定义可选类型,语法糖(常用) let name : String? = nil 可选类型的使用 演练一:给可选类型赋值 // 定义可选类型 var string : Optional<String> = nil // 给可选类型赋值 // 错误写法:因此该可选类型中只能存放字符串 string = 123 // 正确写法: string = "Hello world" // 打印结果 print(string) // 结果:Optional("Hello world")\n 因为是可选类型,所有会带Optional 演练二:取出可选类型的值(解包) // 取出可选类型的真实值 //!强制解包(显示解包) print(string!) // 结果:Hello world\n // 如果可选类型为nil,强制解包会出错 string = nil print(string!) // 报错 // 正确写法应该先判断是否为空 if string != nil { print(string!) } 演练三:取出可选类型的值 // 可选绑定(隐式解包) if let str = string { //此时输出的就是str的值,而不是Optional print(str) } 演练四:取出可选类型的值 guard let str = string else{ return } print(str) 真实应用场景 系统API大量使用 var array:[String] = [String]() array.append("A") array.append("B") array.append("C") //返回String? array1.first array1.last 让代码更加严谨 // 正确写法:使用可选类型来接收 let url : URL? = URL(string: "http://www.abc.edu.cn") // 该方式利用类型推导 let url = URL(string: "http://www.abc.edu.cn") // 通过url来创建request对象:在使用可选类型前要先进行判断是否有值 // 该语法成为可选绑定(如果url有值就解包赋值给tempURL,并且执行{}) if let tempUrl = url { let request = URLRequest(url: tempUrl) }
元组的介绍 元组是Swift中特有(Python中也有),OC中并没有类型 定义与含义 一种数据结构 可以用于定义一组数据 组成元组的数据可以称为“元素” 元组的定义 元组的常见写法 // 使用元组描述一个人的信息 var one = ("1001", "张三", 30, 90) // 给元素加上名称,之后可以通过名称访问元素 var two = (id:"1001", name:"张三", OC_score:80, iOS_score:90) 上面两种写法,查看一下one与two的类型有什么不同 var one: (String, String, Int, Int) var two: (id: String, name: String, OC_score: Int, iOS_score: Int) 元组的简单使用 用元组来描述一个错误信息 // 写法一: let error = (404, "Not Found") //下标访问 print(error.0) print(error.1) // 写法二: let error = (errorCode : 404, errorInfo : "Not Found") //别名访问 print(error.errorCode) print(error.errorInfo) // 写法三: //定义元组变量接收元素的值 let (errorCode, errorInfo) = (404, "Not Found") print(errorCode) print(errorInfo)
字典的介绍 字典是由键值(key:value)对组成的集合 字典中的元素之间是无序的 字典是由两部分集合构成的,一个是键集合,一个是值集合 字典是通过访问键间接访问值的 键集合是不能有重复元素的,而值集合是可以重复的 Swift中的字典类型是Dictionary,也是一个泛型集合 字典的初始化 Swift中的可变和不可变字典 使用let修饰的字典是不可变字典 使用var修饰的字典是可变字典 // 定义一个可变字典 var dict1 : [String : Any] = [String : Any]() // 定义一个不可变字典 let dict2 : [String : Any] = ["name" : "zhangsan", "age" : 18] 在声明一个Dictionary类型的时候可以使用下面的语句之一 var dict1: Dictionary<Int, String> var dict2: [Int: String] 声明的字典需要进行初始化才能使用,字典类型往往是在声明的同时进行初始化的 // 定时字典的同时,进行初始化 var dict:[String : Any] = ["name" : "zhangsan", "age" : 18] // Swift中任意类型用Any表示 var dict : Dictionary<String, Any> dict = ["name" : "zhangsan", "age" : 18] 字典的基本操作 获取长度 dict.count 判空 dict.isEmpty 添加数据 dict["height"] = 1.80 dict["weight"] = 70.0 print(dict) 删除字段 dict.removeValue(forKey: "height") print(dict) 修改字典 //方式一 dict["name"] = "lisi" //方式二 dict.updateValue("lisi", forKey: "name") print(dict) 查询字典 // 可选型 dict["name"] 字典的遍历 遍历字典中所有的值 for value in dict.values { print(value) } 遍历字典中所有的键 for key in dict.keys { print(key) } 遍历所有的键值对 //常用 for (key, value) in dict { print("\(key) --- \(value)") } 枚举方式遍历 //输出的不是key-value,而是索引和(key:value) for (index, value) in dic.enumerated() { print("\(index) -- \(value)") //0 -- (key: "name", value: "zhangsan") //1 -- (key: "age", value: 18) } 字典的合并 // 字典的合并 var dict1: [String : Any] = ["name" : "zhangsan", "age" : 20] var dict2: [String : Any] = ["height" : 1.80, "phoneNum" : "18888888888"] // 字典合并不能像数组那样直接用+ for (key, value) in dict2 { dict1[key] = value } print(dict1) //["phoneNum": "18888888888", "name": "zhangsan", "age": 20, "height": 1.8]
字符串的介绍 字符串在开发中使用得非常频繁 OC和Swift中字符串的区别 在OC中字符串类型是NSString,在Swift中字符串类型是String OC中字符串@"",Swift中字符串"" 使用 String 的原因 String 是一个结构体,性能更高 NSString 是一个 OC 对象,性能略差 String 支持直接遍历 字符的定义 定义不可变字符串 let str = "hello Objective-C" 定义可变字符串 var str = "hello Swift" 多行字符串 """ 静夜思 作者:李白 床前明月光,疑是地上霜。 举头望明月,低头思故乡。 """ 字符串的常用操作 长度 let str = "12345678" let len2 = str.count //8 let len3 = (str as NSString).length //8 拼接 +号 append let str1 = "abc" + "cde" var str = "abc" str.append("cde") 遍历 let str = "abcdefg" for char in str { print(char) } --------------------- let str = "abcdefg" for (index, value) in str.enumerated() { print("\(index) --- \(value)") } 大写或小写 let str = "abcdefg" print(str.lowercased()) //全部小写 print(str.uppercased()) //全部大写 含有字符串 let str = "abcdefg" print(str.contains("cd")) //true print(str.contains("c")) //true print(str.contains("o")) //false 串分割 import Foundation let str = "abcdefg$$aaaa$$bbbb$$ccc" let desc = str.components(separatedBy: "$$") for item in desc { print(item) } 替换 import Foundation let str = "abcdefg$$aaaa$$bbbb$$ccc" let dest11 = str.replacingOccurrences(of: "$$", with: "**") print(dest11) 子串 let str = "Hello World" //截取某字符串的前5个字符串 let sub1 = str.prefix(5) //截取某字符串的后5个字符串 let str1 = str.suffix(5) //也可以换种写法 let index2 = str.index(str.endIndex, offsetBy: -5) let sub2 = str[index2..<str.endIndex] //截取某字符串的第3个字符到第6个字符范围的字符串 let index3 = str.index(str.startIndex, offsetBy: 3) let sub3 = str[str.startIndex..<index3] let index4: String.Index = str.index(str.endIndex, offsetBy: -3) //从某个位置开始 到某个位置结束 let sub4 = str[index3..<index4]
什么是常量和变量 在Swift中规定:在定义一个标识符时必须明确说明该标识符是一个常量还是变量 使用let来定义常量,定义之后不可以修改 使用var来定义变量,定义之后可以修改 变量的基本使用 import UIKit let a : Int = 10 //报错——当一个字段定义为常量时不可以修改 // a = 20 var b : Int = 20 // 因为b定义为变量,因此是可以修改的 b = 30 常量和变量的使用注意: 注意: 在真正的开发过程中,建议先定义常量,如果需要修改再修改为变量(更加安全) 声明为常量不可以修改的意思是指针不可以再指向其他对象,但是可以通过指针拿到对象,修改其中的属性 //定义一个UIView let view : UIView = UIView() //定义一个普通UIView let redView : UIView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) redView.backgroundColor = UIColor.red view.addSubview(redView) // 枚举类型的用法:类型.枚举的值 let btn : UIButton = UIButton(type: UIButtonType.custom) btn.backgroundColor = UIColor.blue btn.setTitle("按钮", for: .normal) btn.frame = CGRect(x: 20, y: 20, width: 60, height: 30) view.addSubview(btn)
数组的介绍 数组(Array)是一堆有序的由相同类型元素构成的集合 数组中的元素是有序的,可重复出现 Swift用Array表示数组,是一个泛型集合(泛型后面会讲,现在可理解为指定数组里面放什么类型的数据) 与OC数组的区别 Array是一个结构体,而不是一个类 可以放普通类型 数组的初始化 数组分成:可变数组和不可变数组 使用let修饰的数组是不可变数组 使用var修饰的数组是可变数组 // 定义一个可变数组,必须初始化才能使用 var array1 : [String] = [String]() // 定义一个不可变数组 let array2 : [NSObject] = ["zhangsan", 18] 在声明一个Array类型的时候可以使用下列的语句之一 var stuArray1 : Array<String> //语法糖 var stuArray2 : [String] 声明的数组需要进行初始化才能使用,数组类型往往是在声明的同时进行初始化的 // 定义时直接初始化 var array = ["zhangsan", "lisi", "wangwu"] // 先定义,后初始化 var array : Array<String> array = ["zhangsan", "lisi", "wangwu"] 对数组的基本操作 获取长度 array.count 判空 array.isEmpty 添加数据 array.append("zhaoliu") 插入元素 array.insert("haojian", at: 0) 删除元素 array.removeFirst() 修改元素 array[0] = "wangqi" 取值 array[1] array.first 倒序 array.reverse() 数组的遍历 普通遍历 for i in 0..<array.count { print(array[i]) } for in方式 for item in array { print(item) } 设置遍历的区间 for item in array[0..<2] { print(item) } 元组方式遍历 let names = ["zhangsan", "lisi", "wangwu"] for (index, name) in names.enumerated() { print(index) print(name) } 数组的合并+ // 数组合并 // 注意:只有相同类型的数组才能合并 var array = ["zhangsan", "lisi", "wangwu"] var array1 = ["zhaoliu", "wangqi"] var array2 = array + array1; //虽然不报错,但是不建议一个数组中存放多种类型的数据 var array3 : [Any] = [2, 3.0, "zhangsan"] var array4 : [Any] = ["lisi", true] var array5 : [Any] = array3 + array4
Swift类型的介绍 Swift中的数据类型也有:整型/浮点型/Bool型/元组/枚举/结构体/对象类型等 基本类型 整型 有符号 Int8 : 有符号8位整型 Int16 : 有符号16位整型 Int32 : 有符号32位整型 Int64 : 有符号64位整型 Int : 和平台相关(默认,相当于OC的NSInteger) 无符号 UInt8 : 无符号8位整型 UInt16 : 无符号16位整型 UInt32 : 无符号32位整型 UInt64 : 无符号64位整型 UInt :和平台相关(默认,相当于OC的NSUInteger) 浮点型 Float : 32位浮点型 Double : 64浮点型(默认) Bool型 true : 真 false : 假 // 定义一个Int类型的变量m,并且赋值为10 var m : Int = 10 // 定义一个Double类型的常量n,并且赋值为3.14 let n : Double = 3.14 //定义一个Bool类型 let t : Bool = true Swift中的类型推导 Swift是强类型的语言,强类型语言也称为强类型定义语言。是一种总是强制类型定义的语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用 Swift中任何一个变量/常量都有明确的类型 注意: 如果定义一个标识符时有直接进行赋值,那么标识符后面的类型可以省略 Swift有类型推导,会自动根据后面的赋值来决定前面的标识符的数据类型 可以通过option+鼠标左键来查看变量的数据类型 查看类型 // 定义变量时没有指定明确的类型,但是因为赋值给i一个20是一个Int类型的数据.因此i为Int类型 var i = 20 // 错误写法:如果之后赋值给i一个浮点型数值,则会报错 // i = 30.5 // 正确写法 var j = 3.33 j = 6.66 Swift中基本运算 Swift中在进行基本运算时必须保证数据的类型一致,否则会出错 只有相同类型的数据才可以进行运算 Swift中没有隐式转换,数据类型的转化必须显示: 类型() Int类型转成Double类型:Double(标识符) Double类型转成Int类型:Int(标识符) let a = 10 let b = 3.14 // 错误写法 // let c = a + b // let c = a * b // 正确写法 let c = Double(a) + b let d = a + Int(b)
循环的介绍 在开发中经常会用到循环 常见C/OC的循环有:for/while/do while. Swift中对应的为:for/while/repeat while. for循环的写法 C风格 循环(淘汰) // 传统写法 已经在Swift3中淘汰 for var i = 0; i < 10; i++ { print(i) } for in 循环 for i in 0..<10 { print(i) } for i in 0...10 { print(i) } 特殊写法 如果在for循环中不需要用到下标i for _ in 0..<10 { print("hello") } while和repeate while循环 while循环 while的判断句必须有正确的真假,没有非0即真 while后面的()可以省略 var a = 0 while a < 10 { print(b) // a++已经在Swift3之后淘汰 a = a + 1 } repeat while循环 var b = 0 repeat { print(b) b = b + 1 } while b < 20
Playground是什么? 从Xcode6开始出现(Swift开始出现) 翻译为:操场/游乐场 对于学习Swift基本语法非常方便 所见即所得(快速查看结果) 语法特性发生改变时,可以快速查看 Playground Swift最基本的语法变化 第一个Playground 导入框架 import UIKit 定义标识符时,必须声明该标识符是变量还是常量 声明标识符的格式: 变量/常量关键字 名称 : 数据类型 语句结束可以不加; 如果同一行有多个语句,则依然需要加 但是不建议一行多条语句 Swift中的打印语句:print(打印的内容) 常见注释 单行注释 Swift 中的注释与C 语言的注释非常相似。 单行注释以双正斜杠(//)作为起始标记 // 注释内容 多行注释 其起始标记为单个正斜杠后跟随一个星号/* 终止标记为一个星号后跟随单个正斜杠*/ /* 这是一个, 多行注释 */ 和与 C 语言多行注释不同,Swift 的多行注释可以嵌套在其它的多行注释之中 /* 这是第一个多行注释的开头 /* 这是第二个被嵌套的多行注释 */ 这是第一个多行注释的结尾 */ 文档注释 Swift中添加文档注释较为简单 使用///可以为方法或者属性添加文档注释 /// 打电话给某人 func callPhone(phoneNum : String) { print("打电话给\(phoneNum)") } 分组注释 Swift中不可以再使用OC中的 #pragma mark - 如果打算对代码进行分组可以使用如下方式// MARK:-
简介 Swift 语言由苹果公司在 2014 年推出,用来撰写 Mac OS 和 iOS 应用程序 Apple WWDC 2014 横空出世 Swift 历史 2010 年 7 月,苹果开发者工具部门总监 Chris Lattner 开始着手 Swift 编程语言的设计工作 用一年时间,完成基本架构 Swift 大约历经 4 年的开发期,2014 年 6 月发表,目前版本4.2 2015年12月4日,苹果公司宣布其Swift编程语言开放源代码 Chris Lattner LLVM 项目的主要发起人与作者之一 Clang 编译器的作者 苹果公司『开发者工具』部门的主管 领导Xcode、Instruments等编译器团队 Swift的大部分基础架构由他完成 Apple -> Tesla -> Google Chris Lattner 特点 特点 从它的语法中能看到JavaScript、Python、Java等语言的影子 语法简单、代码简洁、使用方便 可与Objective-C混合使用(混合编译) 提供了类似 Java 的名字空间(namespace)、泛型(generic)、运算对象重载(operator overloading) 为什么设计Swift语言 专为安全 高效强大 重要性 苹果目前在大力推广Swift 很多企业招聘需要会Swift,以后Swift必将代替OC 学习建议: 先学习Swift基本语法 功能随着学习的深入再深入研究 资源网站 https://www.cnswift.org/ 书籍资源 王巍(onevcat)的电子书籍(收费)
iOS 8 之后,UINavigationController 为开发者提供了一些好用的功能,这些功能以前实现起来可能比较麻烦,而现在只需要一个属性就搞定了。 1. 横屏时,导航栏自动消失 self.navigationController.hidesBarsWhenVerticallyCompact = YES; 1.gif 2. 单击屏幕时,导航栏自动消失 self.navigationController.hidesBarsOnTap = YES; 2.gif 3. 弹出键弹时,导航栏自动消失 self.navigationController.hidesBarsWhenKeyboardAppears = YES; 3.gif 4. 滑动屏幕时,导航栏自动消失 self.navigationController.hidesBarsOnSwipe = YES; 4.gif 经测试,这个属性适用于WebView、UITableView、UICollectionView,但是注意: 导航条隐藏时会出现大黑条遮住状态栏,且导航条不会自动再显示出来; 当多个可滚动的控件同时出现的时候,离导航栏最近的那个视图滚动时会响应,其余滚动的时候不响应。
通过前文iOS开发之使用Git的基本使用(一)的学习,相信大家对如何将iOS项目通过Git传到GitHub账户上有了一个基本的了解,其过程是相对繁琐和容易出错的。本文将告诉大家借助工具来帮助我们实现这些操作,并对前文进行进一步补充。 一、两个软件 Visual Studio Code(以下简称vscode)—— 免费好用的编辑器 Mac上既免费又好用的编辑器比较少,这款微软出的还不错,用过Atom的同学对上手它可以说是零门槛,因为之前用Atom (GitHub专门为程序员推出的一个跨平台文本编辑器)来做Markdown编辑器,后来购买了MWeb,就将其删除了。其启动界面如下: vscode启动界面.png 下载地址: https://code.visualstudio.com/ Sourcetree —— 可视化界面的Git版本控制软件 之前我们都是在终端和Xcode中使用Git命令完成相应的操作,有了Sourcetree,就可以在可视化的窗口中进行操作,简单快捷。其启动界面如下: Sourcetree启动界面.png 下载地址: https://www.sourcetreeapp.com/ 使用它需要一个帐号,也可以跳过,教程: https://www.cnblogs.com/wmm007/p/9184490.html 二、gitignore 在iOS开发之使用Git的基本使用(一)中,我们将所有的文件都传输到了GitHub上,但是很多时候,我们既要把某些文件放到Git的管理范围中,又不能提交它们,怎么办?这时候可以在Git管理的根目录下创建一个特殊的.gitignore文件,然后把要忽略的东西填进去,这时候Git就会自动忽略这些文件。那么如何在.gitignore文件中填写需要忽略的文件?gitignore.io已经为我们准备了各种配置文件,只需要根据自己的情况组合一下就可以了。配置文件网站:https://www.gitignore.io/ gitignore网站.png 这里我以Xcode为例,点击Create,将产生的代码复制到vscode中并保存,文件名为gitignore,该文件保留备用。 三、使用Sourcetree进行版本管理 使用Sourcetree进行版本管理会方便很多,下面按照步骤讲解如何使用 创建自己的项目(以在桌面创建testSourceTree为例),将gitignore文件复制到该目录 创建项目.png 在终端切换到上面的目录,执行如下的命令,让gitignore文件隐藏mv gitignore .gitignore 隐藏文件.png 打开Sourcetree并将项目文件夹拖动其中,点击创建后会显示一个项目 拖入项目.png 双击项目进入主窗口,此时显示的文件尚未有版本信息,所以前面有个?,选中所有文件,点击左上角的提交,此时输入自己的GitHub邮箱,然后输入本次提交的日志并提交 commit.png 在GitHub上创建一个新的仓库testSourceTree,然后在Sourcetree的菜单中打开终端执行命令git remote add origin https://github.com/yungfan/testSourceTree.git 点击testSourceTree上面的推送按钮,选中master分支并确定 push.png 然后输入GitHub帐号和密码等待传输完成 输入帐号.png 刷新GitHub仓库,传输成功 传输成功.png 此时修改一下iOS中ViewController代码,打开Sourcetree,会提醒你有一个文件修改了 修改文件后.png 依次点击拉取、提交和推送,并输入相关日志 修改提交.gif 再次刷新GitHub仓库,修改成功 修改成功.png 至此,流程结束,是不是比之前要简单很多,方便很多?
现在越来越多的公司采用了Git作为版本管理工具,相对于传统的SVN,Git有着明显的优势,今天写一篇入门文章,教大家如何将自己写好的iOS项目通过Git传到GitHub账户上。 一、GitHub准备工作 在GitHub上创建一个属于自己的账户 首先打开 GitHub进行注册 GitHub网站.jpg 创建好帐号以后,登录并点击+创建一个新的repository 创建repository.jpg 简单的话,只需要填写如下信息 填写信息.jpg 然后点击Create按钮创建,此时会出现如下的界面 创建完成.jpg 这里面都是Git常用的命令,至此GitHub准备工作完成。 注意 复制图中箭头指向的那句指令 该网页不要关闭,备用 二、项目准备工作 在你的代码目录下(此处以桌面为例)建一个新的目录(文件夹,此处为testGit) 打开终端,切换到上面的文件夹,使用 git init命令初始化 初始化.jpg 用Xcode创建一个iOS项目,该项目就放在刚刚新建的文件夹下 在Xcode里面选择菜单Source Control下的commit 选择Xcode菜单.jpg 然后填写相关的日志信息后提交,至此项目准备工作完成。 填写日志.jpg 三、关联GitHub和项目代码 GitHub帐号和项目准备完毕后,如何将自己的代码上传到创建的GitHub账户下的repository呢?换句话说GitHub如何识别你的个人信息进行身份认证呢?这里就需要使用到SSH,一种基于RSA非对称加密的认证方式,具体步骤如下: 打开网站 https://help.github.com ,这个是GitHub的帮助网站,搜索ssh,选择红框的选项 搜索ssh.jpg 打开之后里面有教大家如何产生SSH key,主要用到一个命令:ssh-keygen -t rsa -b 4096 -C "your_email@example.com" 注意 使用的时候要将邮箱换成自己注册GitHub时的邮箱 新建一个终端,然后执行上面的命令,一直回车,直到看到如下的界面说明生成SSH key成功 生成SSH key.jpg 此时在用户文件夹下会产生一个ssh的隐藏文件夹,cd切换到该ssh目录下,可以通过ls -a可以看到该文件下有两个重要文件id_rsa和id_rsa.pub 查看SSH key.jpg 在终端直接使用pbcopy < id_rsa.pub命令复制一下密钥,此时密钥已经在剪贴板 重新打开一个GitHub网页,点击个人头像,打开Settings Settings.jpg 选择左边的SSH and PGP keys,然后点击右侧的New SSH key New SSH key.jpg 最后将密钥粘贴过去,添加SSH key 找到GitHub准备工作中让大家复制的指令,打开终端切换到项目文件夹,执行该命令 执行远程命名.jpg 打开Xcode选择菜单Source Control,点击的push按钮,然后输入自己的GitHub帐号和密码,等待上传完成 Xcode 上传代码.jpg 刷新GitHub准备工作中创建完成.jpg中的网页,此时的效果如下: 上传成功.jpg 当然你也可以添加一个Add a README,用Markdown语法介绍一下自己的项目,这样就完成了整个流程。 最终效果.png 注意 步骤 1-4 在同一台电脑只需要操作一次即可
在iOS开发中,开发"表单"界面,字段稍微多一点的一般都用UITableView来做,而XLForm就是这样一个框架,它是创建动态表格视图最牛逼的iOS库, 用它实现表单功能,非常简单,省心省力。但是很可惜,搜索了很多文章都只是翻译官方文档,很多人在使用该库的时候可能都被官方文档带走远了,不知道如何具体使用。正好最近也要用到这个库,所以写个入门使用文章供大家参考。 一、 导入项目 使用CocoaPods或者手动导入库文件,本人选择直接导入项目源文件的方式。 导入项目.png 二、改造表单ViewController 让ViewController继承自XLFormViewController,并重写下面的两个方法 @interface OneViewController : XLFormViewController @end @implementation OneViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self){ [self initializeForm]; } return self; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self){ [self initializeForm]; } return self; } @end 三、构造表单 - (void)initializeForm { // 设置是否显示Cell之间分界线 //self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; // 设置Section的高度 self.tableView.sectionHeaderHeight = 30; XLFormDescriptor * form;//form,一个表单只有一个 XLFormSectionDescriptor * section;//section,一个表单可能有多个 XLFormRowDescriptor * row; //row,每个section可能有多个row // Form form = [XLFormDescriptor formDescriptor]; // First section section = [XLFormSectionDescriptor formSection]; section.title = @"用户"; [form addFormSection:section]; // 普通文本 row = [XLFormRowDescriptor formRowDescriptorWithTag:@"username" rowType:XLFormRowDescriptorTypeText]; // 设置placeholder [row.cellConfig setObject:@"用户名" forKey:@"textField.placeholder"]; // 设置文本颜色 [row.cellConfig setObject:[UIColor redColor] forKey:@"textField.textColor"]; [section addFormRow:row]; // 密码 row = [XLFormRowDescriptor formRowDescriptorWithTag:@"password" rowType:XLFormRowDescriptorTypePassword]; // 设置placeholder的颜色 NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:@"密码" attributes: @{NSForegroundColorAttributeName:[UIColor greenColor], }]; [row.cellConfig setObject:attrString forKey:@"textField.attributedPlaceholder"]; [section addFormRow:row]; // Second Section section = [XLFormSectionDescriptor formSection]; section.title = @"日期"; [form addFormSection:section]; // 日期选择器 row = [XLFormRowDescriptor formRowDescriptorWithTag:@"birthday" rowType:XLFormRowDescriptorTypeDate title:@"出生日期"]; row.value = [NSDate dateWithTimeIntervalSinceNow:60*60*24]; [section addFormRow:row]; // Third Section section = [XLFormSectionDescriptor formSection]; section.title = @"头像"; [form addFormSection:section]; // 图片选择 row = [XLFormRowDescriptor formRowDescriptorWithTag:@"userpic" rowType:XLFormRowDescriptorTypeImage]; [section addFormRow:row]; // Fourth Section section = [XLFormSectionDescriptor formSection]; section.title = @"选择器"; [form addFormSection:section]; // 选择器 row = [XLFormRowDescriptor formRowDescriptorWithTag:@"sex" rowType:XLFormRowDescriptorTypeSelectorPush]; row.noValueDisplayText = @"暂无"; row.selectorTitle = @"性别选择"; row.selectorOptions = @[@"男",@"女",@"其他"]; row.title = @"性别"; [row.cellConfigForSelector setObject:[UIColor redColor] forKey:@"textLabel.textColor"]; [row.cellConfigForSelector setObject:[UIColor greenColor] forKey:@"detailTextLabel.textColor"]; [section addFormRow:row]; // Fifth Section section = [XLFormSectionDescriptor formSection]; section.title = @"加固"; [form addFormSection:section]; // 开关 row = [XLFormRowDescriptor formRowDescriptorWithTag:@"enforce" rowType:XLFormRowDescriptorTypeBooleanSwitch title:@"加固"]; [section addFormRow:row]; // Sixth Section section = [XLFormSectionDescriptor formSection]; [form addFormSection:section]; // 按钮 row = [XLFormRowDescriptor formRowDescriptorWithTag:@"conform" rowType:XLFormRowDescriptorTypeButton]; row.title = @"确定"; [section addFormRow:row]; self.form = form; } -(void)didSelectFormRow:(XLFormRowDescriptor *)formRow{ // 判断是不是点击了确定按钮 if([formRow.tag isEqualToString:@"conform"] && formRow.rowType == XLFormRowDescriptorTypeButton){ //获取表单所有到的值 NSDictionary *values = [self formValues]; NSLog(@"%@", values); } [super didSelectFormRow:formRow]; } //重写改该方法 上面的方法就不会调用了 //-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ // // NSLog(@"%s", __func__); // //} @end 四、效果图 效果图.png 五、总结 前面两步是官方文档中可以找到的,也很简单,关键在于initializeForm方法中具体构造表单的过程,这里有必要强调几点: XLFormViewController实现了UITableViewDataSource, UITableViewDelegate,并且持有一个UITableView,这个从该类的声明可以看出来,所以UITableView 、UITableViewDataSource, UITableViewDelegate中的方法都可以正常使用。 @interface XLFormViewController : UIViewController<UITableViewDataSource, UITableViewDelegate, XLFormDescriptorDelegate, UITextFieldDelegate, UITextViewDelegate, XLFormViewControllerDelegate> XLForm将表单抽象为Form,Section,Row三个层次,分别对应三个类 XLFormDescriptor * form;//form,一个表单只有一个 XLFormSectionDescriptor * section;//section,一个表单可能有多个 XLFormRowDescriptor * row; //row,每个section可能有多个row 每个表单中的具体信息最后都落脚到XLFormRowDescriptor中,通过它可以配置不同样式的表单项,通过构造函数的rowType指定具体的表单类型,该框架提供了非常丰富的rowType,具体可以参考官方文档说明。 更细化配置表单项就需要借助于XLFormRowDescriptor中的属性进行配置,常用的有 @property (nonatomic, readonly, nonnull) NSMutableDictionary * cellConfig; @property (nonatomic, readonly, nonnull) NSMutableDictionary * cellConfigForSelector; 这个配置的时候,往往有同学不知道具体如何才能设置属性,比如怎么设置表单输入框的placeholder?更进一步如何设置placeholder 的颜色。其实它用到了KVC,因为它们两个都是UITextField类中的属性,那么直接进入UITextField查找,发现如下信息: @property(nullable, nonatomic,copy) NSString *placeholder; @property(nullable, nonatomic,copy) NSAttributedString *attributedPlaceholder NS_AVAILABLE_IOS(6_0); 那么设置起来就是 [row.cellConfig setObject:@"用户名" forKey:@"textField.placeholder"]; [row.cellConfig setObject:attrString forKey:@"textField.attributedPlaceholder"]; 注意这里的key的写法,就是KVC的写法。其他的属性依此类推。 如何获取设置好的表单的值?其实非常简单,该框架提供一个方法formValues,它的返回类型是一个NSDictionary,其中key就是XLFormRowDescriptor设置时的Tag。可以直接在控制器中调用该方法获取表单值,上面的效果图设置后的表单信息如下: 表单信息.png
问题描述 接上一个话题,实现了TabBar的点击刷新以后,开始继续写完成功能,刷新UITableView,于是考虑到iOS 10以后,UIScrollView已经有UIRefreshControl的属性了,干脆用自带的写。于是就有了如下的代码: 添加UIRefreshControl到UITableView上去 UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; refreshControl.tintColor = [UIColor grayColor]; refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@"下拉刷新"]; [refreshControl addTarget:self action:@selector(refreshTabView) forControlEvents:UIControlEventValueChanged]; self.newsTableView.refreshControl = refreshControl; 下拉刷新事件 -(void)refreshTabView { //添加一条数据 [self.newsData insertObject:[self.newsData firstObject] atIndex:0]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.newsTableView reloadData]; if ([self.newsTableView.refreshControl isRefreshing]) { [self.newsTableView.refreshControl endRefreshing]; } }); } TabBar点击事件 -(void)doubleClickTab:(NSNotification *)notification{ //这里有个坑 就是直接用NSInteger接收会有问题 数字不对 //因为上个界面传过来的时候封装成了对象,所以用NSNumber接收后再取值 NSNumber *index = notification.object; if ([index intValue] == 1) { //刷新 [self.newsTableView.refreshControl beginRefreshing]; } } 此时的效果如下,直接下拉刷新可以,但是点击TabBar不可以: 刷新异常情况.gif 分析问题 经过Google帮助,终于知道原因,因为系统自带的UIRefreshControl有两个陷阱: 调用-beginRefreshing方法不会触发UIControlEventValueChanged事件; 调用-beginRefreshing方法不会自动显示进度圈。 也就是说,只是调用-beginRefreshing方法是不管用的,那么对应的需要做两件事: 手动设置UIRefreshControl的事件; 手动设置UITableView的ContentOffset,露出进度圈。 解决问题 只需要修改上面第3步中的代码如下: -(void)doubleClickTab:(NSNotification *)notification{ //这里有个坑 就是直接用NSInteger接收会有问题 数字不对 //因为上个界面传过来的时候封装成了对象,所以用NSNumber接收后再取值 NSNumber *index = notification.object; if ([index intValue] == 1) { //刷新 //animated不要为YES,否则菊花会卡死 [self.newsTableView setContentOffset:CGPointMake(0, self.newsTableView.contentOffset.y - self.newsTableView.refreshControl.frame.size.height) animated:NO]; [self.newsTableView.refreshControl beginRefreshing]; [self.newsTableView.refreshControl sendActionsForControlEvents:UIControlEventValueChanged]; } } 最终效果: 刷新正常情况.gif
需求 之前已经实现了自定义TabBar,如图所示: 自定义TabBar.jpeg 现在需要实现一个类似今日头条TabBar的功能 —— 如果继续点击当前TabBar的选中项,那么该界面需要刷新UITableView。 分析 既然已经自定义了TabBar,那么最简单的就是在自定义中给TabBar中需要的UITabBarButton添加事件 —— 点击就发送通知,并且将当前的索引传出去。对应的界面监听通知,拿到索引比对,如果和当前索引一致,就执行对应的操作。 实现 自定义TabBar的layoutSubviews中绑定事件 - (void)layoutSubviews { [super layoutSubviews]; for (UIButton * tabBarButton in self.subviews) { if ([tabBarButton isKindOfClass:NSClassFromString(@"UITabBarButton")]) { //监听tabbar的点击 //绑定tag 标识 tabBarButton.tag = index; //监听tabbar的点击 [tabBarButton addTarget:self action:@selector(tabBarButtonClick:) forControlEvents:UIControlEventTouchUpInside]; } } } 监听事件,发送通知 - (void)tabBarButtonClick:(UIControl *)tabBarBtn{ //判断当前按钮是否为上一个按钮 //再次点击同一个item时发送通知出去 对应的VC捕获并判断 if (self.previousClickedTag == tabBarBtn.tag) { [[NSNotificationCenter defaultCenter] postNotificationName: @"DoubleClickTabbarItemNotification" object:@(tabBarBtn.tag)]; } self.previousClickedTag = tabBarBtn.tag; } 对应的UIViewController监听通知 - (void)viewDidLoad { [super viewDidLoad]; [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(doubleClickTab:) name:@"DoubleClickTabbarItemNotification" object:nil]; } 监听到通知,比对后执行操作 -(void)doubleClickTab:(NSNotification *)notification{ //这里有个坑 就是直接用NSInteger接收会有问题 数字不对 //因为上个界面传过来的时候封装成了对象,所以用NSNumber接收后再取值 NSNumber *index = notification.object; if ([index intValue] == 1) { //刷新 } } 2018.4.28 补充 本文被转载后,有很多好心的读者进行批评指正:这种方式不够优雅,不够简单。怎么最简单呢?其实只要重写UITabBarController的代理就可以实现,方法如下 //这个是UITabBarController的代理方法 - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController{ // 判断哪个界面要需要再次点击刷新,这里以第一个VC为例 if ([tabBarController.selectedViewController isEqual:[tabBarController.viewControllers firstObject]]) { // 判断再次选中的是否为当前的控制器 if ([viewController isEqual:tabBarController.selectedViewController]) { // 执行操作 NSLog(@"刷新界面"); return NO; } } return YES; }
iOS开发中,创建View常见的两种方式一个是纯代码,一个是借助于XIB;创建ViewController常见的也有两种方式一个是纯代码,一个是借助于StoryBoard。 通过交流我发现很多童鞋对这些概念都很模糊,于是通过实验写一篇博客供参考。 重写View的如下方法 @implementation YFView -(instancetype)init{ self = [super init]; NSLog(@"%s", __func__); return self; } -(instancetype)initWithFrame:(CGRect)frame{ self = [super initWithFrame:(CGRect)frame]; NSLog(@"%s", __func__); return self; } -(instancetype)initWithCoder:(NSCoder *)aDecoder{ self = [super initWithCoder:aDecoder]; NSLog(@"%s", __func__); return self; } -(void)awakeFromNib{ [super awakeFromNib]; NSLog(@"%s", __func__); } -(void)layoutSubviews{ NSLog(@"%s", __func__); } @end 重写ViewController的如下方法 @implementation YFViewController -(void)loadView{ NSLog(@"%s", __func__); [super loadView]; } - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. NSLog(@"%s", __func__); self.view.backgroundColor = [UIColor redColor]; } -(void)viewDidAppear:(BOOL)animated{ NSLog(@"%s", __func__); [super viewDidAppear:animated]; } -(instancetype)init{ self = [super init]; NSLog(@"%s", __func__); return self; } -(instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{ self = [super initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil]; NSLog(@"%s", __func__); return self; } -(instancetype)initWithCoder:(NSCoder *)aDecoder{ self = [super initWithCoder:aDecoder]; NSLog(@"%s", __func__); return self; } -(void)awakeFromNib{ NSLog(@"%s", __func__); [super awakeFromNib]; } @end 结论总结 如果控制器是直接通过代码创建出来的,并且调用方法为[[YFViewController alloc]init]创建VC的生命周期为: -[YFViewController initWithNibName:bundle:] -[YFViewController init] -[YFViewController loadView] -[YFViewController viewDidLoad] -[YFViewController viewDidAppear:] 如果创建控制器时勾选了XIB,那么调用方法为[[YFViewController alloc]init]创建VC时的生命周期为: -[YFViewController initWithNibName:bundle:] -[YFViewController init] -[YFViewController loadView] -[YFViewController viewDidLoad] -[YFViewController viewDidAppear:] 如果是通过SB创建控制器,那么VC的生命周期为: -[ViewController initWithCoder:] -[ViewController awakeFromNib] -[ViewController loadView] -[ViewController viewDidLoad] -[ViewController viewDidAppear:] 如果纯代码创建UIView,调用方式为YFView *yfView = [[YFView alloc]init];那么它的生命周期为: -[YFView initWithFrame:] -[YFView init] -[YFView layoutSubviews] 如果纯代码创建UIView,调用方式为YFView *yfView = [[YFView alloc]initWithFrame:[UIScreen mainScreen].bounds];那么它的生命周期为: -[YFView initWithFrame:] -[YFView layoutSubviews] 如果通过XIB创建UIView,调用方式为NSArray *array = [[NSBundle mainBundle]loadNibNamed:@"YFView" owner:nil options:nil];[array lastObject];那么它的生命周期为: -[YFView initWithCoder:] -[YFView awakeFromNib] -[YFView layoutSubviews]
自我革命——发现问题 在开发中,一直有这样一种情境:App的未注册用户可以使用部分功能(访客视图),一旦需要使用一些核心功能或者获取个性化、差异化的服务时,就需要用户登录(登录定制)。一般的情况是: 用户点击某个按钮 ——> 弹出登录界面 ——> 输入信息 ——> 登录验证 ——> 界面发生变化 在几年前做开发时,由于项目需要快速上线,所以顾不上思考(其实是自己太菜),直接在需要判断登录的界面代码里写上如下代码: BOOL isLogin; if(self.isLogin){ //设置登录后的界面 } else{ //显示访客视图 //如果用户点击登录则跳转登录界面 //登录完以后更新当前界面为登录后的界面 } 时间一长,代码一多,就会发现很多界面有如上的重复代码,这很显然违背了Do not repeat yourself的原则,而且跳转的逻辑很烦。于是开始改进代码。 最先的一个哥们儿的做法是将登录字段抽取到一个常量中,需要时判断该值是否为真,然后执行相应的逻辑,这样每个类中就省去了一个字段。 if(CONSTANT.isLogin){ //设置登录后的界面 } else{ //显示访客视图 //如果用户点击登录则跳转登录界面 //登录完以后更新当前界面为登录后的界面 } 随着逻辑的增多,发现公用的代码越来越多,于是抽取了父类,这样逻辑就上移到了父类中,所有子类不需要有类似isLogin的属性来判断用户是否登录。 if(self.isLogin){ //设置登录后的界面 } else{ //显示访客视图 //如果用户点击登录则跳转登录界面 //登录完以后更新当前界面为登录后的界面 } 突然有一天,我们发现某些App访客视图相似度非常大,某些App中的登录界面也是一样的,于是发现上面的代码还是需要精简,于是我们将上面的else部分也上移到父类中,也就是在父类中判断用户是否登录,未登录显示访客视图,并且将用户的是否登录的字段isLogin抽离到一个专门的用户模型中。 摸着石头过河——解决问题 主界面架构是这样的: UITabBarController中嵌套UINavigationController.png 一开始,进入的时候,都显示访客界面,颜色为青色,点击中间的+弹出登录界面,点击登录界面的+表示登录过程,然后主界面显示登录后的各个界面,以不同颜色代替,如下图所示。 登录与访客.gif 主界面的搭建代码这里就不贴了,很简单,主要讲解一下登录与访客的逻辑实现。 父类视图控制器 @interface BaseViewController : UIViewController @property (nonatomic, assign) BOOL isLogin; @end @implementation BaseViewController -(void)loadView{ //UserModel是一个单例用户类 self.isLogin = [UserModel shareInstance].isLogin; //根据isLogin判断用户是否登录 如果未登录就显示访客视图 否则就显示正常界面 //但是注意 子视图同样需要在viewDidLoad中判断用户是否登录 //在登录界面 应该重新赋值window的rootViewController 刷新控制器的状态 self.isLogin ? [super loadView] : [self setupVisitorView]; } -(void)setupVisitorView{ UIView *view = [[UIView alloc]initWithFrame:[UIScreen mainScreen].bounds]; view.backgroundColor = [UIColor cyanColor]; self.view = view; } @end 子类视图控制器(四个子类都差不多,这里贴一个) @implementation FourViewController - (void)viewDidLoad { [super viewDidLoad]; if (self.isLogin) { self.view.backgroundColor = [UIColor purpleColor]; } } @end 中间视图控制器 @implementation CenterViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; [btn setImage:[UIImage imageNamed:@"plusX_Last"] forState:UIControlStateNormal]; btn.bounds = CGRectMake(0, 0, 64, 64); btn.center = self.view.center; [self.view addSubview:btn]; if (!self.isLogin) { self.title = @"未登录"; [btn addTarget:self action:@selector(login) forControlEvents:UIControlEventTouchUpInside]; } else{ self.title = @"已登陆"; [btn addTarget:self action:@selector(close) forControlEvents:UIControlEventTouchUpInside]; } } -(void)login{ [UserModel shareInstance].isLogin = YES; // 一定要重新赋值一次rootViewController [self dismissViewControllerAnimated:YES completion:^{ [UIApplication sharedApplication].keyWindow.rootViewController = [[YFTabbarController alloc]init]; }]; } -(void)close{ [self dismissViewControllerAnimated:YES completion:nil]; } @end
工作 1、日常工作还是以iOS为中心,被迫开始接触H5,但是前端不感兴趣啊…… 2、继续更新技术博客,质有没有下降不知道,量肯定比去年少,因为文章实在太多了,相同的或者太入门的都没写。今年只写了21篇文章,新收获791个粉丝。2018年开始会在其他平台 掘金 同步更新技术文章。 3、工作三年,体制内的工作环境让我开始对人,事,物有了新的认识,做人做事更加小心翼翼,对自己要求更加严格,努力做到无可替代。 学习 1、深入学习 Swift 语言。 2、开始接触Python,听说孩子以后会用得上,哈哈。 3、拾起N年前的基础知识,开始学习H5 相关内容。 4、继续巩固 J2EE 和 Android 的知识。 5、整理迭代了几十个文档,积累了很多总结性,技术型文档。 6、看了几本好书 。 生活 1、搬进了新家,终于有了自己的窝。 2、担心甲醛超标,过完年把孩子留在了安庆老家,国庆节才接过来,努力与孩子增进感情,效果不错。 3、双11前后,狠心给自己买了一个Airpods,iWatch和iPhone X,这对我来说算全年最大手笔了,败家了…… 4、当然还有一件喜事,那就是我又要当爸爸了,期待2018。 2018希望自己有更大进步,投入更多精力在家庭上,希望家人健健康康,快快乐乐!祝福大家新年快乐,万事如意!
Tomcat调优是一个老话题,目的都是为了提高站点的吞吐和并发。这里面涉及到Tomcat本身参数的优化和JVM优化。近期在研究JVM的参数设置和Tomcat集群,所以进行了一下调优实践。需要说明的是:本文的配置肯定不是最好的,仅仅是一次实践和一次记录。步骤可以参考,但参数设置需要针对不同需求的项目来进行调整。 1、安装APR(Apache Portable Runtime) Tomcat 7 以后 Connector 默认启用 APR 协议,但是只有配置了 APR库才可以生效,否则还是会使用 BIO 或者NIO方式。如何配置?可以直接去http://tomcat.apache.org/download-native.cgi下载,里面有32bit和64bit两种库,本人是64位机器,所以将x64文件夹下的两个文件tcnative-1.dll和tcnative-1-src.pdb拷贝到Tomcat的bin目录下,然后启动 Tomcat,如果有如图所示的输出,说明配置成功。 安装Apr后启动状态变化.PNG 为什么使用Apr? 官方如是说(翻译了一下):Tomcat 可以使用 APR 来提供出色的可伸缩性,性能以及与原生服务器技术的更好集成。APR 有许多用途,提供了包括对高级IO功能操作系统级功能和本地进程处理的访问。这些功能使得Tomcat成为一个通用的网络服务器,能够更好地与其他本地网络技术进行整合,并且使整个Java作为一个完整的web服务器平台更加可行。 网上有人测试过,APR相比较BIO、NIO,在处理高并发时效率更高。 2、配置Executor,增加线程数,提高并发能力 打开server.xml,找到被注释的一段 <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="150" minSpareThreads="4"/> 修改成 <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="500" //Tomcat 使用线程来处理请求,该值表示 Tomcat 能创建的最大的线程数 minSpareThreads="20" //最小空闲线程数,Tomcat 启动时的初始化的线程数,表示即使没有人使用也开这么多空线程等待,默认值是 10。 maxIdleTime="60000"/> //当服务的线程数超过最大线程数时,超过maxIdleTime的线程会被杀死回收直到线程数降低到最大数以内 3、配置Connector,设置超时、上传和缓存信息 同样在server.xml中,修改端口号为 8080 的Connector如下 <Connector executor="tomcatThreadPool" //指明上面的executor port="8080" protocol="HTTP/1.1" URIEncoding="UTF-8" connectionTimeout="30000" //网络连接超时,设置为 0 表示永不超时。单位:毫秒 enableLookups="false" //是否反查域名,以返回远程主机的主机名,取值为:true 或 false,如果设置为false,则直接返回IP地址,为了提高处理能力,应设置为 false disableUploadTimeout="false" //上传时是否使用超时机制 connectionUploadTimeout="150000"//上传超时时间 acceptCount="300" //指定当所有可以使用的处理请求的线程数都被使用时,可传入连接请求的最大队列长度,超过这个数的请求将不予处理 keepAliveTimeout="120000" //长连接最大保持时间,表示在下次请求过来之前,Tomcat 保持该连接多久,-1 为不限制超时。单位:毫秒 maxKeepAliveRequests="1" //表示在服务器关闭之前,该连接最大支持的请求数。超过该请求数的连接也将被关闭,1表示禁用,-1表示不限制个数 compression="on" //是否对响应的数据进行 GZIP 压缩,off:表示禁止压缩;on:表示允许压缩(文本将被压缩)、force:表示所有情况下都进行压缩,默认值为off compressionMinSize="2048" //表示压缩响应的最小值,只有当响应报文大小大于这个值的时候才会对报文进行压缩,如果开启了压缩功能 compressableMimeType="text/html,text/xml,text/javascript,text/css,text/plain,image/gif,image/jpg,image/png" //压缩类型,指定对哪些类型的文件进行数据压缩。 redirectPort="8443" /> 4、打开catalina.bat,打开JAVA_OPTS 进行JVM设置 如何设置JVM? 1、集成开发环境下启动并使用JVM,如eclipse需要修改根目录文件eclipse.ini; 2、Windows服务器下安装版Tomcat,可使用Tomcatw.exe工具(Tomcat目录下)和直接修改注册表两种方式修改JVM参数; 3、Windows服务器解压版Tomcat注册Windows服务,方法同上; 4、解压版本的Tomcat, 通过startup.bat启动Tomcat加载配置的,在Tomcat的bin 下catalina.bat 文件内添加; 5、Linux服务器Tomcat设置JVM,修改TOMCAT_HOME/bin/catalina.sh; 本人属于第4种,所以打开catalina.bat,在第二行添加如下信息 set JAVA_OPTS=-server -Xms4G -Xmx4G -Xss512k 5、检测设置是否生效 在设置之前,访问Tomcat首页,点击Server Status查看相关信息 未优化之前的信息.png 优化以后,再次访问查看,很明显发生了很大变化 优化之后的信息.png 参考文献 1、http://blog.csdn.net/centre10/article/details/50639693 2、http://blog.csdn.net/ldx891113/article/details/51735171 附录:JVM常用参数详解(网络摘录) -server:一定要作为第一个参数,在多个 CPU 时性能佳,还有一种叫 -client 的模式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或开发调试,在 32 位环境下直接运行 Java 程序默认启用该模式。Server 模式的特点是启动速度比较慢,但运行时性能和内存管理效率很高,适用于生产环境,在具有 64 位能力的 JDK 环境下默认启用该模式,可以不配置该参数。 -Xms:表示 Java 初始化堆的大小,-Xms 与-Xmx 设成一样的值,避免 JVM 反复重新申请内存,导致性能大起大落,默认值为物理内存的 1/64,默认(MinHeapFreeRatio参数可以调整)空余堆内存小于 40% 时,JVM 就会增大堆直到 -Xmx 的最大限制。 -Xmx:表示最大 Java 堆大小,当应用程序需要的内存超出堆的最大值时虚拟机就会提示内存溢出,并且导致应用服务崩溃,因此一般建议堆的最大值设置为可用内存的最大值的80%。如何知道我的 JVM 能够使用最大值,使用 java -Xmx512M -version 命令来进行测试,然后逐渐的增大 512 的值,如果执行正常就表示指定的内存大小可用,否则会打印错误信息,默认值为物理内存的 1/4,默认(MinHeapFreeRatio参数可以调整)空余堆内存大于 70% 时,JVM 会减少堆直到-Xms 的最小限制。 -Xss:表示每个 Java 线程堆栈大小,JDK 5.0 以后每个线程堆栈大小为 1M,以前每个线程堆栈大小为 256K。根据应用的线程所需内存大小进行调整,在相同物理内存下,减小这个值能生成更多的线程,但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右。一般小的应用, 如果栈不是很深, 应该是128k 够用的,大的应用建议使用 256k 或 512K,一般不易设置超过 1M,要不然容易出现out ofmemory。这个选项对性能影响比较大,需要严格的测试。 -XX:NewSize:设置新生代内存大小。 -XX:MaxNewSize:设置最大新生代新生代内存大小 -XX:PermSize:设置持久代内存大小 -XX:MaxPermSize:设置最大值持久代内存大小,永久代不属于堆内存,堆内存只包含新生代和老年代。 -XX:+AggressiveOpts:作用如其名(aggressive),启用这个参数,则每当 JDK 版本升级时,你的 JVM 都会使用最新加入的优化技术(如果有的话)。 -XX:+UseBiasedLocking:启用一个优化了的线程锁,我们知道在我们的appserver,每个http请求就是一个线程,有的请求短有的请求长,就会有请求排队的现象,甚至还会出现线程阻塞,这个优化了的线程锁使得你的appserver内对线程处理自动进行最优调配。 -XX:+DisableExplicitGC:在 程序代码中不允许有显示的调用“System.gc()”。每次在到操作结束时手动调用 System.gc() 一下,付出的代价就是系统响应时间严重降低,就和关于 Xms,Xmx 里的解释的原理一样,这样去调用 GC 导致系统的 JVM 大起大落。 -XX:+UseConcMarkSweepGC:设置年老代为并发收集,即 CMS gc,这一特性只有 jdk1.5 后续版本才具有的功能,它使用的是 gc 估算触发和 heap 占用触发。我们知道频频繁的 GC 会造面 JVM 的大起大落从而影响到系统的效率,因此使用了 CMS GC 后可以在 GC 次数增多的情况下,每次 GC 的响应时间却很短,比如说使用了 CMS GC 后经过 jprofiler 的观察,GC 被触发次数非常多,而每次 GC 耗时仅为几毫秒。 -XX:+UseParNewGC:对新生代采用多线程并行回收,这样收得快,注意最新的 JVM 版本,当使用 -XX:+UseConcMarkSweepGC 时,-XX:UseParNewGC 会自动开启。因此,如果年轻代的并行 GC 不想开启,可以通过设置 >-XX:-UseParNewGC 来关掉。 -XX:MaxTenuringThreshold:设置垃圾最大年龄。如果设置为0的话,则新生代对象不经过 Survivor 区,直接进入老年代。对于老年代比较多的应用(需要大量常驻内存的应用),可以提高效率。如果将此值设置为一 个较大值,则新生代对象会在 Survivor 区进行多次复制,这样可以增加对象在新生代的存活时间,增加在新生代即被回收的概率,减少Full GC的频率,这样做可以在某种程度上提高服务稳定性。该参数只有在串行 GC 时才有效,这个值的设置是根据本地的 jprofiler 监控后得到的一个理想的值,不能一概而论原搬照抄。 -XX:+CMSParallelRemarkEnabled:在使用 UseParNewGC 的情况下,尽量减少 mark 的时间。 -XX:+UseCMSCompactAtFullCollection:在使用 concurrent gc 的情况下,防止 memoryfragmention,对 live object 进行整理,使 memory 碎片减少。 -XX:LargePageSizeInBytes:指定 Java heap 的分页页面大小,内存页的大小不可设置过大, 会影响 Perm 的大小。 -XX:+UseFastAccessorMethods:使用 get,set 方法转成本地代码,原始类型的快速优化。 -XX:+UseCMSInitiatingOccupancyOnly:只有在 oldgeneration 在使用了初始化的比例后 concurrent collector 启动收集。 -Duser.timezone=Asia/Shanghai:设置用户所在时区。 -Djava.awt.headless=true:这个参数一般我们都是放在最后使用的,这全参数的作用是这样的,有时我们会在我们的 J2EE 工程中使用一些图表工具如:jfreechart,用于在 web 网页输出 GIF/JPG 等流,在 winodws 环境下,一般我们的 app server 在输出图形时不会碰到什么问题,但是在linux/unix 环境下经常会碰到一个 exception 导致你在 winodws 开发环境下图片显示的好好可是在 linux/unix 下却显示不出来,因此加上这个参数以免避这样的情况出现。 -Xmn:新生代的内存空间大小,注意:此处的大小是(eden+ 2 survivor space)。与 jmap -heap 中显示的 New gen 是不同的。整个堆大小 = 新生代大小 + 老生代大小 + 永久代大小。在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的 3/8。 -XX:CMSInitiatingOccupancyFraction:当堆满之后,并行收集器便开始进行垃圾收集,例如,当没有足够的空间来容纳新分配或提升的对象。对于 CMS 收集器,长时间等待是不可取的,因为在并发垃圾收集期间应用持续在运行(并且分配对象)。因此,为了在应用程序使用完内存之前完成垃圾收集周期,CMS 收集器要比并行收集器更先启动。因为不同的应用会有不同对象分配模式,JVM 会收集实际的对象分配(和释放)的运行时数据,并且分析这些数据,来决定什么时候启动一次 CMS 垃圾收集周期。这个参数设置有很大技巧,基本上满足(Xmx-Xmn)(100-CMSInitiatingOccupancyFraction)/100 >= Xmn 就不会出现 promotion failed。例如在应用中 Xmx 是6000,Xmn 是 512,那么 Xmx-Xmn 是 5488M,也就是老年代有 5488M,CMSInitiatingOccupancyFraction=90 说明老年代到 90% 满的时候开始执行对老年代的并发垃圾回收(CMS),这时还 剩 10% 的空间是 548810% = 548M,所以即使 Xmn(也就是新生代共512M)里所有对象都搬到老年代里,548M 的空间也足够了,所以只要满足上面的公式,就不会出现垃圾回收时的 promotion failed,因此这个参数的设置必须与 Xmn 关联在一起。 -XX:+CMSIncrementalMode:该标志将开启 CMS 收集器的增量模式。增量模式经常暂停 CMS 过程,以便对应用程序线程作出完全的让步。因此,收集器将花更长的时间完成整个收集周期。因此,只有通过测试后发现正常 CMS 周期对应用程序线程干扰太大时,才应该使用增量模式。由于现代服务器有足够的处理器来适应并发的垃圾收集,所以这种情况发生得很少,用于但 CPU情况。 -XX:NewRatio:年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值(除去持久代),-XX:NewRatio=4 表示年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1/5,Xms=Xmx 并且设置了 Xmn 的情况下,该参数不需要进行设置。 -XX:SurvivorRatio:Eden 区与 Survivor 区的大小比值,设置为 8,表示 2 个 Survivor 区(JVM 堆内存年轻代中默认有 2 个大小相等的 Survivor 区)与 1 个 Eden 区的比值为 2:8,即 1 个 Survivor 区占整个年轻代大小的 1/10。 -XX:+UseSerialGC:设置串行收集器。 -XX:+UseParallelGC:设置为并行收集器。此配置仅对年轻代有效。即年轻代使用并行收集,而年老代仍使用串行收集。 -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集,JDK6.0 开始支持对年老代并行收集。 -XX:ConcGCThreads:早期 JVM 版本也叫-XX:ParallelCMSThreads,定义并发 CMS 过程运行时的线程数。比如 value=4 意味着 CMS 周期的所有阶段都以 4 个线程来执行。尽管更多的线程会加快并发 CMS 过程,但其也会带来额外的同步开销。因此,对于特定的应用程序,应该通过测试来判断增加 CMS 线程数是否真的能够带来性能的提升。如果还标志未设置,JVM 会根据并行收集器中的 -XX:ParallelGCThreads 参数的值来计算出默认的并行 CMS 线程数。 -XX:ParallelGCThreads:配置并行收集器的线程数,即:同时有多少个线程一起进行垃圾回收,此值建议配置与 CPU 数目相等。 -XX:OldSize:设置 JVM 启动分配的老年代内存大小,类似于新生代内存的初始大小 -XX:NewSize。
1、配置多个Tomcat 复制本机上的Tomcat,分别为apache-tomcat-8.0.26-1,apache-tomcat-8.0.26-2,apache-tomcat-8.0.26-3,表示三个Web服务器,如下图所所示: 三个tomcat.PNG 然后修改三个Tomcat的server.xml配置文件,修改它们的端口分别如下表所示: 取值 Server Connector Connector 默认 8005 8080 8443 8009 8443 apache-tomcat-8.0.26-1 8015 8081 8443 8019 8443 apache-tomcat-8.0.26-2 8025 8082 8443 8029 8443 apache-tomcat-8.0.26-3 8035 8083 8443 8039 8443 然后分别启动三台Tomcat,可以顺利启动表示修改成功,然后分别通过浏览器来分别访问http://localhost:8081/、http://localhost:8082/、http://localhost:8083/都可以访问到Tomcat的首页。 2、配置Nginx 下载稳定版Nginxnginx-1.12.2并解压,找到并修改nginx.conf配置文件,主要添加如下代码来配置集群和Nginx: #服务器的集群,配置三个Tomcat upstream tomcatCluster { #服务器集群名字 server 127.0.0.1:8081 weight=1;#服务器配置,weight是权重的意思,权重越大,分配的概率越大。 server 127.0.0.1:8082 weight=2; server 127.0.0.1:8083 weight=3; } #Nginx的配置 server { listen 80;#监听80端口,可以改成其他端口 server_name localhost;#当前服务的域名 location / { root html; index index.html index.htm; proxy_pass http://tomcatCluster; #这里的名字和upstream后面的名字一致 } 在cmd下切换到nginx-1.12.2的目录,启动Nginx 启动Nginx.PNG 如果没有报任何错误,说明启动成功,也可以去任务管理器看一下有没有该进程来验证一下。此时在浏览器输入http://localhost/,可以打开Tomcat欢迎界面。 配置Nginx后访问服务器.PNG 至此,Nginx配置多个Tomcat服务器已经完成,但是存在一个问题,就是Session共享问题。写一个简单的JSP界面test.jsp来输出Session信息,将该文件分别放到三个Tomcat的webapps\ROOT下 <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>Session共享测试</title> </head> <body> <p>SessionID:<%=session.getId()%></p> <p>SessionIP:<%=request.getServerName()%></p> <p>SessionPort:<%=request.getServerPort()%></p> </body> </html> 然后通过http://localhost/test.jsp多访问几次,看看输出的Session信息是否一致,很显然是不一致的。 Session不一致.gif 因为Session并没有共享,那么多个服务器的Session如何进行共享?请看下面的配置。 3、配置Memcached (1) 下载memcached-win64-1.4.4-14,并解压,然后在cmd下切换到该目录,执行memcached.exe –d install 安装windows服务,然后启动服务。 安装memcached服务器.PNG (2) 关闭启动的Tomcat ,将下图的jar包放入三个Tomcat的lib目录下 需要的jar包.PNG (3) 配置三个Tomcat的context.xml,加入如下的代码 <Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager" memcachedNodes="n1:127.0.0.1:11211" lockingMode="auto" sticky="false" requestUriIgnorePattern= ".*\.(png|gif|jpg|css|js)$" sessionBackupAsync= "false" sessionBackupTimeout= "100" copyCollectionsForSerialization="true" transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory" /> 重新启动三个Tomcat,再次访问http://localhost/test.jsp,结果如下,此时结果一致。 Session一致.gif 4、总结 前2步只要稍加注意应该问题不大(本人一次性成功) 第3步我先下载了最新版的Tomcat 8.5,然后按照文中的配置,Tomcat启动一直报错,应该是jar包版本老了,这些jar包我也是从网上找来的,后来降低了Tomcat版本,成功了。所以注意Tomcat的版本与Memcached的Jar包对应。 第3步配置好了以后,如果立马刷新网页,发现还是不一致,过一会会就好了 所有资料共享地址:http://pan.baidu.com/s/1c15ldEK
由于要批量生成Excel,并且模板已经给定。所以尝试用POI来操作一下。由于自己用的Office版本大于2007,所以在操作的时候遇到各种坑,特此记录一下~~~ 注意点 1、由于POI对于Office2003和2007之后的版本对应的jar包不一样,操作的类也不一样,所以在操作之前一定要准确找到不同版本的Excel对应的jar包。 2、编译时不报错不代表运行不报错,很多操作都是POI在操作Excel时出现问题的,所以除了添加代码中需要Import类所在的jar包之外,必须要加入额外的jar包。 操作2007以上需要的jar包 必须的jar包.jpg 这些jar文件都不需要自己单独下载,只要去Apache下载 poi-bin-3.16-20170419.zip 就有了全部的资料。 对应的jar包.png 示例代码 import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.xssf.usermodel.XSSFCell; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; public class POIExcel { public static void main(String[] args) throws IOException, InvalidFormatException { // 获取Excel模板文件 File file = new File("D:\\座位分布图模板.xlsx"); // 读取Excel模板 XSSFWorkbook wb = new XSSFWorkbook(file); // 读取了模板内sheet的内容 XSSFSheet sheet = wb.getSheetAt(0); // 在相应的单元格进行(读取)赋值 行列分别从0开始 XSSFCell cell = sheet.getRow(14).getCell(8); cell.setCellValue("张三"); // 修改模板内容导出新模板 FileOutputStream out = new FileOutputStream("D:/座位分布图.xlsx"); // 关闭相应的流 wb.write(out); out.close(); wb.close(); } }
根据之前所说,学习一款MVC框架,必须要知道文件怎么上传的,那么今天就来学习一下,SpringMVC是如何进行文件上传的,需要进行哪些配置,代码是如何编写的。 首先是web.xml的配置 <?xml version="1.0" encoding="UTF-8"?> <servlet> <servlet-name>upload</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>upload</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!—加入过滤器就是为了上传名字是中文的文件时 上传到服务器上的文件名不会乱码--> <filter> <filter-name>SpringCharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>SpringCharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> SpringMVC的配置文件upload-servlet.xml <?xml version="1.0" encoding="UTF-8"?> <context:component-scan base-package="com.yf.upload"/> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <!-- SpringMVC上传文件时,需配置MultipartResolver处理器 --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!-- 指定所上传文件的总大小不能超过200KB。注意maxUploadSize属性的限制不是针对单个文件,而是所有文件的容量之和 --> <property name="maxUploadSize" value="200000"/> </bean> <!-- SpringMVC在超出上传文件限制时,会抛出org.springframework.web.multipart.MaxUploadSizeExceededException --> <!-- 该异常是SpringMVC在检查上传的文件信息时抛出来的,而且此时还没有进入到Controller方法中 --> <bean id="exceptionResolver"class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="exceptionMappings"> <props> <!-- 遇到MaxUploadSizeExceededException异常时,自动跳转到/WEB-INF/jsp/error_fileupload.jsp页面 --> <prop key="org.springframework.web.multipart.MaxUploadSizeExceededException">error_fileupload</prop> </props> </property> </bean> </beans> 用于上传的表单页面/WEB-INF/jsp/upload.jsp <%@ page language="java" pageEncoding="UTF-8"%> <form method="POST" enctype="multipart/form-data"> yourfile: <input type="file" name="myfiles"/><br/> yourfile: <input type="file" name="myfiles"/><br/> yourfile: <input type="file" name="myfiles"/><br/> <input type="submit" value="上传文件"/> </form> 上传文件过大时的页面/WEB-INF/jsp/error_fileupload.jsp <%@ page language="java" pageEncoding="UTF-8"%> <h1>文件过大,请重新选择</h1> 最后是核心的UploadController.java /** * SpringMVC中的文件上传步骤: * @第一步:由于SpringMVC使用的是commons-fileupload实现,故将其组件引入项目中,这里用到的是commons-fileupload-1.2.2.jar和commons-io-2.0.1.jar * @第二步:在####-servlet.xml中配置MultipartResolver处理器。可在此加入对上传文件的属性进行限制 *@第三步:在Controller的方法中添加MultipartFile参数。该参数用于接收表单中file组件的内容 *@第四步:编写前台表单。注意enctype="multipart/form-data"以及<input type="file" name="**"/> name要和后台接收参数时的名字相同 */ @Controller public class TestController { @RequestMapping(value="/uploadfile",method=RequestMethod.GET) public String upLoadFile() { return "upload"; } @RequestMapping(value="/uploadfile",method=RequestMethod.POST) public String upLoadFile(@RequestParam MultipartFile[] myfiles, HttpServletRequest request) throws IOException { //如果只是上传一个文件,则只需要MultipartFile类型接收文件即可,而且无需显式指定@RequestParam注解 //如果想上传多个文件,那么这里就要用MultipartFile[]类型来接收文件,并且还要指定@RequestParam注解 //并且上传多个文件时,前台表单中的所有<input type="file"/>的name都应该是myfiles,否则参数里的myfiles无法获取到所有上传的文件 for(MultipartFile myfile : myfiles) { if(myfile.isEmpty()) { System.out.println("文件未上传"); continue; } else { System.out.println("文件长度: " + myfile.getSize()); System.out.println("文件类型: " + myfile.getContentType()); System.out.println("文件名称: " + myfile.getName()); System.out.println("文件原名: " + myfile.getOriginalFilename()); System.out.println("==================="); //如果用的是Tomcat服务器,则文件会上传到\%TOMCAT_HOME%\webapps\YourWebProject\WEB-INF\upload\文件夹中 String realPath = request.getSession().getServletContext().getRealPath("/WEB-INF/upload"); //这里不必处理IO流关闭的问题,因为FileUtils.copyInputStreamToFile()方法内部会自动把用到的IO流关掉,我是看它的源码才知道的 FileUtils.copyInputStreamToFile(myfile.getInputStream(), new File(realPath + "/" +myfile.getOriginalFilename())); } } return " success_fileupload "; } } 上传文件成功的页面/WEB-INF/jsp/success_fileupload.jsp <%@ page language="java" pageEncoding="UTF-8"%> <h1>恭喜你,文件上传成功</h1>
基本介绍 数据回显:模型数据导向视图(模型数据 ---> Controller ---> 视图)说明:SpringMVC在调用方法前会创建一个隐含的模型对象,作为模型数据的存储容器(隐含模型) 一、ModelAndView @RequestMapping(method = RequestMethod.POST) public ModelAndView createUser(User user) { userService.createUser(user); ModelAndView mav = new ModelAndView(); mav.setViewName("user/createSuccess"); mav.addObject("user", user); return mav; } 二、@ModelAttribute 1、如果是用在方法的入参上 @RequestMapping(value = "/handle1") public String handle1(@ModelAttribute("user") User user) { user.setUserId("1000"); return "/user/createSuccess"; } 处理机制如下: A.将HTTP请求参数绑定到User对象中 B.以user为键,将User对象放到数据模型中 --->相当于map.put("user", user); 2、如果是定义在方法之上 @ModelAttribute("user") public User getUser(){ User user = new User(); user.setUserId("1001"); return user; } @RequestMapping(value = "/handle2") public String handle2(@ModelAttribute("user") User user) { user.setUserName("tom"); return "/user/showUser"; } 处理机制如下: 调用目标方法前,扫描控制器中所有方法上包含@ModelAttribute的方法,并将方法的返回值放到模型数据中 (类似于上述的第二步) 执行指定的方法,如果入参有@ModelAttribute,则将(1)中放置于模型数据中的对象赋值给入参的绑定对象 根据HTTP请求继续对绑定对象进行填充和覆盖,得到一个整合版的对象,覆盖模型数据中的原对象 三、Map Model(org.springframework.ui.Model和java.util.Map) SpringMVC一旦发现处理方法有Map或Model类型的入参,就会将隐含模型中对象的引用传给这些入参 @RequestMapping(value = "/handle3") public String handle3(ModelMap modelMap) { modelMap.addAttribute("testAttr","value1"); User user = (User)modelMap.get("user"); user.setUserName("tom"); return "/user/showUser"; } 四、SessionAttributes(SpringMVC会将模型中对应的属性暂存到HttpSession中) @Controller @RequestMapping("/user") @SessionAttributes(“user”) ① 将②处的模型属性自动保存到HttpSession中 public class UserController { @RequestMapping(value = "/handle4") public String handle4(@ModelAttribute(“user”) User user) ② { user.setUserName("John"); return "redirect:/user/handle5.html"; } @RequestMapping(value = "/handle5") public String handle5(ModelMapmodelMap,SessionStatussessionStatus) { User user = (User)modelMap.get(“user”); ③读取模型中的数据 if(user != null) { user.setUserName("Jetty"); sessionStatus.setComplete(); ④让Spring MVC清除本处理器对应的会话属性 } return "/user/showUser"; } } 上面在向handle4发送请求时,抛异常,因为@ModelAttribute 和 @SessionAttributes的处理遵循一个流程: 1、SpringMVC 在调用处理方法前,在请求线程中自动创建一个隐含的模型对象 2、调用所有方法上标注了@ModelAttribute的方法,并将返回值添加到隐含模型对象中 3、查看Session中是否存在@SessionAttributes("XXX")所指定的XXX属性,如果有,将其添加到隐含对象中,此时若隐含对象已经存在了XXX属性,则会覆盖掉 4、对于入参标注了@ModelAttribute(XXX)的方法,处理流程如下: (1) 如果隐含模型中拥有了XXX属性,将其赋值给入参,再将请求消息填充该对象,然后返回;如果隐含模型中不存在XXX,则执行(2) (2) 如果XXX是会话属性,则尝试从从会话中取出该属性,将其复制给入参再将请求消息填充该对象。如果会话对象中找不到XXX属性,则抛出HttpSessionRequiredException异常;如果隐含模型和会话中均不在XXX,则执行(3) (3) 创建入参对象的实例,再用请求消息填充该对象。 由于上面的例子不满足4(2),所以抛出异常,那么可以定义一个带方法级@ModelAttribute的方法,那样就不会执行到4(2),执行到4(1)就已经找到了需要的数据。 SpringMVC 的数据流图总结 1.HTTP请求报文到达服务器,服务器将其封装成HTTPServletRequest对象2.SpringMVC框架截获这个HTTPServletRequest对象3.SpringMVC 创建一个隐含模型对象,作为处理本次请求的上下文数据存放处4.SpringMVC 将一个HTTPServletRequest对象数据绑定到处理方法的入参对象中(表单/命令对象)5.将绑定错误信息、校验错误信息都保存到隐含模型中6.本次请求的对象模型数据放到HTTPServletRequest的属性列表中,暴露给视图对象7.视图对象对已经放在HTTPServletRequest属性列表的模型数据进行渲染8.把渲染后的HTTP响应报文发送给客户端
学习方法 当需要学习一个新的MVC框架需要从以下方面入手: 1、环境搭建(能输出Hello Word)2、如何传递参数到Controller,Controller如何获取参数3、如何从Controller传递Model到View4、如何完成文件的上传5、如何完成验证6、异常的处理7、深入学习一些原理和源代码的学习 分享一下如何传递参数到Controller,Controller如何获取参数 SpringMVC如何进行参数绑定 一、什么是控制器 在一个POJO类定义处标注@Controller,再通过<context:component-scan />扫描响应的包,就可以使POJO类成为一个能处理HTTP请求的控制器 二、请求映射到控制器 用@RequestMapping,可以定义在类和方法上,将一个请求映射到控制器中的方法上。 三、几种典型的处理请求的方法(Controller如何获取请求参数) 1、请求的参数按名称匹配的方式绑定到入参中,返回字符串代表逻辑视图名 @RequestMapping(value="/handle1") public String handle1(@RequestParam("userName") String userName, @RequestParam("password") String password, @RequestParam("realName") String realName) { ... return success; } @RequestParam 用来请求request中的参数,相当于request.getParameter() 其中有三个属性: value用来指定要取得参数的key; required 所取的参数是否必须存在,如果true表示必须存在,若不存在则抛出异常; default 指定该value的默认值。 这一种方式说明参数是URL的一部分,此时也可以去掉@RequestParam,直接写参数就可以了 public String handle1(String userName, String password, String realName) 2、将cookie的值及报文头部属性绑定到入参,方法返回ModelAndView @RequestMapping(value="/handle2") public ModelAndView handle2(@CookieValue("JSESSIONID") String sessionId, @RequestHeader("Accept-Language") String accpetLanguage) { ModelAndView mav = new ModelAndView(); mav.setViewName("success"); mav.addObject("user", new User()); return mav; } 3、请求的参数按名称匹配的方式绑定到 user 属性中,返回字符串代表逻辑视图名 @RequestMapping(value="/handle3") public String handle3(User user) { ... return success; } 按“HTTP请求参数名= 命令/表单对象的属性名”的规则,自动绑定请求数据,自动进行基本类型数据转换 4、直接使用Servlet API 对象作为处理方法的入参,返回字符串代表逻辑视图名 @RequestMapping(value="/handle4") public String handle4(HttpServletRequest request) { ... return success; } 5、以REST技术向springMVC传递参数使用占位符 public ModelAndView show(@PathVariable("userid") String userid) { ModelAndView mav = new ModelAndView(); mav.setViewName("success"); mav.addObject("user", new User()); return mav; } 用@PathVariable来把占位符参数绑定到控制器处理方法的入参中 注意:占位符中的{xxx}要和@PathVariable("xxx")的名字一样 6、还可以使用一些其他的参数,如IO流等
尴尬:access denied for user ’root’@’localhost’ using password yes 有时候,在连接 MySQL 数据的时候,会报一个错误信息 "access denied for user ’root’@’localhost’ using password yes",看到这个错误不要一脸懵逼,造成错误的原因就是数据库访问的用户名或密码不正确,这时候一般又分为以下两种情况,分别说一说解决方法。 一、新安MySQL未设置密码,这种情况就需要添加密码,如何添加? 1、用空密码方式使用root用户登录 MySQL mysql -u root 2、修改root用户的密码: mysql>update 数据库名称 set password=PASSWORD('新的密码') where USER='root' mysql>flush privileges; mysql>quit 3、重新启动MySQL,就可以使用新密码登录了 二、好久不用,忘记密码 1、打开DOS进入mysql下bin目录:本人:D:\Development\mysql-5.5.29-winx64\bin 2、停止mysql服务,net stop mysql 3、在D:\Development\mysql-5.5.29-winx64\bin 后面输入: mysqld --defaults-file="D:\Development\mysql-5.5.29-winx64\bin\my.ini" --console --skip-grant-tables 4、重新打开一个DOS窗口,在D:\Development\mysql-5.5.29-winx64\bin后面输入:mysql -root -p 5、提示输入密码,在Enter后面输入密码,进入mysql> 6、在mysql>下输入: mysql>update 数据库名称 set password=PASSWORD('新的密码') where USER='root' mysql>flush privileges; mysql>quit 注意:若有多个数据库可多次update操作即可。 通过属性文件来配置MySQL 一、常见的连接数据的方式 编码方式,将数据库配置信息直接写入JAVA代码之中 Properties属性文件,将数据库配置信息写在属性文件中,然后在程序中读取该属性文件。 数据源,用JNDI来获取DataSource 对象,从而的到Connection对象。 Hibernate配置 Spring配置 二、属性文件(.properties)配置与读取 1、配置文件users.properties jdbc.drivers=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/databaseName jdbc.username=root jdbc.password=upassword 2、读取属性文件 (1) 创建Properties的对象; Properties properties = new Properties(); 这一步也可以这样做:创建继承Properties的类,并以单例模式获取对象。 (2) 使用Class对象的getResourceAsStream()方法,把指定的属性文件读入到输入流中,并使用Properties类中的load()方法,从输入流中读取属性列表(键/值对); private String resource = "users.properties"; //假如配置文件名为users.properties InputStream in = getClass().getResourceAsStream(resource); properties.load(in); (3) 在使用数据库连接时,使用Properties类中的getProperty()方法,通过key获取value值,从而实现数据库连接的操作。 String drivers = props.getProperty("jdbc.drivers"); String url = props.getProperty("jdbc.url"); String username = props.getProperty("jdbc.username"); String password = props.getProperty("jdbc.password"); //返回的是Connection类的实例 Class.forName(drivers); return DriverManager.getConnection(url, username, password); MySQL连接池 一、为什么使用数据源和连接池 应用程序需要频繁的连接数据库的,如果每次操作都连接数据库,然后关闭,这样做性能一定会受限。所以,一定要想办法复用数据库的连接。使用连接池可以达到复用数据库连接的目的。 二、连接池概念 连接池是用来管理Connection 对象的,连接池可以从数据源中获得连接,连接池中可以有若干个数据库连接对象,这些连接对象可以被重用。应用程序需要连接时,就向连接池申请,如果连接池中有空闲的连接,就会分配给应用程序,如果没有,可能就需要在等待队列里等待。 三、MySQL连接池配置 1、把数据库驱动包 以及JSTL的jar包 copy 到 %CATALINA_HOME%\lib 下。 2、修改 %CATALINA_HOME%\conf\server.xml 文件,在 <Host> 节点下添加: <!-- appName 为项目名 docBase一定要准确 &符号要换成&--!> <Context path="/appName" docBase="appName\WebRoot" auth="Container"> <Resource name="jdbc/MySQLDS" scope="Shareable" type="javax.sql.DataSource" url="jdbc:mysql://localhost:3306/kqxt?useUnicode=true&characterEncoding=utf-8" driverClassName="com.mysql.jdbc.Driver" username="root" password="root" maxWait="3000" maxIdle="100" maxActive="10" /> </Context> 3、修改 web.xml,在 <web-app> 节点下添加下面内容 <resource-ref> <description>Mysql Datasource example</description> <res-ref-name>MySQLDS</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> 4、在代码中获取数据库连接 // 注意导入的包名 import java.sql.Connection; import javax.naming.Context; import javax.naming.InitialContext; import javax.sql.DataSource; public class DBUtil { public static Connection getConnection() throws Exception { Context context = new InitialContext(); // 获取数据源 DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/MySQLDS"); // 获取数据库连接 Connection conn = ds.getConnection(); if (conn != null && !conn.isClosed()) { return conn; } else { return null; } } }
Cookie和Session.jpg 日常现象 登录了某个网站,过一会儿再登录,诶,不用输入密码了,直接是登录状态了,好神奇~~~ 在某网站看了一部手机,接下来浏览其他网站,旁边的广告全是手机和类似信息,好恐怖~~~ 浏览某网站时,提示我是第66666位访问的客户,真的假的? 其实这些都是Cookie和Session在后面作祟,下面就带大家学习学习这两个东东。 Cookie和Session的异同 Cookie与Session都是用来保存用户状态信息的一种方法或者手段; Cookie是保存在客户端的临时文件夹, Session是保存在服务器的内存中的,服务器使用一种类似于散列表的结构来保存信息,一个Session域对象为一个客户浏览器服务; Cookie安全性较差,Session安全性较高; Cookie的保存时间可以很久(以txt格式保存在客户端硬盘),Session保存的时间很短,一般是30分钟; Cookie为多个客户浏览器共享,Session为一个客户浏览器独享; Session是通过Cookie的机制来实现的。 两个经典问题与URL重写 1、客户端禁用Cookie,问Session还能工作吗? 不能(事实)绝大多数的网站是这样,原因是没有使用URL重写机制来解决Cookie被禁用的问题。(URL重写代码量大而且只能应用在动态的页面静态的不行) 能(事实)微乎其微的网站可以(比如:卓越),原因是它使用了URL重写机制。 2、 Cookie可以用来实现购物车功能吗? 能,Session能做的Cookie也能做。 ** 本质 ** 无论Cookie,还是URL重写,目的都是向服务器传递JSESSIONID=32位字符串的key和value名值对。 理解Cookie-Session机制 当程序需要为某个客户端的请求创建一个Session的时候,服务器首先检查这个客户端的请求里是否已包含了一个Session标识——称为 Session id,如果已包含一个Session id则说明以前已经为此客户端创建过Session,服务器就按照Session id把这个 Session检索出来使用。如果客户端请求不包含Session id,则为此客户端创建一个Session并且生成一个与此Session相关联的Session id,这个 Session id将在本次响应中返回给客户端保存。客户端保存这个Session id的方式可以采用Cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发回给服务器。一般这个Cookie的名字都是类似于JSESSIONID。对Session来说,除非应用程序通知服务器删除一个Session,否则服务器会一直保留它。浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器不会知道浏览器已经关闭。之所以会有这种错觉,是大部分Session机制都使用会话Cookie来保存Session id,而关闭浏览器后这个 Session id就消失了,再次连接服务器时也就无法找到原来的Session。如果服务器设置的Cookie被保存到硬盘上,或者使用某种手段改写浏览器发出的HTTP请求头,把原来的Session id发送给服务器,则再次打开浏览器仍然能够找到原来的Session。也就实说关闭浏览器不会导致服务器端Session被删除,但是大量的Session一直在也服务器内存,服务器也受不了,所以服务器为Session设置了一个失效时间,当距离客户端上一次使用Session的时间超过这个失效时间(一般为30分钟)时,服务器就可以认为客户端已经停止了活动,才会把Session删除以节省服务器端的存储空间。
上一篇博文中提到用正则表达式来匹配数据项,但是写起来容易出错,如果有过DOM开发经验或者使用过jQuery的朋友看到BeautifulSoup就像是见到了老朋友一样。 安装BeautifulSoup Mac安装BeautifulSoup很简单,打开终端,执行以下语句,然后输入密码即可安装 sudo easy_install beautifulsoup4 改代码 #coding=utf-8 import urllib from bs4 import BeautifulSoup # 定义个函数 抓取网页内容 def getHtml(url): webPage = urllib.urlopen(url) html = webPage.read() return html # 定义一个函数 抓取网页中的图片 def getNewsImgs(html): # 创建BeautifulSoup soup = BeautifulSoup(html, "html.parser") # 查找所有的img标签 urlList = soup.find_all("img") length = len(urlList) # 遍历标签 下载图片 for i in range(length): imgUrl = urlList[i].attrs["src"] urllib.urlretrieve("http://www.abc.edu.cn/news/"+imgUrl,'news-%s.jpg' % i) # 获取网页 html = getHtml("http://www.abc.edu.cn/news/show.aspx?id=21430&cid=5") # 抓取图片 getNewsImgs(html) 效果:换了一个新闻,抓取了新闻中的三张图片O(∩_∩)O~ 爬虫抓图片.gif
Python爬虫,一般用于抓取特定的内容,最近想学学,通过网络抓取自己想要的内容,于是乎学习了一下Python,用一个小案例来纪念一下学习的成果。 案例程序主要功能:抓取我们学校校园网新闻中的图片 #coding=utf-8 import urllib import re # 定义个函数 抓取网页内容 def getHtml(url): webPage = urllib.urlopen(url) html = webPage.read() return html # 定义一个函数 抓取网页中的图片 def getNewsImgs(html): # 正则表达式 reg = r'src="(.+?\.jpg)"' img = re.compile(reg) # 获取网页中所有符合条件的图片url imglist = re.findall(img,html) x = 0 # 根据图片地址下载图片并重命名 for imgUrl in imglist: urllib.urlretrieve("http://www.abc.edu.cn/news/"+imgUrl,'news-%s.jpg' % x) x += 1 # 获取网页 html = getHtml("http://www.abc.edu.cn/news/show.aspx?id=21413&cid=5") # 抓取图片 print getNewsImgs(html) 效果:成功抓取了新闻中的两张图片O(∩_∩)O~ 爬虫抓图片.gif
自iOS7以后,iOS扫描二维码不需要借助于第三方框架了,苹果在AVFoundation中原生支持了扫描二维码的API,主要涉及到5个类,这5个类在自定义相机或者视频时也用得上,网上有很多介绍,这5个类分别为: AVCaptureSession:媒体捕获会话,负责把捕获的音视频数据输出到输出设备中。 AVCaptureDevice:输入设备,如麦克风、摄像头。 AVCaptureDeviceInput:设备输入数据管理对象,可以根据AVCaptureDevice创建对应的AVCaptureDeviceInput对象,该对象将会被添加到AVCaptureSession中管理。 AVCaptureOutput:输出数据管理对象,用于接收各类输出数据,有很多子类,每个子类用途都不一样,该对象将会被添加到AVCaptureSession中管理。 AVCaptureVideoPreviewLayer:相机拍摄预览图层,是CALayer的子类,使用该对象可以实时查看拍照或视频录制效果,设置好尺寸后需要添加到父view的layer中。 我在参考了网上的很多博客并自己摸索了以后,写了一个具体的实现案例,过程中遇到很多坑,在此记录并分享一下。 运行环境:Xcode 8.3.2 + iOS 8. 4真机、iOS 10.3.1真机 核心步骤: 1、创建AVCaptureSession会话 2、创建AVCaptureDevice设备 3、创建输入AVCaptureDeviceInput与输出设备AVCaptureMetadataOutput,并添加到上面的会话中 4、创建预览层 5、设置扫描区域 实现 从上面的描述看,除了预览层,其他的和UI界面似乎没什么关系,但是实际开发中,扫描界面一般都是设计的比较人性化的,如支付宝、微信等,中间都有一个小框,有个线上下扫,这个其实就是用UI来配合扫描二维码,给用户一种好的体验。 界面布局 界面布局.png 主要代码 #import "ViewController.h" #import <AVFoundation/AVFoundation.h> @interface ViewController () <AVCaptureMetadataOutputObjectsDelegate, CALayerDelegate> /** * UI */ @property (weak, nonatomic) IBOutlet UIView *scanView; @property (weak, nonatomic) IBOutlet UIImageView *scanline; @property (weak, nonatomic) IBOutlet UILabel *result; /** * 扫描区域的高度约束值(宽度一致) */ @property (weak, nonatomic) IBOutlet NSLayoutConstraint *scanViewH; /** * 扫描线的顶部约束值 */ @property (weak, nonatomic) IBOutlet NSLayoutConstraint *scanlineTop; /** * 扫描线的高度 */ @property (weak, nonatomic) IBOutlet NSLayoutConstraint *scanlineH; @property(nonatomic, strong) CALayer *maskLayer; /** * 五个类 */ @property(nonatomic, strong) AVCaptureDevice *device; @property(nonatomic, strong) AVCaptureDeviceInput *input; @property(nonatomic, strong) AVCaptureMetadataOutput *output; @property(nonatomic, strong) AVCaptureSession *session; @property(nonatomic, strong) AVCaptureVideoPreviewLayer *layer; @end @implementation ViewController #pragma mark - 懒加载 -(AVCaptureDevice *)device{ if (_device == nil) { _device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; } return _device; } -(AVCaptureDeviceInput *)input{ if (_input == nil) { _input = [AVCaptureDeviceInput deviceInputWithDevice:self.device error:nil]; } return _input; } -(AVCaptureMetadataOutput *)output{ if (_output == nil) { _output = [[AVCaptureMetadataOutput alloc]init]; [_output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; } return _output; } #pragma mark - ViewController生命周期 /** * 执行扫描动画 */ -(void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; [self startAnim]; } /** * 注册进入前台通知 保证下次进来还有扫描动画 */ -(void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; //注册程序进入前台通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector (startAnim) name: UIApplicationWillEnterForegroundNotification object:nil]; } /** * 移除通知 */ -(void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:animated]; //解除程序进入前台通知 [[NSNotificationCenter defaultCenter] removeObserver:self name: UIApplicationWillEnterForegroundNotification object:nil]; } - (void)viewDidLoad { [super viewDidLoad]; //1、创建会话 AVCaptureSession *session = [[AVCaptureSession alloc]init]; if ([session canSetSessionPreset:AVCaptureSessionPresetHigh]) { [session setSessionPreset:AVCaptureSessionPresetHigh]; } //2、添加输入和输出设备 if([session canAddInput:self.input]){ [session addInput:self.input]; } if([session canAddOutput:self.output]){ [session addOutput:self.output]; } //3、设置这次扫描的数据类型 self.output.metadataObjectTypes = self.output.availableMetadataObjectTypes; //4、创建预览层 AVCaptureVideoPreviewLayer *layer = [AVCaptureVideoPreviewLayer layerWithSession:session]; layer.frame = self.view.bounds; [self.view.layer insertSublayer:layer atIndex:0]; //5、创建周围的遮罩层 CALayer *maskLayer = [[CALayer alloc]init]; maskLayer.frame = self.view.bounds; //此时设置的颜色就是中间扫描区域最终的颜色 maskLayer.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:0.2].CGColor; maskLayer.delegate = self; [self.view.layer insertSublayer:maskLayer above:layer]; //让代理方法调用 将周围的蒙版颜色加深 [maskLayer setNeedsDisplay]; //6、关键设置扫描的区域 方法一:自己计算 // CGFloat x = (self.view.bounds.size.width - self.scanViewH.constant) * 0.5; // // CGFloat y = (self.view.bounds.size.height- self.scanViewH.constant) * 0.5; // // CGFloat w = self.scanViewH.constant; // // CGFloat h = w; // // // self.output.rectOfInterest = CGRectMake(y/self.view.bounds.size.height, x/self.view.bounds.size.width, h/self.view.bounds.size.height, w/self.view.bounds.size.width); //6、关键设置扫描的区域,方法二:直接转换,但是要在 AVCaptureInputPortFormatDescriptionDidChangeNotification 通知里设置,否则 metadataOutputRectOfInterestForRect: 转换方法会返回 (0, 0, 0, 0)。 __weak __typeof(&*self)weakSelf = self; [[NSNotificationCenter defaultCenter] addObserverForName:AVCaptureInputPortFormatDescriptionDidChangeNotification object:nil queue:[NSOperationQueue currentQueue] usingBlock: ^(NSNotification *_Nonnull note) { weakSelf.output.rectOfInterest = [weakSelf.layer metadataOutputRectOfInterestForRect:self.scanView.frame]; }]; //7、开始扫描 [session startRunning]; self.session = session; self.layer = layer; self.maskLayer = maskLayer; } -(void)dealloc{ [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - 代理方法 /** * 如果扫描到了二维码 回调该代理方法 */ - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{ if(metadataObjects.count > 0 && metadataObjects != nil){ AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects lastObject]; NSString *result = metadataObject.stringValue; self.result.text = result; [self.session stopRunning]; [self.scanline removeFromSuperview]; } } /** * 蒙版中间一块要空出来 */ -(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{ if (layer == self.maskLayer) { UIGraphicsBeginImageContextWithOptions(self.maskLayer.frame.size, NO, 1.0); //蒙版新颜色 CGContextSetFillColorWithColor(ctx, [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:0.8].CGColor); CGContextFillRect(ctx, self.maskLayer.frame); //转换坐标 CGRect scanFrame = [self.view convertRect:self.scanView.frame fromView:self.scanView.superview]; //空出中间一块 CGContextClearRect(ctx, scanFrame); } } @end #pragma mark - 自定义方法 /** * 扫描的那条线动起来 */ -(void)startAnim{ //如果是第二次进来 那么动画已经执行完毕 要重新开始动画的话 必须让约束归位 if(self.scanlineTop.constant == self.scanViewH.constant - 4){ self.scanlineTop.constant -= self.scanViewH.constant - 4; [self.view layoutIfNeeded]; } //执行动画 [UIView animateWithDuration:3.0 delay:0 options:UIViewAnimationOptionRepeat animations:^{ self.scanlineTop.constant = self.scanViewH.constant - 4; [self.view layoutIfNeeded]; } completion:nil]; } Info.plist中 不同iOS版本需要添加相应的权限 最终效果 扫描二维码.gif 总结 一、遇到的坑 1、设置了AutoLayout,想要做动画,这时候动画放在viewDidAppear中执行,并且不要用bounds,frame来改变动画,要用具体的约束,但是直接在UIView动画中修改约束是没效果的,需要在设置完约束以后,加上[self.view layoutIfNeeded];。 2、设置扫描区域,也就是设置AVCaptureMetadataOutput的rectOfInterest属性,它是一个CGRect类型,但是它的四个值和传统的不一样,是(y,x,高,宽)且是比例值,取值范围为0~1。那么有两种方案,第一种需要自己计算具体位置的比例,如代码中注释的那些。第二种方案用AVCaptureVideoPreviewLayer的metadataOutputRectOfInterestForRect方法,但是直接设置是没有效果的,必须放到通知里,如文中所示。 3、中间方块是通过CALayer两步实现的,第一步设置整个背景颜色,这个颜色根据中间想显示的样式来设置;第二步在代理方法里面重新设置一次背景颜色,这个颜色根据除中间以外的区域来设置,然后将中间的挖掉。但是必须调用setNeedsDisplay方法,否则代理方法不会调用。 二、参考文献 1、iOS开发系列--音频播放、录音、视频播放、拍照、视频录制2、iOS开发 - 二维码的扫描3、iOS二维码扫描与生成(优化启动卡顿) 三、源代码
在iOS10中,苹果为UICollectionViewCell引入了Pre-Fetching预加载机制用于提升它的性能。主要引入了一个新的数据源协议UICollectionViewDataSourcePrefetching,包含两个方法: @protocol UICollectionViewDataSourcePrefetching <NSObject> @required // 预加载数据 - (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths NS_AVAILABLE_IOS(10_0); @optional // 取消提前加载数据 - (void)collectionView:(UICollectionView *)collectionView cancelPrefetchingForItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths NS_AVAILABLE_IOS(10_0); @end 网上搜了一大圈,讲述原理的(翻译文档)的文章很多,有干货的Demo很少,于是乎自己摸索了一下,写了一个简单的案例,在此记录并分享一下。 运行环境:Xcode 8.2.1 + iOS 10.2 核心步骤: 1、遵从 UICollectionViewDataSourcePrefetching 协议 2、实现 collectionView:prefetchItemsAtIndexPaths 方法和collectionView:cancelPrefetchItemsAtIndexPaths 方法(可选) 3、将第1步中遵从协议的类设置为 UICollectionView 的 prefetchDataSource 属性 实现 一、创建UICollectionViewFlowLayout 自己写一个类继承自UICollectionViewFlowLayout @implementation MyCollectionViewFlowLayout -(void)prepareLayout{ self.minimumLineSpacing = 1;//垂直间距 self.minimumInteritemSpacing = 0;//水平间距 self.sectionInset = UIEdgeInsetsMake(0, 0, 8, 0);//分组间距 } @end 二、用XIB定义一个 里面就一个UIImageView,然后拽线设置一个IBOutlet UICollectionViewCell.png @property (weak, nonatomic) IBOutlet UIImageView *imgView; 三、控制器 注释很详细 #import "ViewController.h" #import "MyCollectionViewFlowLayout.h" #import "ImgCollectionViewCell.h" #define ScreenW [UIScreen mainScreen].bounds.size.width //重用标识 static NSString *cellId = @"imgCell"; //遵守协议 @interface ViewController ()<UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDataSourcePrefetching> //下载图片任务 @property(nonatomic, strong) NSMutableDictionary<NSURL *, dispatch_queue_t> *tasks; //图片地址 @property(nonatomic, copy) NSMutableArray<NSURL *> *imgURLArray; //下载的图片 @property(nonatomic, copy) NSMutableDictionary<NSURL *, UIImage *> *imgs; //UICollectionView @property(nonatomic, weak) UICollectionView *collectionView; @end @implementation ViewController //懒加载imgURLArray -(NSMutableArray<NSURL *> *)imgURLArray{ if (_imgURLArray == nil) { _imgURLArray = [NSMutableArray array]; for (int i = 0; i < 30; i++) { NSURL *imgURL = [NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1494499175005&di=1d8d40ac84f4a71cb26d7bf4a5a845ec&imgtype=0&src=http%3A%2F%2Fimg10.360buyimg.com%2Fyixun_zdm%2Fjfs%2Ft2830%2F11%2F2310606472%2F165925%2F962fa94a%2F575f7664Nfd743845.jpg"]; [_imgURLArray addObject:imgURL]; } } return _imgURLArray; } //懒加载imgs -(NSMutableDictionary<NSURL *,UIImage *> *)imgs{ if (_imgs == nil) { _imgs = [NSMutableDictionary dictionary]; } return _imgs; } //懒加载tasks -(NSMutableDictionary<NSURL *,dispatch_queue_t> *)tasks{ if (_tasks == nil) { _tasks = [NSMutableDictionary dictionary]; } return _tasks; } - (void)viewDidLoad { [super viewDidLoad]; //创建UICollectionView //创建布局 UICollectionViewLayout *layout = [[MyCollectionViewFlowLayout alloc]init]; //初始化一个UICollectionView UICollectionView *collection = [[UICollectionView alloc]initWithFrame:[UIScreen mainScreen].bounds collectionViewLayout:layout]; //设置背景色 collection.backgroundColor = [UIColor groupTableViewBackgroundColor]; //设置代理 collection.dataSource = self; collection.delegate = self; collection.prefetchDataSource = self; //注册Cell UINib *nib = [UINib nibWithNibName:@"ImgCollectionViewCell" bundle:nil]; [collection registerNib:nib forCellWithReuseIdentifier:cellId]; [self.view addSubview:collection]; self.collectionView = collection; } -(void)loadImage:(NSIndexPath *)indexPath{ NSURL *currentURL = [self.imgURLArray objectAtIndex:indexPath.row]; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); __weak typeof(self) weakSelf = self; //异步下载图片 dispatch_async(queue, ^{ NSData *imageData = [NSData dataWithContentsOfURL:currentURL]; UIImage *image = [UIImage imageWithData:imageData]; weakSelf.imgs[currentURL] = image; //更新UI dispatch_async(dispatch_get_main_queue(), ^{ ImgCollectionViewCell *cell = (ImgCollectionViewCell *)[weakSelf.collectionView cellForItemAtIndexPath:indexPath]; cell.imgView.image = image; }); }); //为了取消任务 self.tasks[currentURL] = queue; } #pragma mark <UICollectionViewDataSource> - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{ return self.imgURLArray.count; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{ ImgCollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellId forIndexPath:indexPath]; // 获取URL NSURL *imgURL = self.imgURLArray[indexPath.row]; //对应的URL的图片已经存在 if (self.imgs[imgURL]) { cell.imgView.image = self.imgs[imgURL]; } //不存在 else{ [self loadImage:indexPath]; } return cell; } #pragma mark <UICollectionViewDelegate> //定义每个Item 的大小 - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { CGFloat W = (ScreenW - 1) / 2; return CGSizeMake(W, 100); } #pragma mark <UICollectionViewDataSourcePrefetching> - (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths { for (NSIndexPath * indexPath in indexPaths) { NSURL *currentURL = [self.imgURLArray objectAtIndex:indexPath.row]; //不存在就请求 if (!self.imgs[currentURL]) { [self loadImage:indexPath]; } } } - (void)collectionView:(UICollectionView *)collectionView cancelPrefetchingForItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths { for (NSIndexPath * indexPath in indexPaths){ NSURL *currentURL = [self.imgURLArray objectAtIndex:indexPath.row]; //当前任务存在 if (self.tasks[currentURL]) { dispatch_queue_t queue = self.tasks[currentURL]; dispatch_suspend(queue); self.tasks[currentURL] = nil; } } } @end 效果 效果演示.gif 写在后面的话 1、这个新特性仍然需要探究 2、遇到的一个坑:细心看的话可以发现我的字典是懒加载的,如果直接在viewDidLoad中初始化会在 weakSelf.imgs[currentURL] = image; 一行报错,why?烦请知道的告知。 源代码
在iOS开发中,经常会使用到定位和地图,Apple自带的定位和地图应付一般的场景绰绰有余,但是也缺少一些功能,如POI搜索、不够接地气等。因此作者选择了高德地图,这篇文正主要介绍定位功能的集成。 环境:Xcode7.3.1 + iOS 9.3 + 能访问网络 1、注册开发者,创建应用 首先注册成为开发者,成为开发者以后,登录并进入控制台,然后创建一个iOS应用,注意这一定要填写好你的 Bundle identifier,不知道的后文有说明怎么查询。 进入控制台 获取Key.gif 填写key的信息 填写key的信息.png 生成一个key 申请Key.JPG 2、通过CocoaPods导入需要的SDK platform :ios, '9.0' #手机系统版本 target '集成高德地图' do #新建的Xcode工程名 pod 'AMapLocation' #定位 SDK end 3、配置Info.plist,添加前后台定位的授权信息,到时候会弹出一个对话框让用户选择是否同意(iOS8以后必须要添加),可以先只设置NSLocationWhenInUseUsageDescription NSLocationWhenInUseUsageDescription 表示应用在前台的时候可以搜到更新的位置信息。 NSLocationAlwaysUsageDescription 表示应用在前台和后台(suspend 或 terminated)都可以获取到更新的位置数据。 4、在AppDelegate中初始化key - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 不知道Bundle identifier的通过如下方式查询 // NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; // NSLog(@"%@", bundleIdentifier); [AMapServices sharedServices].apiKey =@"注册产生的key"; return YES; } 5、UIViewController代码,注释非常详细 通过官方文档,我们知道一共有三种定位方式,分别是一次定位、持续定位和后台定位,分别用三个独立的方法分开写了。 #import "ViewController.h" #import <AMapFoundationKit/AMapFoundationKit.h> #import <AMapLocationKit/AMapLocationKit.h> @interface ViewController ()<AMapLocationManagerDelegate> //定位管理器 @property(nonatomic, strong) AMapLocationManager *locationManager; //定位回调次数统计 @property(nonatomic, assign) NSInteger count; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.count = 0; [self onceLoaction]; [self alwaysLoaction]; [self backgroundLoaction]; } #pragma mark - 持续定位 -(void)backgroundLoaction{ //初始化定位管理器 self.locationManager = [[AMapLocationManager alloc]init]; // 设置代理对象 self.locationManager.delegate = self; // 设置反地理编码 self.locationManager.locatingWithReGeocode = YES; //iOS9设置后台定位 if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 9) { self.locationManager.allowsBackgroundLocationUpdates = YES; } //开启持续定位 [self.locationManager startUpdatingLocation]; } #pragma mark - 持续定位 -(void)alwaysLoaction{ //初始化定位管理器 self.locationManager = [[AMapLocationManager alloc]init]; // 设置代理对象 self.locationManager.delegate = self; // 设置反地理编码 self.locationManager.locatingWithReGeocode = YES; //开启持续定位 [self.locationManager startUpdatingLocation]; } #pragma mark - 单次定位 -(void)onceLoaction{ //初始化定位管理器 self.locationManager = [[AMapLocationManager alloc]init]; //单次定位 [self.locationManager requestLocationWithReGeocode:YES completionBlock:^(CLLocation *location, AMapLocationReGeocode *regeocode, NSError *error) { NSLog(@"%@", regeocode); }]; } #pragma mark - 2个代理方法 - (void)amapLocationManager:(AMapLocationManager *)manager didUpdateLocation:(CLLocation *)location{ NSLog(@"我是个倒霉蛋,我不会被调用"); } //若实现了下面的回调,将不会再回调amapLocationManager:didUpdateLocation:方法。 - (void)amapLocationManager:(AMapLocationManager *)manager didUpdateLocation:(CLLocation *)location reGeocode:(AMapLocationReGeocode *)reGeocode{ if (reGeocode) { NSLog(@"reGeocode:%@", reGeocode); } self.count++; if (self.count == 10) { // 超过10次停止定位 [self.locationManager stopUpdatingLocation]; } } @end 总结 三种不同的定位,都可以实现定位,开发时请根据不同的需求选择不同的定位方式: 单次定位:使用简单,只需要一个方法就可以搞定,无需设置代理 持续定位:需要设置代理,通过startUpdatingLocation方法获取定位信息,代理回调函数会被调用多次 后台定位:除了持续定位需要做的以外,还需要设置NSLocationAlwaysUsageDescription字段,否则退出后台会有大蓝条提示,而且iOS9之前和之后的做法也不同。
苹果在 iOS9 之后已经废弃了NSURLConnection,NSURLSession成为其替代者,其基本知识网上很多,主要可以从NSURLSessionDataTask 、NSURLSessionDownloadTask 和NSURLSessionUploadTask入手学习。最近在写案例时发现其中的NSURLSessionUploadTask还是有着不少的坑,在开发时有时候很难一次性成功。所以将研究的过程记录与分享一下。我会以一个完整的案例来讲解如何使用。 服务器开发 环境:IDEA 14 +Tomcat 8.x+JDK 8 1、编写服务器端代码 由于上传数据与下载数据不同,下载的时候只要把数据丢进服务器就可以了。但是上传需要服务器自己来处理。所以以Java Servlet来写服务器端,由于Servlet 3.0 以后可以直接处理文件上传,所以相对比较简单,代码如下,注释很详细。 import javax.servlet.ServletException; import javax.servlet.annotation.MultipartConfig; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import java.io.File; import java.io.IOException; import java.io.PrintWriter; @MultipartConfig //标识Servlet支持文件上传 public class UploadServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //存储路径为我们的根目录 String storePath = req.getServletContext().getRealPath("/"); //获取part对象,参数为客户端表单中的name属性的值 Part part = req.getPart("myfile"); //Servlet3没有提供直接获取文件名的方法,需要从请求头中解析出来 //获取请求头 String header = part.getHeader("Content-Disposition"); //获取文件名 String fileName = header.substring(header.lastIndexOf("=") + 2, header.length() - 1); //把文件写到指定路径 part.write(storePath + File.separator + fileName); //回写数据给客户端 resp.setCharacterEncoding("UTF-8"); PrintWriter pw = resp.getWriter(); pw.print("上传成功"); } } 2、部署代码 找到Tomcat根目录下的conf文件夹,打开server.xml,在最后加上一行代码,path就是访问的项目路径,docBase就是项目编译后的位置。 <Context path="/AppTestAPI" docBase="E:\AppTestAPI\out\artifacts\AppTestAPI" auth="Container" /> 3、启动服务器 通过浏览器访问http://localhost能出现如下的界面,至此完成服务器端工作。( 由于我修改了默认端口,所以没有加8080 ) Paste_Image.png 客户端开发 环境:Xcode 7.3.1 1、创建项目 创建一个项目,布置界面,设置支持http网络访问。主界面如下: 界面.png 2、编写代码 NSURLSession使用都是一个套路:创建请求,创建任务,执行任务,成功回调。但是在使用NSURLSessionUploadTask进行上传时最麻烦的是上传数据的构造,其遵循严格的规范,如下图,不能随意书写,不能随意书写,不能随意书写~,重要的事情说三遍,否则坑得你生活不能自理。 上传必填字段.png 下面是ViewController的代码,其中最核心的是getData方法。 #import "ViewController.h" //分隔符 #define YFBoundary @"AnHuiWuHuYungFan" //换行 #define YFEnter [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding] //NSString转NSData #define YFEncode(string) [string dataUsingEncoding:NSUTF8StringEncoding] @interface ViewController ()<NSURLSessionTaskDelegate> @property (weak, nonatomic) IBOutlet UIProgressView *uploadProgress; @property (weak, nonatomic) IBOutlet UILabel *uploadInfo; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //1、确定URL NSURL *url = [NSURL URLWithString:@"http://192.168.0.5/AppTestAPI/UploadServlet"]; //2、确定请求 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; //3、设置请求头 NSString *head = [NSString stringWithFormat:@"multipart/form-data;boundary=%--@", YFBoundary]; [request setValue:head forHTTPHeaderField:@"Content-Type"]; //4、设置请求方式,上传时必须是Post请求 request.HTTPMethod = @"POST"; //5、创建NSURLSession NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]]; //6、获取上传的数据 NSData *uploadData = [self getData]; //7、创建上传任务 上传的数据来自getData方法 NSURLSessionUploadTask *task = [session uploadTaskWithRequest:request fromData:uploadData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { //上传成功以后改变UILabel文本为服务器返回的数据 self.uploadInfo.text =[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; }]; //8、执行上传任务 [task resume]; } /** * 设置请求体 * * @return 请求体内容 */ -(NSData *)getData { NSMutableData *data = [NSMutableData data]; //1、开始标记 //-- [data appendData:YFEncode(@"--")]; //boundary [data appendData:YFEncode(YFBoundary)]; //换行符 [data appendData:YFEnter]; //文件参数名 Content-Disposition: form-data; name="myfile"; filename="wall.jpg" [data appendData:YFEncode(@"Content-Disposition:form-data; name=\"myfile\"; filename=\"wall.jpg\"")]; //换行符 [data appendData:YFEnter]; //Content-Type 上传文件的类型 MIME [data appendData:YFEncode(@"Content-Type:image/jpeg")]; //换行符 [data appendData:YFEnter]; //换行符 [data appendData:YFEnter]; //2、上传的文件数据 //图片数据 并且转换为Data UIImage *image = [UIImage imageNamed:@"wall.jpg"]; NSData *imagedata = UIImageJPEGRepresentation(image, 1.0); [data appendData:imagedata]; //换行符 [data appendData:YFEnter]; //3、结束标记 //-- [data appendData:YFEncode(@"--")]; //boundary [data appendData:YFEncode(YFBoundary)]; //-- [data appendData:YFEncode(@"--")]; //换行符 [data appendData:YFEnter]; return data; } #pragma mark - 代理方法 只要给服务器上传数据就会调用 可以计算出上传进度 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend { //设置进度条 self.uploadProgress.progress = 1.0 * totalBytesSent / totalBytesExpectedToSend; } @end 最终效果 先看客户端的表象 客户端演示.gif 再看服务器端最终上传的数据,不重要以为是图片,静静观察一会儿~~~有变化 服务器端.gif
Android 开发中,经常有从服务器下载数据的需求出现,尤其是在线更新App的情形。其基本思路是根据本地的App版本号和服务器的版本号进行比较,如果服务器版本较新,再进行提示然后下载Apk最后进行安装。这种需求解决方案有很多,第三方的网络框架基本都有这个功能。 Android自带的DownloadManager是一个很好的下载文件的工具。该类在API level 9之后出现,它已经帮我们处理了下载失败、重新下载等功能,整个下载过程全部交给系统负责,不需要我们过多的处理,非常的nice。关键的是用起来也很简单,很爽,稍微封装一下就可以几句话搞定下载。 下面以一个简单案例来实现从服务器下载一个Apk文件并进行安装。 1、打开本地Tomcat服务器,放入一个Apk文件 Tomcat.png 2、开启Tomcat,然后确保能从浏览器下载 浏览器下载.png 3、编写DownloadManager工具类,代码注释很详细 public class DownloadUtils { //下载器 private DownloadManager downloadManager; //上下文 private Context mContext; //下载的ID private long downloadId; public DownloadUtils(Context context){ this.mContext = context; } //下载apk public void downloadAPK(String url, String name) { //创建下载任务 Request request = new Request(Uri.parse(url)); //移动网络情况下是否允许漫游 request.setAllowedOverRoaming(false); //在通知栏中显示,默认就是显示的 request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE); request.setTitle("新版本Apk"); request.setDescription("Apk Downloading"); request.setVisibleInDownloadsUi(true); //设置下载的路径 request.setDestinationInExternalPublicDir(Environment.getExternalStorageDirectory().getAbsolutePath() , name); //获取DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); //将下载请求加入下载队列,加入下载队列后会给该任务返回一个long型的id,通过该id可以取消任务,重启任务、获取下载的文件等等 downloadId = downloadManager.enqueue(request); //注册广播接收者,监听下载状态 mContext.registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); } //广播监听下载的各个状态 private BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { checkStatus(); } }; //检查下载状态 private void checkStatus() { Query query = new Query(); //通过下载的id查找 query.setFilterById(downloadId); Cursor c = downloadManager.query(query); if (c.moveToFirst()) { int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS)); switch (status) { //下载暂停 case DownloadManager.STATUS_PAUSED: break; //下载延迟 case DownloadManager.STATUS_PENDING: break; //正在下载 case DownloadManager.STATUS_RUNNING: break; //下载完成 case DownloadManager.STATUS_SUCCESSFUL: //下载完成安装APK installAPK(); break; //下载失败 case DownloadManager.STATUS_FAILED: Toast.makeText(mContext, "下载失败", Toast.LENGTH_SHORT).show(); break; } } c.close(); } //下载到本地后执行安装 private void installAPK() { //获取下载文件的Uri Uri downloadFileUri = downloadManager.getUriForDownloadedFile(downloadId); if (downloadFileUri != null) { Intent intent= new Intent(Intent.ACTION_VIEW); intent.setDataAndType(downloadFileUri, "application/vnd.android.package-archive"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); mContext.unregisterReceiver(receiver); } } } 4、编写Activity代码初始化工具类,然后下载 public class MainActivity extends AppCompatActivity { private DownloadUtils downloadUtils; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); downloadUtils = new DownloadUtils(MainActivity.this); downloadUtils.downloadAPK("http://192.168.1.104:8080/XXX.apk", "XXX.apk"); } } 5、配置权限 <!--在SDCard中创建与删除文件权限 --> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> <!-- 往SDCard写入数据权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <!-- 从SDCard读取数据权限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <!-- 访问网络权限 --> <uses-permission android:name="android.permission.INTERNET"/> 6、运行结果 运行以后会去下载服务器的Apk,通知栏也会有相应的通知。当下载完毕以后,会弹出安装界面。 通知栏 通知栏提醒.png 主界面 主界面.gif
感觉好久没有写Android的文章了,囧囧囧!因为Retrofit实在是太火了, 火得我一直跃跃欲试,但是由于种种原因吧,一直都没有用过。周末闲来无事,利用一个以前开发中用过的服务器API来小试牛刀一下,非常简单,记录自己的第一次Retrofit之旅。 重要说明:由于涉及到商业机密,所以对请求的数据和服务器地址进行了处理。自己实验时,请换成自己的服务器。 一、添加依赖、配置项目 这一步就不详细说了,会的都会,基本就是添加两个依赖和网络访问权限。 compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' <uses-permission android:name="android.permission.INTERNET" /> 二、根据服务器端返回的JSON数据配置自己的JavaBean 在项目中用Retrofit的时候,肯定和服务器端的哥们商量好了如何交互,也就是说已经知晓了服务器端会返回什么样的数据(基本都是JSON,只是内容不一样而已),这些数据你可以直接用字符串接收,然后自己处理,但是这么low的事情,Retrofit肯定可以帮我处理了,用上面的converter-gson就可以自动将JSON转成我们的对象,前提是需要与返回的JSON字段进行映射,默认情况下字段名一样就不会出什么问题,但是我却踩了一个坑,下文会介绍。 服务器端返回的JSON数据如下: {"message":"ok","list":["id":"","abstract":"","title":"","url":""},...] 根据上面的JSON数据创建JavaBean import com.google.gson.annotations.SerializedName; import java.util.List; /** * Created by yangfan on 2017/1/7. */ //与服务器端数据进行字段映射 public class NewsInfo { private String message; private List<News> list; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public List<News> getList() { return list; } public void setList(List<News> list) { this.list = list; } public static class News { private String id; //@SerializedName:后面是服务器端返回的数据字段名 可以与本地不一样 该关键字就是用来映射的 @SerializedName("abstract") private String _abstract; private String title; private String url; public String getId() { return id; } public void setId(String id) { this.id = id; } public String get_abstract() { return _abstract; } public void set_abstract(String _abstract) { this._abstract = _abstract; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } } } 踩坑记录:服务器端居然有一个abstract字段,但是这个是Java关键字啊,怎么办?用@SerializedName告诉Retrofit,我想你把该字段映射成我自定义的名字。 配置访问服务器API的接口 这个就是真正的访问服务器的本地API了,我这里只用了GET,其实远不止这个功能,可以参考官方文档。 import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Query; /** * Created by yangfan on 2017/1/7. */ public interface NewsInterface { //获取服务器的新闻列表,由于新闻类型和分页显示的需要,需要传递三个参数 @GET("news/list") Call<NewsInfo> newsList(@Query("type") String type, @Query("pn") String pn, @Query("ps") String ps); } Activity 配置好以上信息以后,就可以开始写Android的程序了。因为我的程序比较简单,所以我很快就成功获取数据了,这一块没怎么踩坑。 public class MainActivity extends AppCompatActivity { private TextView tvResult; //服务器的基地址 与 @GET("news/list")拼接 private static final String baseUrl = "http://服务器地址/"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvResult = (TextView) findViewById(R.id.tvResult); initRetrofit(); } private void initRetrofit() { Retrofit retrofit = new Retrofit.Builder().baseUrl(baseUrl).addConverterFactory(GsonConverterFactory.create()) .build(); NewsInterface newsInterface = retrofit.create(NewsInterface.class); Call<NewsInfo> callNewsInfo = newsInterface.newsList("1", "1", "10"); callNewsInfo.enqueue(new Callback<NewsInfo>() { @Override public void onResponse(Call<NewsInfo> call, Response<NewsInfo> response) { //成功 if (response.code() == 200) { //将返回的数据转换成自定义的对象 NewsInfo newsInfo = response.body(); if(newsInfo.getMessage().equals("ok")){ //最爽的地方---直接可以进行UI操作 tvResult.setText(newsInfo.getList().get(0).get_abstract()); } } else { //请求失败 tvResult.setText("查询失败!响应码为:" + response.code()); } } @Override public void onFailure(Call<NewsInfo> call, Throwable t) { //请求失败 t.printStackTrace(); tvResult.setText("查询失败!错误原因:" + t.getCause()); } }); } } 运行结果 Retrofit小试牛刀.gif Retrofit功能非常强大,我也看了很多文档,上面用的只是冰山一角。目前感觉最大的好处就是可以直接在回调中操作主线程的UI,再也不需要用Handler了,其他牛逼的功能和好处待学习、待发掘。
在Java Web学习的初期,开发的小项目几乎都是JSP+Servlet+JDBC,长期开发下来,会发现当业务逻辑设计的接口一多的时候,充当控制器的Servlet也会越来越多,但是处理的业务逻辑相对单一。后来学习Struts2或者SpringMVC,发现它们处理起来优雅得多,但是配置起来也比纯的Servlet要繁琐,对于经常做小项目的我来说有点大材小用了,于是我根据前人的经验抽离了一个BaseServlet,用反射的机制来处理请求,这样处理业务逻辑的Servlet要相对简单的多,废话不说,把自己使用的一套拿出来晒晒。 BaseServlet 由于服务器端经常是用JSON与Android和iOS客户端进行交互,所以这里返回的就是JSON数据 //这个抽象类,BaseServlet类不需要在web.xml中进行配置 public abstract class BaseServlet extends HttpServlet { // final 防子类复写 public final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request, response); } public final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1、获得执行的方法名 String methodName = request.getParameter("method"); // 默认方法 if (methodName == null) { methodName = "execute"; } System.out.println("BaseServlet : " + this + " , " + methodName); try { // 2、通过反射获得当前运行类中指定方法,形式参数 Method executeMethod = this.getClass().getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class); // 3、反射执行方法 String result = (String)executeMethod.invoke(this, request, response); // 4、将json数据返回 response.getWriter().write(result); } catch (NoSuchMethodException e) { throw new RuntimeException("请求的方法[" + methodName + "]不存在"); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("服务器异常", e); } } /** * 此方法用于复写,方便子类编程,默认执行方法 */ public void execute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } } 真正处理业务逻辑的Servlet 需要做的就是继承上面的BaseServlet,然后根据业务需求写自己的方法即可,乍一看像SpringMVC,但是要注意这里的方法名和返回值,方法名决定了请求时的method参数的值,返回值由于是JSON,所以用的是String。 public class UserServlet extends BaseServlet { public String users(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ArrayList<User> users = new ArrayList<User>(); for (int i = 0; i < 10; i++) { User user = new User(i, "zhangsan" + i, i + 10, "wukong" + i); users.add(user); } Gson gson = new Gson(); return gson.toJson(users); } } ========================================================== //自定义的一个PO public class User { private int id; private String name; private int age; private String nickname; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public User(int id, String name, int age, String nickname) { this.id = id; this.name = name; this.age = age; this.nickname = nickname; } } 部署Servlet 将自己的业务UserServlet部署到web.xml,如下: <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <servlet> <servlet-name>UserServlet</servlet-name> <servlet-class>test.app.api.UserServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>UserServlet</servlet-name> <url-pattern>/UserServlet</url-pattern> </servlet-mapping> </web-app> 访问Servlet 部署并启动tomcat以后,打开浏览器访问:http://localhost/AppTestAPI/UserServlet?method=users 结果如下: 返回JSON结果.png
动态代理通俗解释: A接口有c方法,类B实现A接口,原本应该是执行B类中的c方法,可现在不这样做,可以先声明产生B类的代理类B',由它来冒充B类的“兄弟”并“实现”A接口, 对外界来说B'应该也有c方法,可当真正调用它的时候, 它会去执行与它关联InvocationHandler的invoke()方法, 在这个方法里面你可以做很多事情。 Java怎样实现动态代理呢 第一步,我们要有一个接口,还要有一个接口的实现类,而这个实现类就是我们要代理的类。 public interface Subject { public void request(); } public class RealSubject implements Subject { public void request() { System.out.println("From real subject!"); } } 第二步,我们要自己写一个代理类,它的特点是实现了InvocationHandler接口, 因为代理类的实例在调用实现类的方法的时候,不是去调用真正的实现类的这个方法, 而是调用代理类的invoke()方法,在这个方法中才调用真正的实现类的方法。 import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; /** * 该代理类的内部属性是Object类型,实际使用的时候通过该类的构造方法传递进来一个对象 * 此外,该类还实现了invoke方法,该方法中的method.invoke其实就是调用被代理对象的将要 * 执行的方法,方法参数是sub,表示该方法从属于sub,通过动态代理类,我们可以在执行真实对象的方法前后 * 加入自己的一些额外方法,这里在方法调用前后打印一句话。 * */ public class DynamicSubject implements InvocationHandler { private Object sub; public DynamicSubject(Object obj) { this.sub = obj; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before calling: " + method); method.invoke(sub, args); System.out.println(args == null); System.out.println("after calling: " + method); return null; } } 上述方法体中method.invoke(owner, args)的解释:执行该method.invoke方法的参数是执行这个方法的对象owner,参数数组args,可以这么理解:owner对象中带有参数args的method方法。返回值是Object,也就是该方法的返回值。 第三步,客户端要用代理类的实例去调用实现类的方法。 import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; public class Client { public static void main(String[] args) { RealSubject realSubject = new RealSubject(); InvocationHandler handler = new DynamicSubject(realSubject); Class<?> classType = handler.getClass(); // newProxyInstance()动态生成一个类并加载到内存 // 加载到内存要使用加载器,第一个参数是一个类加载器 Subject subject = (Subject) Proxy.newProxyInstance(classType .getClassLoader(), realSubject.getClass().getInterfaces(), handler); subject.request(); System.out.println(subject.getClass()); } } 对第三步的解释 主要是以下代码: Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 该方法主要做了如下工作: 根据参数loader和interfaces调用方法 getProxyClass(loader, interfaces) 创建代理类$Proxy0,该代理类实现了预先定义的接口(如上面的Subject),并继承了Proxy类。 public final class $Proxy0 extends Proxy implements Subject 实例化$Proxy0(创建代理对象)并在构造方法中把 InvocationHandler(这里handler 是它的实例)传过去 $Proxy0调用父类Proxy的构造器,为InvocationHandler 赋值: public $Proxy0(InvocationHandler invocationhandler) { super(invocationhandler); } ======================================================== class Proxy { protected InvocationHandler h; protected Proxy(InvocationHandler h) { this.h = h; } } 将这个$Proxy0类强制转型成接口类型,当执行接口中的方法时(如上文强转成Subject后调用request()方法),就调用了$Proxy0类中实现的接口方法,在该方法中会调用父类Proxy中的invoke()方法,即InvocationHandler.invoke(),达到做一些其他工作的效果。 public final void request() { try { //m是通过反射得到的方法名 Method类型 super.h.invoke(this, m, null); return; } catch (Error e) { } catch (Throwable throwable) { throw new UndeclaredThrowableException(throwable); } }
大事件 1、因为工作需要,从 Android 转到 iOS 。 2、开始写自己的技术博客,分享曾经积累的和现学现卖的技术,在简书写了51篇文章,收获786个粉丝,获得1923个喜欢。 3、独立开发了一款 Android App 并上架。 4、开发迭代了一个考勤小系统(服务器 + Android + 网页)。 技术成长 1、深入学习 Objective-C 开发语言。 2、熟悉 iOS 开发。 3、学习 Swift 语言。 4、巩固 J2EE 和 Android 的知识。 5、整理了近100个文档,弄懂了很多以前不懂的技术。 6、尝试一些前端的技术,如 Bootstrap 。 2017 1、学习 Mac 开发。 2、专研 iOS 开发,能够熟练地进行 iOS 开发。 3、与时俱进 Java 和 Android 方面的知识。 4、Review 自己以前的代码。 5、尝试更多前端的技术。 6、尝试做一到二个功能系统(服务器 + Android + iOS + 网页)。
在iOS开发之通过代理逆向传值一文中,分析了利用代理模式来逆向传值,其实还有一些其他的方式,如通知、Block等,相比较代理,我个人认为反而要简单些,但是需要处理好细节问题,如Block循环引用。还是用前文的案例,本次使用Block来实现,Block的基本知识本文不再赘述。 一、书写规范 Block传值,需要注意的是,谁传值就需要定义Block,捕获方仅仅需要传递Block给传值方,并处理捕获的值。 传值方 1、定义Block用于传值 2、声明一个上述Block属性,这个属性的具体实现需要捕获方传进来 3、在需要传值的时候调用Block完成传值 捕获方 1、传递一个Block给传值方 2、在Block中捕获传过来的值,并根据需求处理捕获的值 二、Block与逆向传值 还是那句No Code, No BB,案例效果如下: 逆向传值.gif 三、实现步骤 1、传值方 //.h 文件 /** * 类型自定义 */ typedef void (^ReturnValueBlock) (NSString *strValue); @interface NextViewController : UIViewController /** * 声明一个ReturnValueBlock属性,这个Block是获取传值的界面传进来的 */ @property(nonatomic, copy) ReturnValueBlock returnValueBlock; @end ================================================================= //.m 文件 #import "NextViewController.h" @interface NextViewController () @property (weak, nonatomic) IBOutlet UITextField *inputText; - (IBAction)back:(id)sender; @end @implementation NextViewController - (void)viewDidLoad { [super viewDidLoad]; self.navigationItem.title = @"第二个界面"; } /** * 返回上一个界面 * * @param sender 按钮 */ - (IBAction)back:(id)sender { NSString *inputString = self.inputText.text; __weak typeof(self) weakself = self; if (weakself.returnValueBlock) { //将自己的值传出去,完成传值 weakself.returnValueBlock(inputString); } [self.navigationController popViewControllerAnimated:YES]; } @end 2、捕获方 //.m 文件 #import "ViewController.h" #import "NextViewController.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UILabel *nextPassedValue; - (IBAction)next:(id)sender; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; } //点击按钮跳转到第二个界面 - (IBAction)next:(id)sender { NextViewController *nvc = [[NextViewController alloc]init]; //赋值Block,并将捕获的值赋值给UILabel nvc.returnValueBlock = ^(NSString *passedValue){ self.nextPassedValue.text = passedValue; }; [self.navigationController pushViewController:nvc animated:YES]; } @end
在iOS开发中,传值是几乎每个App都会用到的,对于传统的顺向传值应该说是比较简单的,但是逆向传值往往会用到代理模式来实现,很多同学在这一块有迷惑,迷惑的不是怎么逆向传值,而是不理解代理模式,下面就来分析一下,如何理解和快速上手代理模式,并以一个简单的逆向传值为案例,看看代理模式是不是很难。 一、代理模式中的几个概念 讲代理模式之前,首先需要弄清楚两个概念:被代理对象和代理对象。并且需要知道它们之间是靠协议关连起来的。 1、被代理对象 被代理对象往往就是真正有做事意图的那个对象,比如卖房子案例中的想卖房子的房主,保姆婴儿案例中想喝奶的婴儿。但是它们自己做不到,只能借助于房产中介和保姆。 2、代理对象 代理对象就是真正办事的对象,如上面提到的帮卖房子的中介和照顾婴儿的保姆。 3、协议 代理对象必须有一个规范才能成为代理,比如说只有持证的才能成为房产中介,房主才敢把房子交给他代理,只有家政服务的保姆才敢让她过来照顾婴儿,不可能让保姆去卖房子,让中介照顾婴儿。而这个规范就是协议,这个协议谁定的?答案肯定是被代理对象。 二、代理模式的书写规范 弄清楚基本概念以后,接下来就是书写代理模式,很多同学不知道怎么下手,其实这也是有迹可循的。 被代理对象 1、定义协议,在协议中定义方法用于传值,传什么类型的值就设置什么类型的参数 2、声明一个遵守上述协议的delegate属性 3、在需要传值的时候调用代理对象完成传值 代理对象 1、遵守被代理对象定义的协议 2、实现协议中的方法,捕获传过来的值,并根据需求来处理捕获的值 设置被代理对象的delegate属性为代理对象 这一步是在上述两个对象的其中一个里面完成的 三、代理模式与逆向传值 通过如上描述可以看出,其实代理模式只是一种解决特定问题的方法,它有自己的使用场景,比如A想监听B的一些变化,A无法自己处理某些事情等。俗话说No Code, No BB,下面举一个实际案例:现在有两个界面,如图1和图2所示,界面很简单,不再赘述,主要是在第一个界面点击跳转按钮,到达第二个界面,然后第二界面在输入框中输入信息后点击返回按钮,第一个界面中的UILabel能显示第二个界面输入的内容,这是最简单的一个逆向传值。 图1.png 图2.png 四、实现步骤 分析: 现在是第二个界面想传值,但是自己做不了,所以它是被代理对象,第一个界面自然就是代理对象。然后根据上面的规范写代码。 1、被代理对象 //.h 文件 //被代理对象 做以下几件事 //1、创建一个协议,用于传值 //2、声明一个遵守上述协议的delegate属性 /** * 创建协议,里面有一个方法,带一个参数,该参数就是我想传出去的值 */ @protocol passValue <NSObject> -(void)passedValue:(NSString *)inputValue; @end @interface NextViewController : UIViewController /** * 声明一个delegate属性 */ @property(nonatomic, weak) id<passValue> delegate; @end ================================================================= //.m 文件 #import "NextViewController.h" @interface NextViewController () @property (weak, nonatomic) IBOutlet UITextField *inputText; - (IBAction)back:(id)sender; @end @implementation NextViewController - (void)viewDidLoad { [super viewDidLoad]; self.navigationItem.title = @"第二个界面"; } /** * 返回上一个界面 * * @param sender <#sender description#> */ - (IBAction)back:(id)sender { NSString *inputString = self.inputText.text; //3、调用代理对象完成传值 if(self.delegate && [self.delegate respondsToSelector:@selector(passedValue:)]){ [self.delegate passedValue:inputString]; } [self.navigationController popViewControllerAnimated:YES]; } @end 2、代理对象 //.h 文件 #import <UIKit/UIKit.h> #import "NextViewController.h" //代理对象 //实现被代理对象创建的协议,实现其中的方法,捕获传过来的值 @interface ViewController : UIViewController <passValue> @end ================================================================= //.m 文件 #import "ViewController.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UILabel *nextPassedValue; - (IBAction)next:(id)sender; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; } /** * 实现被代理对象的方法,将捕获的值设置到UILabel中 * * @param inputValue 传过来的值 */ -(void)passedValue:(NSString *)inputValue{ self.nextPassedValue.text = inputValue; } //点击返回按钮跳转到第二个界面 - (IBAction)next:(id)sender { NextViewController *nvc = [[NextViewController alloc]init]; //设置被代理对象的delegate属性为代理对象即self nvc.delegate = self; [self.navigationController pushViewController:nvc animated:YES]; } @end 五、实现效果 逆向传值.gif