注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

一篇完整的Swift属性参考,轻松让你提高一个档次!

iOS
属性 提供了更多关于声明和类型的信息。在 Swift 中有两种类型的属性,一种用于声明,一种用于类型。例如,required 属性-当用于类的指定或者便利初始化声明时-指明每个子类都必须实现它的初始化函数。noreturn 属性-当用于函数或者方法的类型时-指...
继续阅读 »

属性 提供了更多关于声明和类型的信息。在 Swift 中有两种类型的属性,一种用于声明,一种用于类型。例如,

required 属性-当用于类的指定或者便利初始化声明时-指明每个子类都必须实现它的初始化函数。noreturn 属性

-当用于函数或者方法的类型时-指明函数或者方法无需返回值。

咋们一起好好看,好好学

你可以用字符 @ 加上属性名和属性参数来指定一个属性:

@属性名
@属性名(属性参数)

含有参数的声明属性可以为属性指定更多的信息,可以用于特殊的声明。这些属性参数 被包含在圆括号里,参数的格式由属性决定。

声明属性

声明属性只能用于声明,当然,你也可以使用 noreturn 属性作为函数或者方法的类型。

assignment

此属性可用于修饰重载复合赋值运算符的函数。这个重载复合赋值运算符的函数必须用 inout 来标记初始输入参数。assignment属性示例参见复合赋值运算符

class_protocol

此属性可用于定义类类型协议。

如果你使用 objc 属性的协议, 那么这个协议就隐式含有 class_protocol 属性,你无需显式标记 class_protocol 属性。

exported

此属性可用于内部声明,可以将当前模块的内部模块、子模块或者声明暴露给外部其他模块。如果另一个模块引用了当前模块,那么这个模块就可以访问当前模块中暴露出来的部分。

final

此属性可用于修饰类或者类的属性、方法或成员下标运算符。用于一个类的时候,表明这个类是不能被继承的。用于类的属性、方法或成员下标运算符的时候,表明这个类的这些成员函数不能在任何子类中重写。

lazy

此属性可用于修饰类或者结构体中的存储变量属性,表明这个属性在第一次被访问时,其初始值最多只能被计算和存储一次。lazy 属性示例参见惰性存储属性

noreturn

此属性用于函数或者方法的声明,表明其函数或者方法相应的类型T是@noreturn T。当一个函数或者方法无需返回其调用者时,你可以用这个属性来修饰其类型。

你可以重写没有标示noreturn属性的函数或者方法。也就是说,你不能够重写有noreturn属性的函数或者方法。当你实现此类型的协议方法时,也有相似的规则。

NSCopying

此属性可用于修饰类中的存储变量属性。被修饰的这个属性的赋值函数是由这个属性值的拷贝组成-由copyWithZone方法返回-而不是这个属性本身的值。此属性类型必须符合NSCopying协议。
NSCopying属性类似于Objective-C中的copy属性。

NSManaged

用于修饰类中的存储变量属性,此类继承于NSManagedObject,表明这个属性的存储和实现是由Core Data基于相关的实体描述实时动态提供的。

objc

此属性可用于能用Objective-C表示的任何声明中-例如,非嵌套的类、协议、类和协议的属性和方法(包括取值函数和赋值函数)、初始化函数、析构函数以及下标运算符。objc属性告诉编译器此声明在Objective-C代码中可用。

如果你使用objc属性修饰类或者协议,它会显式的应用于这个类或者协议中的所有成员。当一个类继承于标注objc属性的另一类时,编译器会显式的为这个类添加objc属性。标注objc属性的协议不能够继承于不含有objc属性的协议。

objc属性可以接受由标识符组成的单个属性参数。当你希望暴露给Objective-C的部分是一个不同的名字时,你可以使用objc属性。你可以使用这个参数去命名类、协议、方法、取值函数、赋值函数以及初始化函数。下面的示例就是ExampleClass的enabled属性的取值函数,它暴露给Objective-C代码的是isEnabled,而不是这个属性的原名。

1.  @objc
2. class ExampleClass {
3. var enabled: Bool {
4. @objc(isEnabled) get {
5. // Return the appropriate value
6. }
7. }
8. }

optional

此属性可用于协议的属性、方法或者成员下标运算符,用来表明实现那些成员函数时,此类型的不是必需实现的。

optional属性只能用于标注objc属性的协议。因此,包含可选成员的协议只有类类型适用。更多的关于怎样使用optional属性,以及怎样访问可选协议成员的指导-例如,当你不确定它们是否实现了此类型时-参见可选协议需求

required

此属性用于类的指定或者便利初始化函数时,表明这个类的每个子类都必须实现这个初始化函数。

需求的指定初始化函数必须被显式的包含。当子类直接实现所有超类的指定初始化函数时(或者子类使用便利初始化函数重写了指定初始化函数时),需求的便利初始化函数必须被显式的包含或者继承。

使用Interface Builder声明属性

Interface Builder属性就是使用Interface Builder声明属性以与Xcode同步。Swift提供了如下几种Interface Builder属性:IBAction,IBdesignable,IBInspectable以及IBOutlet。这些属性理论上与Objective-C中相应的属性一样。

IBOutlet和IBInspectable属性可用于类的属性声明,IBAction属性可用于类的方法声明,IBDesignable属性可用于类的声明。

类型属性

类型属性可以只用于类型。当然noreturn属性也可以用于函数或者方法的声明。

auto_closure

此属性用于延迟表达式的赋值计算,将表达式自动封装成一个无参数的闭包。此属性还可作为函数或者方法的类型,此类型无参数并且其返回的是表达式类型。auto_closure属性示例参见函数类型

noreturn

此属性用于函数或者方法时表明此函数或者方法无返回值。你也可以用此属性标记函数或者方法的声明,以表明其函数或者方法相应的类型T是@noreturn T。

属性语法
attribute → @­attribute-name attribute-argument-clause opt
attribute-name → identifier
attribute-argument-clause → (balanced-tokens­ opt)
attributes → attribute­ attributes­ opt­
balanced-tokens → balanced-token ­balanced-tokens­ opt­
balanced-token → (­balanced-tokens­ opt­)­
balanced-token → [balanced-tokens­ opt­]­
balanced-token → {balanced-tokens­ opt­­}­
balanced-token → 任意标识符,关键字,常量,或运算符
balanced-token → 任意的标点符号 (­, )­, [­, ]­, {­, 或 }­

由于文章篇幅有限,只能点到即止地介绍当前一些工作成果和思考,各个 Swift 还有一些新的方向在探索,如果你对 iOS 底层原理、架构设计、构建系统、如何面试有兴趣了解,你也可以关注我及时获取最新资料以及面试相关资料。如果你有什么意见和建议欢迎给我留言!

写的不好的地方欢迎大家指出,希望大家多留言讨论,让我们共同进步!

喜欢iOS的小伙伴可以关注我,一起学习交流!!!

链接:juejin.cn/post/698169…


作者:在做开发的信哥
链接:https://juejin.cn/post/6988459235797368862

收起阅读 »

啥?iOS长列表还可以这么写

iOS
一般说,iOS界面的一些长列表,比如首页,活动页,长的会比较长,那么写起来总感觉没有那么优雅,那么如何才能做到优雅呢? 我在实践工作利用swift枚举的关联值和自定义组模型方法来实现了 下面是gif图效果 可以看到,有些组是杂乱无章的排列着,而且运营那边...
继续阅读 »

一般说,iOS界面的一些长列表,比如首页,活动页,长的会比较长,那么写起来总感觉没有那么优雅,那么如何才能做到优雅呢?
我在实践工作利用swift枚举的关联值和自定义组模型方法来实现了



  • 下面是gif图效果



可以看到,有些组是杂乱无章的排列着,而且运营那边要求,他们可以在后台自定义这些组的顺序
这可怎么办!🥺
下面看我的实现方式


定义一个组模型枚举



  • 包含可能的定义,每个枚举关联当前组需要显示的数据模型,有可能是一个对象数组,也有可能是一个对象


/// 新版首页组cell的类型
enum OriginGroupCellType {
case marquee(list: [MarqueeModel]) // 跑马灯
case beltAndRoad(list: [GlobalAdModel]) // 一带一路广告位
case shoppingCarnival(list: [GlobalAdModel]) // 购物狂欢节
case walletCard(smallWelfare: WelfareSmallResutlModel) // 钱包卡片
case wallet(list: [HomeNavigationModel]) // 钱包cell
case otc(list: [GlobalAdModel]) // OTC
case hxPrefecture(list: [GlobalAdModel]) // HX商品专区
case middleNav(list: [HomeNavigationModel]) // 中部导航
case bottomNav(list: [HomeNavigationModel]) // 底部导航
case broadcast(topSale: HomeNavigationModel, hot: OriginBroadcastModel, choiceness: OriginBroadcastModel) // 直播cell
case middleAd(list: [GlobalAdModel]) // 中间广告cell
case localService(list: [LocalServiceModel]) // 本地服务cell
case bottomFloat(headerList: [OriginBottomFloatHeaderModel]) // 底部悬停cell
}


  • 考虑到要下拉刷新等问题,可以这些枚举都得遵守Equatable协议


  extension OriginGroupCellType: Equatable {
public static func == (lhs: OriginGroupCellType, rhs: OriginGroupCellType) -> Bool {
switch (lhs, rhs) {
case (.marquee, .marquee): return true
case (.beltAndRoad, .beltAndRoad): return true
case (.shoppingCarnival, .shoppingCarnival): return true
case (.walletCard, .walletCard): return true
case (.wallet, .wallet): return true
case (.otc, .otc): return true
case (.hxPrefecture, .hxPrefecture): return true
case (.middleNav, .middleNav): return true
case (.bottomNav, .bottomNav): return true
case (.broadcast, .broadcast): return true
case (.middleAd, .middleAd): return true
case (.localService, .localService): return true
case (.bottomFloat, .bottomFloat): return true
default:
return false
}
}
}

接下来就是组模型的定义



  • 同时我抽取一个协议GroupProvider,方便复用


protocol GroupProvider {
/// 占位
associatedtype GroupModel where GroupModel: Equatable

/// 是否需要往组模型列表中添加当前组模型
func isNeedAppend(with current: GroupModel, listMs: [GroupModel]) -> Bool
/// 获取当前组模型在组模型列表的下标
func index(with current: GroupModel, listMs: [GroupModel]) -> Int
}

extension GroupProvider {
func isNeedAppend(with current: GroupModel, listMs: [GroupModel]) -> Bool {
return !listMs.contains(current)
}

func index(with current: GroupModel, listMs: [GroupModel]) -> Int {
return listMs.firstIndex(of: current) ?? 0
}
}




  • OriginGroupModel,同样也遵守Equatable协议,防止重复添加


func addTo(listMs: inout [OriginGroupModel]) 



  • 这个方法是方便于下拉刷新时,替换最新数据所用


public struct OriginGroupModel: GroupProvider {
typealias GroupModel = OriginGroupModel

/// 组模型的类型
var cellType: OriginGroupCellType
/// 排序
var sortIndex: Int

/// 把groupModel添加或替换到listMs中
func addTo(listMs: inout [OriginGroupModel]) {
if isNeedAppend(with: self, listMs: listMs) {
listMs.append(self)
} else {
let index = self.index(with: self, listMs: listMs)
listMs[index] = self
}
}
}

extension OriginGroupModel: Equatable {
public static func == (lhs: OriginGroupModel, rhs: OriginGroupModel) -> Bool {
return lhs.cellType == rhs.cellType
}
}


  • 考虑要自定义顺序,所以需要定义一个排序的实体


// MARK: - 新版首页组模型的排序规则模型
struct OriginGroupSortModel {
/// 搜索历史的排序
var marqueeIndex: Int
var beltAndRoadIndex: Int
var shoppingCarnivalIndex: Int
var walletCardIndex: Int
var walletIndex: Int
var otcIndex: Int
var hxPrefectureIndex: Int
var middleNavIndex: Int
var bottomNavIndex: Int
var broadcastIndex: Int
var middleAdIndex: Int
var localServiceIndex: Int
var bottomFloatIndex: Int

static var defaultSort: OriginGroupSortModel {
return OriginGroupSortModel(
marqueeIndex: 0,
beltAndRoadIndex: 1,
shoppingCarnivalIndex: 2,
walletCardIndex: 3,
walletIndex: 4,
otcIndex: 5,
hxPrefectureIndex: 6,
middleNavIndex: 7,
bottomNavIndex: 8,
broadcastIndex: 9,
middleAdIndex: 10,
localServiceIndex: 11,
bottomFloatIndex: 99)
}
}


控制器里定义一个 组模型数组



  • 这里有关键代码是


listMs.sort(by: { return $0.sortIndex < $1.sortIndex }) 



  • 所有的数据加载完毕后,会根据我们的自定义排序规则去排序


    /// 组模型数据
public var listMs: [OriginGroupModel] = [] {
didSet {
listMs.sort(by: {
return $0.sortIndex < $1.sortIndex
})
collectionView.reloadData()
}
}

/// 组模型排序规则(可以由后台配置返回,在这里我们先给一个默认值)
/// 需要做一个请求依赖,先请求排序接口,再请求各组的数据
public lazy var sortModel: OriginGroupSortModel = OriginGroupSortModel.defaultSort


网络请求代码


func loadData(_ update: Bool = false, _ isUHead: Bool = false) {
// 定义队列组
let queue = DispatchQueue.init(label: "getOriginData")
let group = DispatchGroup()

// MARK: - 文字跑马灯
group.enter()
queue.async(group: group, execute: {
HomeNetworkService.shared.getMarqueeList { [weak self] (state, message, data) in
guard let `self` = self else { return }
self.collectionView.uHead.endRefreshing()

defer { group.leave() }
let groupModel = OriginGroupModel(cellType: .marquee(list: data), sortIndex: self.sortModel.marqueeIndex)
guard !data.isEmpty else { return }

/// 把groupModel添加到listMs中
groupModel.addTo(listMs: &self.listMs)
}
})

/// .... 此处省略其它多个请求

group.notify(queue: queue) {
// 队列中线程全部结束,刷新UI
DispatchQueue.main.sync { [weak self] in
self?.collectionView.reloadData()
}
}
}


collectionView的代理方法处理


    func numberOfSections(in collectionView: UICollectionView) -> Int {
return listMs.count
}

func collectionView(_: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let groupModel = listMs[section]
switch groupModel.cellType {
case .marquee, .beltAndRoad, .walletCard, .wallet, .otc, .hxPrefecture, .shoppingCarnival, .middleAd:
return 1
case .middleNav(let list):
return list.count
case .bottomNav(let list):
return list.count
case .broadcast:
return 1
case .localService(let list):
return list.count
case .bottomFloat:
return 1
}
}



  • 同理,collectionView的代理方法中,都是先拿到 cellType 来判断,达到精准定位, 举个栗子


    /// Cell大小
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let groupModel = listMs[indexPath.section]
let width = screenWidth - 2 * margin
switch groupModel.cellType {
case .marquee:
return CGSize(width: screenWidth, height: 32)
case .beltAndRoad:
return CGSize(width: width, height: 46)
case .walletCard:
return CGSize(width: width, height: 85)
case .wallet:
return CGSize(width: width, height: OriginWalletCell.eachHeight * 2 + 10)
case .otc, .hxPrefecture:
return CGSize(width: width, height: 60)
case .middleNav:
let row: CGFloat = 5
let totalWidth: CGFloat = 13 * (row - 1) + 2 * margin
return CGSize(width: (screenWidth - totalWidth) / row, height: CGFloat(98.zh(80).vi(108)))
case .bottomNav:
let isFirstRow: Bool = indexPath.item < 2
let row: CGFloat = isFirstRow ? 2 : 3
let totalWidth: CGFloat = 4 * (row - 1) + 2 * margin
let width = (screenWidth - totalWidth) / row
return CGSize(width: floor(Double(width)), height: 70)
case .shoppingCarnival:
return CGSize(width: width, height: 150)
case .broadcast:
return CGSize(width: screenWidth - 20, height: 114)
case .middleAd:
return CGSize(width: width, height: 114)
case .localService:
let width = (82 * screenWidth) / 375
return CGSize(width: width, height: 110)
case .bottomFloat:
let h = bottomCellHeight > OriginBottomH ? bottomCellHeight : OriginBottomH
return CGSize(width: screenWidth, height: h)
}
}


总结一下这种写法的优势




  • 方便修改组和组之前的顺序问题,甚至可以由服务器下发顺序




  • 方便删减组,只要把数据的添加组注释掉




  • 用枚举的方式,定义每个组,更清晰,加上swift的关联值优势,可以不用在控制器里定义多个数组




  • 考虑到要下拉刷新,所以抽取了一个协议 GroupProvider,里面提供两个默认的实现方法



    • 方法一:获取当前cellType在listMs中的下标

    • 方法二:是否要添加到listMs中




  • 界面长什么样,全部由数据来驱动,这组没有数据,界面就对应的不显示(皮之不存,毛将焉附),有数据就按预先设计好的显示




源码地址(源码内容和gif图中有差异,但是思路是一致的)


github.com/XYXiaoYuan/…


作者:Bruceyuan
链接:https://juejin.cn/post/6939767696846225421

收起阅读 »

SwiftUI与Swift的区别

iOS
引言 SwiftUI 于 2019 年度 WWDC 全球开发者大会上发布,它是基于 Swift 建立的声明式框架。该框架可以用于 watchOS、tvOS、macOS、iOS 等平台的应用开发,等于说统一了苹果生态圈的开发工具。 本人最早开始 iOS 开发时选...
继续阅读 »

引言


SwiftUI 于 2019 年度 WWDC 全球开发者大会上发布,它是基于 Swift 建立的声明式框架。该框架可以用于 watchOS、tvOS、macOS、iOS 等平台的应用开发,等于说统一了苹果生态圈的开发工具。


本人最早开始 iOS 开发时选择了 OC(Objective-C,一种编程语言),当时 OC 不但拥有各种知名的第三方库和完善的社区支持,同时 Swift 语言本身都还在不断颠覆性改进中。但当我看了 2020 年 WWDC 关于 SwiftUI 一系列课程之后,便从 Swift 语言的学习开始,逐步了解并掌握 SwiftUI,并果断抛弃了OC,将新项目全部迁移到了 SwiftUI 框架。


SwiftUI 到底有没有苹果宣传的那么理想化?资深的 iOS 开发者是否有必要转型,以及如何转型?SwiftUI 在实际使用的过程中真实体验如何?这些问题就是这篇文章希望探讨的话题。


在最后,我会分享一些自己的学习心得和材料顺序,借开源的精神与大家共同进步。


什么是 SwiftUI


对于 Swift UI,官方的定义很好找到,也写得非常明确:



SwiftUI is a user interface toolkit that lets us design apps in a declarative way.

可以理解为 SwiftUI 就是⼀种描述式的构建 UI 的⽅式。



单单通过描述,大部分人其实很难对抽象的编程方法,和其中的改进有直观的认识。这篇文章也希望通过尽量口语化的叙述,减少专业词汇和代码的出现来降低阅读门槛,让更多人了解计算机科学,了解程序的世界。


下面是我手头正在做的一个项目,定位是一个原生全平台的电子阅读应用,正在使用 SwiftUI 构建用户界面。


为什么苹果要推出 SwiftUI


SwiftUI 的两个组成部分,Swift + UI,即是这个问题的答案。


Swift:编程语言和体验的一致性


Swift 代表苹果推出的一种现代编程语言


很多苹果用户之所以喜欢苹果的产品,其中一个原因,是不同产品之间由内而外的统一感和协调感。这一点在硬件层面的感知是最明显的,从早期开始苹果出的硬件就是「果味十足」的。即使是新品迭代或者是开发全新的品类,也一定会带有烙印很深的「果味」工业设计。


仅仅外观的统一还不够,苹果真正追求的是内外一致,也就是体验的统一。


然而工业设计可以交给自家精英团队,系统可以相互借鉴,但用户使用的软件是由广大的开发者自由创造的。让人意外的是,这一点苹果做的也很不错,与别家相比,质量精良是很多人对苹果系统上软件的印象。


为了实现这一目标苹果做了大量不为普通消费者所感知的工作。


在设计上,苹果提供了一整套不断在更新的 Human Interface Guidelines,详细规定了与视觉相关的各个方面。在完成开发,准备上架分发之前,苹果的审核团队会对每一款应用进行审核,根据 App Store Review Guidelines 的条款判断应用是否允许上架 App Store,即使是知名的应用违反规定也是说下架就下架,绝不含糊。对于不越狱的移动设备而言,App Store 是唯一可以安装应用的途径,控制了其中的准入也就等于替整个平台做了筛选。


除了控制终端以外,苹果也在想方设法增加开发者的数量,提升单个应用质量。方式也非常符合第一性思维原则——降低开发的难度。所以先有了Swift,紧接着又推出了 SwiftUI。苹果希望直接优化语言本身,并统一所有设备的开发体验,让开发者更容易上手,也更容易将心里的想法转化为运行的程序。


虽说在 2015 年推出 Swift2.0 的时候就进行了开源。但这些年 Swift 在后端或是跨平台的发展上并不是非常顺利。提起 Swift,圈内还是会被默认为特指苹果平台内使用的编程语言,地位有些类似 OC 的接班者。其实为了推广这门语言,苹果本身也做了非常多的工作,像是推出SwiftUI,基本就可以看做苹果在推广新语言的过程中一个里程碑式的节点。


SwiftUI 使用了大量 Swift 的语言特性,特别是 5.0 之后新增的特性。Swift 5.1 的很多特性几乎可以说都是为了 SwiftUI 量身定制的,比如 Opaque return types、Property Delegate 和 Function builder 等。


UI:开发的困局


在 SwiftUI 出现之前,苹果不同的设备之前的开发框架并不互通,移动端的⼯程师和桌⾯端的⼯程师需要掌握的知识,有很⼤⼀部分是差异化的。


从 iOS SDK2.0 开始,移动端的开发者⼀直使⽤ UIKit 进⾏⻚⾯部分的开发。UIKit 的思想继承了成熟的 AppKit 和MVC(Model-View-Controller)模式,作出了⼀些改进,但本质上改动不⼤。UI 包括了⽤⼾能看到的⼀切,包括静⽌的显⽰和动态的动画。


再到后来苹果推出了Apple Watch,在这块狭小屏幕上,又引入了一种新的布局方式。这种类似堆叠的逻辑,在某种程度上可以看做 SwiftUI 的未完全体。


截止此时,macOS 的开发需要使用 AppKit,iOS 的开发需要使用 UIKit,WatchOS 的开发需要使用堆叠,这种碎片化的开发体验无疑会大大增加开发者所需消耗的时间精力,也不利于构建跨平台的软件体验。


即使单看 iOS 平台,UIKit 也不是完美的开发⽅案。


UIKit 的基本思想要求 ViewController 承担绝⼤部分职责,它需要协调 model,view 以及⽤⼾交互。这带来了巨⼤的 sideeffect 以及⼤量的状态,如果没有妥善安置,它们将在 ViewController 中混杂在⼀起,同时作⽤于 view 或者逻辑,从⽽使状态管理愈发复杂,最后甚⾄不可维护⽽导致项⽬崩溃。换句话说,在不断增加新的功能和⻚⾯后,同⼀个ViewControlle r会越来越庞杂,很容易在意想不到的地⽅产⽣ bug。⽽且代码纠缠在⼀起后也会⼤⼤降低可读性,伤害到维护和协作的效率。


SwiftUI的特点


在很多地方都能看到 SwiftUI 针对现有问题的一些解决思路,而且现在的编程思想经过不断以来的演化,也一直就软件工程在开发过程中的各种问题在寻找答案。


近年来,随着编程技术和思想的进步,使用声明式或者函数式的方式来进行界面开发,已经越来越被接受并逐渐成为主流。SwiftUI 不是第一个,也不会是最后一个使用声明式界面开发的框架。


声明式的界面开发方式


在计算机科学的世界内,抽象是一个很重要的概念。从底层的二进制逻辑门,到人类可以阅读和理解的编程语言之间,是由很多层的抽象将它们关联起来的。所谓抽象,简单解释就是通过封装组件,将底层细节打包并隐藏起来,从而明确逻辑降低复杂度。就像把晶体管打包成逻辑门,以及软件工程中的函数对象。在软件开发的过程中,工程师只需负责某个具体功能的实现,而其他人则通过开放的 api 使用该功能。


与曾经的布局方式相比,声明式的页面开发无疑又加了一层抽象。


在 UIKit 框架中,界面上的每一个元素都需要开发者进行布置,期间有不少计算工作,例如长宽的改变或是屏幕可视面积的变化等。这种线性的方式被称为指令式 (imperative) 编程。以一行文字为例,放置在哪个坐标、宽度多少、在哪里换行、怎么断句、字形字号是多少、最终高度多少、是否需要缩小字号来完全显示等,这些都是开发者在制作界面时要考虑和计算妥当的问题。到了第二年,用户可能会换更大屏幕的手机,系统支持动态字体调节等新功能,此时原先的程序不进行适配就可能出现显示问题,开发者就需要回头进行程序的重新调试。


换做 SwiftUI 之后,上述的很多变量就被系统接管了。开发者要做的就是直观的告诉系统放置一个图像,上面加一行文字,右边加一个按钮。系统会根据屏幕大小、方向等自动渲染这个界面,开发者也不再需要像素级的进行计算。这被称为声明式 (declarative) 编程


对比同一个场景界面的实现


作为常用的列表视图,在UIKit中被称为 TableView,而在 SwiftUI 中被叫做 List 或 Form。同样是实现一个列表,在 SwiftUI 仅需要声明这里有一个 List 以及每个单元的内容即可。而在UIKit 中,需要使用委托代理的方式定制每个单元的内容,还需要事无巨细的设置行和组的数量、对触摸的反应、点击过程等各方面。


在我的另一个早期项目 Amos 时间志中就可以看到,为了绘制主页就需要几千行代码。


智能适配不同尺寸的屏幕


除了不同尺寸的屏幕,SwiftUI 还能根据运行平台的区别,将按钮、对话框、设置项等渲染成匹配的样式。由于声明的留白是很大的,当开发者不需要事无巨细的安排好每一个细节时,系统可操作的空间也会变大。


可以想象,假如苹果推出新品例如眼镜,或许同样的界面代码会被展示成与 iPhone 中完全不同的样式。


提高了解决问题时所需要着手的层级,这可以让开发者可以将更多的注意力集中到更重要的创意方面。


链式调用修改属性


链式调用是 Swift 语言的一种特性,就是用来使用函数方法的一种方式。可以像链条那样不断地调用函数,中间不需要断开。使用这种方式开发者可以给界面元素添加各种属性,只要愿意,同样能够事无巨细的安排页面元素的各种细节。


除了系统预制的属性可以调节外,开发者也可以进行自定义。例如将不同字体、字号、行间距、颜色等属性统合起来,可以组合成为一个叫「标题」的文字属性。之后凡是需要将某一行文字设置成标题,直接添加这个自定义的属性即可。


使用这种方式进行开发无疑能够极大的避免无意义的重复工作,更快的搭建应用界面框架。


界面元素组件化


理论上来讲,每一个复杂的视图,都是由大量简单的单元视图构成。但是函数方法可以包装起来,做到仅在有需要的时候进行调取使用。在 UIKit 框架下的页面元素解耦却不太容易,一般都是针对某种特定情境,很难进行移植。有时候可能手机横屏就会让页面元素混乱,就更别论页面元素的组件化了。


不过 SwiftUI 在布局上的特点,却可以便捷的拆分复杂的视图组件。单一的组件不仅可以自由组合,而且在苹果的任意平台上都可以使用该组件,达到跨平台的实现。


一般我个人会将视图组件区分为基础组件、布局组件和功能组件。因为 SwiftUI 的界面不再像 UIKit 那样,用 ViewController 承载各种 UIVew 控件,而是一切都是视图。这种视图的拼装方式提高了界面开发的灵活性和复用性。


响应式编程框架 Combine


在构建复杂界面的过程中,数据的流通一直是指令式编程中相当让人头疼的部分。


在 UIKit 框架下时,会配合 Target-Action 或者 protocol-delegate 模式来交换信息,使用 Key-Value Observing (KVO) 或者 Key-Value Coding (KVC) 来监测变化和读写属性。但即便开发者熟练地使用这些工具,面对日益增长的应用复杂性,掉坑里的可能性还是非常大。因为有太多需要开发者妥善处理的数据流动,例如数据改动后需要通知相关的页面进行刷新,或是让关联数据重新计算等。


像是 React Native 和 Flutter 这样的移动端跨平台方案,由于采用了声明式 UI 的编写方式和严格的数据流动方向,就能够大幅减轻开发者的思考负担。


SwiftUI 很明显也吸收了这些现代的编程思想,在另一个重量级系统框架 Combine 的协助下,实现了单一数据源的管理。


响应式编程的核心是将所有事件转化成为异步的数据流,这刚好就是 Combine 的主要功能。Combine 采用观察者模式,对应多个观察者,可以分别订阅感兴趣的内容。在 SwiftUI 的界面布局过程中,不同的 View 就是观察者,分别订阅了相关联的属性,并在数据发生变化之后就能够自动的重新渲染。


单一数据源


在 WWDC 的介绍视频中,「Source of truth」这个词反复出现,中文可以将这个词理解为单一数据源。


一直以来复杂的UI结构都会创造更为复杂的数据和逻辑管理需求,每次在用户交互,或是数据来源发生变化的时候,能否及时更新相关界面组件,不然就会引起显示问题。


但是在 SwiftUI 中,只要在属性声明时加上 @State 等关键词,就可以将该属性和界面元素联系起来,在每次数据改动后,都有机会决定是否更新视图。这样就可以将所有的属性都集中到一起进行管理和计算,也不再需要手写刷新的逻辑。因为在 SwiftUI 中,页面渲染前会将开发者描述的界面状态储存为结构体,更新界面就是将之前状态的结构体销毁,然后生成新的状态。而在绘制界面的过程中,会自动比较视图中各个属性是否有变化,如果发生变化,便会更新对应的视图,避免全局绘制和资源浪费。


使用这种方式,读和写都集中在一处,开发者就能够更好地设计数据结构,比较方便的增减类型和排查问题。而不用再考虑线程、原子状态、寻找最新数据等各种细节,再决定通知相关的界面进行刷新


与UIKit彼此相容


一般开发者学习新技术有一个最大的障碍就是原先的项目怎么办。但 SwiftUI 在这一点上做的相当不错。由于是一个新发布的框架,UI 组件并不齐全,当 SwiftUI 中并没有提供类似的功能时,就可以把 UIKit 中已有的部分进行封装,提供给 SwiftUI 使用。需要做的仅仅是遵循UIViewRepresentable协议即可。相反,在已有的项目中,也可以仅用 SwiftUI 制作一部分的 UI 界面。


当然两种代码的风格是截然不同的,但在使用上却基本没有性能的损失。到最终成品时,用户也无法分辨出两种界面框架的不同。


从开发者的⻆度看 SwiftUI


回到开头的问题:SwiftUI 到底有没有苹果宣传的那么理想化?


在 WWDC 发布 SwiftUI 时,有一句话让我印象深刻:「不论多复杂,原先布局的 99% 现在都可以使用 SwiftUI 进行构建」。当我查询 SwiftUI 是否可以承担大型项目开发时,又一次从资深开发者那里看到了这句话。


在我实际体验一段时间,并最终将一款全 SwiftUI 开发的应用上架后,认为这句话并没有什么问题,但前提是对编程这件事需要有比较基础且深入的理解。


这有点像我们学习一些优秀的第三方库时候的感受,同样是用 Xcode 写代码,有些人写出来就是白开水,而另一些人就是黑魔法。学习同样的语言特性,但由于理解的深刻程度不同,在使用时也会大不一样。仅仅依靠一些标准的自带组件无法做出一款出色的应用,即使如 UIKit 那样拥有如此丰富的组件也不行。很多时候还是要根据业务需要,或者是一些独特的脑洞做出最合适的界面。


对于个⼈开发者而言,意味着什么?


SwiftUI 的上限有多高,还要看未来一年一度的更新。但与之前的 UIKit 相比,下限被大大拉低已是不争的事实。这里的所谓下限,指的是学习的难度。由于描述性的布局方式与我们平时的阅读习惯非常接近,告诉系统在页面中间放一个图片就像告诉别人在桌子中间放一个苹果那么直观。我认识的好多 UI 设计师就通过短时间的自学掌握了 SwiftUI,并且搭建起可以直接在真机上使用的 Demo。


降低学习成本这件事是非常有意义的,不仅可以增加开发者数量,降低学习门槛,而且就学习本身而言,让初学者感受到成就感和明确学习方向,长久而言是比短时间的学习效率更重要的事情。只有开始的时候培养足够的兴趣,在后期才可能自主自发的研究更深入更困难的问题。


SwiftUI 和 Combine 大量借助了 Swift 的语法特性,尤其是 5.0 之后的几个更新,新特性就仿佛是为了这两个系统及框架量身定做的一般。虽然将 Swift 开源,但苹果无疑还是牢牢地把握着这门语言的发展。这两个框架和编程语言之间的配合默契,也仿佛让开发者体会到了软硬件一体带来的发展潜力。无论 Swift 出圈后的成果能有多少,在苹果的体系内,无疑是能够将各种消费层面的软件体验整合统一。


只要使用 SwiftUI,系统会默认支持白天和黑夜模式的自动切换;在各种尺寸的屏幕间自动适配;为任意控件添加 Haptic Touch 或是动画;在 Apple Watch 上带来独立而完整的体验;将iOS 的应用转换为 macOS 的原生应用,会以最快的速度支持第一方的各种新特性。这种对苹果硬件的深入支持是那些跨平台方案无论如何无法实现的。可以看一些采用第三方框架的知名应用,像是横屏、黑夜模式、小组件等基础的特性,到现在都迟迟没有适配。


所以 SwiftUI 对小工作室或是独立开发者来说是件好事,可以让新的想法快速落地并且接受市场验证,真正的做到敏捷开发。以这种方式在市场中的细分领域获得一席之地,也能让更多人体会到编写程序的感受,甚至是创造财富。


开源我的学习心得


在最后的部分我会分享一些自己学习 SwiftUI 的过程和介绍相关的资源,给一些也对开发感兴趣的小伙伴们做个参考。


首先要学习的是 Swift 编程语言,它与 OC 之间的差别还是挺大的,学习也没有什么捷径,直接阅读官方教程,对照着实例自己写一遍就行。国内有几个非常好的汉化网站,可以一起对照学习。基本上没有必要特意买书,反而不如直接电脑上看了就敲来的方便。



  1. 官网

  2. SwiftGG

  3. GitHub 汉化库


对语言有了大概的了解后,就可以开始对 SwiftUI 的学习,假如遇到问题可以反复回去查看之前的资料。很多被忽略的细节,或是当时初看没概念的部分,结合具体的案例就能够有比较透彻的理解。



  1. 斯坦福公开课 CS193P·2020 年春:该课程强推,我当年学习 OC 看的就是它,现在到SwiftUI了还是先看这个,系统且细致,结合案例和编程过程中的小技巧介绍,是很好的入门课程。

  2. 苹果官方 SwiftUI 课程:打开Xcode,照着官方的教学,从头到尾学着做一遍应用。

  3. Hacking with swift:这是国外一个程序员用业余时间搭建的分享网站,有大量的文章可以阅读,还有推荐初学者跟着做的「100 Days of SwiftUI」课程。

  4. 苹果官方文档:文档是必读的,虽然很多文档缺乏工程细节,但是文档涉及很多概念性的内容,你可以知道官方是怎么思考的,并且有很多具体的机制参数。我本人有一个习惯,要是工程涉及某个框架,会把相关的文档都翻译一遍。

  5. Stack Overflow:有问题查询专用,在谷歌中搜索错误代码或者关键词基本都会由该网站给答案。

  6. 阅读 SwiftUI 库的源代码。


基本到此假如能够顺利完成下来,就可以开启自己的项目。开发想要提高的关键就是亲自写代码和不断地阅读学习。初期学习的关键能力就是英语,而到后期需要的就是真正的兴趣和一些数学能力。


作者:洋仔
链接:https://juejin.cn/post/6997313521067229214
收起阅读 »

Swift:基石库——R.swift

iOS
这是我参与更文挑战的第4天,活动详情查看: 更文挑战何为基石库?做一个App无外乎两大要素:获取数据通过数据驱动页面也许你的App没有网络请求或者网络请求少,你可以不需要Alamofire。也许你的App的UI不是特别复杂,简单的xib和storyb...
继续阅读 »

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

何为基石库?

做一个App无外乎两大要素:

  • 获取数据

  • 通过数据驱动页面

也许你的App没有网络请求或者网络请求少,你可以不需要Alamofire。

也许你的App的UI不是特别复杂,简单的xib和storyboard就可以胜任。

但是在当下一个App中,图片资源、字符串资源等,作为一个App开发者,你是不得不用的。

举个栗子,传统的获取一个image资源我们都是这么写:

let image = UImage(named: "saber")

这么写的最大弊端就是saber这是一个字符串硬编码,靠的的是纯手工敲打,一旦出错,界面就会出现异常。

在开发中,需要尽量避免这种硬编码,如何高效将这种硬编码的表达方式更换为高效安全的方式,就由本次的主角出场了--R.swift

统和所有的资源,以现代化的方式引用资源,项目中使用它,虽然不会让你的App上升一个层次,不过却给你的编码极度舒适。

let image = R.image.saber()

同样是Ex咖喱棒,味道却完全不同,哈哈。

基石库就是那些你无法避免不得不用的库,而R.swift恰恰就是。

R.swift

何为R,即Resource的缩写,我们先看看官方给出的一些例子:

使用R.swift函数前:

let icon = UIImage(named: "settings-icon")
let font = UIFont(name: "San Francisco", size: 42)
let color = UIColor(named: "indicator highlight")
let viewController = CustomViewController(nibName: "CustomView", bundle: nil)
let string = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.current, "Arthur Dent")

使用R.swift函数后:

let icon = R.image.settingsIcon()
let font = R.font.sanFrancisco(size: 42)
let color = R.color.indicatorHighlight()
let viewController = CustomViewController(nib: R.nib.customView)
let string = R.string.localizable.welcomeWithName("Arthur Dent")

所有的资源都函数化后,编写过程想出错都难,特别需要注意的是最后一个涉及国际化的函数R.string.localizable.welcomeWithName("Arthur Dent"),Arthur Dent这个字符串需要自己具体制定,可以通过在做国际化时,通过info.strings进行处理。

R.swift目前支持下面这些资源文件管理:

  • Images
  • Fonts
  • Resource files
  • Colors
  • Localized strings
  • Storyboards
  • Segues
  • Nibs
  • Reusable cells

基本上覆盖了绝大多数的App中的资源管理。

安装和使用

安装

R.swift使用其他特别舒服,不过它的安装确实比其他的第三方库稍微麻烦一点,正所谓工欲善其事必先利其器,这一点麻烦是值得的。

1.添加'R.swift' 在工程的Podfile文件中,并运行pod install。 2.如下图所示。添加脚本:

image.png

3.如下图所示,移动脚本的位置,让它在Compile Sources phase和Check Pods Manifest.lock之间:

image.png

4.添加脚本:

image.png

在shell,下面这一栏添加"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/R.generated.swift"

在input Files通过+号添加$TEMP_DIR/rswift-lastrun

在Output Files通过+号添加$SRCROOT/R.generated.swift

5.运行添加R.generated.swift:

完成第4步后,进行command + B编译,然后在工程的根目录下面会找到R.generated.swift文件: image.png

将这个文件拖入到工程中,并且不要勾选Copy items if needed

image.png

这样,R.swift就安装完成啦。

使用

每一次添加了新的资源文件,就运行一次command + B一次,这样R.generated.swift文件就将新加入的资源文件更新,使用使用的时候只用通过R.来进行引用了。

更多用法,参考上面写的例子,以及官方文档

明天周末怎么破?

最怕周末更文,因为作为一个奶爸,休息都不是自己的,我争取做到不水文,至少讲一些知识点,明日继续,大家加油。


收起阅读 »

Swift:解包的正确姿势

iOS
嗯,先来一段感慨 在掘金里面看见iOS各路大神各种底层与runtime,看得就算工作了好几年的我也一脸蒙圈,于是只好从简单的入手。 文章最初发布在简书上面,有段时间了,考虑以后大部分时间都会在掘金学习,于是把文章搬过来了。稍微做了点润色与排版。 对于Swift...
继续阅读 »

嗯,先来一段感慨


在掘金里面看见iOS各路大神各种底层与runtime,看得就算工作了好几年的我也一脸蒙圈,于是只好从简单的入手。


文章最初发布在简书上面,有段时间了,考虑以后大部分时间都会在掘金学习,于是把文章搬过来了。稍微做了点润色与排版。


对于Swift学习而言,可选类型Optional是永远绕不过的坎,特别是从OC刚刚转Swift的时候,可能就会被代码行间的?与!,有的时候甚至是??搞得稀里糊涂的。


这篇文章会给各位带来我对于可选类型的一些认识以及如何进行解包,其中会涉及到Swift中if let以及guard let的使用以及思考,还有涉及OC部分的nullablenonnull两个关键字,以及一点点对两种语言的思考。


var num: Int? 它是什么类型?


在进行解包前,我们先来理解一个概念,这样可能更有利于对于解包。


首先我们来看看这样一段代码:



var num: Int?

num = 10

if num is Optional<Int> {

print("它是Optional类型")

}else {

print("它是Int类型")

}



请先暂时不要把这段代码复制到Xcode中,先自问自答,num是什么类型,是Int类型吗?


好了,你可以将这段代码复制到Xcode里去了,然后在Xcode中的if上一定会出现这样一段话:



'is' test is always true



num不是Int类,它是Optional类型


那么Optional类型是啥呢--可选类型,具体Optional是啥,Optional类型的本质实际上就是一个带有泛型参数的enum类型,各位去源码中仔细看看就能了解到,这个类型和Swift中的Result类有异曲同工之妙。


var num: Int?这是一个人Optional的声明,意思不是“我声明了一个Optional的Int值”,而是“我声明了一个Optional类型,它可能包含一个Int值,也可能什么都不包含”,也就是说实际上我们声明的是Optional类型,而不是声明了一个Int类型!


至于像Int!或者Int?这种写法,只是一种Optional类型的糖语法写法。


以此类推String?是什么类型,泛型T?是什么类型,答案各位心中已经明了吧。


正是因为num是一个可选类型。所以它才能赋值为nil, var num: Int = nil。这样是不可能赋值成功的。因为Int类型中没有nil这个概念!


这就是Swift与OC一个很大区别,在OC中我们的对象都可以赋值为nil,而在Swift中,能赋值为nil只有Optional类型!


解包的基本思路,使用if let或者guard let,而非强制解包


我们先来看一个简单的需求,虽然这个需求在实际开发中意义不太大:


我们需要从网络请求获取到的一个人的身高(cm为单位)以除以100倍,以获取m为单位的结果然后将其结果进行返回。


设计思路:


由于实际网络请求中,后台可能会返回我们的身高为空(即nil),所以在转模型的时候我们不能定义Float类型,而是定义Float?便于接受数据。


如果身高为nil,那么nil除以100是没有意义的,在编译器中Float?除以100会直接报错,那么其返回值也应该为nil,所以函数的返回值也是Float?类型


那么函数应该设计成为这个样子是这样的:



func getHeight(_ height: Float?) -> Float?



如果一般解包的话,我们的函数实现大概会写成这样:



func getHeight(_ height: Float?) -> Float? {

if height != nil {

return height! / 100

}

return nil

}



使用!进行强制解包,然后进行运算。


我想说的是使用强制解包固然没有错,不过如果在实际开发中这个height参数可能还要其他用途,那么是不是每使用一次都要进行强制解包?


强制解包是一种很危险的行为,一旦解包失败,就有崩溃的可能,也许你会说这不是有if判断,然而实际开发中,情况往往比想的复杂的多。所以安全的解包行为应该是通过if let 或者guard let来进行。



func getHeight(_ height: Float?) -> Float? {

if let unwrapedHeight = height {

return unwrapedHeight / 100

}

return nil

}



或者:



func getHeight(_ height: Float?) -> Float? {

guard let unwrapedHeight = height else {

return nil

}

return unwrapedHeight / 100

}



那么if let和guard let 你更倾向使用哪个呢?


在本例子中,其实感觉二者的差别不大,不过我个人更倾向于使用guard let。




原因如下:


在使用if let的时候其大括号类中的情况才是正常情况,而外部主体是非正常情况的返回的nil;


而在使用guard let的时候,guard let else中的大括号是异常情况,而外部主体返回的是正常情况。


对于一个以返回结果为目的的函数,函数主体展示正常返回值,而将异常抛出在判断中,这样不仅逻辑更清晰,而且更加易于代码阅读。




解包深入


有这么一个需求,从本地路径获取一个json文件,最终将其转为字典,准备进行转模型操作。


在这个过程中我们大概有这么几个步骤:


1. 获取本地路径 


func path(forResource name: String?, ofType ext: String?) -> String?


2. 将本地路径读取转为Data 


init(contentsOf url: URL, options: Data.ReadingOptions = default) throws


3. JSON序列化


class func jsonObject(with data: Data, options opt: JSONSerialization.ReadingOptions = []) throws -> Any


4. 是否可以转为字典类型


我们可以看到以上几个函数中,获取路径获取返回的路径结果是一个可选类型而转Data的方法是抛出异常,JSON序列化也是抛出异常,至于最后一步的类型转换是使用as? [Sting: Any]这样的操作


这个函数我是这来进行设计与步骤分解的:


函数的返回类型为可选类型,因为下面的4步中都有可能失败进而返回nil。


虽然有人会说第一步获取本地路径,一定是本地有的才会进行读取操作,但是作为一个严谨操作,凡事和字符串打交道的书写都是有隐患的,所以我这里还是用了guard let进行守护。


这个函数看起来很不简洁,每一个guard let 后面都跟着一个异常返回,甚至不如使用if let看着简洁


但是这么写的好处是:在调试过程中你可以明确的知道自己哪一步出错



func getDictFromLocal() -> [String: Any]? {

/// 1 获取路径

guard let path = Bundle.main.path(forResource: "test", ofType:"json") else {

return nil

}

/// 2 获取json文件里面的内容

guard let jsonData = try? Data.init(contentsOf: URL.init(fileURLWithPath: path)) else {

return nil

}

/// 3 解析json内容

guard let json = try? JSONSerialization.jsonObject(with: jsonData, options:[]) else {

return nil

}

/// 4 将Any转为Dict

guard let dict = json as? [String: Any] else {

return nil

}

return dict

}



当然,如果你要追求简洁,这么写也未尝不可,一波流带走



func getDictFromLocal() -> [String: Any]? {

guard let path = Bundle.main.path(forResource: "test", ofType:"json"),

let jsonData = try? Data.init(contentsOf: URL.init(fileURLWithPath: path)),

let json = try? JSONSerialization.jsonObject(with: jsonData, options:[]),

let dict = json as? [String: Any] else {

return nil

}

return dict

}



guard let与if let不仅可以判断一个值的解包,而且可以进行连续操作


像下面这种写法,更加追求的是结果,对于一般的调试与学习,多几个guard let进行拆分,未尝不是好事。


至于哪种用法更适合,因人而异。


可选链的解包


至于可选链的解包是完全可以一步到位,假设我们有以下这个模型。



class Person {

var phone: Phone?

}

class Phone {

var number: String?

}



Person类中有一个手机对象属性,手机类中有个手机号属性,现在我们有位小明同学,我们想知道他的手机号。


小明他不一定有手机,可能有手机而手机并没有上手机号码。



let xiaoming = Person()

guard let number = xiaoming.phone?.number else {

return

}

print(number)



这里只是抛砖引玉,更长的可选链也可以一步到位,而不必一层层进行判断,因为可选链中一旦有某个链为nil,那么就会返回nil。


nullable和nonnull


我们先来看这两个函数,PHImageManager在OC与Swift中通过PHAsset实例获取图片的例子



[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:size contentMode:PHImageContentModeDefault options:options resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {

//、 非空才进行操作 注意_Nullable,Swift中即为nil,注意判断

if (result) {

}

}];




PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .default, options: options, resultHandler: { (result: UIImage?, info: [AnyHashable : Any]?) in

guard let image = result else { return }

})



在Swift中闭包返回的是两个可选类型,result: UIImage?与info: [AnyHashable : Any]? 


而在OC中返回的类型是 UIImage * _Nullable result, NSDictionary * _Nullable info


注意观察OC中返回的类型UIImage * 后面使用了_Nullable来修饰,至于Nullable这个单词是什么意思,我想稍微有点英文基础的应该一看就懂--"可以为空",这不恰恰和Swift的可选类型呼应吗?


另外还有PHFetchResult遍历这个函数,我们再来看看在OC与Swift中的表达



PHFetchResult *fetchResult;

[fetchResult enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];




let fetchResult: PHFetchResult

fetchResult.enumerateObjects({ (obj, index, stop) in

})



看见OC中Block中的回调使用了Nonnull来修饰,即不可能为空,不可能为nil,一定有值,对于使用这样的字符修饰的对象,我们就不必为其做健壮性判断了。


这也就是nullable与nonnull两个关键字出现的原因吧--与Swift做桥接使用以及显式的提醒对象的状态


一点点Swift与OC的语言思考


我之前写过一篇文章,是说有关于一个字符串拼接函数的


从Swift来反思OC的语法


OC函数是这样的:



- (NSString *)stringByAppendingString:(NSString *)aString;



Swift中函数是这样的:



public mutating func append(_ other: String)



仅从API来看,OC的入参是很危险的,因为类型是NSString *


那么nil也可以传入其中,而传入nil的后果就是崩掉,我觉得对于这种传入参数为nil会崩掉的函数需要特别提醒一下,应该写成这样:



- (NSString *)stringByAppendingString:(NSString * _Nonnull)aString;

/// 或者下面这样

- (NSString *)stringByAppendingString:(nonnull NSString *)aString;



以便告诉程序员,入参不能为空,不能为空,不能为空,重要的事情说三遍!!!


反观Swift就不会出现这种情况,other后面的类型为String,而不是String?,说明入参是一个非可选类型。


基于以上对于代码的严谨性,所以我才更喜欢使用Swift进行编程。


当然,Swift的严谨使得它失去部分的灵活性,OC在灵活性上比Swift卓越。


作者:season_zhu
链接:https://juejin.cn/post/6931154052776460302

收起阅读 »

iOS 无感知上拉

iOS
本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!RxSwift编写wanandroid客户端现已开源目前RxSwift编写wanandroid客户端已经开源了——项目链接。记得给个star喔!附上一张效果图片:本篇文章是从6月更...
继续阅读 »

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

RxSwift编写wanandroid客户端现已开源

目前RxSwift编写wanandroid客户端已经开源了——项目链接。记得给个star喔!

附上一张效果图片:

RPReplay_Final1625472730.2021-07-05 16_13_58.gif

本篇文章是从6月更文中热心网友的留言中进行的开发与探索:

Snip20210709_1.png

6月确实因为日更的原因,这个功能没有实现,趁着7月的时候,解决了。

废话了这么多,那么我们进入主题吧。

什么是无感知上拉加载更多

什么是无感知,这个这样理解:在网络情况正常的情况下,用户对列表进行连续的上拉时,该列表可以无卡顿不停出现新的数据。

如果要体验话,Web端很多已经做到了,比如掘金的首页,还有比如掘金iOS的App,列表都是无感知上拉加载更多。

说来惭愧,写了这久的代码,还真的没有认真思考这个功能怎么实现。

如何实现无感知上拉加载更多

我在看见这位网友留言的时候,就开始思考了。

在我看来,有下面几个着手点:

  • 列表滑动时候的是如何知道具体滑动的位置以触发接口请求,添加更多数据?

  • 从UIScrollView的代理回调中去找和scrollView的位置(contentOffset)大小(contentSize)关系密切的回调。

  • 网络上有没有比较成熟的思路?

顺着这条线,我先跑去看了UIScrollViewDelegate的源码:

public protocol UIScrollViewDelegate : NSObjectProtocol {


@available(iOS 2.0, *)
optional func scrollViewDidScroll(_ scrollView: UIScrollView) // any offset changes

@available(iOS 3.2, *)
optional func scrollViewDidZoom(_ scrollView: UIScrollView) // any zoom scale changes

.
.
.
.
.
.
/// 代码很多,这里就不放上来,给大家压力了。
}

直接上结论吧:看了一圈,反正没有和contentSize或者位置相关的回调代理。scrollViewDidScroll这个回调里面虽然可以回参scrollView,但是对于我们需要的信息还不够具体。

思考:既然UIScrollViewDelegate的代理没有现成的代理回调,自己使用KVO去监听试试?

网上的思路(一)

就在我思考的同时,我也在网络上需求实现这个功能的答案,让后看到这样一个思路:

实现方法很简单,需要用到tableView的一个代理方法,就可轻松实现。- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath就是这个方法,自定义显示cell。这个方法不太常用。但是这个方法可在每个cell将要第一次出现的时候触发。然后我们可设置当前页面第几个cell将要出现时,触发请求加载更多数据。

我看了之后,心想着,多写一个TableView的代理,总比写KVO的代码少,先试试再说,于是代码撸起:

extension SwiftCoinRankListController: UITableViewDelegate {

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let row = indexPath.row
let distance = dataSource.count - 25
print("row: \(row), distance:\(distance) ")
if row == distance {
loadMore()
}
}
}

本代码可以在开源项目中的SwiftCoinRankListController.swift文件查看具体的逻辑,其主要就是通过cell显示的个数去提前请求加载数据,然后我们看看效果:

620A94AE4920C54C6E1B85E1776AC83C.2021-07-09 17_47_45.gif

Gif可能看起来还好,我说我调试的感受:

虽然做到了上拉无感知,但是当手滑的速度比较快的时候,到底了新的数据没有回来,就会在底部等一段时间。

功能达到了,但是感受却不理想,果然还是监听的细腻程度不够。

网上的思路(二)

然后在继续的搜索中,我看到了另外一个方案:

很多时候我们上拉刷新需要提前加载新数据,这时候利用MJRefreshAutoFooter的属性triggerAutomaticallyRefreshPercent就可以实现,该属性triggerAutomaticallyRefreshPercent默认值为1,然后改成0的话划到底部就会自动刷新,改成-1的话,在快划到底部44px的时候就会自动刷新。

MJRefresh?使用MJRefreshAutoFooter,这个简单,我直接把基类的footer给替换掉就可以了,本代码可以在开源项目中的BaseTableViewController.swift文件查看:

/// 设置尾部刷新控件,更新为无感知加载更多
let footer = MJRefreshAutoFooter()
footer.triggerAutomaticallyRefreshPercent = -1
tableView.mj_footer = footer

再来看看效果:

992BC78FBAC7B8CB36A6DC679897DA21.2021-07-09 18_04_09.gif

直接说感受:

代码改动性少,编写简单,达到预期效果,爽歪歪。比方案一更丝滑,体验好。

到此,功能就实现,难道就完了?

当然,不会,我们去看看源码吧。

MJRefresh代码的追根朔源

首先我们看看MJRefreshAutoFooter.h文件:

image.png

这里有个专门的属性triggerAutomaticallyRefreshPercent去做自动刷新,那么我们去MJRefreshAutoFooter.m中去看看吧:

image.png

注意看喔,这个.m文件有一个- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change方法,并且还调用了super,从这个方法名中我们可以明显的得到当scrollView的contentOffset变化的时候进行回调的监听。,我们顺藤摸瓜,看看super是什么,会不会有新的发现:

image.png

稍微跟着一下源代码,MJRefreshAutoFooter的继承关系如下:

MJRefreshAutoFooter => MJRefreshFooter => MJRefreshComponent

所以这个super的调用我们就去MJRefreshComponent.m里面去看看吧:

image.png

通过上面的截图我们可以得到下面的一些信息与结论:

  • MJRefreshComponent是通过KVO去监听scrollView的contentOffset变化,思路上我们对齐一致了。

  • 该类并没有实现其具体方法,而是将其交由其子类去实现,这一点通过看MJRefreshComponent.h的注释可以得到:

image.png

  • MJRefreshComponent从本质上更像虚基类。

总结

如果不是掘友提出这个问题,我可能都不会太仔细的去研究这个功能,也许继续普普通通的使用一般的上拉加载更多就够了。

这次的实践,其实是从思路到寻找方法,最后再到源码阅读。

思路也许不困难,但是真正一点点实现并完善功能,每一步都并不容易,这次我也仅仅是继续使用了MJRefresh这个轮子。

想起有一天,在群里吹水看见的一张图:

云程序员来了.jpeg

灵魂拷问,直击人心,大部分时间我们不也是云程序员呢?

知行合一方能开拓新的天地。


收起阅读 »

swift 键盘收起

iOS
直接调用就能收起键盘,无需调用其他方法        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), t...
继续阅读 »







直接调用就能收起键盘,无需调用其他方法    

    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)

收起阅读 »

iOS 底层原理探索 之 结构体内存对齐

iOS
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之 路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。 目录如下:iOS 底层原理探索之 alloc以上内容的总结专栏iOS 底层原理探索 之 阶段总结准备Objective-C...
继续阅读 »


写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索之 alloc

以上内容的总结专栏


准备

Objective-C ,通常写作ObjC或OC,是扩充C的面向对象编程语言。它主要适用于Mac OS X 和 GNUstep者两个使用OpenStep标准的系统,而在NeXTSTEP和OpenStep中它更是基本语言。

GCC和Clang含Objective-C的编译器,Objective-C可以在GCC以及Clang运作的系统上编译。

我们平时开发用的Objective-C语言,编译后最终会转化成C/C++语言。

为什么要研究结构体的内存对齐呢? 因为作为一名iOS开发人员,随着对于底层的不断深入探究,我们都知道,所有的对象在底层中都是一个结构体。那么结构体的内存空间又会被系统分配多少空间,这个问题,值得我们一探究竟。

首先,从大神Cooci那里盗取了一张各数据类型占用的空间大小图片,作为今天探究结构体内存对齐原理的依据。

image.png

当我们创建一个对象的时候,我们并不需要过多的在意属性的顺序,因为系统会帮我们做优化处理。但是,在创建结构体的时候,就需要我们去分析了,因为这个时候系统并不会帮助我们做优化。

接下来,我们看下面两个结构体:

struct Struct1 {    
double a;
char b;
int c;
short d;
char e;
}struct1;

struct Struct2 {
double a;
int b;
char c;
short d;
char e;
}struct2;


两个结构体拥有的数据类型是相同的,按照图片中double 是8字节, char 是1字节, int 是4字节,short 是2字节, 那么 两个结构体应该是占 16字节的内存空间,也就是分配16字节空间即可,然而,我们看下面的结果:

    printf("%lu--%lu", sizeof(struct1), sizeof(struct2));
------------
24--16

那么,这就是有问题的了,两个拥有相同数据类型的结构体,被系统分配到的内存空间是不一样的,这是为什么呢?今天的重点就是这里,结构体的

内存对齐原则:

1:数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置
要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是
数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址
开始存储。

2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从
其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,
b里有char,int ,double等元素,那b应该从8的整数倍开始存储)

3:收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员
的整数倍,不足的要补⻬。

那么,我们按照以上内存对齐原则再来分析下 struct1 和 struct2 :


struct Struct1 { /// 18 --> 24
double a; //8 [0 1 2 3 4 5 6 7]
char b; //1 [8 ]
int c; //4 [9 [12 13 14 15]
short d; //2 [16 17]
char e; //1 [18]
}struct1;


struct Struct2 { /// 16 --> 16
double a; //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [ 12 ]
short d; //2 [14 15]
char e; // 1 [16]
}struct2;


接着,我们看下下面的结构体

struct Struct3 {    
double a;
int b;
char c;
short d;
int e;
struct Struct1 str;
}struct3;


打印输出结果为 48 ,分析如下:

    double a;           //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [12]
short d; //2 [ 14 15 ]
int e; //4 [ 16 17 18 19]
struct Struct1 str; //24 [24 ... 47]

所以,struct3 大小为48。


猜想:内存对齐的收尾工作中的内部最大成员指的是什么的大小呢?

接下来我们来一一验证一下

struct LGStruct4 {          /// 40 --> 48 
double a; //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [12]
short d; //2 [14 15]
int e; //4 [16 17 18 19]
struct Struct2 str; //16 [24 ... 39]
}struct4;

按照我对于内存对齐原则中收尾工作的理解, 最终的大小 应该是 Struct2 的 大小 16 的整数倍 也就是 48 才对。然而, 结果却是:

    NSLog(@"%lu", sizeof(struct4));
--------
SMObjcBuild[8076:213800] 40

对,是40你没有看错,这样的话,很显然,我理解的就是错误的, 结构体内部最大成员应该指的是这里的 double,那么我们接下来验证一下: 1、

struct Struct2 {    ///16
double a; //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [ 12 ]
short d; //2 [14 15]
}struct2;

struct LGStruct4 { /// 24

short d; //2 [0 1]

struct Struct2 str; // 16 [8 ... 23]

}struct4;

结果是 :24


因为,结构体内部最大成员是 double也就是8;并不是按照 LGStruct4中的str长度为16的整数倍来计算,所以最后的结果是24。

总结

结构体内部最大成员指的是结构体内部的数据类型,所以,结构体内包含结构体的时候,并不是按照内部的结构体长度的整数倍来计算的哦。


收起阅读 »

iOS 底层原理探索 之 alloc

iOS
iOS 底层原理探索 之 alloc写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之 路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。 内容的总结专栏iOS 底层原理探索 之 阶段总结序作为一名iOS开发人员,在平时开发工...
继续阅读 »

iOS 底层原理探索 之 alloc

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。


内容的总结专栏


作为一名iOS开发人员,在平时开发工作中,所有的对象我们使用最多的是alloc来创建。那么alloc底层做了哪些操作呢?接下来我会一步一步探究alloc方法的底层实现。

初探

我们先来看下面的代码

    SMPerson *p1 = [SMPerson alloc];
SMPerson *p2 = [p1 init];
SMPerson *p3 = [p1 init];

NSLog(@"%@-%p-%p", p1, p1, &p1);
NSLog(@"%@-%p-%p", p2, p2, &p2);
NSLog(@"%@-%p-%p", p3, p3, &p3);

打印内容:

    <SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15088
<SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15080
<SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15078

可见,在 SMPerson 使用 alloc 方法从系统中申请开辟内存空间后 init方法并没有对内存空间做任何的处理,地址指针的创建来自于 alloc方法。如下所示:

地址.001.jpeg

注:细心的你一定注意到了,p1、p2、p3都是相差了8个字节。 这是因为,指针占内存空间大小为8字节,p1、p2、p3 都是从栈内存空间上申请的,且栈内存空间是连续的。同时,他们都指向了同一个内存地址。

那么, alloc 是如何开辟内存空间的呢?

首先,第一反应是,我们要Jump to Definition,

2241622899100_.pic_hd.jpg

结果,Xcode中并不能直接跳转后显示其底层实现,所以 并不是我们想要的。

2251622899278_.pic_hd.jpg

WX20210605-214250@2x.png

中探

接下来,我们通过三种方法来一探究竟:

方法1

既然不可以直接跳转到API文档来查看alloc的内部实现,那么我们还可以通过下 符号断点 来探寻 其实现原理。

WX20210605-212725@2x.png

接下来我们就来到此处

WX20210605-213213@2x.png

一个名为 libobjc.A.dylib 的库,至此,我们就应该要去找苹果开源的库,以寻找我们想要的答案。

点击查看苹果开源源码汇总

方法2

我们也可以直接在alloc那一行打一个断点,代码运行到此处后,按住control键 点击 step into, 接下来,就来到里这里

WX20210605-214413@2x.png 我们可以看到一个 objc_alloc 的函数方法到调用,此时,我们再下一个符号断点,同样的,我们还是找到了 libobjc.A.dylib 这个库。

WX20210605-215027@2x.png

方法3

此外,我们还是可以通过汇编来调试和查找相应的实现内容,断点依然是在alloc那一行。

Debug > Debug Workflow > Always Show Disassembly

WX20210605-215336@2x.png

找到 callq 方法调用那一行, WX20210605-215715@2x.png

接着, step into 进去, 我们找到了 objc_alloc 的调用, 之后的操作和 方法2的后续步骤一样,最终,可以找到 libobjc.A.dylib 这个库。 WX20210605-215732@2x.png

深探

下载源码 objc4-818.2

接下来对源码进行分析,

alloc方法会调用到此处

WX20210605-231454@2x.png

接着是 调用 _objc_rootAlloc

WX20210605-231517@2x.png

之后调用 到 callAlloc

WX20210605-231545@2x.png

跟着断点会来到 _objc_rootAllocWithZone

WX20210605-231647@2x.png

之后是 _class_createInstanceFromZone

此方法是重点

WX20210605-231758@2x.png

_class_createInstanceFromZone 方法中,该方法就是一个类初始化所走的流程,重点的地方有三处

第一处是:
    // 计算出开辟内存空间大小
size = cls->instanceSize(extraBytes);

内部实现如下: WX20210605-231838@2x.png 其中在计算内存空间大小时,会调用 cache.fastInstanceSize(extraBytes) 方法,

最终会调用 align16(size + extra - FAST_CACHE_ALLOC_DELTA16) 方法。 align16 的实现如下:

static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}

可见, 系统会进行 16字节 的对齐操作,也就是说,一个对象所占用的内存大小至少是16字节。

在这里 我们举个例子: size_t x = 8; 那么 align16操作后的大小计算过程如下:

    (8 + 15) & ~15;

0000 0000 0000 1000 8
0000 0000 0000 1111 15

= 0000 0000 0001 0111 23
1111 1111 1111 0000 ~15

= 0000 0000 0001 0000 16


第二处是:
    ///向系统申请开辟内存空间,返回地址指针;
obj = (id)calloc(1, size);

第三处是:
    /// 将类和指针做绑定
obj->initInstanceIsa(cls, hasCxxDtor);

总结:

所以,最后我们总结一下, alloc的底层调用流程如下:

alloc流程.001.jpeg

就是这样一个流程,系统就帮我们创建出来一个类对象。

补充

image.png

  • lldb 如何打印实力对象中成员为 double 类型的数值: e -f f -- <值>
收起阅读 »

拒绝编译等待 - 动态研发模式 ARK

iOS
背景 pod install 时间长:编译优化绝大部分任务放在了 CocoaPods 上,CocoaPods 承担了更多工作,执行时间因此变长。编译时间长:虽然现阶段绝大部分工程已经从源码编译转型成二进制编译,但编译耗时依旧在十分钟左右,且现有工程基础上已无更...
继续阅读 »



背景

iOS 业界研发模式多为 CocoaPods + Xcode + Git 的多仓组件化开发模型。为追求极致的研发体验、提升研发效率,对该研发模式进行了大量优化,但目前遇到了以下瓶颈,亟需突破:

  • pod install 时间长:编译优化绝大部分任务放在了 CocoaPods 上,CocoaPods 承担了更多工作,执行时间因此变长。

  • 编译时间长:虽然现阶段绝大部分工程已经从源码编译转型成二进制编译,但编译耗时依旧在十分钟左右,且现有工程基础上已无更好优化手段。

  • 超大型工程通病:Xcode Index 慢、爆内存、甚至卡死,链接时间长。

如何处理这些问题?

究其本质,产生这些问题的原因在于工程规模庞大。据此我们停下了对传统模式各节点的优化工作,以"缩小工程规模"为切入点,探索新型研发模式——动态研发模式 ARK。

ARK[1] 是全链路覆盖的动态研发模式,旨在保证工程体验的前提下缩小工程规模:通过基线构建的方式,提供线下研发所需物料;同时通过实时的动态库转化技术,保证本地研发仅需下载和编译开发仓库。

Show Case

动态研发模式本地研发流程图如下。接下来就以抖音产品为例,阐述如何使用 ARK 做一次本地开发。
演示基于字节跳动本地研发工具 MBox[2]

  1. 仓库下载

ARK 研发模式下,本地研发不再拉取主仓代码,取而代之的是 ARK 仓库。ARK 仓库含有与主仓对应的所有配置,一次适配接入后期不需要持续维护。

相较传统 APP 仓库动辄几个 GB 的大小,ARK 仓库贯彻了缩减代码规模这一概念。仓库仅有应用配置信息,不包含任何组件代码。ARK 仓库大小仅 2 MB,在 1 s 内可以完成仓库下载 。

在 MBox 中的使用仅需几步点击操作。首先选择要开发的产品,然后勾选 ark 模式,选择开发分支,最后点击 Create 便可以数秒完成仓库下载。

  1. 开发组件

CocoaPods 下进行组件开发一般是将组件仓库下载到本地,修改 Podfile 对应组件 A 为本地引用 pod A, :path =>'./A' ,之后进行本地开发。而在 MBox 和 ARK 的研发流程中,仅需选择要开发的组件点击 Add 便可进行本地开发。

动态研发模式 ARK 通过解析 Podfile.lock 支持了 Checkout From Commit 功能,该功能根据宿主的组件依赖信息自动拉取相应的组件版本到本地,带来便捷性的同时也保证了编译成功率。

  1. pod install

传统研发模式下 pod install 必须要经历 解析 Podfile 依赖、下载依赖、创建 Pods.xcodeproj 工程、集成 workspace 四个步骤,其中依赖解析和下载依赖两个步骤尤为耗时。

ARK 研发模式下 Podfile 中没有组件,因此依赖解析、下载依赖这两个环节耗时几乎为零。其次由于工程中仅需开发组件步骤中添加的组件,在创建 Pods 工程、集成工程这两个环节中代码规模的降低,对提升集成速度的效果非常显著。

没有依赖信息,编译、链接阶段显然不能成功。ARK 解决方案通过自研 cocoapods-ark 及配套工具链来保证编译、链接、运行的成功,其原理后续会在系列文章中介绍。

  1. 开发组件编译&调试

和传统模式一样通过 Xcode 打开工程的 xcworkspace ,即可正常开发、调试完整的应用。

工程中仅保留开发组件,但是依然有变量、函数、头文件跳转能力;参与 Index、编译的规模变小,Xcode 几乎不存在 loading 状态,大型工程也可以秒开;编译速度大幅提升。在整个动态研发流程中,通过工具链将组件从静态库转化成动态库,链接时间明显缩短。

  1. 查看全源码

ARK 工程下默认只有开发组件的源码,查看全源码是开发中的刚需。动态研发流程提供了 pod doc 异步命令实现该能力,此命令可以在开发时执行,命令执行完成后重启工程即可通过 Document Target 查看工程中其他组件源码。

pod doc 优点:

  • 支持异步和同步,执行过程中不影响本地开发。

  • 执行指令时跳过依赖解析环节,从服务端获取依赖信息,下载源码。

  • 通过 xcodegen 异步生成 Document 工程,大幅降低 pod install 时间。

  • 仅复用 pod installer 中的资源下载、缓存模块。

  • 支持仓库统一鉴权,自动跳过无权限组件仓库。

收益

体验上: 与传统模式开发流程一致,零成本切换动态研发模式。

工具上: 站在巨人的肩膀上,CocoaPods 工具链相关优化在 ARK 同样生效。

时间上: 传统研发模式中,历经各项优化后虽然能将全链路开发时间控制在 20 分钟左右,但这样的研发体验依旧不够友好。开发非常容易在这个时间间隔内被其他事情打断,良好的研发体验应该是连贯的。结合本地开发体验我们认为,一次连贯的开发体验应该将工程集成时间控制在分钟级,当前研发模式成功做到了这一点,将全链路开发时间控制在 5 分钟以内。

成功率: 成功率一直是研发效率中容易被忽视的一个指标。据不完全数据统计,集团内应用的本地全量编译成功率不足五成。一半的同学会在首次编译后执行重试。显然,对于工程新手来说就是噩梦,这意味着很长时间将在这一环节中浪费。而 ARK 从平台基线到本地工具链贯彻 Sandbox 的理念,整体上提高编译成功率。

写在最后

ARK 当前已经在字节跳动内部多业务落地使用。从初期技术方案的探索到实际落地应用,遇到了很多技术难点,也对研发模式有了新的思考。

相关技术文章将陆续分享,敬请期待。

扩展阅读

[1] ARK: https://github.com/kuperxu/KwaiTechnologyCommunication/blob/master/5.WWDC-ARK.pdf
[2] MBox: https://mp.weixin.qq.com/s/5_IlQPWnCug_f3SDrnImCw

作者:字节跳动终端技术——徐纪光
来源:https://blog.csdn.net/YZcoder/article/details/121374743


收起阅读 »

手把手带你,优化一个滚动时流畅的TableView

iOS
手把手带你,优化一个滚动时流畅的TableView这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战我的专栏iOS 底层原理探索iOS 底层原理探索 之 阶段总结意识到我的问题平时使用手机的时间不算少,每天阅读新闻的时候会感觉到新闻类的app优化的还是...
继续阅读 »

手把手带你,优化一个滚动时流畅的TableView

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战


我的专栏

  1. iOS 底层原理探索
  2. iOS 底层原理探索 之 阶段总结

意识到我的问题

平时使用手机的时间不算少,每天阅读新闻的时候会感觉到新闻类的app优化的还是很好的,TableView的Cell滚动的时候不会去加载显示图片内容,当一次滑动结束之后,Cell上的新闻图片便开始逐个的加载显示出来,所以整个滑动的过程是很流畅的。这中体验也是相当nice的。

我最开始的做法

开发中TableView的使用是非常值频繁的,当TableViewCell上需要加载图片的时候,是一件比较头疼的事。因为,用户一边滑动TableView,TableView需要一边从网络获取图片。之前的操作都是放在 cellForRowAtIndexPath 中来处理,这就导致用户在滑动TableView的时候,会特别的卡(尤其是滑动特别快时),而且,手机的CPU使用率也会飙的非常的高。对于用户来说,这显然是一个十分糟糕的体验。

糟糕的图片显示 代码

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

ImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
cell.index = indexPath;

NSMutableDictionary *info = [self.dataSource objectAtIndex:cell.index.row];

NSString *url = [info objectForKey: @"img" ];
NSData *iData = [NSData dataWithContentsOfURL:[NSURL URLWithString: url ]];
cell.img.image = [UIImage imageWithData:iData];
cell.typeL.text = [NSString stringWithFormat:@"%ld-%ld", cell.index.section, cell.index.row];

return cell;
}

糟糕的手机CPU飙升率

未命名.gif

糟糕的用户滑动体验

未命名1.gif

不只是用户,对于开发这来讲,这也是不可以接受的体验。

平时接触并使用的app也非常的多,发现他们多处理方式就是,当用户滑动列表的时候,不再加载图片,等用户的滑动结束之后,会开始逐一的加载图片。这是非常好的优化思路,减轻了CPU的负担,也不会基本不会让用户感觉到页面滚动时候的卡顿。这也就是最开始我描述的我看新闻app的使用体验。

收到这个思路的启发,我们开始着手将上面糟糕的体验作一下优化吧。

总结思路开启优化之路

那么,带着这个优化思路,我开始了对于这个TableView 的优化。

  • 首先,我们只加载当前用户可以看到的cell上的图片。
  • 其次,我们一次只加载一张图片。

要完成以上两点,图片的加载显示就不能在cellForRowAtIndexPath中完成,我们要定义并实现一个图片的加载显示方法,以便在合适的时机,调用刷新内容显示。

loadSeeImage 加载图片的优化

#pragma mark load Images
- (void)loadSeeImage {

//记录本次加载的几张图片
NSInteger loadC = 0;

// 用户可以看见的cells
NSArray *cells = [self.imageTableView visibleCells];

// 调度组
dispatch_group_t group = dispatch_group_create();

for (int i = 0; i < cells.count; i++) {

ImageTableViewCell *cell = [cells objectAtIndex:i];

NSMutableDictionary *info = [self.dataSource objectAtIndex:cell.index.row];
NSString *url = [info objectForKey: @"img" ];

NSString *data = [info objectForKey:@"data"];

if ([data isKindOfClass:[NSData class]]) {


}else {

// 添加调度则到我们的串行队列中去
dispatch_group_async(group, self.loadQueue, ^{

NSData *iData = [NSData dataWithContentsOfURL:[NSURL URLWithString: url ]];
NSLog(@" load image %ld-%ld ", cell.index.section, cell.index.row);
if (iData) {
// 缓存
[info setValue:@"1" forKey:@"isload"];
[info setValue:iData forKey:@"data"];
}
NSString *isload = [info objectForKey:@"isload"];

if ([isload isEqualToString:@"0"]) {

dispatch_async(dispatch_get_main_queue(), ^{

cell.img.image = [UIImage imageNamed:@""];
}); }else {

if (iData) {

dispatch_async(dispatch_get_main_queue(), ^{
//显示加载后的图片
cell.img.image = [UIImage imageWithData:iData];
});
}
}

});

if (i == cells.count - 1) {

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 全部加载完毕的通知
NSLog(@"load finished");
});
}

loadC += 1;
}
}

NSLog(@"本次加载了 %ld 张图片", loadC);
}

其次就是 loadSeeImage 调用时机的处理,我们要做到用户在滑动列表之后加载,就是在下面两处加载:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView   {  

[self loadSeeImage];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {

if (scrollView.isDragging || scrollView.isDecelerating || scrollView.isTracking) {
return;
}
[self loadSeeImage];
}

当然,首次进入页面,列表数据加载完毕后,我们也要加载一次图片的哦。 好的下面看下优化后的结果:

优化xcode.gif

优化phone.gif

CPU占用率比之前最高的时候降低了一半多,app在滑动的时候也没有明显卡顿的地方。 完美。

收起阅读 »

Swift 指针的应用

iOS
Swift与指针由于Swift本身是一门较为现代的语言,支持很多高级特性,所以对于程序员来说,大部分时候不需要用到指针这种更“底层”的特性。而Swift语言的设计者也在尽可能希望开发者能尽量少的使用指针。但是,“慎用”不代表“不能用”,更不代表“没用”。相反,...
继续阅读 »

Swift与指针

由于Swift本身是一门较为现代的语言,支持很多高级特性,所以对于程序员来说,大部分时候不需要用到指针这种更“底层”的特性。而Swift语言的设计者也在尽可能希望开发者能尽量少的使用指针。

但是,“慎用”不代表“不能用”,更不代表“没用”。相反,指针非常有用,在某些场景下还是必不可少的特性。尤其是开发工作和系统底层特性、内存处理、高性能需求息息相关时。

所以,Swift通过在施加某种限制的前提下为开发者暴露了指针的使用接口,本篇文章重点介绍Swift使用指针的相关类型、函数,以及在实践应用中灵活使用指针解决问题的技巧。

类型限定的指针 UnsafePointer

Swift通过UnsafePointer<T>来指向一个类型为T的指针,该指针的内容是 只读 的,对于一个UnsafePointer<T>变量来说,通过pointee成员即可获得T的值。

func call(_ p: UnsafePointer<Int>) {
print("\(p.pointee)")
}
var a = 1234
call(&a) // 打印:1234

以上例子中函数call接收一个UnsafePointer<Int>类型作为参数,变量a通过在变量名前面加上&将其地址传给call。函数call直接打印指针的pointee成员,该成员就是a的值,所以最终打印结果为1234

注1:&aswift提供的语法特性,用于传递指针,但它有严格的适用场景限制。

注2:注意示例中对于变量a使用了var声明,而事实上UnsafePointer是“常量指针”,并不会修改a的内容,即使是这样a还是必须用var声明,如果用let会报错Cannot pass immutable value as inout argument: 'a' is a 'let' constant。这是因为swift规定UnsafePointer作为参数只能接收inout修饰的类型,而inout修饰的类型必然是可写的,所以使用var在所难免。

内容可写的类型限定指针 UnsafeMutablePointer

既然有 内容只读 指针,必须也得有 内容可读写 指针搭配才行,在Swift中,内容可读写的类型限定指针为UnsafeMutablePointer<T>类型,就和名字描述的那样,它和UnsafePointer最大的区别就是它指向的内容是可更改的,并且更改后指向的“数据源”也会被改动。

func modify(_ p: UnsafeMutablePointer<Int>) {
p.pointee = 5678
}
var a = 1234
modify(&a)
print("\(a)") // 打印:5678

在以上的例子中,指针p指向的值被重新赋值为5678,这也使得指针的“源”,即变量a的值发生变化,最终打印a的结果可以看出a被修改为5678

指针的辅助函数 withUnsafePointer

通过函数withUnsafePointer,获得指定类型的对应指针。该函数原型如下:

func withUnsafePointer<T, Result>(to value: inout T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result

这个函数原型看似很复杂很长,其实只要理解它所需要的信息只有两个:

  1. 指针指向类型是什么。
  2. 想要返回的指针地址是什么。
var a = 1234
let p = withUnsafePointer(to: &a) { $0 }
print("\(p.pointee)") // 打印:1234

以上例子是withUnsafePointer最精简的调用例子,我们定义了一个整形a,而p就是指向a的整形指针,事实上它的类型会被自动转换为UnsafePointer<Int>,第二个参数被简化为了{ $0 },它传入了一个代码块,代码块接收一个UnsafePointer<Int>参数,该参数即是a的地址,直接通过$0将它返回,即得到了a的指针,最终它被传给了p

对于第二个参数,或许有人会产生疑问,它似乎是没有意义的参数,大部分时候我们不是直接返回a的地址吗,为什么要多此一举通过代码块返回一次?这个疑问是合理的,绝大多数时候确实第二个参数显得有些多余,当然了,有时候可以通过第二个参数提供指针偏移的灵活性,如下例子可以提供一个案例。

var a = [1234, 5678]
let p = withUnsafePointer(to: &a[0]) { $0 + 1 }
print("\(p.pointee)") // 打印:5678

以上例子中,通过在第二个参数中对地址施加偏移,可以原来指向数组首个元素的地址偏移到第二个地址中。

另外,由于withUnsafePointer带着两个泛型参数,这意味着第二个参数可以是不同的类型。

var a = 1234
let p = withUnsafePointer(to: &a) { $0.debugDescription }
print("\(p)")

以上例子中,withUnsafePointer返回的并不是UnsafePointer<Int>类型,甚至不是指针,而是一个字符串,字符串保存着a对应指针的debug信息。

注1:同样的,和withUnsafePointer相对应的,还有withUnsafeMutablePointer,一样是只读和可读写的区别。读者可以自行测试用法。 注2:基本上Swift指针操作的with系列函数都提供了第二个参数用来灵活的提供函数的返回类型。

获取指针并进行字节级操作 withUnsafeBytes

有时候,我们需要对某块内存进行字节级编程。比如我们用一个32位整形来表示一个32位的内存块,对内存中的每个字节进行读写操作。

通过withUnsafeBytes,可以得到某个类型的数据的字节指针,从而可以对它们进行字节级编程。

var a: UInt32 = 0x12345678
let p = withUnsafeBytes(of: &a) { $0 }
var log = ""
for item in p {
let hex = NSString(format: "%x", item)
log += "\(hex)"
}
print("\(p.count)") // 打印:4
print("\(log)") // 对于小端机器会打印:78563412

在以上例子中,withUnsafeBytes返回了一个类型UnsafeRawBufferPointer,该类型代表着一个字节级的内存块,并提供了等价于数组操作,所以你可以通过下标索引、for循环的方式来处理返回的对象。

例子中的a是一个32位整形,所以p指针的count返回的是4,单位为字节。 在本例中,对内存块p从低到高逐字节的打印每个字节的16进制值。 具体打印出来的结果因运行的机器而异,在大端机器上,打印的结果是12345678,而在小端机器上打印结果则是78563412

注:大端和小端决定了一个基础数据单元在内存中是如何按序存放的,例如小端机器会将基本数据单元的低位放在内存的低位,由低到高排列,而大端机器则相反。具体相关知识可查阅维基百科。大部分情况下,同一台机器采用的字节序列是一致的,某些CPU可以配置大小端的切换。

指向连续内存的指针 UnsafeBufferPointer

Swift的数组提供了函数withUnsafeBufferPointer,通过它我们可以方便的用指针来处理数组。如下例子:

let a: [Int32] = [1, 2, -1, -2, 5, 6]
let p = a.withUnsafeBufferPointer { $0 }
print("\(p.count)") // 打印:6
print("\(p[3])") // 打印:-2

在该例子中,通过withUnsafeBufferPointer,可以获得变量pp的类型为UnsafeBufferPointer<Int32>,它代表着一整块的连续内存,我们可以像看待数组一样看待它,并且它也支持大部分数组操作。

指针的类型转换

介绍了那么多Swift中的指针类型,每一种都有各自的用途,但是在实际开发中,很可能我们需要将一个指针类型转换为特定的指针类型。

以下例子提供了几个类型指针之间的转换

let a: [Int32] = [1, 2, -1, -2, 5, 6]
// 类型 p: UnsafeBufferPointer<Int32>
let p = a.withUnsafeBufferPointer { $0 }
// 类型 p2: UnsafePointer<UInt32>
let p2 = p.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: p.count) { $0 }
// 类型 p3: UnsafeBufferPointer<UInt32>
let p3 = UnsafeBufferPointer(start: p2, count: p.count)
print("\(p3.count)") // 打印:6
print("\(p3[3])") // 打印:4294967294

以上例子中,我们获得了以下三个指针类型

  1. UnsafeBufferPointer<Int32>类型的指针p
  2. UnsafePointer<UInt32>类型的指针p2
  3. UnsafeBufferPointer<UInt32>类型的指针p3

该例子有部分细节必须讲明,首先是baseAddress,通过该成员得到UnsafeBufferPointer基地址,获得的数据类型是UnsafePointer<>

由于a指向的元数据类型是Int32,所以其baseAddress类型即是UnsafePointer<Int32>

在本例中,我们将元数据类型由Int32改为UInt32,这里用到了UnsafePointer的成员函数withMemoryRebound,通过它将UnsafePointer<Int32>转换为UnsafePointer<UInt32>

最后一部分,我们创建了一个新的指针UnsafeBufferPointer,通过其构造函数,我们让该指针的起始位置设定为p2,元素个数设定为p的元素个数,这样就成功得到了一个UnsafeBufferPointer<UInt32>类型。

接下来的打印语句,我们可以看到p3类型的count成员依然是6,而p3[3]打印的结果却是4294967294,而不是数组a对应元素的-2,这是因为从p3的角度来看,它是用UInt32类型来“看待”原先的Int32数据元素。

回调函数的实用性

前面讨论withUnsafePointer时我曾经提过第二个回调参数似乎略显鸡肋,事实上它非常有用,通过回调函数,我们可以对上一段代码进行“优化”。

let a: [Int32] = [1, 2, -1, -2, 5, 6]
// 类型 p: UnsafeBufferPointer<Int32>
let p = a.withUnsafeBufferPointer { $0 }
// 类型 p3: UnsafeBufferPointer<UInt32>
let p3 = p.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: p.count) {
UnsafeBufferPointer(start: $0, count: p.count)
}
print("\(p3.count)") // 打印:6
print("\(p3[3])") // 打印:4294967294

可以看到利用回调函数,我们把原先的p2p3代码合并了,这样可以让withMemoryRebound立刻返回UnsafeBufferPointer<UInt32>类型。

注:事实上该回调还可以不断“套娃”,也就是说可以直接把p3部分的代码和p也进行合并,但是出于可读性考虑,开发者应自己根据需要选择性进行嵌套。

Swift中的空指针:UnsafeRawPointer

就像C语言有void*(即空指针)一样,Swift也有自己的空指针,它通过类型UnsafeRawPointer来获得,我们知道,空指针没有指向特定的类型,又“可以”指向任何类型,灵活性极高,也需要程序员自己能够理解和处理好对应的细节。

同样是将UnsafeBufferPointer<Int32>转换为UnsafeBufferPointer<UInt32>,以下代码通过UnsafeRawPointer来实现。

let a: [Int32] = [1, 2, -1, -2, 5, 6]
let p = a.withUnsafeBufferPointer { $0 }
let p2 = UnsafeRawPointer(p.baseAddress!).assumingMemoryBound(to: UInt32.self)
let p3 = UnsafeBufferPointer(start: p2, count: p.count)
print("\(p3.count)") // 打印:6
print("\(p3[3])") // 打印:4294967294

在该例子中我们通过空指针完成了如下操作:

  1. UnsafeRawPointer通过构造函数接收了p的“基地址”构造了一个空指针类型。
  2. 由于构造的是空指针类型,我们需要对它进行类型转换,通过assumingMemoryBound把它转换成新的数据类型UnsafePointer<UInt32>
  3. 通过UnsafeBufferPointer构造函数重新构造了一个新的指针UnsafeBufferPointer<UInt32>

通过指针动态创建、销毁内存

有时候我们需要动态开辟和管理一块内存,最后释放它,Swift提供了UnsafeMutablePointer的成员函数allocate来处理该工作。

let p = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
p.initialize(to: 0) // 初始化
p.pointee = 32
print("\(p.pointee)") // 打印:32
p.deinitialize(count: 1) // 反初始化
p.deallocate()

以上例子中我们提供了一个存放32位整形的内存块,容量为1(即其容量为1个32位整形,实际就是 4 个字节)。 接下来的代码示例较为简单,即开辟内存,初始化、赋值、反初始化,释放内存的流程。

Swift指针类型和C指针类型的对应关系

Swift的指针类型看似繁多,事实上只是对C指针类型进行了封装和类别整理,并增加了一定程度上的安全性。

下表提供了SwiftC部分指针类型和函数的大致等价关系。

SwiftC描述
UnsafeMutableRawPointervoid*空指针
UnsafeMutablePointerT*类型指针
UnsafeRawPointerconst void*常量空指针
UnsafePointerconst T*常量类型指针
UnsafeMutablePointer.allocate(int32_t*)malloc分配内存

可以看出Swift的指针并不神秘,它只是映射了C语言指针的对应操作(只是乍看一下更复杂)。

进阶实践:C标准库函数的映射调用

Swift提供了大量的C标准库的桥接调用,也就是说,我们可以像调用C语言库函数一样调用Swift函数。这其中包括很多有用的函数,如memcpystrcpy等。

下面通过一段示例程序来展现这类函数的调用。

var n = 10086
// malloc
let p = malloc(MemoryLayout<Int32>.size)!
// memcpy
memcpy(p, &n, MemoryLayout<Int32>.size)
let p2 = p.assumingMemoryBound(to: Int32.self)
print("\(p2.pointee)") // 打印:10086
// strcpy
let str = "abc".cString(using: .ascii)!
if str.count != MemoryLayout<Int32>.size {
return
}
let pstr = p.assumingMemoryBound(to: CChar.self)
strcpy(pstr, str)
print("\(String(cString: pstr))") // 打印:abc
// strlen
print("\(strlen(pstr))") // 打印: 3
// memset
memset(p, 0, MemoryLayout<Int32>.size)
print("\(p2.pointee)") // 打印:0
// strcat
strcat(pstr, "h".cString(using: .ascii)!)
strcat(pstr, "i".cString(using: .ascii)!)
print("\(String(cString: pstr))") // 打印:hi
// strstr
let s = strstr(pstr, "i")!
print("\(String(cString: s))") // 打印:i
// strcmp
print("\(strcmp(pstr, "hi".cString(using: .ascii)!))") // 打印:0
// free
free(p)

以上demo提供了如memsetstrcpyC库函数原型的调用方式。通过该例子可以看出指针操作的灵活性,对于开辟的一块4个字节的内存,我们既可以把它看做一个32位整形,又可以把它看做4个ascii字符,当把它看做4个字符时,我们可以用它存放abc三个字符,并在最后一个字节用\0作为终止符。

总结

指针可以让我们用更底层的视角来看待程序和数据,在某些场景下,通过指针我们有机会开发出更高性能的代码。但同时指针的使用有时也是极复杂易出错的。如何使用好这把双刃剑,全看开发者自身的能力和态度。本文仅仅是抛砖引玉的提供了Swift指针的基本框架和使用技巧,大量细节因为篇幅原因并未提及,还需要读者自行不断研究和学习。

本文的样例代码已上传至我的github,请参见地址:github.com/FengHaiTong… 。


作者:风海铜锣
链接:https://juejin.cn/post/7030789069915291661
来源:稀土掘金

收起阅读 »

Swift热更新(1)- 免费版接入

iOS
SOT学习和使用的成本主要集中在前期,主要涉及编译流程的修改。之前介绍了纯OC项目如何接入「 OC接入例子 」。本文介绍如何给纯Swift项目接入SOT,包括免费版和网站版。本文以开源的「 SwiftMessages 」Demo为例,该工程全部用Swift语言...
继续阅读 »

SOT学习和使用的成本主要集中在前期,主要涉及编译流程的修改。之前介绍了纯OC项目如何接入「 OC接入例子 」。本文介绍如何给纯Swift项目接入SOT,包括免费版和网站版。

本文以开源的「 SwiftMessages 」Demo为例,该工程全部用Swift语言开发。这里把修改好的版本上传到了git上,分支为 「 sotdemo 」,Debug模式下接入了免费版,Release模式接入了网站版,读者也可以直接用该分支测试。

现在开始从头讲解,clone原本的工程后,命令行cd进入根目录,使用版本切换命令:git checkout 1e49de7b3780b699(因本文档制作于21年10月21号,以当日版本为准)。首先进入Demo目录,打开Demo.xcodeproj工程,scheme默认就已经选中了Demo:...

我使用的是Xcode12.4,可以直接编译成功,启动APP能看到画面(模拟器):

......

点击最上面的MESSAGE VIEW控件,会弹出一个错误提示窗口,今天我们就来用SOT热更的方式修改错误提示的文案:...

Step1: 配置编译环境

参考「 免费版 」的step1到step3,step3拷贝的sotconfig.sh放到项目的Demo的目录下:...

用文本编辑器打开sotconfig.sh,修改EnableSot=1:...

Step2: 修改编译选项

添加热更需要的编译选项,添加SOT虚拟机静态库等,步骤如下:

  1. 选中Demo工程,然后选择Demo这个Target,再选择Build Settings:...

  2. Other Linker Flags添加-sotmodule $(PRODUCT_NAME) /Users/sotsdk-1.0/libs/libsot_free.a -sotsaved $(SRCROOT)/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/sotconfig.sh,每个选项的意义如下:

    • -sotmodule是module的名字,可以直接用$(PRODUCT_NAME),也可以自定义名字;
    • -sotsaved是编译中间产物保存的目录,补丁自动化生成需要对比前后编译的产物来生成补丁;
    • -sotconfig指定了项目sotconfig.sh的路径,该脚本控制sot编译器的工作,用$(SRCROOT)引用到
    • /Users/sotsdk-1.0/libs/libsot_free.a是SOT虚拟机静态库的路径,链接的是免费版的虚拟机
  3. Other C Flags以及Other Swift Flags添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/sotconfig.sh,意义跟上一步是一样的,需要保持一致。经过上面两步,相关的编译配置结果如下图:...

  4. 因为SOT SDK库文件编译时不带Bitcode,所以也需要把Target的Enable Bitcode设为No...


Step3: 增加拷贝补丁脚本

SDK里提供了一个便利脚本,路径在sdk目录的project-script/sot_package.sh,它会把生成的补丁拷贝到Bundle文件夹下,在每次项目编译成功时调用该脚本,添加步骤如下:

...

脚本内容为:sh /Users/sotsdk-1.0/project-script/sot_package.sh "$SOURCE_ROOT/sotconfig.sh" "$SOURCE_ROOT/sotsaved/$CONFIGURATION" Demo...

把Based on dependency analysis的勾去掉


Step4: 链接C++库

SOT需要压缩库和c++标准库的支持,还是在这个页面下,打开Link Binary With Libraries页...

点击加号,分别加入这两,libz.tbdlibc++.tbd...


Step5: 调用SDK API

需要用Swift代码调用OC代码,已经提供了一个样例代码在SDK的swift-call-objc目录中,可以直接添加到Demo工程中。点击Xcode软件的File按钮,接着点击Add Files to "Demo",如下图所示:...

选择到SDK目录swift-call-objc中,同时选中callsot.h和callsot.m两个文件,勾选下面的Copy items if needed,勾选Add to targets:中的Demo target,如下图所示:...

点击Add按钮,添加后会弹出询问:是否创建桥接文件。点击按钮Create Bridging Header:...

然后可以看到项目中多了3个文件,分别是callsot.h,callsot.m和Demo-Bridging-Header.h:...

打开Demo-Bridging-Header.h,加入一行代码#import "callsot.h"...

打开AppDelegate.swift,加入两行代码let sot = CallSot()sot.initSot()...


测试热更

Step1: 热更注入

按上面配置完之后,确保sotconfig.sh的配置是,EnableSot=1以及GenerateSotShip=0,先Clean Build Folder一下,然后再Build:...

然后看编译日志的输出,Link日志可以看到run sot link等输出,会告诉你每个文件里哪些函数可以被热更等信息:......

项目编译成功了,该APP可以正常启动。同时它具备了热更能力,可以加载补丁改变程序的代码逻辑,下面介绍如何生成补丁来测试它。


Step2: 生成补丁

上一步进行了热更注入的编译,当时的代码保存到了Demo/sotsaved这个文件夹下,用来和新代码比较生成补丁。生成补丁步骤如下:

  1. 首先启动SOT生成补丁模式,修改sotconfig.sh为EnableSot=1GenerateSotShip=1
  2. ...
  3. 接下来直接在Xcode里修改源代码,把ViewController.swift文件的”Something is horribly wrong!“改成了”SOT is great“:......
  4. 生成补丁跟OC项目不一样,每次都需要先Clean项目,再Build项目。然后查看编译日志输出,可以看到生成了补丁并且被脚本拷贝到了Bundle目录下,然后再展开Link Demo(x86_64)的编译日志:...可以看到此时的Link是用来生成补丁的,日志里也显示了函数demoBasics被修改了:...
  5. 生成出来的补丁原始文件保存到了Demo/sotsaved/Debug/x86_64/ship/ship.sot,还记得之前加了一个script到Build Phase中吗?它会每次编译结束时,会把这个补丁拷贝到了Bundle目录中,并且添加CPU架构到文件名中。可以在Bundle中看到这个补丁,至此完毕。

Step3: 加载补丁

启动APP,API会判断Bundle内是否有补丁,有则加载,加载成功的日志大概如下,提示有一个模块加载了热更补丁:...之后点击最上面的MESSAGE VIEW控件,发现弹出的文案变成了SOT is great:...

如果去Xcode断点调试demoBasics,会发现无法断住了,因为实际执行补丁代码的是SOT虚拟机。

顺便提一嘴,GenerateSotShip=1时,编译APP用的是保存在sotsaved目录下的代码,所以无论怎么修改Xcode里的代码,如果没有把补丁拷贝到Bundle目录里,那么APP都是最后一次GenerateSotShip=0热更注入时的样子。

如果怀疑,可以把拷贝补丁的Script脚本从Build Phases删除,可以发现怎么改代码都不会生效了。


作者:忒修斯科技
链接:https://juejin.cn/post/7026197659006287903
来源:稀土掘金

收起阅读 »

Swift开发规范

iOS
Swift开发规范前言开发规范的目的是保证统一项目成员的编码风格,并使代码美观,每个公司对于代码的规范也不尽相同,希望该份规范能给大家起到借鉴作用。本文为原创,如需转载请说明原文地址链接。命名规约代码中的命名严禁使用拼音及英文混合的方式,更不允许直接出现中文的...
继续阅读 »

Swift开发规范

前言

开发规范的目的是保证统一项目成员的编码风格,并使代码美观,每个公司对于代码的规范也不尽相同,希望该份规范能给大家起到借鉴作用。本文为原创,如需转载请说明原文地址链接。

命名规约

  • 代码中的命名严禁使用拼音及英文混合的方式,更不允许直接出现中文的方式,最好也不要使用下划线或者美元符号开头;
  • 文件名、class、struct、enum、protocol 命名统一使用 UpperCamelCase 风格;
  • 方法名、参数名、成员变量、局部变量、枚举成员统一使用 lowerCamelCase 风格
  • 全局常量命名使用 k 前缀 + UpperCamelCase 命名;
  • 扩展文件,用“原始类型名+扩展名”作为扩展文件名,其中原始类型名及扩展名也使用 UpperCamelCase 风格,如UIView+Frame.swift
  • 工程中文件夹或者 Group 统一使用 UpperCamelCase 风格,一律使用单数形式;
  • 命名中出现缩略词时,缩略词要么全部大写,要么全部小写,以首字母大小写为准,通用缩略词包括 JSON、URL 等;如class IDUtil {}func idToString() { }
  • 不要使用不规范的缩写,如 AbstractClass“缩写”命名成 AbsClass 等,不怕名称长,就怕名称不明确。
  • 文件名如果有复数含义,文件名应使用复数形式,如一些工具类;

修饰规约

  • 能用 let 修饰的时候,不要使用 var;
  • 修饰符顺序按照 注解、访问限制、static、final 顺序;
  • 尽可能利用访问限制修饰符控制类、方法等的访问限制;
  • 写方法时,要考虑这个方法是否会被重载。如果不会,标记为 final,final 会缩短编译时间;
  • 在编写库的时候需要注意修饰符的选用,遵循开闭原则;

格式规约

  • 类、函数左大括号不另起一行,与名称之间留有空格
  • 禁止使用无用分号
  • 代码中的空格出现地点
    • 注释符号与注释内容之间有空格
    • 类继承, 参数名和类型之间等, 冒号前面不加空格, 但后面跟空格
    • 任何运算符前后有空格
    • 表示返回值的 -> 两边
    • 参数列表、数组、tuple、字典里的逗号后面有一个空格
  • 方法之间空一行
  • 重载的声明放在一起,按照参数的多少从少到多向下排列
  • 每一行只声明一个变量
  • 如果是一个很长的数字时,建议使用下划线按照语言习惯三位或者四位一组分割连接。
  • 表示单例的静态属性,一般命名为 shared 或者 default
  • 如果是空的 block,直接声明{ },括号之间不需换行
  • 解包时推荐使用原有名字,前提是解包后的名字与解包前的名字在作用域上不会形成冲突
  • if 后面的 else\else if, 跟着上一个 if\else if 的右括号
  • switch 中, case 跟 switch 左对齐
  • 每行代码长度应小于 100 个字符,或者阅读时候不应该需要滚动屏幕,在正常范围内可以看到完整代码
  • 实现每个协议时, 在单独的 extension 里来实现

简略规约

  • Swift 会被结构体按照自身的成员自动生成一个非 public 的初始化方法,如果这个初始化方法刚好适合,不要自己再声明
  • 类及结构体初始化方法不要直接调用.init,直接直接省略,使用()
  • 如果只有一个 get 的计算属性,忽略 get
  • 数据定义时,尽量使用字面量形式进行自动推断,如果上下文不足以推断字面量类型时,需要声明赋值类型
  • 省略默认的访问权限(internal)
  • 过滤, 转换等, 优先使用 filter, map 等高阶函数简化代码,并尽量使用最简写
  • 使用闭包时,尽量使用最简写
  • 使用枚举属性时尽量使用自动推断,进行缩写
  • 无用的代码及时删除
  • 尽量使用各种语法糖
  • 访问实例成员或方法时尽量不要使用 self.,特殊场景除外,如构造函数时
  • 当方法无返回值时,不需添加 void

注释规约

  • 文档注释使用单行注释,即///,不使用多行注释,即/***/。 多行注释用于对某一代码段或者设计进行描述
  • 对于公开的类、方法以及属性等必须加上文档注释,方法需要加上对应的Parameter(s)ReturnsThrows 标签,强烈建议使用⌥ ⌘ /自动生成文档模板
  • 在代码中灵活的使用一些地标注释,如MARKFIXMETODO,当同一文件中存在多种类型定义或者多种逻辑时,可以使用Mark进行分组注释
  • 尽量将注释另起一行,而不是放在代码后

其他

  • 不要使用魔法值(即未经定义的常量);
  • 函数参数最多不得超过 8 个;寄存器数目问题,超过 8 个会影响效率;
  • 图形化的字面量,#colorLiteral(...)#imageLiteral(...)只能用在 playground 当做自我练习使用,禁止在项目工程中使用
  • 避免强制解包以及强制类型映射,尽量使用if let 或 guard let进行解包,禁止try!形式处理异常,避免使用隐式解包
  • 避免判断语句嵌套层次太深,使用 guard 提前返回
  • 如果 for 循环在函数体中只有一个 if 判断,使用 for where 进行替换
  • 实现每个协议时, 尽量在单独的 extension 里来实现;但需要考虑到协议的方法是否有 override 的可能,定义在 extension 的方法无法被 override,除非加上@objc 方法修改其派发方式
  • 优先创建函数而不是自定义操作符
  • 尽可能少的使用全局命名空间,如常量、变量、方法等
  • 赋值数组、字典时每个元素分别占用一行时,最后一个选项后面也添加逗号;这样未来如果有元素加入会更加方便
  • 布尔类型属性使用 is 作为属性名前缀,返回值为布尔型类型的方法名使用 is 作为方法名作为前缀
  • 类似注解的修饰词单独占一行,如@objc,@discardableResult 等
  • extension 上不用加任何修饰符,修饰符加在 extension 内的变量或方法上
  • 使用 guard 来提前结束条件,避免形成判断嵌套;
  • 善用字典去减少判断,可将条件与结果分别当做 key 及 value 存入字典中;
  • 封装时善用 assert,方便问题排查;
  • 在闭包中使用 self 时使用捕获列表[weak self]避免循环引用,闭包开始判断 self 的有效性
  • 使用委托和协议时,避免循环引用,定义属性的时候使用 weak 修饰

工具

SwiftLint 工具 提示格式错误

SwiftFormat 工具 提示并修复格式错误

两者大部分格式规范都是一致的,少许规范不一致,两个工具之间使用不冲突,可以在项目中共存。我们通过配置文件可以控制启用或者关闭相应的规则,具体使用规则参照对应仓库的 REAMME.md 文件。

相关规范

Swift 官方 API 设计指南

google 发布的 Swift 编码规范


有一个技术的圈子与一群同道众人非常重要,来我的技术公众号及博客,这里只聊技术干货。


链接:https://juejin.cn/post/6976282985695969294
收起阅读 »

? 我的独立开发的故事

iOS
🐻 我的独立开发的故事我是独立开发者熊大,最近一年尝试了独立开发的滋味,也想和大家聊一聊独立开发的心历路程。 如果你也有开发一款app的想法,那你可以看一看我的独立开发的故事。我做过直播、相机、社交类APP。个人独立app 《imi》《今日计划》2020年,我...
继续阅读 »

🐻 我的独立开发的故事

我是独立开发者熊大,最近一年尝试了独立开发的滋味,也想和大家聊一聊独立开发的心历路程。 如果你也有开发一款app的想法,那你可以看一看我的独立开发的故事。

  • 我做过直播、相机、社交类APP。
  • 个人独立app 《imi》《今日计划》
  • 2020年,我想要尝试一下独立开发的方向。

第一款app的开发周期

做第一款软件《今日计划》时,周一到周六工作,大小周,晚上会有一些开发时间。

总体如下:

  • 每天1小时写app代码 * 60 = 60小时
  • 每周周日有4个小时 * 8 = 32小时
  • 清明节三天 (按照8小时/天tian计算):3*8 = 24小时

一共约120个小时:完成了设计到上线。

我也买了阿里云的ECS,用vapor搭建了后台,维护成本有点高,果断放弃了。

当我开心的把它分享给朋友时,朋友们都说他很丑,于是被贴上一系列标签『丑』、『直男审美』、『搭配有问题』、『太简单了吧』····,总而言之,没什么好的形容词。

(PS:T M D 我自己都感觉有点坑)

 报着期望,又紧急改版一次,更换了icon,改了一些设计。也就是现在的这一版。我在圈子里又推广了一波,登顶效率榜Top20(其实是各位兄弟给面子)。

后来陆陆续续也有一些下载,但由于工作紧张,没能持续更新迭代。

离职风波

《水印相机》这款app目前,摄影榜Top20,很荣幸是我从零带到百万日活的,深知好产品的指数爆发增长。我内心真的想去外边看看,想见识更多优秀的、有趣的人,于是世界那么大,我想出去看看,真的成为了我离职的最主要理由。

从上家公司收获的最大的便是经验,一份让我受用很多年的经验。

离职后,并不缺少内推的机会,但我还没想好该怎么走接下来的路,我在思考,是去大厂深造,还是开启自由职业呢?自己一直是个骄傲的人,毕业时我的薪资就是 xx k,不能为五斗米而折腰,干脆做个自由职业好了。于是把想法讲给周围的人,最后还是找了份工作,公司就在我家的旁边,上下班5分钟。

于是从7月份开始,我就几乎每天晚上有两个小时的时间为开启我的自由职业之路做准备,只要副业收入过万,就开始全职独立开发。

新app上线

2020.08 一个小伙伴,会飞的猪,加入了开发阵营。

2020.10 小满 加了开发阵营。

(由于特殊原因,名字保密)

2021年1月上线了新的免费App《imi-成就最好的自己》,这次的app,至少在UI上取得了程序员的好评,我们还没有正式推广,只是在小圈子里发了一下动态试试水。

我们小团队也开了个新的公·众·号:《独立开发者基地》,感兴趣额可以关注。

惭愧的是,由于新公司较忙,进行了几次通宵加班后,我严重的拖累了小团队的开发进度,本来应该是2020年底就应该上线的。

《imi》

这是一款风格可爱简单的规划、计划类软件,致敬自己,致敬青春。

imi寓意:我就是我,我们一定是不完美的,也许不成功,也许不漂亮,但这就是我,与众不同。

给张图看看:

这个idea是我想的,简单说就是一个计划类软件,里边有

  • 人生节点
  • 座右铭
  • 成就
  • 笔记
  • 喜欢的人
  • 倒计时
  • 指纹解锁
  • 云同步。

设计这款软件希望能让大家觉得有用,不知道软件的初衷是不是个伪命题。让时间见证吧。

独立开发者应该都知道霸榜很久的《时间规划局》,这次《imi》就是冲着它去的,她将作为我们的竞品之一,我想我们这么有情怀的app对标这样的工具类软件,是有点希望的(怕怕)。

希望大家下载: imi-成就 给予我们支持 ^_^

给独立开发者的福利

这个应该算是福利吧,我们小团队,整理出了app的加速库,《今日计划》《imi-成就》两款app都是基于这个加速库开发的。接下来的其他app也会基于这个加速库开发,意味着我们会持续完善、维护这个加速库。里边有很多实用的功能,欢迎star🌟。

加速库SpeedySwift仓库:https://github.com/Tliens/SpeedySwift

imi 中用到的第三方库:

  # Pods for App1125
pod 'HWPanModal', '~> 0.8.1'
pod 'RealmSwift', '~> 10.5.0'
pod 'ZLPhotoBrowser', '~> 4.1.2'
pod 'SwiftDate', '~> 6.3.1'
pod 'IceCream',:path =>'Dev-pods/IceCream' # 数据同步icloud
# pod 'FSPagerView' # 轮播图
# pod 'SwiftyStoreKit' # 内购组件
pod 'Schedule', '~> 2.1.0'
pod 'Hero', '~> 1.5.0'
pod 'BiometricAuthentication'
#依赖库
pod 'UMCCommon', '~> 2.1.4'
#统计 SDK
pod 'UMCAnalytics', '~> 6.1.0'


回顾2020

get的技能:

  • 有幸能主导组件化开发
  • 函数响应式编程
  • go服务端

展望2021

希望大家健康、开心

我们会继续维护,维护今日计划、imi。也会有新的app出现。

最后

天行健君子以自强不息,地势坤君子以厚德载物。

虽大部分努力都没有收获,但热爱诞生创造的婴孩。

与君共勉!!!

写于 2021.01.13 北京·安贞门
链接:https://juejin.cn/post/6917058456184684557
收起阅读 »

Swift-Router 自己写个路由吧,第三方总是太复杂

iOS
Swift-Router 自己写个路由吧,第三方总是太复杂先看看这个路由的使用吧如果是网络地址,会直接自动跳转到 OtherWKWebViewController如果是应用内部的手动调用跳转直接跳转视图控制器EPRouter.pushViewControlle...
继续阅读 »

Swift-Router 自己写个路由吧,第三方总是太复杂

先看看这个路由的使用吧
  1. 如果是网络地址,会直接自动跳转到 OtherWKWebViewController
  2. 如果是应用内部的手动调用跳转
  • 直接跳转视图控制器
    • EPRouter.pushViewController(EPSMSLoginViewController())
  • 先在 RouteDict 注册映射关系再跳转
    • EPRouter.pushAppURLPath("goods/detail?spellId=xxx&productId=xxx")
  1. 又服务器来控制跳转 也得在 RouteDict 注册映射关系,只不过多加了一个 scheme
    • EPRouter.pushURLPath("applicationScheme://goods/detail?spellId=xxx&productId=xxx")

**!!!支持Swift、OC、Storyboard的跳转方式,可以在 loadViewController 看到实现方式 **

EPRouter的全部代码
class EPRouter: NSObject {

    private static let RouteDict:[String:String] = [
        "order/list"            :"OrderListPageViewController",   // 订单列表 segmentIndex
        "order/detail"          :"OrderDetailViewController",     // 订单详情 orderId
        "goods/detail"          :"GoodsDetailViewController",     // 商品详情productId
        "goods/list"            :"GoodsCategoryViewController", // type brandId 跳转到某个分类;跳转到某个品牌
        "goods/search"          :"SearchListViewController", // 搜索商品 text
        "coupon/list"           :"CouponListViewController",      // 优惠券列表
        "cart/list"             :"CartViewController",        // 购物车列表
        "address/list"          :"AddressListViewController",     // 收货地址列表
    ]


// 返回首页,然后指定选中模块
public static func backToTabBarController(index: NSInteger, completion:(()->())?=nil) {

guard let vc = EPCtrlManager.getTopVC(), let nav = vc.navigationController, let tabBarCtrl = nav.tabBarController  else {
return
}

nav.popToRootViewController(animated: false)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+0.1) {
tabBarCtrl.selectedIndex = index
completion?()
}
}


// 销毁n个界面 不建议使用这个方法 可以在pushAppURLPath方法中设置destroyTime达到一样的效果,又可以避免用户侧滑返回
public static func popViewController(animated: Bool, time:NSInteger=1) {

guard let nav = EPCtrlManager.getTopVC()?.navigationController else {
return
}
let vcs = nav.viewControllers
let count = vcs.count
let index = (count - 1) - time
if index >= 0 {
let vc = vcs[index]
nav.popToViewController(vc, animated: true)
} else {
nav.popViewController(animated: true)
}
}


    /// 回到目标控制器
    public static func popViewController(targetVC: UIViewController.Type, animated: Bool, toRootVC: Bool=true) {

        popViewController(targetVCs: [targetVC], animated: animated, toRootVC: toRootVC)
    }

    

    /// 回到目标控制器[vc],从前到后 没有目标控制器是否回到根视图
    public static func popViewController(targetVCs: [UIViewController.Type], animated: Bool, toRootVC: Bool=true) {

        guard let nav = EPCtrlManager.getTopVC()?.navigationController else {
            return
        }
        let vcs = nav.viewControllers
        var canPop = false
        for vc in vcs {
            for tvc in targetVCs {
                if vc.isMember(of: tvc) {
                    canPop = true
                    nav.popToViewController(vc, animated: animated)
                    break
                }
            }
        }
        if !canPop && toRootVC {
            nav.popToRootViewController(animated: animated)
        }
    }

    /// push 一个vc --- destroyTime: push之前要销毁的几个压栈vc
    @objc public static func pushAppURLPath(_ path: String, query: [AnyHashable: Any]=[:], animated: Bool=true, destroyTime:NSInteger=0) {

        var urlString = "applicationScheme://"+path
        if path.contains("http://") || path.contains("https://") {
            urlString = path
        }
        pushURLString(urlString, query: query, animated: animated, destroyTime: destroyTime)
    }


    @objc public static func pushURLString(_ urlString: String, query: [AnyHashable: Any]=[:], animated: Bool=true, destroyTime:NSInteger=0) {

        guard let tvc = loadViewControllerWitURI(urlString, query: query) else {
            return
        }
        pushViewController(tvc, animated: animated, destroyTime: destroyTime)
    }


    @objc public static func pushViewController(_ tvc: UIViewController, query: [AnyHashable: Any]=[:], animated: Bool=true, destroyTime:NSInteger=0) {

        guard let vc = EPCtrlManager.getTopVC() else {
            return
        }

        if let _ = tvc.pushInfo {
            tvc.pushInfo?.merge(query, uniquingKeysWith: { (_, new) in new })
        }else {
            tvc.pushInfo = query
        }
        guard let nav = vc.navigationController else {
            vc.present(tvc, animated: true, completion: nil)
            return
        }
        tvc.hidesBottomBarWhenPushed = true

        if destroyTime > 0 {
            let vcs = nav.viewControllers
            let count = vcs.count
            var index = (count - 1) - destroyTime
            if index < 0 { // destroyTime 很多时,直接从根视图push
                index = 0
            }

            var reVCS = [UIViewController]()
            for vc in nav.viewControllers[0...index] {
                reVCS.append(vc)
            }
            reVCS.append(tvc)
            nav.setViewControllers(reVCS, animated: animated)
        }else {
            nav.pushViewController(tvc, animated: animated)
        }
    }

    public static func loadViewController(_ className: String, parameters: [AnyHashable: Any]? = nil) -> UIViewController? {

        var desVC: UIViewController?
        let spaceName = (Bundle.main.infoDictionary?["CFBundleExecutable"] as? String) ?? "ApplicationName"

        if let vc = storyboardClass(className) { // storyboard
            desVC = vc
        }else if let aClass = NSClassFromString("\(spaceName).\(className)") { // Swift
            if aClass is UIViewController.Type {
                let type = aClass as! UIViewController.Type
                desVC = type.init()
            }
        }else if let aClass = NSClassFromString("\(className)") { // OC
            if aClass is UIViewController.Type {
                let type = aClass as! UIViewController.Type
                desVC = type.init()
            }
        }

        desVC?.pushInfo = parameters
        return desVC
    }


    public static func loadViewController(_ viewController: UIViewController, parameters: [AnyHashable: Any]? = nil) -> UIViewController {

        viewController.pushInfo = parameters
        return viewController

    }

    public static func loadViewControllerWitURI(_ urlString: String, query: [AnyHashable: Any]? = nil) -> UIViewController? {

        

        // 先进行编码,防止有中文的带入, 不行进行二次编码
        var urlString = urlString
        if (URLComponents(string: urlString) == nil) {
            urlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? urlString
        }

        guard let url = URLComponents(string: urlString), let scheme = url.scheme else {
            HGLog("无效的地址:\(urlString)")
            return nil
        }

        if scheme == "http" || scheme == "https" {

            let webVC = OtherWKWebViewController()
            webVC._urlStr = urlString
            return webVC

        } else if String(format: "%@://", scheme) == "appcationScheme://" {
            let path = (url.host ?? "") + url.path
            guard  var vcClassName = RouteDict[path] else {
                HGLog("没有配置视图控制器呢。。。:\(urlString)")
                return nil
            }

            var info: [AnyHashable: Any]?
            if query?.count ?? 0 > 0 {
                info = [AnyHashable: Any]()
                for (key, value) in query! {
                    info![key] = value
                }
            }

            if let queryItems = url.queryItems {
                if info == nil {
                    info = [AnyHashable: Any]()
                }
                for item in queryItems {
                    if let value = item.value {
                        info![item.name] = value
                    }
                }
            }
            return loadViewController(vcClassName, parameters: info)
        }

        HGLog("未知scheme:\(urlString)")
        return nil

    }

    

    private static func storyboardClass(_ className: String) -> UIViewController? {

        if className == "VIPWithdrawViewController" { // 提现
            let vc = UIStoryboard.init(name: "VIP", bundle: nil).instantiateViewController(withIdentifier: "withdrawTVC")
            return vc
        }else if className == "VIPRecordListViewController" { // 提现记录
            let vc = UIStoryboard.init(name: "VIP", bundle: nil).instantiateViewController(withIdentifier: "recordListVC")
            return vc
        }
        return nil
    }
}

用来跳转传递数据的扩展属性
extension UIViewController {

    private struct PushAssociatedKeys {
        static var pushInfo = "pushInfo"
    }

    @objc open var pushInfo: [AnyHashable: Any]? {
        get {
            return objc_getAssociatedObject(self, &PushAssociatedKeys.pushInfo) as? [AnyHashable : Any]
        }
        set(newValue) {
            objc_setAssociatedObject(self, &PushAssociatedKeys.pushInfo, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

可见视图控制器的获取
class EPCtrlManager: NSObject {

    public static let `default`: EPCtrlManager = {
        return EPCtrlManager()
    }()

    // MARK: **- 查找顶层控制器、**
    // 获取顶层控制器 根据window
    @objc public static func  getTopVC() -> UIViewController? {

        var window = UIApplication.shared.keyWindow
        //是否为当前显示的window
        if window?.windowLevel != UIWindow.Level.normal{
            let windows = UIApplication.shared.windows
            for  windowTemp in windows{
                if windowTemp.windowLevel == UIWindow.Level.normal{
                    window = windowTemp
                    break
                }
            }
        }
        let vc = window?.rootViewController
        return getTopVC(withCurrentVC: vc)
    }

    ///根据控制器获取 顶层控制器
    private static func  getTopVC(withCurrentVC VC :UIViewController?) -> UIViewController? {

        if VC == nil {
            print("🌶: 找不到顶层控制器")
            return nil
        }

        if let presentVC = VC?.presentedViewController {
            //modal出来的 控制器
            return getTopVC(withCurrentVC: presentVC)
        }else if let tabVC = VC as? UITabBarController {
            // tabBar 的跟控制器
            if let selectVC = tabVC.selectedViewController {
                return getTopVC(withCurrentVC: selectVC)
            }
            return nil
        } else if let naiVC = VC as? UINavigationController {
            // 控制器是 nav
            return getTopVC(withCurrentVC:naiVC.visibleViewController)
        } else {
            // 返回顶控制器
            return VC
        }
    }
}
收起阅读 »

Xcode 的拼写检查,你开启了吗?

iOS
Xcode 的拼写检查,你开启了吗?这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战引言作为一名开发人员,当我们编写代码时,我们会更多地关注逻辑和算法,而不是拼写和语法。但它也是我们编码的一个重要部分,特别是当我们从注释生成文档的时候。...
继续阅读 »

Xcode 的拼写检查,你开启了吗?

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战


引言

作为一名开发人员,当我们编写代码时,我们会更多地关注逻辑和算法,而不是拼写和语法。但它也是我们编码的一个重要部分,特别是当我们从注释生成文档的时候。

拼写检查帮助我们找出拼写错误,让我们有更多的时间关注代码逻辑。


拼写检查能识别什么

答案就是代码中与Spelling and Grammer相关的所有内容

  • 变量名
  • 方法
  • 注释
  • 字符串的字面量(包括本地化)

先来看一段代码:

image.png

在上面的代码中,包括类、方法、变量和注释,但没有启用Spelling and Grammer。猛一看去,好像没啥问题,但如果我们仔细检查,就会发现很多拼写错误。

现在让我们启用Spelling and Grammer,看看会发生什么-

image.png

在上面的代码中我们可以看到,当我们启用拼写检查时,它能检测到所有的拼写错误,并用红色高亮显示。现在我们就省去了找错误的时间,可以直接去修改了。


如何开启

image.png

Edit > Format > Spelling and Grammar

可以看到有三个可用的选项,我们依次来看下:

Check Spelling While Typing

启用后,会把项目中的所有输入错误一次性、全部以红色高亮显示,就像上面的例子一样。

另外,开启这个选项后,还可以选中要修改的单词,然后右键,菜单中会出现 Xcode 建议的单词。

image.png

Check Document Now

它将在当前文件中逐个显示输入错误。为了检查当前文件中的所有错误,可以重复这个命令
Edit > Format > Spelling and Grammar > Check Document Now

或者使用快捷键
command 和分号(;)的组合

Show Spelling and Grammar

它会打开所有建议的更改。我们可以单击其中任何一个进行替换。 使用命令
Edit > Format > Spelling and Grammar > Show Spelling and Grammar

或者使用快捷键
command 和冒号(:)的组合

image.png


Learn Spelling 和 Ignore Spelling

有时候我们需要使用一些在系统词典中没有定义的独特词汇,比如应用程序前缀、开发者名称、公司名称等。Xcode 也会检查这些单词的错误。

所以就用Learn Spelling或者Ignore Spelling处理这些特殊的单词。

通过菜单

右键选中要处理的单词

image.png

通过 command + :

image.png


结语

快去探索一下 Edit > Format > Spelling and Grammar 下面的三个选项吧~

收起阅读 »

让你的 Swift 代码更 Swift

iOS
让你的 Swift 代码更 Swift这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战引言Swift 有很多其他语言所没有的独特的结构和方法,因此很多刚开始接触 Swift 的开发者并没有发挥它本身的优势。所以,我们就来看一看那些让你的...
继续阅读 »

让你的 Swift 代码更 Swift

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战


引言

Swift 有很多其他语言所没有的独特的结构和方法,因此很多刚开始接触 Swift 的开发者并没有发挥它本身的优势。

所以,我们就来看一看那些让你的 Swift 代码更 Swift 的写法吧~


有条件的 for 循环

现在,我们要对view.subviews中的UIButton做一些不可描述的事情,用 for 循环怎么来遍历呢?

在下面的写法中,更推荐后面两种写法:


for subView in view.subviews {
if let button = subView as? UIButton {
//不可描述的事情
}
}


for case let button as UIButton in view.subviews {
//不可描述的事情
}


for button in view.subviews where button is UIButton {
//不可描述的事情
}



enumerated()

在 Swift 中进行 for 循环,要拿到下标值,一般的写法要么定义局部变量记录下标值,要么遍历 0..<view.subviews.count。其实还有个更方便的写法:enumerated(),可以一次性拿到下标值和遍历的元素。

  • ❌ 第一种肯定是不推荐的,因为还要定义额外的局部变量,容易出错,pass

  • ✅ 第二种在只需要用到下标值的时候,是可以用的,但如果还要用到下标值对应的元素,就还得再取一次,麻烦,pass

  • ✅ 第三种就比较完美,虽然一次性可以拿到下标值和元素,但其中一个用不到就可以用 _


var index: Int = 0
for subView in view.subviews {
//不可描述的事情
index += 1
}


for index in 0..<view.subviews.count {
let subView = view.subviews[index]
//不可描述的事情
}


//index 和 subView 在循环体中都能使用到
for (index, subView) in view.subviews.enumerated() {
//不可描述的事情
}

//只用到 index
for (index, _) in view.subviews.enumerated() {
//不可描述的事情
}

//只用到 subView
for (_, subView) in view.subviews.enumerated() {
//不可描述的事情
}


first(where: )

filter 是 Swift 中几个高级函数之一,过滤集合中的元素时非常的好用,不过在某些情况下,比如获取集合中满足条件的第一个元素时,有一个更好的选择first(where: )

let article1 = ArticleModel(title: "11", content: "内容1", articleID: "11111", comments: [])

let article2 = ArticleModel(title: "11", content: "内容2", articleID: "22222", comments: [])

let article3 = ArticleModel(title: "33", content: "内容3", articleID: "3333", comments: [])

let articles = [article1, article2, article3]


if let article = articles.filter({ $0.articleID == "11111" }).first {
print("\(article.title)-\(article.content)-\(article.articleID)")
}


if let article = articles.first(where: {$0.articleID == "11111"}) {
print("\(article.title)-\(article.content)-\(article.articleID)") //11-内容1-11111
}


contains(where: )

这个和上面的first(where: )几乎一样,比如这里要判断文章列表里是否包含 articleID 为 11111 的文章:


if !articles.filter({ $0.articleID == "11111" }).isEmpty {
//不可描述的事情
}


if articles.contains(where: { $0.articleID == "11111"}) {
//不可描述的事情
}


forEach

当循环体内的逻辑比较简单时,forEach 往往比 for...in...来的更加简洁:

func removeArticleBy(ID: String) {
//删库跑路
}


for article in articles {
removeArticleBy(ID: $0.articleID)
}


articles.forEach { removeArticleBy(ID: $0.articleID) }


计算属性 vs 方法

我们知道计算属性本身不存储数据,而是在 get 中返回计算后的值,在 set 中设置其他属性的值,所以和方法很类似,但比方法更简洁。一起来看下面的示例:


class YourManager {
static func shared() -> YourManager {
//不可描述的事情
}
}

let manager = YourManager.shared()


extension Date {
func formattedString() -> String {
//不可描述的事情
}
}

let string = Date().formattedString()



class YourManager {
static var shared: YourManager {
//不可描述的事情
}
}

let manager = YourManager.shared


extension Date {
var formattedString: String {
//不可描述的事情
}
}

let string = Date().formattedString


协议 vs 子类化

尽量使用协议而不是继承。协议可以让代码更加灵活,因为类可同时遵守多个协议。

此外,结构和枚举不能子类化,但是它们可以遵守协议,这就更加放大了协议的好处

Struct vs Class

尽可能使用 Struct 而不是 Class。Struct 在多线程环境中更安全,更快。

它们最主要的区别, Struct 是值类型,而 Classe 是引用类型,这意味着 Struct 的每个实例都有它自己的唯一副本,而 Class 的每个实例都有对数据的单个副本的引用。

这个链接是苹果官方的文档,解释如何在 Struct 和 Class 之间做出选择。 developer.apple.com/documentati…


结语

让我们的 Swift 代码更 Swift 的方法远不止上面这些,这里要说的是,平时写代码时,要刻意的使用 Swift 强大的特性,才能发挥它本身的价值。

而这些特性就需要大家去多看看官网的例子,或者一些主流的 Swift 第三方库,看看他们是如何运用 Swift 的特性的。

收起阅读 »

2022 年移动开发的最佳 React Native 替代方案

iOS
截至 2021 年 8 月,Android 和 iOS 平台占据移动操作系统市场份额的 99.15%。这些平台多年来一直主导着移动应用市场。结果是各种移动开发技术的兴起,包括跨平台框架。   React Native 是其中最受欢迎的一种...
继续阅读 »

截至 2021 年 8 月,Android 和 iOS 平台占据移动操作系统市场份额的 99.15%。这些平台多年来一直主导着移动应用市场。结果是各种移动开发技术的兴起,包括跨平台框架。  


image.png


React Native 是其中最受欢迎的一种。 


为什么?


React Native 允许开发人员跨平台共享多达 70% 的代码库。更快的开发、降低的成本和易于调试是该框架的一些好处。Facebook 的支持还确保 React Native 保持最佳运行状态。但是,就像其他所有框架一样,它也有其局限性。  


React Native 工程师经常面临兼容性问题和缺乏自定义模块。此外,使用此框架构建的应用程序因其近乎原生的功能而受到的性能影响较小。考虑到这一点,React Native 是一个不错的选择吗?这个问题的答案取决于您的产品要求。为了帮助您做出决定,我们编制了一份 React Native 替代方案列表,这些替代方案可为您的应用程序提供强大、便捷的功能。最后,您将能够知道要使用哪种技术。 


让我们开始吧!


需要考虑的 React Native 替代方案


原生平台:


本机应用程序编程语言是一些最流行的替代方案。它们是用于为操作系统开发移动应用程序的特定于平台的技术。此类操作系统的示例包括 Android、iOS 或 Windows。使用这些语言构建的本机应用程序往往会提供更好的性能和用户体验。开发人员对 Apple 应用程序使用 Swift 和 Objective-C,对原生 Android 应用程序使用 Java 和 Kotlin。


优点:




  • 出色的性能



这些编程语言直接与平台的底层资源交互。有了这个,开发人员可以充分利用系统的图形元素、计算功能或其他组件来构建快速执行的应用程序。 




  • 易于扩展  



在扩展应用程序的功能时,总会有遇到乏味问题的风险。本机代码减少了出现此问题的可能性。它们受 iOS 和 Android IDE 以及 SDK 工具包的支持。利用这一优势,您可以为每个平台实施基本、高级甚至最新的功能,而无需担心兼容性问题。  




  • 更容易使用



根据2021 年 Stack Overflow 开发人员调查,Swift 在其他 38 种编程语言中排名第 8。在类似的列表中,React Native 是 13 个框架中的第 9 个选择。Java 在最常用的语言中排名第 5。React Native 在 13 个最常用的框架中排名第 6。这表明这两个原生代码更易于使用和学习。使用它们来构建应用程序可以减轻中级和有经验的开发人员可能遇到的复杂性。


缺点




  • 开发成本高



Native 主要基于“一个产品,两个应用程序”的概念。因此,它可能会很昂贵,因为您需要两个对 iOS 和 Android 本机代码具有广泛知识的专业开发团队。




  • 耗时



Android 和 iOS 应用程序需要不同的代码库,这使得跨平台重用代码变得不可能。相反,每个产品都需要单独构建、测试、更新和管理。对于时间敏感的项目,这种缓慢的开发和部署过程是一个主要缺点。 




  • 稀缺人才库



尽管 Java 甚至在本机应用程序开发之外也被广泛采用,但该类别中的其他语言则相反。Stack Overflow 发现,Swift 和 Kotlin 分别被 5.1% 和 8.32% 的开发人员使用。或许,这可能归功于这些编程语言的年轻化。Objective-C 以 2.8% 位居榜首。但 React Native 遥遥领先,为 14.51%。因此,找到Swift 开发人员或其他对 Kotlin 和 Objective-C 具有广泛知识的编码人员可能会令人望而生畏。 


想阅读 React Native 和 Swift 之间的详细比较吗?阅读这篇文章


可以使用 Native Tech Stack 构建哪些应用程序/产品?


本机技术非常适合游戏应用程序、特定于操作系统的媒体播放器或其他需要完全访问设备功能的应用程序。


Flutter


image.png


Flutter 是 Google 于 2018 年创建并正式推出的一项年轻的开源技术。与 React Native 类似,Flutter 支持使用一个代码库来构建跨平台的类原生应用程序。它是用 Dart 开发的,Dart 是一种同样由 Google 提供的面向对象语言。多年来,Flutter 的受欢迎程度稳步上升,超过了其主要竞争对手 React Native。


优点




  • 更快的开发



与 React Native 一样,Flutter 允许更快的开发和部署时间。您可以从一个代码构建两个应用程序(iOS 和 Android)。它的小部件和交互式资产(例如,热重载)减轻了诸如测试和调试之类的繁琐任务。此外,Dart 是 Flutter 的编程语言。它快速、简洁,并且无需额外的抽象即可编译为本机代码。这总结了通过更短的上市时间实现快速开发和竞争优势。 




  • 优质的跨平台体验



Flutter 的 Material 和 Cupertino 小部件与 Apple 和 Google 的设计指南兼容。开发人员可以利用这些现成的 UI 元素在两个平台上构建具有令人印象深刻的界面的应用程序。更重要的是,Flutter 的渲染引擎 Skia 允许对每个像素进行完整的管理。这反过来又确保了使用 Flutter 构建的 UI 在多个平台或操作系统版本上启动时保持一致。




  • 轻松调试



使用热重载,无需重新启动整个应用程序即可查看更改。相反,Flutter 开发人员可以进行和查看实时更改,而无需在此之后重新编译代码。只需为两个平台构建一个应用程序这一事实确保检测到和修复的任何错误都将反映在两个版本中。




  • 低成本



就像使用 React Native 一样,使用 Flutter 开发应用程序的成本低于使用原生应用程序。这是因为您可以使用小型开发团队在更短的时间内为 iOS 和 Android 构建一个应用程序。  


缺点




  • 重量级应用



使用 Flutter 构建的应用程序文件很大。这些应用程序可能加载缓慢并占用空间和电池性能。为了扩大规模,开发人员可能经常使用较少的包和库,从而在某些功能上妥协。结果是质量低劣的产品。 




  • 技术不成熟



作为一个年轻的框架,Flutter 还没有广泛的资源基础。这意味着您可能找不到开发所需的第三方库和包。Flutter 不成熟的另一个缺点是它的增长潜力。未来不太有利的变化可能会给框架带来一些复杂性,使其更难管理。鉴于谷歌终止项目的历史,Flutter 也有可能不会持续下去。  




  • 对 iOS 功能的支持不佳



Flutter 允许快速、无缝地开发 Android 应用程序。但 iOS 的情况并非如此。访问平台的本机组件可能会出现问题。这使得几乎不可能实现特殊的 iOS 功能,例如引导访问或默认页面转换等简单功能。 


想阅读 React Native 和 Flutter 的详细比较吗?阅读这篇文章


Flutter 可以构建哪些应用/产品?


您可以使用 Flutter 开发需要快速或实时访问的产品。它包括客户服务、金融服务提供商、电子商务公司或任何接受当面付款的商家的应用程序。


Xamarin


image.png


另一种常见的 React Native 替代方案是 Xamarin。它是微软提供的跨平台技术。它始于 2011 年的 MonoTouch 和 Mono for Android,直到微软于 2016 年收购它。 Xamarin 使用 C# 语言和 .NET 框架来开发 iOS、Android 和 Windows 移动应用程序。 


优点




  • 快速发展



借助 Xamarin 的一种产品、一种技术堆栈方法,开发人员可以跨平台重用多达 90% 的代码。您无需在开发环境之间切换,因为您可以在 Visual Studio 中构建 Xamarin 应用程序。更重要的是,该框架允许访问所有支持平台上的公共资源。总而言之,开发时间更短,成本更低。 




  • 灵活的



Xamarin 的组件存储使开发人员可以访问跨平台的标准化 UI 控件、集成的开源库和第三方服务。借助这些广泛的资源,您可以选择多个元素或在您的应用中实现所需的功能。 




  • 出色的性能



Xamarin.Essentials 库提供对本机组件的访问。程序员可以使用 Xamarin.iOS 和 Xamarin.Android 分别构建 iOS 和 Android 应用程序。这些导致产品在性能上接近本机应用程序。React Native 在这方面并不接近。您还可以在运行时将应用程序的 UI 转换为原生元素,以确保接近原生的设计和性能。




  • 易于扩展



调试和维护更容易,因为开发人员可以从一个源代码跨平台发现和更改。此外,Xamarin 与其支持平台的 SDK 和 API 集成。一旦更改可用,这使得在 iOS 和 Android 应用程序中更新或实施新功能变得容易。  




  • 广泛的技术支持



Microsoft 提供学习资源和综合解决方案,使开发人员能够测试、监控和保护他们的应用程序。它包括 Azure 云、Xamarin Insights 和 Xamarin TestCloud。


缺点




  • 不适合图形繁重的应用程序 



在 Xamarin 中,开发人员主要可以共享业务逻辑而不是 UI 代码。这只是意味着您需要为每个平台构建一个单独的 UI。考虑到这一点,构建需要复杂动画或大量交互 UI 的游戏应用程序或其他产品会更慢且乏味。 




  • 有限的社区 



在最近的 Stack Overflow 开发人员调查中,只有 5.8% 的受访者使用 Xamarin。因此,可能很难聘请具有丰富经验和知识的Xamarin 开发人员。但是,随着框架的不断发展,这种劣势可能不会持续很长时间。如果您有紧急需求,请联系我们,让您与经过预先审查的 Xamarin 专家联系。 




  • 昂贵的许可证



Xamarin 加快了开发时间,降低了成本。但是,考虑到其 IDE(Microsoft Visual Studio)的价格,这种优势可能不那么令人印象深刻。对于商业项目,Enterprise 和 Professional 许可证是理想的选择。Enterprise 第一年的年度定价为每位用户 5,999 美元,然后续订 2,569 美元。首次专业订阅者将在以后支付 1,999 美元和 799 美元。 




  • 固有限制



尽管 Xamarin 是为原生应用开发量身定制的,但它并不是纯粹的原生应用。因此,它有几个限制。这包括对开源库的限制访问、更新或集成特定于平台的新 API 的延迟以及更大的应用程序大小。 


可以使用 Xamarin 构建哪些应用程序/产品?


Xamarin 在具有繁重逻辑或简单 UI 的应用程序上表现良好。它包括用于调查、项目管理、旅行、杂货或跟踪的应用程序。 


NativeScript


image.png


与 React Native 类似,该框架使用 JavaScript 为 iOS 和 Android 构建跨平台移动应用程序。它还支持 TypeScript、Angular 和相关框架。使用 NativeScript 构建的应用程序会生成完全原生的应用程序。 


优点




  • 原生功能



NativeScript 将 iOS 和 Android API 注入到 JS 虚拟机中,以便更容易地与原生资源集成。这使开发人员可以快速访问插件、Android SDK、iOS 依赖项管理器——Cocoapods 和其他相关技术,以构建具有本机性能的应用程序。它还带来了直观的用户界面和更好的用户体验。




  • 更广泛的开发人才



NativeScript 使用 JS 和 CSS 的一个子集,它们都是成熟的。对这些技术有一定了解的开发人员可以更快地构建本机应用程序。此外,这个 NativeScript 支持各种 JS 框架,例如 Angular、Vue.js 或 TypeScript。 




  • 更少的开发时间



使用 NativeScript 构建时,开发人员可以在模拟器屏幕上实时查看代码更改。因此,此后您无需重新编译应用程序。再加上 NativeScript 中的单一代码库方法,这意味着每次修改都可以应用于其他平台。因此,该框架提高了开发速度。 


缺点




  • 本土专业知识



根据您的项目范围,您可能需要实现高级本机功能。这需要在特定于平台的 UI 标记和元素方面具有专业知识的软件顾问




  • 插件质量不确定



虽然 NativeScript 上有几个免费插件,但并不是全部都经过验证。这使开发人员面临使用有问题的开源插件的风险,这些插件可能会导致严重的瓶颈或更糟糕的最终产品。




  • 比本机更大的应用程序大小



无论 NativeScript 应用程序与真正的 Native 多么接近,它们的大小都相对较大。NativeScript 上空白 android 项目的默认大小为 12MB。但这仍然低于 React Native 的默认 APK 大小,它可以高达 23MB 


可以使用 NativeScript 构建哪些应用程序/产品?


NativeScript 最适合需要利用硬件组件功能的实时应用程序或产品。它包括用于流媒体、实时馈送和简单游戏的应用程序。 


Ionic


image.png


Ionic 是一种 React Native 替代方案,可让您构建跨平台应用程序。这个开源 SDK 最初是基于 Apache Cordova 和 AngularJS 构建的。但后来,它增加了对 React、Vue.js 和 Angular 等其他 JS 框架的支持。 


优点




  • 原生功能




使用 Apache Cordova 和 Capacitor 插件,Ionic 可以访问移动操作系统的相机、蓝牙、麦克风、指纹扫描仪、GPS 等功能。此外,Ionic 的 UI 组件及其内置的自适应样式通过对设计进行轻微更改来确保应用程序保持原生的感觉。 




  • 跨平台体验



Ionic 利用网络标准和通用 API 为任何平台构建应用程序。有了这个,开发人员可以构建一个应用程序,然后使用一个代码库将它定制到所有支持的平台。 




  • 更短的开发时间



使用 Ionic 的预构建功能,无需为每个开发构建 UI 组件。相反,开发人员可以重用或自定义每个元素,在更短的时间内构建功能性应用程序。 


缺点




  • 不适合游戏应用



与大多数跨平台框架一样,Ionic 可能不适合具有高级图形的应用程序。这是因为 Ionic 使用 CSS,这在开发 3D 游戏应用程序时受到限制。在这种情况下,本地化可能是最好的选择。 




  • 兼容性问题



集成的本机插件可能会相互冲突,从而产生大大减慢开发过程的问题。 




  • 安全问题




开发跨平台意味着您需要同时考虑 Web 和本机移动应用程序的安全性。尽管现有解决方案可以解决此问题,但对于需要高端安全性的应用程序而言,这可能既乏味又昂贵。 


想要阅读 React Native 和 Ionic 之间的详细比较吗?阅读这篇文章


可以使用 Ionic 构建哪些应用程序/产品?


Ionic 可用于需要即时信息或类似本机功能的应用程序。这包括用于新闻、生活方式、流媒体和金融服务的应用程序。 


Apache Cordova


image.png


Apache Cordova 由 Nitobi 创建,于 2011 年被 Adobe 收购,并更名为 PhoneGap。随后,它作为 PhoneGap 的开源版本发布。Apache Cordova 使开发人员能够使用 HTML、CSS 和 JavaScript 构建移动应用程序。可以通过命令行界面 (CLI) 使用此 React Native 替代方案开发跨平台应用程序。对于接近本机的应用程序,您可以使用 Cordova 以平台为中心的工作流程。 


优点




  • 丰富的插件集



开发人员在使用 Apache Cordova 进行构建时有大量插件可供选择。这些插件提供对本机设备 API 的访问,从而更轻松地在应用程序中实现广泛的功能,以获得更好的性能和用户体验。 




  • 无障碍技能集



Cordova 使用的标准技术 JS、CSS 和 HTML 已经成熟。具有这些技术编程背景的移动开发人员可以快速适应构建 Apache Cordova 应用程序。易于找到开发人员、温和的学习曲线和快速的上市时间潜力是直接的好处。




  • 跨平台支持



本着“一次编写,随处运行”的原则,代码可以跨平台重用。这确保了应用程序可以适应任何平台的UI。此外,无需将特定于平台的编程语言作为一个代码库来学习可以胜任。


缺点




  • 特定于平台的限制



因为 Apache Cordova 应用程序不是纯原生的,它们依赖插件来利用设备的功能。这些第三方自定义插件可能不容易获得、更新或跨平台兼容。 




  • 可能需要本地开发人员



如前所述,使用 Cordova 构建的应用程序可能会遇到某些插件的兼容性问题。您可能需要可以从头开始编写自定义插件的专业本机开发人员。这转化为延长的开发时间和成本。 




  • 潜在的性能问题



使用 Cordova 的默认功能构建高性能应用程序可能很困难。这是因为其技术中存在的限制会减慢应用程序的速度。此类缺点在于其 WebView 和移动浏览器组件以及 JavaScript 中缺乏多线程功能。


可以使用 Apache Cordova 构建哪些应用程序/产品?


您可以使用 Cordova 开发结合本机组件和 WebView 以访问设备 API 的应用程序。它包括用于健身、运动、跟踪和市场的应用程序。 


Framework 7


image.png


Framework 7 是您应该考虑的另一个 React Native 替代方案。它是一个开源 HTML 框架,用于构建具有近乎本机功能的混合 Web 和移动应用程序。Framework 7 兼容 Android 和 iOS 平台。


优点




  • 反应灵敏



从基本元素到高级元素,Framework 7 具有广泛的 UI 组件。开发人员可以访问诸如延迟加载、无限滚动、复选框列表等控件。使用这些资源构建具有干净、本机界面的动态应用程序。




  • 多框架支持



Framework 7 可以与 Angular、React 和 Vue.js 等 JS 框架一起使用。这些结构为开发过程贡献了它们的力量和简单性




  • 对开发者友好



开发人员不仅限于自定义标签。在使用 Framework 7 时,他们可以轻松地使用由 JS 和 CSS 补充的纯 HTML 代码。这意味着至少具有这些语言甚至 jQuery 中级知识的程序员可以扩展。 


缺点




  • 有限的平台支持



目前,Framework 7 仅支持 iOS 和 Android 平台。希望为其他平台开发应用程序的开发人员可能会评估其他框架。




  • iOS 专用



Framework 7 最初是为 Apple 环境开发的。这开辟了在为 Android 开发时遇到渲染问题的可能性。




  • 最少的文档



用户可以轻松找到有关如何在此框架中实现任何元素集的资源。然而,大多数高级需求可能没有现成的答案,因为文档不像其他框架那样广泛。  


Framework 7 可以构建哪些应用程序/产品?


Framework 7 可用于构建依赖于设备硬件的渐进式 Web 应用程序或 iOS 和 Android 应用程序。 


jQuery Mobile


image.png


jQuery Mobile 是一个开源 JavaScript 库,用于开发跨平台移动应用程序和网站。它利用了 jQuery 的特性,jQuery 以实现动画、AJAX 和文档对象模型 (DOM) 操作的简便性和快速性而闻名。  


优点




  • 较低的学习曲线



这项技术建立在 jQuery Core 之上,大多数程序员可能已经在过去使用过它。这使得它更容易学习和使用。




  • 跨平台、跨浏览器兼容性



使用 jQuery Mobile 框架,您可以构建与流行的桌面浏览器和平台兼容的高度响应的应用程序和网站。其支持的平台包括 iOS、Android、Windows、WebOS 和 Blackberry。 




  • 出色的动画页面过渡效果



基于渐进式增强原理,jQuery Mobile 导航系统允许页面通过 Ajax 加载到 DOM。这确保了页面得到改进,然后以高质量的过渡显示。




  • 简单方便



开发人员只需几行代码即可处理 HTML 事件、AJAX 请求和 DOM 操作。这在 JavaScript 中需要更长的行。 




  • 轻量级



由于其有限的图像依赖性,jQuery Mobile 的最小大小为 40 KB。这有助于它的速度。 


缺点




  • 最小主题



jQuery 移动版中可用的 CSS 主题使自定义应用程序变得容易。然而,它们是有限的。开发人员可能会构建与使用此技术构建的其他产品不同的应用程序。




  • 使用其他框架非常耗时




jQuery Mobile 与 PhoneGap 等其他移动应用程序框架相结合,以获得更好的性能。但这会减慢开发过程。 




  • 移动设备运行速度较慢



即使在最新的 iOS 和 Android 平台上,这项技术也明显变慢。如果您希望开发一个快速的移动应用程序,您可能需要考虑其他替代方案。


可以使用 jQuery Mobile 构建哪些应用程序/产品?


jQuery Mobile 是针对旧浏览器、内容管理系统或其他需要一些动画和较少用户交互的产品的应用程序的理想选择。 


PhoneGap


image.png


渐进式 Web 应用程序 (PWA)


渐进式 Web 应用程序是应用程序软件,可以像常规网站一样在 Web 浏览器上加载和执行。它结合了 Web 功能和本机应用程序的功能(例如推送通知和对硬件功能的访问),以提供出色的用户体验。与传统应用程序不同,PWA 无法从应用程序商店安装到设备中。相反,它可以添加为用户的主屏幕。渐进式 Web 应用程序使用 HTML、JavaScript 和 CSS 等标准 Web 技术构建。 


优点:




  • 反应灵敏



PWA 可以轻松适应多种设备的屏幕尺寸,无论是平板电脑、台式机、Android 和 iOS 移动设备,还是其他直接尺寸。 




  • 安全的



利用 HTTPS,在 PWA 上广播的信息被加密。在大多数情况下,如果没有安全连接,用户将无法访问某些功能,例如地理定位。这提供了高端安全性和针对路径攻击或其他网络威胁的更多保护。 




  • 极具吸引力的用户体验



PWA 是使用渐进改进原则构建的。这些应用程序在符合标准的浏览器上提供更好的用户体验,在不符合标准的浏览器上至少提供可接受的界面。此外,这些应用程序通过现代网络标准提供本机应用程序功能和感觉。这些功能进一步丰富了移动体验。 




  • 减少对网络的依赖



构建渐进式 Web 应用程序的最大优势之一是它们能够在连接速度缓慢的情况下运行。如果用户访问过某个站点,即使没有网络,他们也可以访问该内容。这可以通过 Service Workers、缓存 API 和离线存储站点资产的客户端存储技术实现。也就是说,PWA 利用这一点来享受更快的加载速度。 




  • 易于访问和维护



作为一个基于网络的应用程序,PWA 享有更高的知名度,因为它可以被搜索引擎发现和排名,给他们更多的知名度。此外,用户无需额外安装即可轻松进行测试和升级,因为这些应用程序可以在线访问。 


缺点




  • 对硬件组件的访问受限



虽然它可以访问相当多的功能,但 PWA 无法完全使用设备的大量硬件组件。对高级相机控制、通话功能、蓝牙的支持,并且某些功能在某些设备中仍然不发达。




  • 弱 iOS 支持



iOS 设备中 PWA 的一个常见缺点是缺乏推送通知支持。这使得无法通过新内容或更新重新吸引 iOS 用户,从而导致转化次数减少。 




  • 没有可靠的第三方控制



因为 PWA 不能从应用商店下载,所以没有监管标准。因此,其大多数类本机应用程序的 UI 质量可能不一致。 


哪些应用程序/产品可以构建为渐进式 Web 应用程序?


PWA 最适用于由于网络缓慢而易于失败的软件、需要更高流量的应用程序或很少使用的应用程序。它包括为电子商务公司、叫车服务、市场代理等提供的产品。


Bootstrap


image.png


Bootstrap 是一个结合了 Javascript、CSS 和 HTML 的工具包。它广泛用于开发响应式、移动优先的网页和完全嵌入浏览器的渐进式 Web 应用程序 (PWA)。 


什么是 PWA?


渐进式 Web 应用程序是应用程序软件,可以像常规网站一样在 Web 浏览器上加载和执行。它结合了 Web 功能和本机应用程序的功能(例如推送通知和对硬件功能的访问),以提供出色的用户体验。与传统应用程序不同,PWA 无法从应用程序商店安装到设备中。相反,它可以添加为用户的主屏幕。 


优点




  • 高度响应



Bootstrap 的流体网格系统是其主要优势之一。它具有定义明确的类和各种简单的布局。一旦实施,它将在所有平台上提供一致的外观。这些组件也可以定制以匹配每个项目的设计。 




  • 广泛的文档



Bootstrap被称为“世界上最流行的 HTML、CSS 和 JS 库”,拥有丰富的文档。考虑到这一点,移动开发人员很可能会为此框架找到基本和高级问题的解决方案。 




  • 对jQuery插件的内置支持




通过这些内置插件,Bootstrap 可以从 JS API 访问更多 UI 组件。工具提示和对话框等界面也可以提高预先存在的界面的性能。 




  • 安稳



Bootstrap 的 PWA 通过 HTTPS 广播信息。在大多数情况下,如果没有安全连接,用户将无法访问某些功能,例如地理定位。这提供了针对大多数网络威胁的高端安全性和更多保护。


缺点




  • 设备功能有限



在默认模式下使用 Bootstrap 可以将几个未使用的元素和代码加载到您的项目中。这会转化为较大的应用程序大小和缓慢的加载时间。




  • 其他自定义设置




使用此框架构建需要智能手机广泛功能的 Web 应用程序并不是一个好的选择。原因是用 JS 和 Bootstrap 编写的 Web 应用程序无法完全访问设备的传感器和功能。




  • 可能对开发人员不友好



使用 Bootstrap 默认组件开发的 Web 应用看起来很相似。要自定义应用程序,您需要手动覆盖样式表。这个额外的步骤通常会破坏使用这个框架的目的。 




  • 可能对开发人员不友好



某些任务(例如访问预定义的类或自定义)可能需要更长的时间来学习。 


可以使用 Bootstrap 构建哪些应用程序/产品?


Bootstrap 主要用于设计响应式网页和网络应用程序。 


image.png


image.png


最后


在竞争激烈的移动应用程序开发世界中,错过跨多个平台构建应用程序是一个很大的风险。选择正确的替代方案可以帮助您在重要的平台上保持存在感,同时降低开发成本。


链接:https://juejin.cn/post/7036615302007750692
来源:稀土掘金
收起阅读 »

Swift 中的 Self & Self.Type & self

iOS
Swift 中的 Self & Self.Type & self这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战你可能在写代码的时候已经用过很多次 self 这个关键词了,但是你有没有想过什么是 self 呢?今天我们...
继续阅读 »

Swift 中的 Self & Self.Type & self

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战


你可能在写代码的时候已经用过很多次 self 这个关键词了,但是你有没有想过什么是 self 呢?今天我们就来看看:

  • 什么是 self、Self 和 Self.Type?
  • 都在什么情况下使用?

self

这个大家用的比较多了,self 通常用于当你需要引用你当前所在范围内的对象时。所以,例如,如果在 Rocket 的实例方法中使用 self,在这种情况下,self 将是该 Rocket 的实例。这个很好理解~

struct Rocket {
    func launch() {
        print("10 秒内发射 \(self)")
    }
}

let rocket = Rocket()
rocket.launch() //10 秒内发射 Rocket()

但是,如果要在类方法或静态方法中使用 self,该怎么办?在这种情况下,self 不能作为对实例的引用,因为没有实例,而 self 具有当前类型的值。这是因为静态方法和类方法存在于类型本身而不是实例上。

class Dog {
    class func bark() {
        print("\(self) 汪汪汪!")
    }
}

Dog.bark() //Dog 汪汪汪!


struct Cat {
    static func meow() {
        print("\(self) 喵喵喵!")
    }
}

Cat.meow() // Cat 喵喵喵!


元类型

还有个需要注意的地方。所有的值都应该有一个类型,包括 self。就像上面提到的,静态和类方法存在于类型上,所以在这种情况下,self 就拥有了一种类型:Self.Type。比如:Dog.Type 就保存所有 Dog 的类型值。

包含其他类型的类型称为元类型

有点绕哈,简单来说,元类型 Dog.Type 不仅可以保存 Dog 类型的值,还可以保存它的所有子类的值。比如下面这个例子,其中 Labrador 是 Dog 的一个子类。

class Dog {
    class func bark() {
        print("\(self) 汪汪汪!")
    }
}

class Labrador: Dog {

}

Labrador.bark() //Labrador 汪汪汪!

如果你想将 type 本身当做一个属性,或者将其传递到函数中,那么你也可以将 type 本身作为值使用。这时候,就可以这样用:Type.self。

let dogType: Dog.Type = Labrador.self

func saySomething(dog: Dog.Type) {
    print("\(dog) 汪汪汪!")
}

saySomething(dog: dogType) // Labrador 汪汪汪!


Self

最后,就是大写 s 开头的 Self。在创建工厂方法或从协议方法返回具体类型时,非常的有用:

struct Rocket {
    func launch() {
        print("10 秒内发射 \(self)")
    }
}

extension Rocket {
    static func makeRocket() -> Self {
        return Rocket()
    }
}

protocol Factory {
    func make() -> Self
}

extension Rocket: Factory {
    func make() -> Rocket {
        return Rocket()
    }
}

收起阅读 »

iOS小技能:快速创建OEM项目app

iOS
iOS小技能:快速创建OEM项目app这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战。引言贴牌生产(英语:Original Equipment Manufacturer, OEM)因采购方可提供品牌和授权,允许制造方生产贴有该品牌的...
继续阅读 »

iOS小技能:快速创建OEM项目app

这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战

引言

贴牌生产(英语:Original Equipment Manufacturer, OEM)

因采购方可提供品牌和授权,允许制造方生产贴有该品牌的产品,所以俗称“贴牌生产”。

需求背景: SAAS平台级应用系统为一个特大商户,提供专属OEM项目,在原有通用app的基础上进行定制化开发

例如去掉开屏广告,删除部分模块,保留核心模块。更换专属app icon以及主题色

I 上架资料

  1. 用户协议及隐私政策
  2. App版本、 审核测试账号信息
  3. icon、名称、套装 ID(bundle identifier)
  4. 关键词:
  5. app描述:
  6. 技术支持网址使用:

kunnan.blog.csdn.net/article/det…

II 开发小细节

  1. 更换基础配置信息,比如消息推送证书、第三方SDK的ApiKey、启动图、用户协议及隐私政策。
  2. 接口修改:比如登录接口新增SysId请求字段用于区分新旧版、修改域名(备案信息)
  3. 废弃开屏广告pod 'GDTMobSDK' ,'4.13.26'

1.1 更换高德定位SDK的apiKey

    NSString *AMapKey = @"";
[AMapServices sharedServices].apiKey = AMapKey;


1.2 更新消息推送证书和极光的appKey

  1. Mac 上的“钥匙串访问”创建证书签名请求 (CSR)

a. 启动位于 /Applications/Utilities 中的“钥匙串访问”。

b. 选取“钥匙串访问”>“证书助理”>“从证书颁发机构请求证书”。

c. 在“证书助理”对话框中,在“用户电子邮件地址”栏位中输入电子邮件地址。

d. 在“常用名称”栏位中,输入密钥的名称 (例如,Gita Kumar Dev Key)。

e. 将“CA 电子邮件地址”栏位留空。

f. 选取“存储到磁盘”,然后点按“继续”。

help.apple.com/developer-a…

在这里插入图片描述

  1. 从developer.apple.com 后台找到对应的Identifiers创建消息推送证书,并双击aps.cer安装到本地Mac,然后从钥匙串导出P12的正式上传到极光后台。

docs.jiguang.cn//jpush/clie…在这里插入图片描述

  1. 更换appKey(极光平台应用的唯一标识)
        [JPUSHService setupWithOption:launchOptions appKey:@""
channel:@"App Store"
apsForProduction:YES
advertisingIdentifier:nil];


http://www.jiguang.cn/accounts/lo…

1.3 更换Bugly的APPId

    [Bugly startWithAppId:@""];//异常上报


1.4 app启动的新版本提示

更换appid

    [self checkTheVersionWithappid:@""];


检查版本

在这里插入图片描述


- (void)checkTheVersionWithappid:(NSString*)appid{


[QCTNetworkHelper getWithUrl:[NSString stringWithFormat:@"http://itunes.apple.com/cn/lookup?id=%@",appid] params:nil successBlock:^(NSDictionary *result) {
if ([[result objectForKey:@"results"] isKindOfClass:[NSArray class]]) {
NSArray *tempArr = [result objectForKey:@"results"];
if (tempArr.count) {


NSString *versionStr =[[tempArr objectAtIndex:0] valueForKey:@"version"];
NSString *appStoreVersion = [versionStr stringByReplacingOccurrencesOfString:@"." withString:@""] ;
if (appStoreVersion.length==2) {
appStoreVersion = [appStoreVersion stringByAppendingString:@"0"];
}else if (appStoreVersion.length==1){
appStoreVersion = [appStoreVersion stringByAppendingString:@"00"];
}

NSDictionary *infoDic=[[NSBundle mainBundle] infoDictionary];
NSString* currentVersion = [[infoDic valueForKey:@"CFBundleShortVersionString"] stringByReplacingOccurrencesOfString:@"." withString:@""];

currentVersion = [currentVersion stringByReplacingOccurrencesOfString:@"." withString:@""];
if (currentVersion.length==2) {
currentVersion = [currentVersion stringByAppendingString:@"0"];
}else if (currentVersion.length==1){
currentVersion = [currentVersion stringByAppendingString:@"00"];
}



NSLog(@"currentVersion: %@",currentVersion);


if([self compareVesionWithServerVersion:versionStr]){



UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"%@%@",QCTLocal(@"Discover_a_new_version"),versionStr] message:QCTLocal(@"Whethertoupdate") preferredStyle:UIAlertControllerStyleAlert];
// "Illtalkaboutitlater"= "稍后再说";
// "Update now" = "立即去更新";
// "Unupdate"= "取消更新";

[alertController addAction:[UIAlertAction actionWithTitle:QCTLocal(@"Illtalkaboutitlater") style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"取消更新");
}]];
[alertController addAction:[UIAlertAction actionWithTitle:QCTLocal(@"Updatenow") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@",appid]];
if (@available(iOS 10.0, *)) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) {
}];
} else {
// Fallback on earlier vesions
[[UIApplication sharedApplication] openURL:url];
}
}]];
[[QCT_Common getCurrentVC] presentViewController:alertController animated:YES completion:nil];
}
}
}
} failureBlock:^(NSError *error) {
NSLog(@"检查版本错误: %@",error);
}];
}


see also

更多内容请关注 #小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向

链接:https://juejin.cn/post/7035926626366029837

收起阅读 »

objc_msgsend(中)方法动态决议

iOS
引入在学习本文之前我们应该了解objc_msgsend消息快速查找(上) objc_msgsend(中)消息慢速查找 当快速消息查找和消息慢速查找都也找不到imp时,苹果系统后续是怎么处理的我们一起来学习! 方法动态决议主要做了哪些事情?准...
继续阅读 »


引入

在学习本文之前我们应该了解

当快速消息查找和消息慢速查找都也找不到imp时,苹果系统后续是怎么处理的我们一起来学习! 方法动态决议主要做了哪些事情?

准备工作

resolveMethod_locked动态方法决议

1.png

  • 赋值imp = forward_imp

  • 做了个单例判断动态控制执行流程根据behavior方法只执行一次。

2.png

对象方法的动态决议

3.png

类方法的动态决议

3.png

lookUpImpOrForwardTryCache

4.png

cache_getImp

5.png

  • 苹果给与一次动态方法决议的机会来挽救APP
  • 如果是类请用resolveInstanceMethod
  • 如果是元类请用resolveClassMethod

如果都没有处理那么imp = forward_imp ,const IMP forward_imp = (IMP)_objc_msgForward_impcache;

_objc_msgForward_impcache探究

6.png

  • __objc_forward_handler主要看这个函数处理

__objc_forward_handler

7.png

代码案例分析

   int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGTeacher *p = [LGTeacher alloc];
        [t sayHappy];
[LGTeacher saygood];
    }
    return 0;
}


崩溃信息

2021-11-28 22:36:39.223567+0800 KCObjcBuild[12626:762145] +[LGTeacher sayHappy]: unrecognized selector sent to class 0x100008310

2021-11-28 22:36:39.226012+0800 KCObjcBuild[12626:762145] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[LGTeacher sayHappy]: unrecognized selector sent to class 0x100008310'

复制代码

动态方法决议处理对象方法找不到

代码动态决议处理imp修复崩溃

@implementation LGTeacher

-(void)text{
    NSLog(@"%s", __func__ );
}

+(void)say777{

    NSLog(@"%s", __func__ );
}

// 对象方法动态决议

+(BOOL**)resolveInstanceMethod:(SEL)sel{

    if (sel == @selector(sayHappy)) {

        IMP imp =class_getMethodImplementation(self, @selector(text));
        Method m = class_getInstanceMethod(self, @selector(text));
        const char * type = method_getTypeEncoding(m);
        return** class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];

}

//类方法动态决议

+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(saygood)) {
        IMP  imp7 = class_getMethodImplementation(objc_getMetaClass("LGTeacher"), @selector(say777));
        Method m  = class_getInstanceMethod(objc_getMetaClass("LGTeacher"), @selector(say777));
        const char type = method_getTypeEncoding(m);
        return class_addMethod(objc_getMetaClass("LGTeacher"), sel, imp7, type);
    }

    return [super resolveClassMethod:sel];

}

@end


运行打印信息

2021-11-29 16:30:46.403671+0800 KCObjcBuild[27071:213498] -[LGTeacher text]

2021-11-29 16:30:46.404186+0800 KCObjcBuild[27071:213498] +[LGTeacher say777]

  • 找不到imp我们动态添加一个imp ,但这样处理太麻烦了。
  • 实例方法方法查找流程 类->父类->NSObject->nil
  • 类方法查找流程 元类->父类->根元类-NsObject->nil

最终都会找到NSobject.我们可以在NSObject统一处理 所以我们可以给NSObject创建个分类

@implementation NSObject (Xu)
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (@selector(sayHello) == sel) {
NSLog(@"--进入%@--",NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self , @selector(sayHello2));
Method meth = class_getInstanceMethod(self , @selector(sayHello2));
const char * type = method_getTypeEncoding(meth);
return class_addMethod(self ,sel, imp, type);;

}else if (@selector(test) == sel){
NSLog(@"--进入%@--",NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(object_getClass([self class]), @selector(newTest));
Method meth = class_getClassMethod(object_getClass([self class]) , @selector(newTest));
const char * type = method_getTypeEncoding(meth);
return class_addMethod(object_getClass([self class]) ,sel, imp, type);;
}
return NO;
}

- (void)sayHello2{
NSLog(@"--%s---",__func__);
}

+(void)newTest{
NSLog(@"--%s---",__func__);
}

@end


实例方法是类方法调用,系统都自动调用了resolveInstanceMethod方法,和上面探究的吻合。 动态方法决议优点

  • 可以统一处理方法崩溃的问题,出现方法崩溃可以上报服务器,或者跳转到首页
  • 如果项目中是不同的模块你可以根据命名不同,进行业务的区别
  • 这种方式叫切面编程熟成AOP

方法动态决议流程图

9.png

问题

  • resolveInstanceMethod为什么调用两次?
  • 统一处理方案怎么处理判断问题,可能是对象方法崩溃也可能是类方法崩溃,怎么处理?
  • 动态方法决议后苹果后续就没有处理了吗?

链接:https://juejin.cn/post/7035965819955707935
收起阅读 »

系统学习iOS动画 —— 渐变动画

iOS
系统学习iOS动画 —— 渐变动画这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战这个是希望达成的效果,主要就是下面字体的渐变动画以及右拉手势动画:先创建需要的控件:class ViewController: UIViewContro...
继续阅读 »

系统学习iOS动画 —— 渐变动画

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

这个是希望达成的效果,主要就是下面字体的渐变动画以及右拉手势动画:

请添加图片描述

先创建需要的控件:

class ViewController: UIViewController {
let timeLabel = UILabel()

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(timeLabel)
view.backgroundColor = .gray
timeLabel.text = "9:42"
timeLabel.font = UIFont.systemFont(ofSize: 72)
timeLabel.textColor = .white
timeLabel.frame = CGRect(x: 0, y: 100, width: timeLabel.intrinsicContentSize.width, height: timeLabel.intrinsicContentSize.height)
timeLabel.center.x = view.center.x

}


}

然后创建一个文件,然后写一个继承自UIView的类来编写动画的界面。

import UIKit
import QuartzCore

class AnimatedMaskLabel: UIView {

}

CAGradientLayer是CALayer的另一个子类,专门用于渐变的图层。这里创建一个CAGradientLayer来做渐变。这里

  • startPoint和endPoint定义了渐变的方向及其起点和终点
  • Colors是渐变的颜色数组
  • location: 每个渐变点的位置,范围 0 - 1 ,默认为0。

let gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()

// Configure the gradient here
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
let colors = [
UIColor.yellow.cgColor,
UIColor.green.cgColor,
UIColor.orange.cgColor,
UIColor.cyan.cgColor,
UIColor.red.cgColor,
UIColor.yellow.cgColor
]
gradientLayer.colors = colors

let locations: [NSNumber] = [
0.0, 0.0, 0.0, 0.0, 0.0, 0.25
]
gradientLayer.locations = locations

return gradientLayer
}()


在layoutSubviews里面为gradient设置frame,这里设置宽度为三个屏幕宽度大小来让动画看起来更加顺滑。

 override func layoutSubviews() {
gradientLayer.frame = CGRect(
x: -bounds.size.width,
y: bounds.origin.y,
width: 3 * bounds.size.width,
height: bounds.size.height)
}

接着需要声明一个text,当text被赋值的时候,将文本渲染为图像,然后使用该图像在渐变图层上创建蒙版。

 var text: String! {
didSet {
setNeedsDisplay()

let image = UIGraphicsImageRenderer(size: bounds.size)
.image { _ in
text.draw(in: bounds, withAttributes: textAttributes)
}

let maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.frame = bounds.offsetBy(dx: bounds.size.width, dy: 0)
maskLayer.contents = image.cgImage

gradientLayer.mask = maskLayer
}
}

这里还需要为文本创建一个文本属性

  let textAttributes: [NSAttributedString.Key: Any] = {
let style = NSMutableParagraphStyle()
style.alignment = .center
return [
.font: UIFont(
name: "HelveticaNeue-Thin",
size: 28.0)!,
.paragraphStyle: style
]
}()

最后在didMoveToWindow中添加gradientLayer为自身子view并且为gradientLayer添加动画。

  override func didMoveToWindow() {
super.didMoveToWindow()
layer.addSublayer(gradientLayer)

let gradientAnimation = CABasicAnimation(keyPath: "locations")
gradientAnimation.fromValue = [0.0, 0.0, 0.0, 0.0, 0.0, 0.25]
gradientAnimation.toValue = [0.65, 0.8, 0.85, 0.9, 0.95, 1.0]
gradientAnimation.duration = 3.0
gradientAnimation.repeatCount = Float.infinity

gradientLayer.add(gradientAnimation, forKey: nil)
}

接下来在viewController中添加这个view。 声明一个animateLabel

    let animateLabel = AnimatedMaskLabel()

之后在viewDidLoad里面添加animateLabel在子view并且设置好各属性,这样animateLabel就有一个渐变动画了。

view.addSubview(animateLabel)
animateLabel.frame = CGRect(x: 0, y: UIScreen.main.bounds.size.height - 200, width: 200, height: 40)
animateLabel.center.x = view.center.x
animateLabel.backgroundColor = .clear
animateLabel.text = "Slide to reveal"

接下来为animateLabel添加滑动手势,这里设置滑动方向为向右滑动。

   let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSlide))
swipeGesture.direction = .right
animateLabel.addGestureRecognizer(swipeGesture)


然后在响应方法里面添加动画,这里先创建一个临时变量并且让其在屏幕外面,然后第一次动画的时候让timeLabel上移,animateLabel下移,然后让image跑到屏幕中间。完了之后在创建一个动画让timeLabel和animateLabel复原,把image移动到屏幕外,然后把image移除掉。

  @objc func handleSlide() {
// reveal the meme upon successful slide
let image = UIImageView(image: UIImage(named: "meme"))
image.center = view.center
image.center.x += view.bounds.size.width
view.addSubview(image)

UIView.animate(withDuration: 0.33, delay: 0.0,
animations: {
self.timeLabel.center.y -= 200.0
self.animateLabel.center.y += 200.0
image.center.x -= self.view.bounds.size.width
},
completion: nil
)

UIView.animate(withDuration: 0.33, delay: 1.0,
animations: {
self.timeLabel.center.y += 200.0
self.animateLabel.center.y -= 200.0
image.center.x += self.view.bounds.size.width
},
completion: {_ in
image.removeFromSuperview()
}
)
}

这样动画就完成了,完整代码:

import UIKit

class ViewController: UIViewController {
let timeLabel = UILabel()
let animateLabel = AnimatedMaskLabel()

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(timeLabel)
view.addSubview(animateLabel)

view.backgroundColor = .gray
timeLabel.text = "9:42"
timeLabel.font = UIFont.systemFont(ofSize: 72)
timeLabel.textColor = .white
timeLabel.frame = CGRect(x: 0, y: 100, width: timeLabel.intrinsicContentSize.width, height: timeLabel.intrinsicContentSize.height)
timeLabel.center.x = view.center.x


animateLabel.frame = CGRect(x: 0, y: UIScreen.main.bounds.size.height - 200, width: 200, height: 40)
animateLabel.center.x = view.center.x
animateLabel.backgroundColor = .clear
animateLabel.text = "Slide to reveal"
let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSlide))
swipeGesture.direction = .right
animateLabel.addGestureRecognizer(swipeGesture)

}

@objc func handleSlide() {
// reveal the meme upon successful slide
let image = UIImageView(image: UIImage(named: "meme"))
image.center = view.center
image.center.x += view.bounds.size.width
view.addSubview(image)

UIView.animate(withDuration: 0.33, delay: 0.0,
animations: {
self.timeLabel.center.y -= 200.0
self.animateLabel.center.y += 200.0
image.center.x -= self.view.bounds.size.width
},
completion: nil
)

UIView.animate(withDuration: 0.33, delay: 1.0,
animations: {
self.timeLabel.center.y += 200.0
self.animateLabel.center.y -= 200.0
image.center.x += self.view.bounds.size.width
},
completion: {_ in
image.removeFromSuperview()
}
)
}

}


import UIKit
import QuartzCore


class AnimatedMaskLabel: UIView {

let gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()

// Configure the gradient here
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
let colors = [
UIColor.yellow.cgColor,
UIColor.green.cgColor,
UIColor.orange.cgColor,
UIColor.cyan.cgColor,
UIColor.red.cgColor,
UIColor.yellow.cgColor
]
gradientLayer.colors = colors

let locations: [NSNumber] = [
0.0, 0.0, 0.0, 0.0, 0.0, 0.25
]
gradientLayer.locations = locations

return gradientLayer
}()

var text: String! {
didSet {
setNeedsDisplay()

let image = UIGraphicsImageRenderer(size: bounds.size)
.image { _ in
text.draw(in: bounds, withAttributes: textAttributes)
}

let maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.frame = bounds.offsetBy(dx: bounds.size.width, dy: 0)
maskLayer.contents = image.cgImage

gradientLayer.mask = maskLayer
}
}

let textAttributes: [NSAttributedString.Key: Any] = {
let style = NSMutableParagraphStyle()
style.alignment = .center
return [
.font: UIFont(
name: "HelveticaNeue-Thin",
size: 28.0)!,
.paragraphStyle: style
]
}()

override func layoutSubviews() {
layer.borderColor = UIColor.green.cgColor
gradientLayer.frame = CGRect(
x: -bounds.size.width,
y: bounds.origin.y,
width: 3 * bounds.size.width,
height: bounds.size.height)
}

override func didMoveToWindow() {
super.didMoveToWindow()
layer.addSublayer(gradientLayer)

let gradientAnimation = CABasicAnimation(keyPath: "locations")
gradientAnimation.fromValue = [0.0, 0.0, 0.0, 0.0, 0.0, 0.25]
gradientAnimation.toValue = [0.65, 0.8, 0.85, 0.9, 0.95, 1.0]
gradientAnimation.duration = 3.0
gradientAnimation.repeatCount = Float.infinity

gradientLayer.add(gradientAnimation, forKey: nil)
}
}



收起阅读 »

iOS中加载xib

iOS
iOS中加载xib「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」关于 xib 或 storyboard共同点都用来描述软件界面都用 interface builder 工具来编辑本质都是转换成代码去创建控件不同点xib是轻量级的...
继续阅读 »

iOS中加载xib

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

关于 xib 或 storyboard

  • 共同点
    • 都用来描述软件界面
    • 都用 interface builder 工具来编辑
    • 本质都是转换成代码去创建控件
  • 不同点
    • xib是轻量级的,用来描述局部UI界面
    • storyboard是重量级的,用来描述整个软件的多个界面,并且能够展示多个界面的跳转关系

加载xib

xib 文件在编译的后会变成 nib 文件

11975486-4f7dfbf345c0bff5.png

  • 第一种加载方式
    NSArray * xibArray = [[NSBundle mainBundle]loadNibNamed:NSStringFromClass(self) owner:nil options:nil] ;
    return xibArray[0];

  • 第二种加载方式
    UINib *nib = [UINib nibWithNibName:NSStringFromClass(self) bundle:nil];
    NSArray *xibArray = [nib instantiateWithOwner:nil options:nil];
    return xibArray[0];

    xibArray中log打印 log.png

控制器加载xib

  1. 首先需要对 xib 文件进行一些处理,打开 xib 文件

  2. 点击 "File‘s Owner",设置 Class 为 xxxViewControler 点击

  3. 右键 "Files‘s Owner",里面有个默认的IBOutlet变量view,看一下后面有没有做关联,如果没有就拉到下面的View和视图做个关联

    Files‘s Owner与View做关联

  • 第一种加载方式,传入指定的 xib(如CustomViewController)

    CustomViewController *custom = [[CustomViewController alloc]initWithNibName:@"CustomViewController" bundle:nil];

  • 第二种加载方式,不指定 xib

    CustomViewController *custom = [[CustomViewController alloc]initWithNibName:nil bundle:nil];

    • 第一步:寻找有没有和控制器类名同名的xib,如果有就去加载(XXViewController.xib)

      控制器类名同名的xib.png

    • 第二步:寻找有没有和控制器类名同名但是不带Controller的xib,如果有就去加载(XXView.xib)

      11975486-e40e19dd11cafbc5.png

    • 第三步:如果没有找到合适的 xib,就会创建一个 view(白色View,为系统自己创建的)


xib自定义控件与代码自定义的区别

这是自定义的一个 view,我们通过不同的初始化方式去判断它的执行方法

#import "CustomViw.h"
@implementation CustomViw
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%s",__func__);
}
return self;
}

- (instancetype)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame]) {
NSLog(@"%s",__func__);
}
return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder{

if (self = [super initWithCoder:aDecoder]) {
}
NSLog(@"%s",__func__);
return self;
}

- (void)awakeFromNib{
[super awakeFromNib];
NSLog(@"%s",__func__);
}
@end

  • 通过 init 方法初始化自定义控件

    @implementation ViewController
    - (void)viewDidLoad {
    [super viewDidLoad];
    CustomViw *customView = [[CustomViw alloc] init];
    }
    @end

    log:

    通过init方法初始化自定义控件log打印.png

  • 通过加载 xib 方法初始化自定义控件

    @implementation ViewController
    - (void)viewDidLoad {
    [super viewDidLoad];
    CustomViw *customView = [[[NSBundle mainBundle]loadNibNamed:NSStringFromClass([CustomViw class]) owner:nil options:nil] lastObject];
    }
    @end

    log(打印三次是因为CustomViw的xib文件里有三个View) 通过加载xib方法初始化自定义控件log打印.png

小结:

  • 通过代码初始化自定义控件是不会自动加载xib的,它会执行 initWithFrame 和 init
  • 通过加载 xib 初始化自定义控件,仅仅执行 initWithCoder 和 awakeFromNib,如果要通过代码修改 xib 的内容,一般建议放在 awakeFromNib 方法内

控件封装

一般封装一个控件,为了让开发者方便使用,通常会在自定义的控件中编写俩个方法初始化方法,这样不管是通过 init 还是加载xib都可以实现相同的效果

#import "CustomViw.h"
@implementation CustomViw

- (instancetype)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame]) {
[self setup];
}
return self;
}

- (void)awakeFromNib{
[super awakeFromNib];
[self setup];
}

- (void)setup{
[self setBackgroundColor:[UIColor redColor]];
}
@end

收起阅读 »

iOS中的Storyboard

iOS
iOS中的Storyboard「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」关于StoryboardStoryboard 是最先在 iOS5 中引入的一项新特性,它的出现使得开发人员大幅缩减构建App用户界面所需的时间关于Sto...
继续阅读 »


iOS中的Storyboard

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

关于Storyboard

Storyboard 是最先在 iOS5 中引入的一项新特性,它的出现使得开发人员大幅缩减构建App用户界面所需的时间

关于Storyboard的加载方式

  • 一般在新建工程后,我们便可以看到Xcode会默认加载 Storyboard,但是在实际开发中,我们更常用的是自己新建 Storyboard,所以,这里主要讲手动创建控制器时,加载 Storyboard 的方式

  • 通常在新建的项目中,我们首先要将Xcode加载 Storyboard 去掉

    这里写图片描述

  • 关于 Storyboard 创建控制器

    第一种:

    这里写图片描述

    self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController *vc = [sb instantiateInitialViewController];
    self.window.rootViewController = vc;
    [self.window makeKeyAndVisible];

    第二种:

    这里写图片描述

    self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"WAKAKA"];
    self.window.rootViewController = vc;
    [self.window makeKeyAndVisible];


关于UIStoryboardSegue

在 Storyboard 中,用来描述界面跳转的线,都属于 UIStoryboardSegue 的对象(简称:Segue

这里写图片描述

Segue的属性

  • 唯一标识(identifier
  • 来源控制器(sourceViewController
  • 目标控制器(destinationViewController

Segue的类型

  • 自动型(点击某控件,不需要进行某些判断可直接跳转的)

    这里写图片描述

  • 手动型(点击某控件,需要进行某些判断才跳转的) 这里写图片描述

  • 手动设置 Segue 需要设置

    这里写图片描述

    使用 perform 方法执行对应的 Segue

    //根据Identifier去storyboard中找到对应的线,之后建立一个storyboard的对象
    [self performSegueWithIdentifier:@"showinfo" sender:nil];

    如果需要做传值或跳转到不同的UI,需要在这个方法里代码实现

    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    //比较唯一标识
    if ([segue.identifier isEqualToString:@"showInfo"]) {
    //来源控制器
    UINavigationController *nvc = segue.sourceViewController;
    //目的控制器
    ListViewController *vc = segue.destinationViewController;
    vc.info = @
    "show";
    }
    }

    链接:https://juejin.cn/post/7035408728509644814
收起阅读 »

iOS小技能: 图片的平铺和拉伸、图片的加载方式、内容模式

iOS
iOS小技能: 图片的平铺和拉伸、图片的加载方式、内容模式这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战。引言例子:按照比例显示图片全部内容,并自动适应高度I 图片的平铺和拉伸 #import "UIImage+ResizableI...
继续阅读 »

iOS小技能: 图片的平铺和拉伸、图片的加载方式、内容模式

这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

引言

例子:按照比例显示图片全部内容,并自动适应高度

I 图片的平铺和拉伸


#import "UIImage+ResizableImage.h"

@implementation UIImage (ResizableImage)


+ (UIImage*)resizableImageWithName:(NSString *)name {
NSLog(@"%s--%@",__func__,name);
UIImage *image = [UIImage imageNamed:name];
//裁剪图片方式一:
//Creates and returns a new image object with the specified cap values.
/*right cap is calculated as width - leftCapWidth - 1
bottom cap is calculated as height - topCapWidth - 1
*/

return [image stretchableImageWithLeftCapWidth:image.size.width*0.5 topCapHeight:image.size.height*0.5];
//方式二:
// CGFloat top = image.size.width*0.5f-1;
// CGFloat left = image.size.height*0.5f-1;
// UIEdgeInsets insets = UIEdgeInsetsMake(top, left, top, left);
// UIImage *capImage = [image resizableImageWithCapInsets:insets resizingMode:UIImageResizingModeTile];
//
}




/**
CGFloat top = 0; // 顶端盖高度
CGFloat bottom = 0 ; // 底端盖高度
CGFloat left = 0; // 左端盖宽度
CGFloat right = 0; // 右端盖宽度

// UIImageResizingModeStretch:拉伸模式,通过拉伸UIEdgeInsets指定的矩形区域来填充图片
// UIImageResizingModeTile:平铺模式,通过重复显示UIEdgeInsets指定的矩形区域来填充图片


@param img <#img description#>
@param top <#top description#>
@param left <#left description#>
@param bottom <#bottom description#>
@param right <#right description#>
@return <#return value description#>
*/

- (UIImage *) resizeImage:(UIImage *) img WithTop:(CGFloat) top WithLeft:(CGFloat) left WithBottom:(CGFloat) bottom WithRight:(CGFloat) right
{
UIImage * resizeImg = [img resizableImageWithCapInsets:UIEdgeInsetsMake(self.size.height * top, self.size.width * left, self.size.height * bottom, self.size.width * right) resizingMode:UIImageResizingModeStretch];

return resizeImg;
}



//返回一个可拉伸的图片
- (UIImage *)resizeWithImageName:(NSString *)name
{
UIImage *normal = [UIImage imageNamed:name];

// CGFloat w = normal.size.width * 0.5f ;
// CGFloat h = normal.size.height *0.5f ;

CGFloat w = normal.size.width*0.8;
CGFloat h = normal.size.height*0.8;
//传入上下左右不需要拉升的编剧,只拉伸中间部分
return [normal resizableImageWithCapInsets:UIEdgeInsetsMake(h, w, h, w)];

// [normal resizableImageWithCapInsets:UIEdgeInsetsMake(<#CGFloat top#>, <#CGFloat left#>, <#CGFloat bottom#>, <#CGFloat right#>)]

// 1 = width - leftCapWidth - right
// 1 = height - topCapHeight - bottom

//传入上下左右不需要拉升的编剧,只拉伸中间部分,并且传入模式(平铺/拉伸)
// [normal :<#(UIEdgeInsets)#> resizingMode:<#(UIImageResizingMode)#>]

//只用传入左边和顶部不需要拉伸的位置,系统会算出右边和底部不需要拉升的位置。并且中间有1X1的点用于拉伸或者平铺
// 1 = width - leftCapWidth - right
// 1 = height - topCapHeight - bottom
// return [normal stretchableImageWithLeftCapWidth:w topCapHeight:h];
}




@end


II 图片的加载方式

优先选择3x图像,而不是2x图像时使用initWithContentsOfFile

 NSString *path = [[NSBundle mainBundle] pathForResource:@"smallcat" ofType:@"png"];
UIImage *image = [[UIImage alloc]initWithContentsOfFile:path];
// 在ipone5 s、iphone6和iphone6 plus都是优先加载@3x的图片,如果没有@3x的图片,就优先加载@2x的图片



  • 优先加载@2x的图片
  • [UIImage imageNamed:@"smallcat"]

iphone5s和iphone6优先加载@2x的图片,iphone6 plus是加载@3x的图片。

加载图片注意点:如果图片比较小,并且使用非常频繁,可以使用imageName:(eg icon),如果图片比较大,并且使用比较少,可以使用imageWithContentsOfFile:(eg 引导页 相册)。 imageName:

  • 1、当对象销毁的时候,图片占用的内存不会随着一起销毁,内存由系统来管理,程序员不可控制
  • 2、加载的图片,占用的内存非常大
  • 3、相同的图片不会被重复加载到内存

imageWithContentsOfFile:

  • 1、当对象销毁的时候,图片占用的内存会随着一起销毁
  • 2、加载的图片占用的内存较小

3、相同的图片如果被多次加载就会占据多个内存空间

III 内容模式

首先了解下图片的内容模式

3.1 内容模式

  • UIViewContentModeScaleToFill

拉伸图片至填充整个UIImageView,图片的显示尺寸会和imageVew的尺寸一样 。

This will scale the image inside the image view to fill the entire boundaries of the image view.

  • UIViewContentModeScaleAspectFit

图片的显示尺寸不能超过imageView尺寸大小

This will make sure the image inside the image view will have the right aspect ratio and fits inside the image view’s boundaries.

  • UIViewContentModeScaleAspectFill

按照图片的原来宽高比进行缩放(展示图片最中间的内容),配合使用 tmpView.layer.masksToBounds = YES;

This will makes sure the image inside the image view will have the right aspect ratio and fills the entire boundaries of the image view. For this value to work properly, make sure that you have set the clipsToBounds property of the image view to YES.

  • UIViewContentModeScaleToFill : 直接拉伸图片至填充整个imageView

划重点:

  1. UIViewContentModeScaleAspectFit : 按照图片的原来宽高比进行缩放(一定要看到整张图片)

使用场景:信用卡图片的展示

在这里插入图片描述

  1. UIViewContentModeScaleAspectFill : 按照图片的原来宽高比进行缩放(只能图片最中间的内容)

引导页通常采用UIViewContentModeScaleAspectFill


// 内容模式
self.contentMode = UIViewContentModeScaleAspectFill;
// 超出边框的内容都剪掉
self.clipsToBounds = YES;




3.2 例子:商品详情页的实现

  • [商品详情页(按照图片原宽高比例显示图片全部内容,并自动适应高度)

](kunnan.blog.csdn.net/article/det…)

  • 背景图片的拉伸(中间空白的小矩形设置为可拉伸,保证有形状的地方不进行缩放)
- (void)awakeFromNib
{
[super awakeFromNib];
// 拉伸
// self.backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"bg_dealcell"]];
// 平铺
// self.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"bg_dealcell"]];


[self setAutoresizingMask:UIViewAutoresizingNone];



}




- (void)drawRect:(CGRect)rect
{
// 平铺
// [[UIImage imageNamed:@"bg_dealcell"] drawAsPatternInRect:rect];
// 拉伸
[[UIImage imageNamed:@"bg_dealcell"] drawInRect:rect];
}



背景图片的拉伸(中间空白的小矩形设置为可拉伸,保证有形状的地方不进行缩放)
UIImage *resizableImage = [image resizableImageWithCapInsets:UIEdgeInsetsMake(heightForLeftORRight, widthForTopORBottom, heightForLeftORRight, widthForTopORBottom)];

see also

只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向

链接:https://juejin.cn/post/7035421386168336398

收起阅读 »

美团外卖iOS多端复用的推动、支撑与思考

iOS
前言美团外卖2013年11月开始起步,随后高速发展,不断刷新多项行业记录。截止至2018年5月19日,日订单量峰值已超过2000万,是全球规模最大的外卖平台。业务的快速发展对技术支撑提出了更高的要求:为线上用户提供高稳定的服务体验,保障全链路业务和系统高可用运...
继续阅读 »



前言

美团外卖2013年11月开始起步,随后高速发展,不断刷新多项行业记录。截止至2018年5月19日,日订单量峰值已超过2000万,是全球规模最大的外卖平台。业务的快速发展对技术支撑提出了更高的要求:为线上用户提供高稳定的服务体验,保障全链路业务和系统高可用运行的同时,要提升多入口业务的研发速度,推进App系统架构的合理演化,进一步提升跨部门跨地域团队之间的协作效率。

而另一方面随着用户数与订单数的高速增长,美团外卖逐渐有了流量平台的特征,兄弟业务纷纷尝试接入美团外卖进行推广和发布,期望提供统一标准化服务平台。因此,基础能力标准化,推进多端复用,同时输出成熟稳定的技术服务平台,一直是我们技术团队追求的核心目标。

多端复用的端

这里的“端”有两层意思:

  • 其一是相同业务的多入口

美团外卖在iOS下的业务入口有三个,『美团外卖』App、『美团』App的外卖频道、『大众点评』App的外卖频道。

值得一提的是:由于用户画像与产品策略差异,『大众点评』外卖频道与『美团』外卖频道和『美团外卖』虽经历技术栈融合,但业务形态区别较大,暂不考虑上层业务的复用,故这篇文章主要介绍美团系两大入口的复用。

在2015年外卖C端合并之前,美团系的两大入口由两个不同的团队研发,虽然用户感知的交互界面几乎相同,但功能实现层面的代码风格和技术栈都存在较大差异,同一需求需要在两端重复开发显然不合理。所以,我们的目标是相同功能,只需要写一次代码,做一次估时,其他端只需做少量的适配工作。

  • 其二是指平台上各个业务线

外卖不同兄弟业务线都依赖外卖基础业务,包括但不限于:地图定位、登录绑定、网络通道、异常处理、工具UI等。考虑到标准化的范畴,这些基础能力也是需要多端复用的。

img

图1 美团外卖的多端复用的目标

关于组件化

提到多端复用,不免与组件化产生联系,可以说组件化是多端复用的必要条件之一。大多数公司口中的“组件化”仅仅做到代码分库,使用Cocoapods的Podfile来管理,再在主工程把各个子库的版本号聚合起来。但是能设计一套合理的分层架构,理清依赖关系,并有一整套工具链支撑组件发版与集成的相对较少。否则组件化只会导致包体积增大,开发效率变慢,依赖关系复杂等副作用。

整体思路

A. 多端复用概念图

img

图2 多端复用概念图

多端复用的目标形态其实很好理解,就是将原有主工程中的代码抽出独立组件(Pods),然后各自工程使用Podfile依赖所需的独立组件,独立组件再通过podspec间接依赖其他独立组件。

B. 准备工作

确认多端所依赖的基层库是一致的,这里的基层库包括开源库与公司内的技术栈。

iOS中常用开源库(网络、图片、布局)每个功能基本都有一个库业界垄断,这一点是iOS相对于Android的优势。公司内也存在一些对开源库二次开发或自行研发的基础库,即技术栈。不同的大组之间技术栈可能存在一定差异。如需要复用的端之间存在差异,则需要重构使得技术栈统一。(这里建议重构,不建议适配,因为如果做的不够彻底,后续很大可能需要填坑。)

就美团而言,美团平台与点评平台作为公司两大App,历史积淀厚重。自2015年底合并以来,为了共建和沉淀公共服务,减少重复造轮子,提升研发效率,对上层业务方提供统一标准的高稳定基础能力,两大平台的底层技术栈也在不断融合。而美团外卖作为较早实践独立App,同时也是依托于两大平台App的大业务方,在外卖C端合并后的1年内,我们也做了大量底层技术栈统一的必要工作。

C. 方案选型

在演进式设计与计划式设计中的抉择。

演进式设计指随着系统的开发而做设计变更,而计划式设计是指在开发之前完全指定系统架构的设计。演进的设计,同样需要遵循架构设计的基本准则,它与计划的设计唯一的区别是设计的目标。演进的设计提倡满足客户现有的需求;而计划的设计则需要考虑未来的功能扩展。演进的设计推崇尽快地实现,追求快速确定解决方案,快速编码以及快速实现;而计划的设计则需要考虑计划的周密性,架构的完整性并保证开发过程的有条不紊。

美团外卖iOS客户端,在多端复用的立项初期面临着多个关键点:频道入口与独立应用的复用,外卖平台的搭建,兄弟业务的接入,点评外卖的协作,以及架构迁移不影响现有业务的开发等等,因此权衡后我们使用“演进式架构为主,计划式架构为辅”的设计方案。不强求历史代码一下达到终极完美架构,而是循序渐进一步一个脚印,满足现有需求的同时并保留一定的扩展性。

演进式架构推动复用

术语解释

  • Waimai:特指『美团外卖』App,泛指那些独立App形式的业务入口,一般为project。

  • Channel:特指『美团』App中的外卖频道,泛指那些以频道或者Tab形式集成在主App内的业务入口,一般为Pods。

  • Special:指将Waimai中的业务代码与原有工程分离出来,让业务代码成为一个Pods的形态。

  • 下沉:即下沉到下层,这里的“下层”指架构的基层,一般为平台层或通用层。“下沉”指将不同上层库中的代码统一并移动到下层的基层库中。

在这里先贴出动态的架构演进过程,让大家有一个宏观的概念,后续再对不同节点的经历做进一步描述。

图3 演进式架构动态图

原始复用架构

如图4所示,在过去一两年,因为技术栈等原因我们只能采用比较保守的代码复用方案。将独立业务或工具类代码沉淀为一个个“Kit”,也就是粒度较小的组件。此时分层的概念还比较模糊,并且以往的工程因历史包袱导致耦合严重、逻辑复杂,在将UGC业务剥离后发现其他的业务代码无法轻易的抽出。(此时的代码复用率只有2.4%。)

鉴于之前的准备工作已经完成,多端基础库已经一致,于是我们不再采取保守策略,丰富了一些组件化通信、解耦与过渡的手段,在分层架构上开始发力。

img

图4 原始复用架构

业务复用探索

在技术栈已统一,基础层已对齐的背景下,我们挑选外卖核心业务之一的Store(即商家容器)开始了在业务复用上的探索。如图5所示,大致可以理解为“二合一,一分三”的思路,我们从代码风格和开发思路上对两边的Store业务进行对齐,在此过程中顺势将业务类与技术(功能)类的代码分离,一些通用Domain也随之分离。随着一个个组件的拆分,我们的整体复用度有明显提升,但开发效率却意外的受到了影响。多库开发在版本的发布与集成中增加了很多人工操作:依赖冲突、lock文件冲突等问题都阻碍了我们的开发效率进一步提升,而这就是之前“关于组件化”中提到的副作用。

于是我们将自动发版与自动集成提上了日程。自动集成是将“组件开发完毕到功能合入工程主体打出测试包”之间的一系列操作自动化完成。在这之前必须完成一些前期铺垫工作——壳工程分离。img

图5 商家容器下沉时期

壳工程分离

如图6所示,壳工程顾名思义就是将原来的project中的代码全部拆出去,得到一个空壳,仅仅保留一些工程配置选项和依赖库管理文件。

为什么说壳工程是自动集成的必要条件之一?

因为自动集成涉及版本号自增,需要机器修改工程配置类文件。如果在创建二进制的过程中有新业务PR合入,会造成commit树分叉大概率产生冲突导致集成失败。抽出壳工程之后,我们的壳只关心配置选项修改(很少),与依赖版本号的变化。业务代码的正常PR流程转移到了各自的业务组件git中,以此来杜绝人工与机器的冲突。

img

图6 壳工程分离

壳工程分离的意义主要有如下几点:

  • 让职能更加明确,之前的综合层身兼数职过于繁重。

  • 为自动集成铺路,避免业务PR与机器冲突。

  • 提升效率,后续Pods往Pods移动代码比proj往Pods移动代码更快。

  • 『美团外卖』向『美团』开发环境靠齐,降低适配成本。

img

图7 壳工程分离阶段图

图7的第一张图到第二张图就是上文提到的壳工程分离,将“Waimai”所有的业务代码打包抽出,移动到过渡仓库Special,让原先的“Waimai”成为壳。

第二张图到第三张图是Pods库的内部消化。

前一阶段相当于简单粗暴的物理代码移动,后一阶段是对Pods内整块代码的梳理与分库。

内部消化对齐

在前文“多端复用概念图”的部分我们提到过,所谓的复用是让多端的project以Pods的方式接入统一的代码。我们兼容考虑保留一端代码完整性,降低回接成本,决定分Subpods使用阶段性合入达到平滑迁移。

img

图8 代码下沉方案

图8描述了多端相同模块内的代码具体是如何统一的。此时因为已经完成了壳工程分离,所以业务代码都在“Special”这样的过渡仓库中。

“Special”和“Channel”两端的模块统一大致可分为三步:平移 → 下沉 → 回接。(前提是此模块的业务上已经确定是完全一致。)

平移阶段是保留其中一端“Special”代码的完整性,以自上而下的平移方式将代码文件拷贝到另一端“Channel”中。此时前者不受任何影响,后者的代码因为新文件拷贝和原有代码存在重复。此时将旧文件重命名,并深度优先遍历新文件的依赖关系补齐文件,最终使得编译通过。然后将旧文件中的部分差异代码加到新文件中做好一定的差异化管理,最后删除旧文件。

下沉阶段是将“Channel”处理后的代码解耦并独立出来,移动到下层的Pods或下层的SubPods。此时这里的代码是既支持“Special”也支持“Channel”的。

回接阶段是让“Special”以Pods依赖的形式引用之前下沉的模块,引用后删除平移前的代码文件。(如果是在版本的间隙完成固然最好,否则需要考虑平移前的代码文件在这段时间的diff。)

实际操作中很难在有限时间内处理完一个完整的模块(例如订单模块)下沉到Pods再回接。于是选择将大模块分成一个个子模块,这些子模块平滑的下沉到SubPods,然后“Special”也只引用这个统一后的SubPods,待一个模块完全下沉完毕再拆出独立的Pods。

再总结下大量代码下沉时如何保证风险可控:

  • 联合PM,先进行业务梳理,特殊差异要标注出来。

  • 使用OClint的提前扫描依赖,做到心中有数,精准估时。

  • 以“Special”的代码风格为基准,“Channel”在对齐时仅做加法不做减法。

  • “Channel”对齐工作不影响“Special”,并且回接时工作量很小。

  • 分迭代包,QA资源提前协调。

中间件层级压平

经过前面的“内部消化”,Channel和Special中的过渡代码逐渐被分发到合适的组件,如图9所示,Special只剩下AppOnly,Channel也只剩下ChannelOnly。于是Special消亡,Channel变成打包工程。

AppOnly和ChannelOnly 与其他业务组件层级压平。上层只留下两个打包工程。

img

图9 中间件层级压平

平台层建设

如图10所示,下层是外卖基础库,WaimaiKit包含众多细分后的平台能力,Domain为通用模型,XunfeiKit为对智能语音二次开发,CTKit为对CoreText渲染框架的二次开发。

针对平台适配层而言,在差异化收敛与依赖关系梳理方面发挥重要角色,这两点在下问的“衍生问题解决中”会有详细解释。

外卖基础库加上平台适配层,整体构成了我们的外卖平台层(这是逻辑结构不是物理结构),提供了60余项通用能力,支持无差异调用。

img

图10 外卖平台层的建设

多端通用架构

此时我们把基层组件与开源组件梳理并补充上,达到多端通用架构,到这里可以说真正达到了多端复用的目标。

img

图11 多端通用架构完成

由上层不同的打包工程来控制实际需要的组件。除去两个打包工程和两个Only组件,下面的组件都已达到多端复用。对比下“Waimai”与“Channel”的业务架构图中两个黑色圆圈的部分。

img

图12 “Waimai”的业务架构

img

图13 “Channel”的业务架构

衍生问题解决

差异问题

A.需求本身的差异

三种解决策略:

  • 对于文案、数值、等一两行代码的差异我们使用 运行时宏(动态获取proj-identifier)或预编译宏(custome define)直接在方法中进行if else判断。

  • 对于方法实现的不同 使用Glue(胶水层),protocol提供相同的方法声明,用来给外部调用,在不同的载体中写不同的方法实现。

  • 对于较大差异例如两边WebView容器不一样,我们建多个文件采用文件级预编译,可预编译常规.m文件或者Category。(例如WMWebViewManeger_wm.m&WMWebViewManeger_mt.m、UITableView+WMEstimated.m&UITableView+MTEstimated.m)

进一步优化策略:

用上述三种策略虽然完成差异化管理,但差异代码散落在不同组件内难以收敛,不便于管理。有了平台适配层之后,我们将差异化判断收敛到适配层内部,对上层提供无差异调用。组件开发者在开发中不用考虑宿主差异,直接调用用通用接口。差异的判断或者后续优化在接口内部处理外部不感知。

图14给出了一个平台适配层提供通用接口修改后的例子。

img

图14 平台适配层接口示例

B.多端节奏差异

实际场景中除了需求的差异还有可能出现多端进版节奏的差异,这类差异问题我们使用分支管理模型解决。

前提条件既然要多端复用了,那需求的大方向还是会希望多端统一。一般较多的场景是:多端中A端功能最少,B端功能基本算是是A端的超集。(没有绝对的超集,A端也会有较少的差异点。)在外卖的业务中,“Channel”就是这个功能较少的一端,“Waimai”基本是“Channel”的超集。

两端的差异大致分为了这5大类9小类:

  1. 需求两端相同(1.1、提测上线时间基本相同;1.2、“Waimai”比“Channel”早3天提测 ;1.3、“Waimai”比“Channel”晚3天提测)。

  2. 需求“Waimai”先进版,“Channel”下一版进 (2.1、频道下一版就上;2.2、频道下两版本后再上)。

  3. 需求“Waimai”先进版,“Channel”不需要。

  4. 需求“Channel”先进版,“Waimai”下一版进(4.1、需要改动通用部分;4.2、只改动“ChannelOnly”的部分)。

  5. 需求“Channel”先进版,“Waimai”不需要(只改动“ChannelOnly”的部分)。

img

图15 最复杂场景下的分支模型

也不用过多纠结,图15是最复杂的场景,实际场合中很难遇到,目前的我们的业务只遇到1和2两个大类,最多2条线。

编译问题

以往的开发方式初次全量编译5分钟左右,之后就是差量编译很快。但是抽成组件后,随着部分子库版本的切换间接的增加了pod install的次数,此时高频率的3分钟、5分钟会让人难以接受。

于是在这个节点我们采用了全二进制依赖的方式,目标是在日常开发中直接引用编译后的产物减少编译时间。

img

图16 使用二进制的依赖方式

如图所示三个.a就是三个subPods,分了三种Configuration:

  1. debug/ 下是 deubg 设置编译的 x64 armv7 arm64。

  2. release/ 下是 release 设置编译的 armv7 arm64。

  3. dailybuild/ 下是 release + TEST=1编译的 armv7 arm64。

  4. 默认(在文件夹外的.a)是 debug x64 + release armv7 + release arm64。

这里有一个问题需要解决,即引用二进制带来的弊端,显而易见的就是将编译期的问题带到了运行期。某个宏修改了,但是编译完的二进制代码不感知这种改动,并且依赖版本不匹配的话,原本的方法缺失编译错误,就会带到运行期发生崩溃。解决此类问题的方法也很简单,就是在所有的打包工程中都配置了打包自动切换源码。二进制仅仅用来在开发中获得更高的效率,一旦打提测包或者发布包都会使用全源码重新编译一遍。关于切源码与切二进制是由环境变量控制拉取不同的podspec源。

并且在开发中我们支持源码与二进制的混合开发模式,我们给某个binary_pod修饰的依赖库加上标签,或者使用.patch文件,控制特定的库拉源码。一般情况下,开发者将与自己当前需求相关联的库拉源码便于Debug,不关联的库拉二进制跳过编译。

依赖问题

如图17所示,外卖有多个业务组件,公司也有很多基础Kit,不同业务组件或多或少会依赖几个Kit,所以极易形成网状依赖的局面。而且依赖的版本号可能不一致,易出现依赖冲突,一旦遇到依赖冲突需要对某一组件进行修改再重新发版来解决,很影响效率。解决方式是使用平台适配层来统一维护一套依赖库版本号,上层业务组件仅仅关心平台适配层的版本。

img

图17 平台适配层统一维护依赖

当然为了避免引入平台适配层而增加过多无用依赖的问题,我们将一些依赖较多且使用频度不高的Kit抽出subPods,支持可选的方式引入,例如IM组件。

再者就是pod install 时依赖分析慢的问题。对于壳工程而言,这是所有依赖库汇聚的地方,依赖关系写法若不科学极易在analyzing dependency中耗费大量时间。Cocoapods的依赖分析用的是Molinillo算法,链接中介绍了这个算法的实现方式,是一个具有前向检察的回溯算法。这个算法本身是没有问题的,依赖层级深只要依赖写的合理也可以达到秒开。但是如果对依赖树叶子节点的版本号控制不够严密,或中间出现了循环依赖的情况,会导致回溯算法重复执行了很多压栈和出栈操作耗费时间。美团针对此类问题的做法是维护一套“去依赖的podspec源”,这个源中的dependency节点被清空了(下图中间)。实际的所需依赖的全集在壳工程Podfile里平铺,统一维护。这么做的好处是将之前的树状依赖(下图左)压平成一层(下图右)。

img

图18 依赖数的压平

效率问题

前面我们提到了自动集成,这里展示下具体的使用方式。美团发布工程组自行研发了一套HyperLoop发版集成平台。当某个组件在创建二进制之前可自行选择集成的目标,如果多端复用了,那只需要在发版创建二进制的同时勾选多个集成的目标。发版后会自行进行一系列检查与测试,最终将代码合入主工程(修改对应壳工程的依赖版本号)。

img

图19 HyperLoop自动发版自动集成

img

图20 主工程commit message的变化

以上是“Waimai”的commit对比图。第一张图是以往的开发方式,能看出工程配置的commit与业务的commit交错堆砌。第二张图是进行壳工程分离后的commit,能看出每条message都是改了某个依赖库的版本号。第三张图是使用自动集成后的commit,能看出每条message都是画风统一且机器串行提交的。

这里又衍生出另一个问题,当我们用壳工程引Pods的方式替代了project集中式开发之后,我们的代码修改散落到了不同的组件库内。想看下主工程6.5.0版本和6.4.0版本的diff时只能看到所有依赖库版本号的diff,想看commit和code diff时必须挨个去组件库查看,在三轮提测期间这样类似的操作每天都会重复多次,很不效率。

于是我们开发了atomic diff的工具,主要原理是调git stash的接口得到版本号diff,再通过版本号和对应的仓库地址深度遍历commit,再深度遍历commit对应的文件,最后汇总,得到整体的代码diff。

img

图21 atomic diff汇总后的commit message

整套工具链对多端复用的支撑

上文中已经提到了一些自动化工具,这里整理下我们工具链的全景图。

img

图22 整套工具链

  1. 在准备阶段,我们会用OClint工具对compile_command.json文件进行处理,对将要修改的组件提前扫描依赖。

  2. 在依赖库拉取时,我们有binary_pod.rb脚本里通过对源的控制达到二进制与去依赖的效果,美团发布工程组维护了一套ios-re-sankuai.com的源用于存储remove dependency的podspec.json文件。

  3. 在依赖同步时,会通过sync_podfile定时同步主工程最新Podfile文件,来对依赖库全集的版本号进行维护。

  4. 在开发阶段,我们使用Podfile.patch工具一键对二进制/源码、远端/本地代码进行切换。

  5. 在引用本地代码开发时,子库的版本号我们不太关心,只关心主工程的版本号,我们使用beforePod和AfterPod脚本进行依赖过滤以防止依赖冲突。

  6. 在代码提交时,我们使用git squash对多条相同message的commit进行挤压。

  7. 在创建PR时,以往需要一些网页端手动操作,填写大量Reviewers,现在我们使用MTPR工具一键完成,或者根据个人喜好使用Chrome插件。

  8. 在功能合入master之前,会有一些jenkins的job进行检测。

  9. 在发版阶段,使用Hyperloop系统,一键发版操作简便。

  10. 在发版之后,可选择自动集成和联合集成的方式来打包,打包产物会自动上传到美团的“抢鲜”内测平台。

  11. 在问题跟踪时,如果需要查看主工程各个版本号间的commit message和code diff,我们有atomic diff工具深度遍历各个仓库并汇总结果。

感想总结

  • 多端复用之后对PM-RD-QA都有较大的变化,我们代码复用率由最初的2.4%*达到了*84.1%,让更多的PM投入到了新需求的吞吐中,但研发效率提升增大了QA的工作量。一个大的尝试需要RD不断与PM和QA保持沟通,选择三方都能接受的最优方案。

  • 分清主次关系,技术架构等最终是为了支撑业务,如果一个架构设计的美如画天衣无缝,但是落实到自己的业务中确不能发挥理想效果,或引来抱怨一片,那这就是个失败的设计。并且在实际开发中技术类代码修改尽量选择版本间隙合入,如果与业务开发的同学产生冲突时,都要给业务同学让路,不能影响原本的版本迭代速度。

  • 时刻对 “不合理” 和 “重复劳动”保持敏感。新增一个埋点常量要去改一下平台再发个版是否成本太大?一处订单状态的需求为什么要修改首页的Kit?实际开发中遇到别扭的地方多增加一些思考而不是硬着头皮过去,并且手动重复两次以上的操作就要思考有没有自动化的替代方案。

  • 一旦决定要做,在一些关键节点决不能手软。例如某个节点为了不Block别人,加班不可避免。在大量代码改动时也不用过于紧张,有提前预估,有Case自测,还有QA的三轮回归来保障,保持专注,放手去做就好。

作者简介

尚先,美团资深工程师。2015年加入美团,目前作为美团外卖iOS端平台化虚拟小组组长,主要负责业务架构、持续集成和工程化相关工作,致力于提升研发效率与协作效率。


作者:美团技术团队
来源:https://juejin.cn/post/6844903629753679886

收起阅读 »

iOS集成

IM 和 客服 并存开发指南—iOS篇 ...
继续阅读 »




IM 和 客服 并存开发指南—iOS篇











 如果觉得哪里描述的不清晰,可评论内指出,会不定期更新。


 一、SDK 介绍

      HelpDesk.framework 为 客服SDK(带实时音视频)

      HelpDeskLite.framework 为 客服SDK(不带实时音视频)

      Hyphenate.framework 为 IM SDK(带实时音视频)

      HyphenateLite.framework 为 IM SDK(不带实时音视频)

      环信客服SDK 基于 IM SDK 3.x , 如果同时集成 客服 和 IM,只需要在初始化、登录、登出操作时使用客服SDK 提供的相应API,IM 的其他API均不受影响。

      UI 部分集成需要分别导入 HelpDeskUI 和 IM demo 中的UI文件(也可以自定义UI)。 下面详细介绍IM 和 客服共存的开发步骤。

二、注意事项

      1、开发过程中,初始化、登录和登出,务必只使用客服访客端SDK的API。

      2、需要联系商务开通客服长连接。

           不开通长连接,会出现用户长时间(一天或几天)不使用app,再打开app会无法正常使用im相关功能的问题,报错信息一般是User is not login。

      3、IM SDK 和客服SDK 都包括了模拟器的CPU 架构,在上传到app store时需要剔除模拟器的CPU 架构,保留  armv7、arm64,参考文档:上传appstore以及打包ipa注意事项。 

三、资源准备

      到环信官网下载客服访客端的开源的商城Demo源码 + SDK,下载链接:http://www.easemob.com/download/cs  选  择“iOS SDK”下载(如下图)。

      

下载客服.png



      到环信官网下载IM的开源的Demo源码 + SDK ,下载链接:http://www.easemob.com/download/im 选择 iOS SDK(如下图)。

      

下载IM.png




下载的 IM SDK+Demo 和 客服SDK+Demo 中都有 IM 的
Hyphenate.framework 或 HyphenateLite.framework,为了保持版本的匹配,我们只使用 IM Demo 中的
UI, 而不使用 IM SDK 中 的 Hyphenate.framework 或 HyphenateLite.framework 文件。

四、集成步骤

      1、阅读客服访客端SDK集成文档,集成客服,地址:http://docs.easemob.com/cs/300visitoraccess/iossdk。 

      2、阅读 IM 的集成文档,地址:http://docs-im.easemob.com/im/ios/sdk/prepare 

      3、将 IM Demo 中的 UI 文件按照自己的需求分模块导入到工程中

      4、将 IM 的 UI 所依赖的第三方库集成到项目中(IM集成文档内有说明)

      5、在pch文件中引入 EMHeaders.h 

          #ifdef __OBJC__ 

            //包含实时音视频功能 

            #import  

            // 若不包含实时音视频,则替换为 

            // #import  

            #import "HelpDeskUI.h" 

            #import "EMHeaders.h" 

         #endif

      6、由于HelpDeskUI 和 IM UI 中都使用了 第三方库,如果工程中出现三方库重复的问题,可将重复文件删除,如果部分接口已经升级或弃用可自行升级、调整。

提供的兼容Demo介绍:

     1、Demo集成了初始化sdk、登录、退出登录、IM单聊、联系客服的简单功能,处理了第三方库冲突的问题。

     2、pch文件中的appkey等信息需要换成开发者自己的。

     3、Demo源码下载地址: https://pan.baidu.com/s/1v1TUl-fqJNLQrtsJfWYGzw 

         提取码: kukb 
收起阅读 »

ios客服云集成常见报错

注意:向自己工程中添加环信SDK和UI文件的时候,不要直接向xcode中拖拽添加,先把SDK和UI文件粘贴到自己工程的finder目录中,再从finder中向xcode中拖拽添加,避免出现找不到SDK或者UI文件的情况。   1、很多同学在首次“导入...
继续阅读 »
注意:向自己工程中添加环信SDK和UI文件的时候,不要直接向xcode中拖拽添加,先把SDK和UI文件粘贴到自己工程的finder目录中,再从finder中向xcode中拖拽添加,避免出现找不到SDK或者UI文件的情况。

 

1、很多同学在首次“导入SDK”或“更新SDK重新导入SDK”后,Xcode运行报以下的error:

dyld: Library not loaded: @rpath/Hyphenate.framework/Hyphenate

  Referenced from:
/Users/shenchong/Library/Developer/CoreSimulator/Devices/C768FE68-6E79-40C8-8AD1-FFFC434D51A9/data/Containers/Bundle/Application/41EA9A48-4DD5-4AA4-AB3F-139CFE036532/CallBackTest.app/CallBackTest

  Reason: image not found

       这个原因是工程未加载到 framework,正确的处理方式是在TARGETS → General → Embedded
Binaries 中添加HelpDesk.framework和Hyphenate.framework依赖库,且 Linked
Frameworks and Libraries中依赖库的Status必须是Required。

1访客端_image_not_found.png



 

2、运行之后,自变量为nil,这就有可能是因为上面所说的依赖库的status设置为了Optional,需要改成Required。

2访客端自变量为nil.png



 

3、打包后上传到appstore报错

(1)ERROR ITMS-90535: "Unexpected CFBundleExecutable Key. The bundle at
'Payload/toy.app/HelpDeskUIResource.bundle' does not contain a bundle
executable. If this bundle intentionally does not contain an executable,
consider removing the CFBundleExecutable key from its Info.plist and
using a CFBundlePackageType of BNDL. If this bundle is part of a
third-party framework, consider contacting the developer of the
framework for an update to address this issue."

方法:把HelpDeskUIResource.bundle里的Info.plist删掉就即可。

3访客端打包90535.png



(2)This bundle is invalid. The value for key CFBundleShortVersionString
‘1.2.2.1’in the Info.plist must be a period-separated list of at most
three non-negative integers. 

4访客端打包90060.png



把sdk里的plist文件的版本号改成3位数即可

5访客端打包1.2_.2_.1位置_.png



(3)Invalid Mach-O Format.The Mach-O in bundle
“SMYG.app/Frameworks/Hyphenate.framework” isn’t consistent with the
Mach-O in the main bundle.The main bundle Mach-O contains armv7(bitcode)
and arm64(bitcode),while the nested bundle Mach-O contains
armv7(machine code) and arm64(machine code).Verify that all of the
targets for a platform have a consistent value for the ENABLE_BITCODE
build setting.”

6访客端打包90636.png



将TARGETS-Build Settings-Enable Bitcode改为NO

7访客端打包bitcode改为NO.png



(4)还有很多同学打包失败,看不出什么原因

8访客端打包需剔除.png



那么可以先看看有没有按照文档剔除x86_64 i386两个平台

文档链接:http://docs.easemob.com/cs/300visitoraccess/iossdk#%E4%B8%8A%E4%BC%A0appstore%E4%BB%A5%E5%8F%8A%E6%89%93%E5%8C%85ipa%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9

 

4、那么剔除x86_64 i386时会遇到can't open input file的错误,这是因为cd的路径错误,把“/HelpDesk.framework”删掉。是cd到framework所在的路径,不是cd到framework

9访客端剔除cd错误.png



 

5、下图中的报错,需要创建一个pch文件,并且在pch文件添加如下判断,将环信的和自己的头文件都引入到#ifdef内部,参考文档:iOS访客端sdk集成准备工作

   #ifdef __OBJC__

   #endif

(swift项目也需这样操作)

10pch加判断1.png



11pch加判断2.png



pch加判断3.png




6、集成环信HelpDeskUI的时候,由于HelpDeskUI内部使用了第三方库,如果与开发者第三方库产生冲突,可将HelpDeskUI中冲突的第三方库删除,如果第三方库中的接口有升级的部分,请酌情进行升级。

12第三方库冲突.png



 

7、集成1.2.2版本demo中的HelpDeskUI,Masonry报错:Passing ‘CGFloat’(aka ‘double’) to parameter of incompatible type ‘__strong id’

需要在pch中添加#define MAS_SHORTHAND_GLOBALS

注意:要在#import "Masonry.h"之前添加此宏定义

13访客端Masonry报错.png



 

8、Xcode11运行demo,PSTCollectionView第三方库会有如下报错

iOS13中PSTCollectionView报错.png



标明下类型就行了,如图

iOS13中PSTCollectionView报错1.png



 

9、Xcode12.3编译报错(Building for iOS, but the linked and embedded framework......)

xcode12.3报错1_.jpg
解决方案:
e1d64313718a467a6bc19b70fadd4543.png
 或者打开xcode,左上方点击File --- Workspace Settings,按照截图修改试下(不建议)

xcode12.3报错2_.jpg
收起阅读 »

ios客服云集成常见问题

1、UI上很多地方显示英文,比如聊天页面的工具栏 把客服demo中配置的国际化文件添加到您自己的工程中。拖之前要打开国际化文件,全部选中这三个,再进行拖入。   2、进入聊天页面没有加载聊天记录 这种情况一般出现在只使用了 HDMessageView...
继续阅读 »




1、UI上很多地方显示英文,比如聊天页面的工具栏

显示英文1.png



把客服demo中配置的国际化文件添加到您自己的工程中。拖之前要打开国际化文件,全部选中这三个,再进行拖入。

显示英文2.png



 

2、进入聊天页面没有加载聊天记录

这种情况一般出现在只使用了 HDMessageViewController 没有使用 HDChatViewController 的时候

在HDMessageViewController 的 viewDidLoad 方法中, 将 [self
tableViewDidTriggerHeaderRefresh]; 的注释打开,再在这句代码之前加上
self.showRefreshHeader = YES; 

 

3、发送表情却显示字符串

访客端表情符号.png



把下面这段代码添加到appdelegate中就可以了

[[HDEmotionEscape sharedInstance] setEaseEmotionEscapePattern:@"\\[[^\\[\\]]{1,3}\\]"];

[[HDEmotionEscape sharedInstance] setEaseEmotionEscapeDictionary:[HDConvertToCommonEmoticonsHelper emotionsDictionary]];

 

4、文本消息,收发双方的布局不一样,如图

文本消息布局错误1.png



参考一下截图修改即可

文本消息布局错误2.png




5、客服能收到访客的消息,访客收不到客服的消息

(1)客服和im同时使用的话,初始化sdk、登录、登出用的是im的api会出现这种情况。必须使用客服的api。

(2)IM sdk升级为客服sdk,不兼容导致的,这种情况可以线上发起会话咨询。

      
6、发送的消息,出现在聊天页面的左侧

一般是由于当前访客没有登录或者登录失败,断点仔细检查下。

7、修改聊天页面导航栏标题
修改_title的值

ff6ff7a40cfea125e0d59e70efb131b8.png









收起阅读 »

接手一个不合格的业务线代码,我是如何去维护以及重构的

iOS
项目背景IM聊天功能作为整个产品业务功能的补充和重要支撑,相信很多的App都会集成这么一个业务功能在,很多App的的IM功能相信都是集成的第三方提供的的SDK服务。相信作为产品业务的有力支撑,IM的消息对于各个公司来说都有不同的业务需求,也就是说普通的图片、文...
继续阅读 »

项目背景

IM聊天功能作为整个产品业务功能的补充和重要支撑,相信很多的App都会集成这么一个业务功能在,很多App的的IM功能相信都是集成的第三方提供的的SDK服务。

相信作为产品业务的有力支撑,IM的消息对于各个公司来说都有不同的业务需求,也就是说普通的图片、文字、红包甚至语音这种常用的消息类型并不能有力支撑起一个IM的业务,今天说的这个的IM业务功能正式在这种背景下。

曾经接手了一个IM模块业务功能,刚开始是起因于解决线上的一个bug,于是开始梳理了一下代码逻辑,于是。。。懵逼了好久好久好久。 虽然IM的代码已经在线上跑了很久了,在我开始解决bug之前貌似有大概有大半年将近一年的的时间少有人来维护,从架构设计上、业务代码实现等点来看,可维护性不高。

梳理代码

这个IM的架构设计大概是在几年以前,长连接协议使用的是WebSocket,业务逻辑有一些复杂,目测从数据逻辑到业务逻辑再到UI逻辑,代码量可能会接近5w这个量级,所以说一开始就一行一行的看代码逻辑显然是不太理智的。

梳理第一步

第一步的主要目的是熟悉代码的主要脉络,于是我开始有序的梳理沿着数据流向梳理主干,要点如下:

  • 分析各个数据模型(model)。
  • 整理各个HTTP请求的API。
  • 整理并备注各个Notification的Key,并标记使用场景。
  • 整理各个Delegate回调函数的使用场景。
  • 整理并备注各个枚举值的含义以及使用场景。

因为此部分项目代码开发周期很久了且开发维护人员换了好几茬之后,代码量大且逻辑比较混乱,在一开始梳理的时候大部分时间都花在了备注各种代码上。

梳理第二步

第一步之后,我其实已经对于IM的架构逻辑开始有了一个初步的比较宽泛的了解。第二步的的主要目的是整理在使用的主要的几个组件:

  • 数据库的初始化创建以及使用逻辑。
  • HTTP代码的初始化创建以及使用逻辑。
  • 长连接代码的初始化创建以及使用逻辑,特别是与服务端沟通和保活的部分。
  • 针对以上基础控件的二次封装控件的整理。

梳理了上面几个之后,我陆续整理了如下:

  • 数据库的表结构设计、初始化创建以及销毁等逻辑。
  • 了解HTTP的接口功能,进一步了解了基础的业务设计。
  • 通过和服务端的同事沟通,明确了在当前的长连接协议下,两端是如何保活、沟通数据以及沟通各种状态的
  • 各个API的使用场景以及使用逻辑。

梳理第三步

上面两步,基本上把最核心的工具整理完成,下面就开始将代码逻辑串起来,整理业务逻辑:

  • 群聊的收发消息逻辑。
  • 私聊的收发消息逻辑。
  • 登录、退出登录、更换账号登录以及绑定账号之后的业务逻辑。
  • 自后台唤醒之后重连以及获取最新消息等的业务逻辑。
  • 收到推送消息之后的业务逻辑。

整理好了以上之后,我利用流程图工具ProcessOn创建了大概有10张左右的流程表,数据流向和业务逻辑一清二楚。

特别是聊天的数据流转逻辑,从HTTP请求、长连接推送以及数据库操作,无所不包。但是图整理完之后,对于主要流程几乎是掌握的比较清楚了,即使回头有忘记的,回头查看表之后就会一清二楚,不仅便于我熟悉逻辑,对以后的维护也很有益处。

维护

遇到的困境

虽然代码质量较差,但是代码中的bug还算比较少,所以在解决线上bug这个问题上,没有遇到过多的麻烦。但是其中也有几个bug十分麻烦,找了好久才找到问题的原因。

其中有一个,找原因大概找了一周多的时间,最后定位问题在我们的账号系统上。因为历史原因,我们的IM业务和其他业务是两套账号体系,中间经历过一次账号变更,但是由于某些奇葩的原因(有部分业务经历了hack),导致部分用户的账号没有成功的过渡账号变更,导致了一些问题。

表象原因是代码逻辑混乱,bug原因复杂,无法复现并难以定位问题。 但更深的原因是时间久远,我们对当时的代码设计以及业务逻辑变更没有记录,且逻辑上存在缺陷,导致无法快速定位到问题的原因。

思考

对于一个逻辑复杂的业务,首先要考虑的是易拓展、易维护和模块组件间的高度解耦。

对于一个成熟的业务来说,我认为易于维护性更为重要,因为业务已经进入成熟阶段的话就意味着大块功能增加的几率比较低,那么对于线上bug修复、小修小补的功能上的优化就是主要工作。

当前的这个IM业务就是这样的,大家看梳理的代码的第一步应该能看出来,我耗费了大量的时间去做各种备注,然而,这些重要节点的备注应该在开发阶段就应该写好了。包括对于之前两套账号系统存在的原因,包括变更一次账号的原因,是否需要有一个详细的记录?

那么对于这种这种有助于后期维护的记录,我认为我们需要对重要的业务变更以及业务设计要有一个详细的记录,无论是自己作为记录还是为后面接手的同学做个参考,我认为都是极为重要的。

重构

思索需求

经过最开始的了解和一段时间维护,我发现遇到的最大麻烦是数据逻辑、业务逻辑、和UI操作逻辑混到了一起,简直可以说是牵一发而动全身。特别是数据逻辑十分混乱,因为数据逻辑的混乱,导致我对于后面业务逻辑的变更十分费力,测试成本也是指数级上涨,另外UI逻辑杂乱,适配iPhone X的时候也遇到了一些小麻烦。

现在的架构设计的原因是什么?这样设计业务需求是否合理?是否有优化的空间?

分析现状与判断未来

无论是客户端还是服务端,亦或是两端数据交互上,IM业务的架构设计本身就存在很多问题。因为时间短暂,不太可能一次性解决所有的问题,特别是对于服务端来说,一次性的大规模重构可能性极低。

对于现在来说:

  • UI重构是首当其冲的, 在保证现有逻辑的基础上,重新设计UI层的逻辑结构,保证代码的复用性和可扩展性,为了将来有可能的业务升级留足空间。
  • 梳理基础组件,比如HTTP、长连接协议和数据库,还有其他的一些工具类,通过封装成组件和组件引用来是他们能从业务逻辑中独立出来。便于独立维护、升级甚至完整替换
  • 将原有的数据操作逻辑从UI逻辑中完整抽离出来,需要达到向下对基础组件要有封装和控制,向上对业务逻辑要有承接,而且依然要做到耦合度尽量的低。
  • 因为IM业务在多个App中都存在,功能逻辑上也有非常多的代码重合,所有需要考虑多个项目通用兼容的问题。

架构设计

  • 工厂模式
  • 瘦Model
  • 去model化的Cell
  • 项目优先,分离核心业务模块组成pod
  • 考虑业务变更可能性,尽可能向上保持API稳定性
  • KVO进行反向传值

着手动工

UI层重构

UI层的重构是最先开始的,无论架构怎么变,UI层都是直接面向用户的,直接承载了产品的业务功能实现,所以为了灵活适应业务的升级或变化,给用户一个好用流畅的入口,UI层设计上要尽可能的灵活。耦合尽可能的小,流畅性上要有保证。

重构思路相对简单,重写View和Controller,去除冗余复杂的UI逻辑代码,规范并统一第三方框架使用,封装公用组件,隔离胶水代码,设计灵活的UI结构。

因为IM系统的最主要UI仍然是TableView,所以针对TableView的各种优化就是重中之重,我着重说一下我对于复杂Cell类型的设计方案。

1、共有的组件有很多,比如时间、头像、背景气泡等等,所以说子类继承父类是最基础的方案。
2、弃置Autolayout的UI书写方式,完全用Frame来写UI布局。Cell高度以及内部UI组件的布局和位置,通过异步计算并缓存为LayoutModel,通过这种方式降低计算的重复耗时操作。
3、有的消息类型只是负责展示,但是有的确有相对复杂的业务逻辑,但是为了防止Cell代码的膨胀,采用了瘦Cell的方式分离逻辑,力求使Cell尽量只负责UI的承载和展示,增加helper层处理相关逻辑。
4、因为虽然是相同的一个数据,但是呈现的方式会存才差异化,所以采用瘦Model的形式,通过创建Helper对取到的原始数据进行相对应的加工,直接提供给业务逻辑处理好的数据。在AFNetworking给的Demo中,是一个典型的胖Model的例子,倒不是说他的例子不好,只是随着业务逻辑的复杂以及生数据和熟数据的差异越来越大的时候,胖Mode的代码量会几何级数般的膨胀,所以还是要因地制宜,具体情况具体分析。
5、利用Factory模式分离出关于复杂Cell类型的判断,包括初始化、赋值等。
6、使用KVO取代delegate进行反向传值,用以减少代码耦合。

关于如何保证UI性能以及优化ViewController,可参考我的其他几篇Blog,iOS 性能优化的探索复杂业务下UIViewController的减负工作

最终结果,第一个完整case的UI层Controller代码,从3000行直接缩减到了1200行,Controller中没有复杂的多方数据处理逻辑,复杂的逻辑判断。只作为UI展示以及接口调用,完全剥离了数据逻辑的处理,所有的处理逻辑由下面的数据逻辑层处理。

数据逻辑层重构

我在分析了业务需求并设计了架构之后,决定重构以自下而上的顺序来进行,于是第一部分就是对于数据逻辑层的重构。步骤如下: 此部分,分为以下三层结构:

1、业务数据逻辑层
2、适配器层(adapter)
3、基础组件服务层(server)

1、业务数据逻辑层

这部分的主要作用是直接受到UI层的调用,负责长连接以及短连接的建立,数据库的初始化操作等。 向上直接承接UI逻辑和业务逻辑,是高度面向业务封装的接口。比如在发送照片消息的时候,只需要将调用API传入Image对象,其他的流程比如说是上传资源以及组成message对象等,则不需要上一层调用和考虑。

所以这一层尽可能的会很薄,不会有特别多的逻辑代码。

2、适配器层(adapter)

这部分的主要工作是承上启下,承接上一层的面向业务的封装,调用下一层基础组件服务层的接口,可以说绝大多数的接口封装都集中在这一层。因为我们有些业务的长连接和短连接的使用上不是很合理,所以我将长短连接都封装到了一个网络服务的类中,此后假如长短连接的业务产生了变化,但仍可以保持向上的接口稳定性。

举个例子说,当推送来一条消息之后,是通过长连接,但是需要收到数据之后在进行AFN的操作完成消息体完整数据的获取,之后要存入数据库并且将是否读取状态设置为NO,当用户读取当前消息之后,将这部分消息还要更新为已读状态。

这部分操作涉及到了所有的组件的操作,但是反馈到最上面一层的时候,大概只是新的消息,并且是完整的消息,然后再刷新UI。所以说,这一层的业务量比较大,几乎是要按照各种标准操作,完整的处理好所有组件的接口。

3、基础组件服务层(server)

这部分基本可以说是基于IM本身的业务特点,对于基础组件的调用封装。 包括:

1、数据库部分,对于IM消息的数据结构,封装的对于数据库的创建,以及增删改查等接口。以及基于业务的一些接口,例如一次性设置当前聊天的所有消息为已读状态等接口。
2、AFN部分,这部分相对来说就很简单了,基本上是依赖于AFN封装的接口,比如获取当前User的详细信息等。
3、长连接部分,包括对于长连接协议的创建连接、断连以及心跳超时上报等操作,也包括了发送消息和收到消息回调等底层操作。
复制代码

拆分之后的Manager层代码量所见到原来的40%左右,于是改名为Session层。

基础组件层的重构以及封装

这部分因为属于公用的基础组件,所以相对来说只是基础组件的比如说AFN以及数据库(FMDB)是整个App的组成,所以没什么其他的操作,只是单纯做了一层逻辑上分层。

但是对于长连接我们做了一些定制,比如:

1、增加了重连的逻辑机制。
2、增加创建连接以及断掉连接时候各种状态的判断等。
复制代码

主要任务还是集中在对于协议库本身的逻辑补充和健壮性优化等。

其他操作

1、创建枚举文件,扩展标准化的枚举变量。
2、合并以及分割Model,随着业务的扩展,原来的Model设计已经不符合当下的业务发展,根据现在固定的业务,重新设计了Model的集成关系,对于分化严重的也做了重新分割。
3、分离并封装了胶水代码到一个大的工具类,便于调用和调试。

走过的弯路

1、过度思考代码解耦合而忽略了业务逻辑复杂性,错将组件化各组件的解耦合的逻辑应用在了本来就是高耦合的MVC架构上。尝试使用去model化的Cell,但是实际操作环节发现增加了大量的逻辑判断,无形中将Model本该处理的业务逻辑转接到Controller和View上,表面上看上去API简洁到家,但是上手代码量并不算小,不利于维护。

2、在一开始采用了MVCS的设计重构UI层,简化Controller中对于Model的处理,在Store中进行了主动和被动网络逻辑、本地数据库调用等。但事实上最后通过封装统一入口的方式将数据处理的逻辑全部从UI逻辑层剥离开,下沉到了数据逻辑层,对于UI层来说只需要考虑的是进行了调用获取数据的API操作或者是被动受到了新的数据,不需要考虑数据来自于服务端、Cache还是本地数据库,也不需要考虑后面的逻辑,当然,从另一个角度说仍然是MVCS,只不过Store相对复杂且庞大。

3、对于UI层和下一层的数据沟通,虽然采用了KVO的方式回调,降低了耦合性,但是仍然存在参数复杂的情况下,传递过多的Key的情况,导致解析稍显困难和复杂。

4、Cell的继承,看上去是一个很直观的设计,但是随着重构代码量的增加以及业务变化发现继承过程中会存在很多问题,通过面向协议等方式或许可以解决继承中庞大Api的问题。

总结以及思考

架构设计的时候,一定要预判用户的使用习惯,判断未来的业务导向,尽可能的降低代码侵入性和耦合性。对于性能产生的影响的地方,通过以上几点来设计架构,模块健壮性以及可扩展性是设计之初就要优先考虑到的。

架构设计分层要清晰,API设计要尽可能简洁,避免暴露过多的接口和参数,避免模块之间的紧耦合,UI设计要尽可能灵活。 重构前,需要思考切入点,是从上值下、从下至上,还是模块化抽离。

已不再维护这部分业务,部分逻辑全凭记忆整理,如果有疏漏或错误,还请大家海涵。

Refrence


作者:derek
链接:https://juejin.cn/post/6844904054577954824

收起阅读 »

iOS Operation 自定义的注意点

iOS
问题 碰到一个问题,就是做一个点击后添加动画效果,连续点击则有多个动画效果按顺序执行,通过自定Operation,以队列实现,但是发现每次点击玩上次动画效果还没完全执行完点击之后的动画就出来,不符合需求。 后来查资料得知自定义Operation中有两个属性分...
继续阅读 »
问题


  • 碰到一个问题,就是做一个点击后添加动画效果,连续点击则有多个动画效果按顺序执行,通过自定Operation,以队列实现,但是发现每次点击玩上次动画效果还没完全执行完点击之后的动画就出来,不符合需求。

  • 后来查资料得知自定义Operation中有两个属性分别表示任务是否在执行以及是否执行完毕,如下


@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
复制代码


因此在自定义Operation时设置这两个属性,同时在完全当前队列中任务时给予标识表明任务完成,具体代码如下




  • CustomOperation 类


@interface CustomOperation : NSOperation

@end

#import "CustomOperation.h"

@interface CustomOperation ()

@property(nonatomic,readwrite,getter=isExecuting)BOOL executing; // 表示任务是否正在执行
@property(nonatomic,readwrite,getter=isFinished)BOOL finished; // 表示任务是否结束

@end

@implementation CustomOperation

@synthesize executing = _executing;
@synthesize finished = _finished;

- (void)start
{
    @autoreleasepool {
        self.executing = YES;
        if (self.cancelled) {
            [self done];
            return;
        }
        // 执行任务
        __weak typeof(self) weakSelf = self;
        dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));
        dispatch_after(delayTime, dispatch_get_main_queue(), ^{
            NSLog(@"动画完毕");
            // 任务执行完毕手动关闭
            [weakSelf done];
        });
    }
}

-(void)done
{
    self.finished = YES;
    self.executing = NO;
}

#pragma mark - setter -- getter
// 监听并设置executing
- (void)setExecuting:(BOOL)executing
{
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

- (BOOL)isExecuting
{
    return _executing;
}

// 监听并设置finished
- (void)setFinished:(BOOL)finished
{
    if (_finished != finished) {
        [self willChangeValueForKey:@"isFinished"];
        _finished = finished;
        [self didChangeValueForKey:@"isFinished"];
    }
}

- (BOOL)isFinished
{
    return _finished;
}

// 返回YES 标识并发Operation
- (BOOL)isAsynchronous
{
    return YES;
}

@end


  • swift版


class AnimationOperation: Operation {

    var animationView:AnimationView? // 动画view
    var superView:UIView? // 父视图
    var finishCallBack:(()->())? // 完成动画的回调

    override var isExecuting: Bool {
        return operationExecuting
    }

    override var isFinished: Bool {
        return operationFinished
    }

    override var isAsynchronous: Bool {
        return true
    }
// 监听
    private var operationFinished:Bool = false {
        willSet {
            willChangeValue(forKey: "isFinished")
        }
        didSet {
            didChangeValue(forKey: "isFinished")
        }
    }

    private var operationExecuting:Bool = false {
        willSet {
            willChangeValue(forKey: "isExecuting")
        }
        didSet {
            didChangeValue(forKey: "isExecuting")
        }
    }

// 每次点击添加动画队列
    class func addOperationShowAnimationView(animationView:AnimationView,superView:UIView) -> AnimationOperation {

        let operation = AnimationOperation()
        operation.animationView = animationView
        operation.superView = superView
        return operation
    }

    override func start() {
        self.operationExecuting = true
        if isCancelled == true {
            self.done()
            return
        }

        guard let superView = self.superView,
              let subView = self.animationView,
              let callback = self.finishCallBack else {
            print("superView == nil")
            return
        }

        OperationQueue.main.addOperation {[weak self] in
            superView.addSubview(subView)
            subView.finishCallBack = {
                self?.done()
                callback()
            }
            subView.showAnimation()
        }
    }

    

    private func done() {
        self.operationFinished = true
        self.operationExecuting = false
    }

}

作者:取个有意思的昵称
链接:https://juejin.cn/post/7034518171314815007

收起阅读 »

如何系统性治理 iOS 稳定性问题

iOS
字节跳动如何系统性治理 iOS 稳定性问题本文是丰亚东讲师在2021 ArchSummit 全球架构师峰会中「如何系统性治理 iOS 稳定性问题」的分享全文首先做一下自我介绍:我是丰亚东,2016 年 4 月加入字节跳动,先后负责今日头条 App 的工程架构、...
继续阅读 »

字节跳动如何系统性治理 iOS 稳定性问题

本文是丰亚东讲师在2021 ArchSummit 全球架构师峰会中「如何系统性治理 iOS 稳定性问题」的分享全文

首先做一下自我介绍:我是丰亚东,2016 年 4 月加入字节跳动,先后负责今日头条 App 的工程架构、基础库和体验优化等基础技术方向。2017 年 12 月至今专注在 APM 方向,从 0 到 1 参与了字节跳动 APM 中台的建设,服务于字节的全系产品,目前主要负责 iOS 端的性能稳定性监控和优化。 请添加图片描述 本次分享主要分为四大章节,分别是:1.稳定性问题分类;2.稳定性问题治理方法论;3.疑难问题归因;4.总结回顾。其中第三章节「疑难问题归因」是本次分享的重点,大概会占到60%的篇幅。

一、稳定性问题分类

在讲分类之前,我们先了解一下背景:大家都知道对于移动端应用而言,闪退是用户能遇到的最严重的 bug,因为在闪退之后用户无法继续使用产品,那么后续的用户留存以及产品本身的商业价值都无从谈起。 这里有一些数据想和大家分享:有 20% 的用户在使用移动端产品的时候,最无法忍受的问题就是闪退,这个比例仅次于不合时宜的广告;在因为体验问题流失的用户中,有 1/3 的用户会转而使用竞品,由此可见闪退问题是非常糟糕和严重的。 请添加图片描述 字节跳动作为拥有像抖音、头条等超大量级 App 的公司,对稳定性问题是非常重视的。过去几年,我们在这方面投入了非常多的人力和资源,同时也取得了不错的治理成果。过去两年抖音、头条、飞书等 App 的异常崩溃率都有 30% 以上的优化,个别产品的部分指标甚至有 80% 以上的优化。 通过上图中右侧的饼状图可以看出:我们以 iOS 平台为例,根据稳定性问题不同的原因,将已知稳定性问题分成了这五大类,通过占比从高到低排序:第一大类是 OOM ,就是内存占用过大导致的崩溃,这个比例能占到 50% 以上;其次是 Watchdog,也就是卡死,类比于安卓中的 ANR;再次是普通的 Crash;最后是磁盘 IO 异常和 CPU 异常。 看到这里大家心里可能会有一个疑问:字节跳动究竟做了什么,才取得了这样的成果?接下来我会将我们在稳定性治理方面沉淀的方法论分享给大家。

二、稳定性问题治理的方法论

在这里插入图片描述 首先我们认为在稳定性问题治理方面,从监控平台侧视角出发,最重要的就是要有完整的能力覆盖,比如针对上一章节中提到所有类型的稳定性问题,监控平台都应该能及时准确的发现。 另外是从业务研发同学的视角出发:稳定性问题治理这个课题,需要贯穿到软件研发的完整生命周期,包括需求研发、测试、集成、灰度、上线等,在上述每个阶段,研发同学都应该重视稳定性问题的发现和治理。

上图中右侧是我们总结的两条比较重要的治理原则: 第一条是控制新增,治理存量。一般来说新增的稳定性问题可能是一些容易爆发的问题,影响比较严重。存量问题相对来说疑难的问题居多,修复周期较长。 第二条比较容易理解:先急后缓,先易后难。我们应该优先修复那些爆发的问题以及相对容易解决的问题。 在这里插入图片描述 如果我们将软件研发周期聚焦在稳定性问题治理这个方向上,又可以抽象出以下几个环节: 首先第一个环节是问题发现:当用户在线上遇到任何类型的闪退,监控平台都应该能及时发现并上报。同时可以通过报警以及问题的自动分发,将这些问题第一时间通知给开发者,确保这些问题能够被及时的修复。 第二个阶段是归因:当开发者拿到一个稳定性问题之后,要做的第一件事情应该是排查这个问题的原因。根据一些不同的场景,我们又可以把归因分为单点归因、共性归因以及爆发问题归因。 当排查到问题的原因之后,下一步就是把这个问题修复掉,也就是问题的治理。在这里我们有一些问题治理的手段:如果是在线上阶段,我们首先可以做一些问题防护,比如网易几年前一篇文章提到的基于 OC Runtime 的线上 Crash 自动修复的方案大白,基于这种方案我们可以直接在线上做 Crash 防护;另外由于后端服务上线导致的稳定性问题爆发,我们可以通过服务的回滚来做到动态止损。除了这两种手段之外,更多的场景还是需要研发在线下修复 native 代码,再通过发版做彻底的修复。 最后一个阶段也是最近几年比较火的一个话题,就是问题的防劣化。指的是需求从研发到上线之间的阶段,可以通过机架的自动化单元测试/UI自动化测试,以及研发可以通过一些系统工具,比如说 Xcode 和 Instruments,包括一些第三方工具,比如微信开源的 MLeaksFinder 去提前发现和解决各类稳定性问题。

如果我们想把稳定性问题治理做好的话,需要所有研发同学关注上述每一个环节,才能达到最终的目标。 可是这么多环节我们的重点究竟在哪里呢?从字节跳动的问题治理经验来看,我们认为最重要的环节是第二个——线上的问题的归因。因为通过内部的统计数据发现:线上之所以存在长期没有结论,没有办法修复的问题,主要还是因为研发并没有定位到这些问题的根本原因。所以下一章节也是本次分享的重点:疑难问题归因。

三、疑难问题归因

我们根据开发者对这些问题的熟悉程度做了一下排序,分别是:Crash、Watchdog、OOM 和 CPU&Disk I/O。每一类疑难问题我都会分享这类问题的背景和对应的解决方案,并且会结合实战案例演示各种归因工具究竟是如何解决这些疑难问题的。

3.1 第一类疑难问题 —— Crash

在这里插入图片描述 上图中左侧这张饼状图是我们根据 Crash 不同的原因,把它细分成四大类:包括 Mach 异常、 Unix Signal 异常、OC 和 C++ 语言层面上的异常。其中比例最高的还是 Mach 异常,其次是 Signal 异常,OC 和 C++ 的异常相对比较少。 为什么是这个比例呢? 大家可以看到右上角有两个数据。第一个数据是微软发布的一篇文章,称其发布的 70% 以上的安全补丁都是内存相关的错误,对应到 iOS 平台上就是 Mach 异常中的非法地址访问,也就是 EXC_BAD_ACCESS。内部统计数据表明,字节跳动线上 Crash 有 80% 是长期没有结论的,在这部分 Crash 当中,90% 以上都是 Mach 异常或者 Signal 异常。 看到这里,大家肯定心里又有疑问了,为什么有这么多 Crash 解决不了?究竟难在哪里?我们总结了几点这些问题归因的难点:

  • 首先不同于 OC 和 C++ 的异常,可能开发者拿到的崩溃调用栈是一个纯系统调用栈,这类问题显然修复难度是非常大的;
  • 另外可能有一部分Crash是偶发而不是必现的问题,研发同学想在线下复现问题是非常困难的,因为无法复现,也就很难通过 IDE 调试去排查和定位这些问题;
  • 另外对于非法地址访问这类问题,崩溃的调用栈可能并不是第一现场。这里举一个很简单的例子:A业务的内存分配溢出,踩到了B业务的内存,这个时候我们认为 A 业务应该是导致这个问题的主要原因,但是有可能B业务在之后的某一个时机用到了这块内存,发生了崩溃。显然这种问题实际上是 A 业务导致的,最终却崩在了 B 业务的调用栈里,这就会给开发者排查和解决这个问题带来非常大的干扰。

看到这里大家可能心里又有问题:既然这类问题如此难解,是不是就完全没有办法了呢?其实也并不是,下面我会分享字节内部两个解决这类疑难问题非常好用的归因工具。 在这里插入图片描述

3.1.1 Zombie 检测

首先第一个是 Zombie 检测,大家如果用过 Xcode 的 Zombie 监控,应该对这个功能比较熟悉。如果我们在调试之前打开了 Zombie Objects 这个开关,在运行的时候如果遇到了 OC 对象野指针造成的崩溃,Xcode 控制台中会打印出一行日志,它会告诉开发者哪个对象在调用什么消息的时候崩溃了。

这里我们再解释一下 Zombie 的定义,其实非常简单,指的是已经释放的 OC 对象。 Zombie 监控的归因优势是什么呢?首先它可以直接定位到问题发生的类,而不是一些随机的崩溃调用栈;另外它可以提高偶现问题的复现概率,因为大部分偶现问题可能跟多线程的运行环境有关,如果我们能把一个偶现问题变成必现问题的话,那么开发者就可以借助 IDE 和调试器非常方便地排查问题。但是这个方案也有自己的适用范围,因为它的底层原理基于 OC 的 runtime 机制,所以它仅仅适用于 OC 对象野指针导致的内存问题。 在这里插入图片描述 这里再和大家一起回顾一下 Zombie 监控的原理:首先我们会 hook 基类 NSObject 的 dealloc 方法,当任意 OC 对象被释放的时候,hook 之后的那个 dealloc 方法并不会真正的释放这块内存,同时将这个对象的 ISA 指针指向一个特殊的僵尸类,因为这个特殊的僵尸类没有实现任何方法,所以这个僵尸对象在之后接收到任何消息都会 Crash,与此同时我们会将崩溃现场这个僵尸对象的类名以及当时调用的方法名上报到后台分析。 在这里插入图片描述 这里是字节的一个真实案例:这个问题是飞书在某个版本线上 Top 1 的 Crash,当时持续了两个月没有被解决。首先大家可以看到这个崩溃调用栈是一个纯系统调用栈,它的崩溃类型是非法地址访问,发生在视图导航控制器的一次转场动画,可能开发者一开始看到这个崩溃调用栈是毫无思路的。 在这里插入图片描述 那么我们再看 Zombie 功能开启之后的崩溃调用栈:这个时候报错信息会更加丰富,可以直接定位到野指针对象的类型,是 MainTabbarController 对象在调用 retain 方法的时候发生了 Crash。

看到这里大家肯定有疑问了,MainTabbarController 一般而言都是首页的根视图控制器,理论上在整个生命周期内不应该被释放。为什么它变成了一个野指针对象呢?可见这样一个简单的报错信息,有时候还并不足以让开发者定位到问题的根本原因。所以这里我们更进一步,扩展了一个功能:将 Zombie 对象释放时的调用栈信息同时上报上来。 在这里插入图片描述 大家看倒数第二行,实际上是一段飞书的业务代码,是视图导航控制器手势识别的代理方法,这个方法在调用的时候释放了 MainTabbarController。因为通过这个调用栈找到了业务代码的调用点,所以我们只需要对照源码去分析为什么会释放 TabbarController,就可以定位到这个问题的原因。 在这里插入图片描述 上图中右侧是简化之后的源码(因为涉及到代码隐私问题,所以通过一段注释代替)。历史上为了解决手势滑动返回的冲突问题,在飞书视图导航控制器的手势识别代理方法中写了一段 trick 代码,正是这个 trick 方案导致了首页视图导航控制器被意外释放。 排查到这里,我们就找到了问题的根本原因,修复的方案也就非常简单了:只要下掉这个 trick 方案,并且依赖导航控制器的原生实现来决定这个手势是否触发就解决了这个问题。

3.1.2 Coredump

刚才也提到:Zombie 监控方案是有一些局限的,它仅适用于 OC 对象的野指针问题。大家可能又会有疑问: C 和 C++ 代码同样可能会出现野指针问题,在 Mach 异常和 Signal 异常中,除了内存问题之外,还有很多其他类型的异常比如 EXC_BAD_INSTRUCTION和SIGABRT。那么其他的疑难问题我们又该怎么解决呢?这里我们给出了另外一个解决方案 —— Coredump。 在这里插入图片描述 这个先解释一下什么是 Coredump:Coredump 是由 lldb 定义的一种特殊的文件格式,Coredump 文件可以还原 App 在运行到某一时刻的完整运行状态(这里的运行状态主要指的是内存状态)。大家可以简单的理解为:Coredump文件相当于在崩溃的现场打了一个断点,并且获取到当时所有线程的寄存器信息,栈内存以及完整的堆内存。

Coredump 方案它的归因优势是什么呢?首先因为它是 lldb 定义的文件格式,所以它天然支持 lldb 的指令调试,也就是说开发者无需复现问题,就可以实现线上疑难问题的事后调试。另外因为它有崩溃时现场的所有内存信息,这就为开发者提供了海量的问题分析素材。

这个方案的适用范围比较广,可以适用于任意 Mach 异常或者 Signal 异常问题的分析。 在这里插入图片描述 下面也带来一个线上真实案例的分析:当时这个问题出现在字节的所有产品中,而且在很多产品中的量级非常大,排名Top 1 或者 Top 2,这个问题在之前两年的时间内都没有被解决。

大家可以看到这个崩溃调用栈也全是系统库方法,最终崩溃在 libdispatch 库中的一个方法,异常类型是命中系统库断言。 在这里插入图片描述 我们将这次崩溃的 Coredump 文件上报之后,用前面提到的 lldb 调试指令去分析,因为拥有崩溃时的完整内存状态,所以我们可以分析所有线程的寄存器和栈内存等信息。

这里最终我们分析出:崩溃线程的 0 号栈帧(第一行调用栈),它的 x0 寄程器实际上就是 libdispatch 中定义的队列结构体信息。在它起始地址偏移 0x48 字节的地方,也就是这个队列的 label 属性(可以简单理解为队列的名字)。这个队列的名字对我们来说是至关重要的,因为要修复这个问题,首先应该知道究竟是哪个队列出现了问题。通过 memory read 指令我们直接读取这块内存的信息,最终发现它是一个 C 的字符串,名字叫 com.apple.CFFileDescriptor,这个信息非常关键。我们在源码中全局搜索这个关键字,最终发现这个队列是在字节底层的网络库中创建的,这也就能解释为什么字节所有产品都有这个崩溃了。 在这里插入图片描述 最终我们和网络库的同学一起排查,同时结合 libdispatch 的源码,定位到这个问题的原因是 GCD 队列的外部引用计数小于0,存在过度释放的问题,最终命中系统库断言导致崩溃。 在这里插入图片描述 排查到问题之后,解决方案就比较简单了:我们只需要在这个队列创建的时候,使用 dispatch_source_create 的方式去增加队列的外部引用计数,就能解决这个问题。和维护网络库的同学沟通后,确认这个队列在整个 App 的生命周期内不应该被释放。这个问题最终解决的收益是直接让字节所有产品的 Crash 率降低了8%。

3.2 第二类疑难问题 —— Watchdog

我们进入疑难问题中的第二类问题 —— Watchdog 也就是卡死。 在这里插入图片描述 上图中左侧是我在微博上截的两张图,是用户在遇到卡死问题之后的抱怨。可见卡死问题对用户体验的伤害还是比较大的。那么卡死问题它的危害有哪些呢?

首先卡死问题通常发生于用户打开 App 的冷启动阶段,用户可能等待了10 秒什么都没有做,这个 App 就崩溃了,这对用户体验的伤害是非常大的。另外我们线上监控发现,如果没有对卡死问题做任何治理的话,它的量级可能是普通 Crash 的 2-3 倍。另外现在业界普遍监控 OOM 崩溃的做法是排除法,如果没有排除卡死崩溃的话,相应的就会增加 OOM 崩溃误判的概率。

卡死类问题的归因难点有哪些呢?首先基于传统的方案——卡顿监控:认为主线程无响应时间超过3秒~5秒之后就是一次卡死,这种传统的方案非常容易误报,至于为什么误报,我们下一页中会讲到。另外卡死的成因可能非常复杂,它不一定是单一的问题:主线程的死锁、锁等待、主线程 IO 等原因都有可能造成卡死。第三点是死锁问题是一类常见的导致卡死问题的原因。传统方案对于死锁问题的分析门槛是比较高的,因为它强依赖开发者的经验,开发者必须依靠人工的经验去分析主线程到底跟哪个或者哪些线程互相等待造成死锁,以及为什么发生死锁。 在这里插入图片描述 大家可以看到这是基于传统的卡顿方案来监控卡死,容易发生误报。为什么呢?图中绿色和红色的部分是主线程的不同耗时阶段。假如主线程现在卡顿的时间已经超过了卡死阈值,刚好发生在图中的第5个耗时阶段,我们在此时去抓取主线程调用栈,显然它并不是这次耗时的最主要的原因,问题其实主要发生在第4个耗时阶段,但是此时第4个耗时阶段已经过去了,所以会发生一次误报,这可能让开发者错过真正的问题。

针对以上提到的痛点,我们给出了两个解决方案:首先在卡死监控的时候可以多次抓取主线程调用栈,并且记录每次不同时刻主线程的线程状态,关于线程状态包括哪些信息,下一页中会提到。 另外我们可以自动识别出死锁导致的卡死问题,将这类问题标识出来,并且可以帮助开发者自动还原出各个线程之间的锁等待关系。 在这里插入图片描述 首先是第一个归因工具——线程状态,这张图是主线程在不同时刻调用栈的信息,在每个线程名字后面都有三个 tag ,分别指的是三种线程的状态,包括当时的线程 CPU 占用、线程运行状态和线程标志。

上图中右侧是线程的运行状态和线程标志的解释。当看到线程状态的时候,我们主要的分析思路有两种:第一种,如果看到主线程的 CPU 占用为 0,当前处于等待的状态,已经被换出,那我们就有理由怀疑当前这次卡死可能是因为死锁导致的;另外一种,特征有所区别,主线程的 CPU 占用一直很高 ,处于运行的状态,那么就应该怀疑主线程是否存在一些死循环等 CPU 密集型的任务。 在这里插入图片描述 第二个归因工具是死锁线程分析,这个功能比较新颖,所以首先带领大家了解一下它的原理。基于上一页提到的线程状态,我们可以在卡死时获取到所有线程的状态并且筛选出所有处于等待状态的线程,再获取每个线程当前的 PC 地址,也就是正在执行的方法,并通过符号化判断它是否是一个锁等待的方法。

上图中列举了目前我们覆盖到的一些锁等待方法,包括互斥锁、读写锁、自旋锁、 GCD 锁等等。每个锁等待的方法都会定义一个参数,传入当前锁等待的信息。我们可以从寄存器中读取到这些锁等待信息,强转为对应的结构体,每一个结构体中都会定义一个线程id的属性,表示当前这个线程正在等待哪个线程释放锁。对每一个处于等待状态的线程完成这样一系列操作之后,我们就能够完整获得所有线程的锁等待关系,并构建出锁等待关系图。 在这里插入图片描述 通过上述方案,我们可以自动识别出死锁线程。假如我们能判断 0 号线程在等待 3 号线程释放锁, 同时3 号线程在等待0号线程释放锁,那么显然就是两个互相等待最终造成死锁的线程。

大家可以看到这里主线程我们标记为死锁,它的 CPU 占用为 0,状态是等待状态,而且已经被换出了,和我们之前分析线程状态的方法论是吻合的。 在这里插入图片描述 通过这样的分析之后,我们就能够构建出一个完整的锁等待关系图,而且无论是两个线程还是更多线程互相等待造成的死锁问题,都可以自动识别和分析。 在这里插入图片描述 这是上图中死锁问题的一段示意的源码。它的问题就是主线程持有互斥锁,子线程持有 GCD 锁,两个线程之间互相等待造成了死锁。这里给出的解决方案是:如果子线程中可能存在耗时操作,尽量不要和主线程有锁竞争关系;另外如果在串行队列中同步执行 block 的话,一定要慎重。 在这里插入图片描述 上图是通过字节内部线上的监控和归因工具,总结出最常见触发卡死问题的原因,分别是死锁、锁竞争、主线程IO、跨进程通信。

3.3 第三类疑难问题 —— OOM

OOM 就是 Out Of Memory,指的是应用占用的内存过高,最终被系统强杀导致的崩溃。 在这里插入图片描述 OOM 崩溃的危害有哪些呢?首先我们认为用户使用 App 的时间越长,就越容易发生 OOM 崩溃,所以说 OOM 崩溃对重度用户的体验伤害是比较大的;统计数据显示,如果 OOM 问题没有经过系统性的治理,它的量级一般是普通 Crash 的 3-5 倍。最后是内存问题不同于 Crash 和卡死,相对隐蔽,在快速迭代的过程中非常容易劣化。

那么 OOM 问题的归因难点有哪些呢?首先是内存的构成是非常复杂的事情,并没有非常明确的异常调用栈信息。另外我们在线下有一些排查内存问题的工具,比如 Xcode MemoryGraph 和 Instruments Allocations,但是这些线下工具并不适用于线上场景。同样是因为这个原因,如果开发者想在线下模拟和复现线上 OOM 问题是非常困难的。 在这里插入图片描述 这里我们给出解决线上 OOM 疑难问题的归因工具是MemoryGraph。这里的 MemoryGraph 主要指的是在线上环境中可以使用的 MemoryGraph。跟 Xcode MemoryGraph 有一些类似,但是也有不小的区别。最大的区别当然是它能在线上环境中使用,其次它可以对分散的内存节点进行统计和聚合,方便开发者定位头部的内存占用。

这里带领大家再回顾一下线上 MemoryGraph 的基本原理:首先我们会定时的去检测 App 的物理内存占用,当它超过危险阈值的时候,就会触发内存 dump,此时 SDK 会记录每个内存节点符号化之后的信息,以及他们彼此之间的引用关系,如果能判定出是强引用还是弱引用,也会把这个强弱引用关系同时上报上来,最终这些信息整体上报到后台之后,就可以辅助开发者去分析当时的大内存占用和内存泄露等异常问题。

这里我们还是用一个实战案例带领大家看一下 MemoryGraph 到底是如何解决 OOM 问题的。 在这里插入图片描述 分析 MemoryGraph 文件的思路一般是抽丝剥茧,逐步找到根本原因。

上图是 MemoryGraph 文件分析的一个例子,这里的红框标注了不同的区域:左上角是类列表,会把同一类型对象的数量以及它们占用的内存大小做一个汇总;右侧是这个类所有实例的地址列表,右下角区域开发者可以手动回溯对象的引用关系(当前对象被哪些其他对象引用、它引用了哪些其他对象),中间比较宽的区域是引用关系图。

因为不方便播放视频,所以这边就跟大家分享一些比较关键的结论:首先看到类列表,我们不难发现 ImageIO 类型的对象有 47 个,但是这 47 个对象居然占了 500 多 MB 内存,显然这并不是一个合理的内存占用。我们点开 ImageIO 的类列表,以第一个对象为例,回溯它的引用关系。当时我们发现这个对象只有一个引用,就是 VM Stack: Rust Client Callback ,它实际上是飞书底层的 Rust 网络库线程。 排查到这里,大家肯定会好奇:这 47 个对象是不是都存在相同的引用关系呢?这里我们就可以用到右下角路径回溯当中的 add tag 功能,自动筛选这 47 个对象是否都存在相同的引用关系。大家可以看到上图中右上角区域,通过筛选之后,我们确认这 47 个对象 100% 都有相同的引用关系。

我们再去分析 VM Stack: Rust Client Callback这个对象。发现它引用的对象中有两个名字非常敏感,一个是 ImageRequest,另外一个是 ImageDecoder ,从这两个名字我们可以很容易地推断出:应该是图片请求和图片解码的对象。 在这里插入图片描述 我们再用这两个关键字到类列表中搜索,可以发现 ImageRequest 对象有 48 个,ImageDecoder 对象有 47 个。如果大家还有印象的话,上一页中占用内存最大的对象 ImageIO 也是 47 个。这显然并不是一个巧合,我们再去排查这两类对象的引用关系,发现这两类对象也同样是 100% 被 VM Stack: Rust Client Callback 对象所引用。

最终我们和飞书图片库的同学一起定位到这个问题的原因:在同一时刻并发请求 47 张图片并解码,这不是一个合理的设计。问题的根本原因是飞书图片库的下载器依赖了 NSOperationQueue 做任务管理和调度,但是却没有配置最大并发数,在极端场景下就有可能造成内存占用过高的问题。与之相对应的解决方案就是对图片下载器设置最大并发数,并且根据待加载图片是否在可视区域内调整优先级。 在这里插入图片描述 上图是通过字节内部的线上监控和归因工具,总结出来最常见的几类触发 OOM 问题的原因,分别是:内存泄露,这个较为常见;第二个是内存堆积,主要指的是 AutoreleasePool 没有及时清理;第三是资源异常,比如加载一张超大图或者一个超大的 PDF 文件;最后一个是内存使用不当,比如内存缓存没有设计淘汰清理的机制。

3.4 第四类疑难问题 —— CPU 异常和磁盘 I/O 异常

这里之所以把这两类问题合并在一起,是因为这两类问题是高度相似的:首先它们都属于资源的异常占用;另外它们也都不同于闪退,导致崩溃的原因并不是发生在一瞬间,而都是持续一段时间的资源异常占用。 在这里插入图片描述 异常 CPU 占用和磁盘 I/O 占用危害有哪些呢?首先我们认为,这两类问题即使最终没有导致 App 崩溃,也特别容易引发卡顿或者设备发烫等性能问题。其次这两类问题的量级也是不可以被忽视的。另外相比之前几类稳定性问题而言,开发者对这类问题比较陌生,重视程度不够,非常容易劣化。

这类问题的归因难点有哪些呢?首先是刚刚提到它的持续时间非常长,所以原因也可能并不是单一的;同样因为用户的使用环境和操作路径都比较复杂,开发者也很难在线下复现这类问题;另外如果 App 想在用户态去监控和归因这类问题的话,可能需要在一段时间内高频的采样调用栈信息,然而这种监控手段显然性能损耗是非常高的。 在这里插入图片描述 上图中左侧是我们从 iOS 设备中导出的一段 CPU 异常占用的崩溃日志,截取了关键部分。这部分信息的意思是:当前 App 在 3 分钟之内的 CPU 时间占用已经超过80%,也就是超过了 144 秒,最终触发了这次崩溃。

上图中右侧是我截取苹果 WWDC2020 一个 session 中的截图,苹果官方对于这类问题,给出了一些归因方案的建议:首先是 Xcode Organizer,它是苹果官方提供的问题监控后台。然后是建议开发者也可以接入 MetricKit ,新版本有关于 CPU 异常的诊断信息。 请添加图片描述 上图中左侧是磁盘异常写入的崩溃日志,也是从 iOS 设备中导出,依然只截取了关键部分:在 24 小时之内,App 的磁盘写入量已经超过了 1073 MB,最终触发了这次崩溃。

上图中右侧是苹果官方的文档,也给出了对于这类问题的归因建议。同样是两个建议:一个是依赖 Xcode Organizer,另一个是依赖 MetricKit。我们选型的时候最终确定采用 MetricKit 方案,主要考虑还是想把数据源掌握在自己手中。因为 Xcode Organizer 毕竟是一个苹果的黑盒后台,我们无法与集团内部的后台打通,更不方便建设报警、问题自动分配、issue状态管理等后续流程。 请添加图片描述 MetricKit 是苹果提供的官方性能分析以及稳定性问题诊断的框架,因为是系统库,所以它的性能损耗很小。在 iOS 14 系统以上,基于Metrickit,我们可以很方便地获取 CPU 和磁盘 I/O 异常的诊断信息。它的集成也非常方便。我们只需要导入系统库的头文件,设置一个监听者,在对应的回调中把 CPU 和磁盘写入异常的诊断信息上报到后台分析就好了。 请添加图片描述 其实这两类异常的诊断信息格式也是高度类似的,都是记录一段时间内所有方法的调用以及每个方法的耗时。上报到后台之后,我们可以把这些数据可视化为非常直观的火焰图。通过这样直观的形式,可以辅助开发者轻松地定位到问题。对于上图中右侧的火焰图,我们可以简单的理解为:矩形块越长,占用的 CPU 时间就越长。那么我们只需要找到矩形块最长的 App 调用栈,就能定位到问题。图中高亮的红框,其中有一个方法的关键字是 animateForNext,看这个名字大概能猜到这是动画在做调度。

最终我们和飞书的同学一起定位到这个问题的原因:飞书的小程序业务有一个动画在隐藏的时候并没有暂停播放,造成了 CPU 占用持续比较高。解决方案也非常简单,只要在动画隐藏的时候把它暂停掉就可以了。

四、总结回顾

请添加图片描述 在第二章节稳定性问题治理方法论中,我提到“如果想把稳定性问题治理好,就需要将这件事情贯穿到软件研发周期中的每一个环节,包括问题的发现、归因、治理以及防劣化”,同时我们认为线上问题特别是线上疑难问题的归因,是整个链路中的重中之重。针对每一类疑难问题,本次分享均给出了一些好用的归因工具:Crash 有 Zombie 监控和 Coredump;Watchdog 有线程状态和死锁线程分析;OOM 有 MemoryGraph;CPU 和磁盘 I/O 异常有 MetricKit。 请添加图片描述 本次分享提到的所有疑难问题的归因方案,除了MetricKit 之外,其余均为字节跳动自行研发,开源社区尚未有完整解决方案。这些工具和平台后续都将通过字节火山引擎应用开发套件 MARS 旗下的 APM Plus 平台提供一站式的企业解决方案。本次分享提到的所有能力均已在字节内部各大产品中验证和打磨多年,其自身的稳定性以及接入后所带来的业务效果都是有目共睹的,欢迎大家持续保持关注。

链接:https://juejin.cn/post/7034418275728097288

收起阅读 »

Swift路由组件(一)使用路由的目的和实现思想

iOS
Swift路由组件(一)使用路由的目的和实现思想这个为本人原创,转载请注明出处:juejin.cn/post/703216…目的项目开发到一定程度,功能之间的调用会变的越来越复杂这里用一个商品购买的逻辑举例从图上看,问题就是业务之间的跳转很多,而且乱。还有就是...
继续阅读 »

Swift路由组件(一)使用路由的目的和实现思想

这个为本人原创,转载请注明出处:juejin.cn/post/703216…

目的

项目开发到一定程度,功能之间的调用会变的越来越复杂

这里用一个商品购买的逻辑举例

image.png

从图上看,问题就是业务之间的跳转很多,而且乱。还有就是当跳同一个页面时,跳转要带的参数都一致,如何保证?如果代码分散到各个业务里面去跳就难免会到处维护的问题。

这就需要路由了。

而且路由做好了,还能有一个好处就是后端或者前端,他们按路由协议统一处理跳转,app就可以不考虑业务之间的跳转了。

下面是加上路由模块的跳转图。

image.png

这下清晰了。

从图上来看,路由,他主要负责业务的跳转,从一个页面跳转到另一个页面等。

实现的思想

为了能跳,那么就需要知道路。所以可以这样理解,路由他需要知道你要跳转到哪里去,去的地方需要什么入参。

所以得有一个key,map到一个ViewController,然后ViewController需要什么入参,就顺便带过来。

解决这个key的问题,业界比较常见的做法是有一个路由表

  1. 比如维护一个plist文件,开发的时候把对应的key映射controller维护到plist里面,运行的时候一次性load到内存中。然后路由要跳转的时候就只需要查表来跳。
  2. 或者在运行的时候通过业务注册,每个业务把key注册到路由里面去,在内存中维护一个路由表。

两种方法都可以。结果大概是这样。

keyvalue
to_home_pageHomeViewController
to_buy_pageBuyViewController
......

路由跳转他要解决三种跳转逻辑

  1. 通过后端下发,直接让App打开某个原生或者Web页面
    • 比如推送消息,点击消息就可以进入某个原生或者Web页面
    • 比如后端返回的商品卡片,点击商品进入某个原生或者Web页面
  2. 比如活动页面,点击按钮进入某个原生或者Web页面
  3. 比如原生页面的某个按钮,点击按钮进入某个原生或者Web页面

总结起来也就两种,

  1. 一种是远程调用,
  2. 一种是app内部调用。

所谓远程调用就是app提供的跳转能力,允许外面调用的。再者理解,可以被别的app打开调用,比如微信的分享,支付等。相对的内部调用就是app内部由A页面跳转到B页面的。

所以针对上面的用处,从命名上可以做好区分,比如内部调用加native://做为开头,表示是内部调用。外部就加weixinapp://(用app名更容易调用者理解),或者加http/https,毕竟可以直接兼容http://www.baidu.com 这样的网页

之所以要好明确区分,是因为可以利用路由做好统一的权限管理。比如外部调用可以加某一种校验后直接打开,内部调用就加另一种检验,特别是内部跳转要做好权限控制,确保真的是你自己的app调用的内部调用才能打开,防止别人只是用URL Schemes就打开了你的内部页面。

想到这,那是不是可以加多种前缀呢,答案肯定是可以的,具体看不同公司的业务。这里就先加两种先。 如下:

keyvalue
native://to_home_pageHomeViewController
native://to_buy_pageBuyViewController
httpWebViewController
httpsWebViewController
......

上面是说,

  1. 当key是native://to_home_page的时候,就进入主页,打开HomeViewController这个页面。
  2. 当key是http的时候。就进入网页,打开WebViewController这个页面渲染。

看到这,那么路由的定义也就出来了。 统一的入口和传参,如:

YYRouter.push(jumpParams: [:])

然后调用上面的路由表如下:

YYRouter.push(jumpParams: ["to":"native://to_home_page"]) // 去到首页
YYRouter.push(jumpParams: ["to":"http://www.baidu.com"]) // 打开网页
YYRouter.push(jumpParams: ["to":"https://www.baidu.com"]) // 打开网页

想传参数,那就这样。

YYRouter.push(jumpParams: ["to":"native://to_home_page", "name": "名字"])
YYRouter.push(jumpParams: ["to":"http://www.baidu.com&a=1&b=2", "name": "名字"])
YYRouter.push(jumpParams: ["to":"https://www.baidu.com&c=3&d=4", "name": "名字"])

终上一个路由的定义就出来了。

下一编,再讲一个路由的具体实现。 Swift路由组件(二)路由的实现

链接:https://juejin.cn/post/7032164814210203685/

收起阅读 »

Metal 框架之设置加载和存储操作

iOS
Metal 框架之设置加载和存储操作「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」 概述 通过设置 MTLLoadAction 和 MTLStoreAction 属性,可以定义渲染通道加载和存储 MTLRenderPassAtt...
继续阅读 »

Metal 框架之设置加载和存储操作


「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战


概述


通过设置 MTLLoadAction 和 MTLStoreAction 属性,可以定义渲染通道加载和存储 MTLRenderPassAttachmentDescriptor 对象 的方式。为渲染目标设置适当的操作,在渲染通道的开始(加载)或结束(存储)时,可以避免昂贵且不必要的工作。


在 texture 属性上设置渲染目标的纹理,在 loadAction 和 storeAction 属性上设置它的动作:



let renderPassDescriptor = MTLRenderPassDescriptor()


// Color render target

renderPassDescriptor.colorAttachments[0].texture = colorTexture

renderPassDescriptor.colorAttachments[0].loadAction = .clear

renderPassDescriptor.colorAttachments[0].storeAction = .store

// Depth render target

renderPassDescriptor.colorAttachments[0].texture = depthTexture

renderPassDescriptor.colorAttachments[0].loadAction = .dontCare

renderPassDescriptor.colorAttachments[0].storeAction = .dontCare


// Stencil render target

renderPassDescriptor.colorAttachments[0].texture = stencilTexture

renderPassDescriptor.colorAttachments[0].loadAction = .dontCare

renderPassDescriptor.colorAttachments[0].storeAction = .dontCare



选择加载操作


有多个加载操作选项可用,选择哪一个选项,取决于渲染目标的加载需求。



  • 不需要渲染目标的先前内容,而是渲染到其所有像素时,选择 MTLLoadAction.dontCare


此操作不会产生任何成本,并且在渲染通道开始时像素值始终未定义。


不需要考虑加载操作.png



  • 不需要渲染目标的先前内容,只需渲染其部分像素时,选择 MTLLoadAction.clear


此操作会产生将渲染目标的清除值写入每个像素的成本。


清楚成本.png



  • 需要渲染目标的先前内容,并且只渲染到它的一些像素时,选择 MTLLoadAction.load


此操作会产生从内存中加载每个像素的先前值的成本,明显慢于 MTLLoadAction.dontCare 或 MTLLoadAction.clear。


加载成本.png


选择存储操作


有多个存储操作选项可用,选择哪一个选项,取决渲染目标的存储需求。



  • 不需要保留渲染目标的内容,选择 MTLStoreAction.dontCare


 此操作不会产生任何成本,并且在渲染通道结束时像素值始终未定义。 在渲染通道中,为中间渲染目标选择此操作,之后不需要该中间的结果。 对于深度和模板渲染目标这是正确的选择。


不关心存储.png



  • 确实需要保留渲染目标的内容,选择 MTLStoreAction.store


此操作将每个像素的值存储到内存,会产生成本。 对于可绘制对象,这始终是正确的选择。


存储成本.png



  • 渲染目标是多重采样纹理


当执行多重采样时,可以选择存储渲染目标的多重采样或解析数据。 对于多重采样数据,其存储在渲染目标的 texture 属性中。 对于解析的数据,其存储在渲染目标的 resolveTexture 属性中。 多重采样时,请参考此表选择存储操作: 





































多重采样数据存储解析数据存储需要解析纹理需要的存储操作
是 是 是  MTLStoreAction.storeAndMultisampleResolve
 MTLStoreAction.store
 MTLStoreAction.multisampleResolve
  MTLStoreAction.dontCare

要在单个渲染通道中存储和解析多采样纹理,请始终选择 MTLStoreAction.storeAndMultisampleResolve 操作并使用单个渲染命令编码器。



  • 需要推迟存储选择 


在某些情况下,在收集更多渲染通道信息之前,可能不知道要对特定渲染目标使用哪个存储操作。 要推迟存储操作选择,请在创建 MTLRenderPassAttachmentDescriptor 对象时设置 MTLStoreAction.unknown 值。 设置未知的存储操作,可以避免消耗潜在的成本(因为设置另一个存储操作成本更高)。 但是,在完成对渲染通道的编码之前,必须指定有效的存储操作; 否则,会发生错误。


评估渲染通道之间的操作


可以在多个渲染过程中使用相同的渲染目标。 对于任何两个渲染通道之间的同一渲染目标,可能有多种加载和存储组合,选择哪一种组合,取决于渲染目标从一个渲染通道到另一个渲染通道的需求。



  • 下一个渲染通道中,不需要渲染目标的先前内容 


在第一个渲染通道中,选择 MTLStoreAction.dontCare 以避免存储渲染目标的内容。 在第二个渲染通道中,选择 MTLLoadAction.dontCare 或 MTLLoadAction.clear 以避免加载渲染目标的内容。


渲染通道的评估1.png


渲染通道的评估2.png



  • 需要在下一个渲染通道中使用渲染目标的先前内容


在第一个渲染通道中,选择 MTLStoreAction.store、MTLStoreAction.multisampleResolve 或 MTLStoreAction.storeAndMultisampleResolve 来存储渲染目标的内容。 在第二个渲染通道中,选择 MTLLoadAction.load 以加载渲染目标的内容。


渲染通道间内容传递.png


总结


本文介绍了根据渲染的需要,来设置渲染目标的加载和存储操作,合理的设置这些操作,可以避免昂贵且不必要的工作。通过图文详细介绍了,根据不同的渲染需求,设置不同的加载和存储操作达到的渲染效果。


作者:__sky
链接:https://juejin.cn/post/7033731322850148366
来源:稀土掘金

收起阅读 »

iOS 封装一个简易 UITableView 链式监听点击事件的功能思路与实现

iOS
废话开篇:RxSwift 对于其功能可以说是 swift 语言的高度封装了,但是它里面也用到了一些 OC 特性,比如交换方法实现。RxSwift 对于 UITableView 的点击事件就进行了二次封装,里面就交换了 respondsToSelector 方法...
继续阅读 »

废话开篇:RxSwift 对于其功能可以说是 swift 语言的高度封装了,但是它里面也用到了一些 OC 特性,比如交换方法实现。RxSwift 对于 UITableView 的点击事件就进行了二次封装,里面就交换了 respondsToSelector 方法及重写了消息转发机制下的 forwardInvocationRxSwift 源码太复杂,因此,简单用 OC 写一个 demo,来理解一下 RxSwift 对于 UITableView 的点击事件的绑定。


1、实现原理


1、修改一个对象的 respondsToSelector 方法,当进来判断的 sel 是要求继续进行的方法时,返回 YES,这里很明显就是判断 tableView:didSelectRowAtIndexPath: 这个方法。这里注意的是,一个对象即使没有遵循代理协议而只要你实现了代理方法,那么,它也是可以正常执行的,代理协议的遵守只是方便开发通过编译器提示去实现代理方法的。


也就是说,让对象作为 UITableView 可执行 tableView:didSelectRowAtIndexPath: 方法的代理,但是不去实现这个代理方法。


2、修改一个对象的 forwardInvocation 方法,当一个对象调用方法出现没有实现的时候就要进行消息转发了,那么,在转发的时候截获 tableView:didSelectRowAtIndexPath: 方法的参数,进而转到别的对象去执行后续操作。


2、代码效果


image.png


当点击 cell 的时候,就通过上图中的 block 进行响应。这里其实从风格上有点类似 RX,但是,这里并没有对创建中对象的内存进行管理,下面有提到,一般 RxSwift 会有 Disposable 对象的返回,它就是来控制序列中创建的对象何时释放的类,可以用属性保存 DisposeBag,让序列与当前使用类生命周期一致,也可以在方法执行的最后面直接执行 dispose 销毁。


3、UITableView 的 rxRegistSelected 方法的实现


这里创建一个 UITableView 的分类:


UITableView+KDS.h


image.png


UITableView+KDS.m


image.png


这里圈出1KDSDelegateProxy 类就是 tableView:didSelectRowAtIndexPath: 代理方法处理类,并且圈出2UITableView 提供了一个 delegate,并在此之前调用 saveNeedDelegateSel 保存了需要消息转发的代理方法,这个方法后面解释。


到了这里,UITableView 就可以执行 rxRegistSelected 这个方法了,并且要为这个方法返回的 block1 传一个 UITableViewCell 点击事件响应的 block2block2 是真正执行的点击事件具体实现,block1 仅为仿写 RAC 而写。


4、KDSDelegateProxy 对象的实现内容


KDSDelegateProxy.h


image.png


KDSDelegateProxy.m


image.png


image.png


4、KDSTableViewDelegateProxy


KDSTableViewDelegateProxy 是遵循 UITableViewDelegate 协议的对象,并为该对象保存外界传进来的 cell 点击的代理方法


image.png


5、总结与思考


RxSwift 远比上述复杂的多,换句话说个人能力有限说很难从一个百米高楼中去推断夯实地基的具体细节,因为毕竟不是参与施工人员,所以,这里也仅仅是个人思路。那么,说一下为什么没有类似 Disposable 对象,因为 demo 代码的的对象用的是 static 修饰的,如果不用全局变量,那么,作用域外对象就会销毁,代码也就无法运行了,所以,完全可以封装一个 WSLDisposable 类,在最里层的 block 里作为返回值,在最外层进行 dispose 销毁操作,来临时控制中间过程中的对象生命周期。或者封装类似 WSLDisposeBag,将它作为属性保存在例如控制器下,生命周期与当前 控制器一致。


好了,文章本意也仅分享,代码拙劣,大神勿笑。


作者:头疼脑胀的代码搬运工
链接:https://juejin.cn/post/7033679440613736456
收起阅读 »

iOS中的事件

iOS
iOS中的事件「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战」iOS中的事件在用户使用APP过程中,会产生各种各样的事件,可以分为三大类触摸事件(如点击...)加速器事件(如摇一摇...)远程控制事件(如耳机可以控制手机音量......
继续阅读 »

iOS中的事件

「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

iOS中的事件

  • 在用户使用APP过程中,会产生各种各样的事件,可以分为三大类
  • 触摸事件(如点击...)
  • 加速器事件(如摇一摇...)
  • 远程控制事件(如耳机可以控制手机音量...)

响应者对象(UIResponder)

说到触摸事件,首先需要了解一个概念:响应者对象

  • 在iOS中不是任何对象都能处理事件,只有继承了 UIResponder 的对象才能接收并处理事件,通常被称为“响应者对象”。如 UIApplicationUIViewControllerUIView 等等

  • UIResponder 内部提供了以下方法来处理事件

  • 触摸事件

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
    - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

  • 加速器事件

    - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
    - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

  • 远程控制方法

    - (void)remoteControlReceivedWithEvent:(UIEvent *)event;


UIView 的触摸事件处理

  • UIView 是 UIResponder 的子类,可以覆盖以下4个方法处理不同的触摸事件

  • 一根或者多根手指开始触摸 view,系统会自动调用 view 的下面方法

    - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;

  • 一根或者多根手指在 view 上移动,系统会自动调用 view 的下面方法

    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

  • 一根或者多根手指离开 view,系统会自动调用 view 的下面方法

        - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

  • 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用 view 的下面方法

    - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;


UITouch

  • 当用户用一根手指触摸屏幕时,会创建一个与手指关联的 UITouch 对象
  • 一根手指对应一个 UITouch 对象
  • UITouch 的作用:保存着跟手指相关的信息,比如触摸的位置、时间、阶段
  • 当手指移动时,系统会更新同一个 UITouch 对象,使之能够一直保存该手指在的触摸位置
  • 当手指离开屏幕时,系统会销毁相应的 UITouch 对象
  • UITouch 相关属性
    • window 触摸产生时所处的窗口
    • view 触摸产生时所处的视图
    • tapCount 短时间内按屏幕的次数,根据 tapCount 判断单击、双击或更多点击
    • timestamp 记录了触摸事件产生或变化的时间,单位是秒
    • phase 当前触摸事件所处的状态
  • UITouch 相关方法
    • 返回值表示触摸在 view 上的位置,这里返回的位置是针对view的坐标系的(以 view 的左上角为原点(0,0)),调用时如果传入的 view 参数是 nil 的话,返回的时触摸点在 window 的位置

      [touch locationInView:view];

    • 该方法记录了上一个点的位置

      [touch previousLocationInView:view];

注:
iPhone开发中,要避免使用双击事件
如果要在一个 view 中监听多个手指,需要设置属性

//需要view支持多个手
view.multipleTouchEnabled = YES;

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%ld",(long)touches.count); //2
}


UIEvent

  • 每产生一个事件,就会产生一个 UIEvent 对象
  • UIEvent 被称为事件对象,用于记录事件产生的时刻和类型
  • UIEvent 相关属性
    • 事件类型
      • type 枚举类型(触摸事件、加速器事件、远程控制事件)
      • subtype
    • timestamp 事件产生时间
  • UIEvent 相关方法
    • UIEvent 提供相应方法用于获取在某个 view 上面的接触对象(UITouch

简单示例

实现需求:一个按钮可以在屏幕任务拖拽

Kapture 2021-11-22 at 22.49.40.gif

1.自定义一个 UIImageView

@implementation InputImageView

- (instancetype)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    if (self) {        
        UIImage *image = [UIImage imageNamed:@"inputButton"];
        self.image = image;
        self.userInteractionEnabled = YES;
    }
    return self;
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch *touch = touches.anyObject;
//获取当前点
CGPoint currentPoint = [touch locationInView:self];
//获取上一个点的位置
CGPoint previousPoint = [touch previousLocationInView:self];
//获取x轴偏移量
    CGFloat offsetX = currentPoint.x - previousPoint.x;
    CGFloat offsetY = currentPoint.y - previousPoint.y;
    //修改view的位置
    self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}
@end

2.实际调用

#import "ViewController.h"
#import "InputImageView.h"

@interface ViewController ()
@property (nonatomic,strong) InputImageView *redView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    InputImageView *inputImageView = [[InputImageView alloc]initWithFrame:CGRectMake(150, 150, 56, 56)];
    [self.view addSubview:inputImageView];
}
@end


收起阅读 »

iOS 获取图片的主题色

iOS
iOS 获取图片的主题色目录1.需求背景2.代码部分3.使用效果及代码地址需求背景有时候我们会有这样的需求,用户从相册选择一张照片,返回展示的时候,除了展示照片还要让整体背景也是和照片相近颜色,最近自己写了一个图片加水印的项目,想加上此功能,然鹅谷歌搜了一圈发...
继续阅读 »

iOS 获取图片的主题色

目录

1.需求背景
2.代码部分
3.使用效果及代码地址

需求背景

  • 有时候我们会有这样的需求,用户从相册选择一张照片,返回展示的时候,除了展示照片还要让整体背景也是和照片相近颜色,最近自己写了一个图片加水印的项目,想加上此功能,然鹅谷歌搜了一圈发现全是OC代码写的,直接使用好像还存在一些问题,所以本文分别用swift和OC实现相关功能。

代码部分

  • 主要逻辑:
  1. 将图片按比例缩小,因为后续遍历图片每个像素点,循环次数是图片width x height,如果直接原图去遍历,可能一次循环就要跑几十万、百万次,需要时间非常久,所以要将图片缩小。
  2. 获取图片的所有像素的RGB值,每组RGB使用数组存储(可以根据自己的需求过滤部分颜色),然后用Set将数组装起来。
  3. 统计Set里面相同次数最多的色值,即是整个图片的主题色

swift实现代码:

ssss.png

调用:

selectedImage.subjectColor({[unowned self] color in
guard let subjectColor = color else { return }
self.view.backgroundColor = subjectColor
})

因为里面是两个for循环,时间复杂度是On^2,如果设置的width和Height比较大的话,会比较耗时,在主线程里面执行可能会卡住,所以使用了gcd开启子线程去执行,完成后回到主线程执行回调。

OC实现代码:

Snipaste_2021-11-24_16-19-45.png

使用效果及代码地址

54786116-bc1d-45ab-8eb7-2af3fe2a5520.gif

demon地址

收起阅读 »

2020-iOS最新面试题解析(原理篇)

runtime怎么添加属性、方法等ivar表示成员变量class_addIvarclass_addMethodclass_addPropertyclass_addProtocolclass_replaceProperty是否可以把比较耗时的操作放在NSNoti...
继续阅读 »

runtime怎么添加属性、方法等


  • ivar表示成员变量
  • class_addIvar
  • class_addMethod
  • class_addProperty
  • class_addProtocol
  • class_replaceProperty


是否可以把比较耗时的操作放在NSNotificationCenter中


  • 首先必须明确通知在哪个线程中发出,那么处理接受到通知的方法也在这个线程中调用
  • 如果在异步线程发的通知,那么可以执行比较耗时的操作;
  • 如果在主线程发的通知,那么就不可以执行比较耗时的操作


runtime 如何实现 weak 属性


首先要搞清楚weak属性的特点


weak策略表明该属性定义了一种“非拥有关系” (nonowning relationship)。
为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似;
然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)


那么runtime如何实现weak变量的自动置nil?


runtime对注册的类,会进行布局,会将 weak 对象放入一个 hash 表中。
用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会调用对象的 dealloc 方法,
假设 weak 指向的对象内存地址是a,那么就会以a为key,在这个 weak hash表中搜索,找到所有以a为key的 weak 对象,从而设置为 nil。


weak属性需要在dealloc中置nil么


  • 在ARC环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理
  • 即便是编译器不帮我们做这些,weak也不需要在dealloc中置nil
  • 在属性所指的对象遭到摧毁时,属性值也会清空


// 模拟下weak的setter方法,大致如下
- (void)setObject:(NSObject *)object
{
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
[object cyl_runAtDealloc:^{
_object = nil;
}];
}


一个Objective-C对象如何进行内存布局?(考虑有父类的情况)


  • 所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中
  • 父类的方法和自己的方法都会缓存在类对象的方法缓存中,类方法是缓存在元类对象中
  • 每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的如下信息
    • 对象方法列表
    • 成员变量的列表
    • 属性列表
  • 每个 Objective-C 对象都有相同的结构,如下图所示

Objective-C 对象的结构图

ISA指针

根类(NSObject)的实例变量

倒数第二层父类的实例变量

...

父类的实例变量

类的实例变量


  • 根类对象就是NSObject,它的super class指针指向nil
  • 类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类


一个objc对象的isa的指针指向什么?有什么作用?


  • 每一个对象内部都有一个isa指针,这个指针是指向它的真实类型
  • 根据这个指针就能知道将来调用哪个类的方法


下面的代码输出什么?


@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end


  • 答案:都输出 Son
  • 这个题目主要是考察关于objc中对 self 和 super 的理解:
    • self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者
    • 当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
    • 而当使用 super时,则从父类的方法列表中开始找。然后调用父类的这个方法
    • 调用[self class] 时,会转化成 objc_msgSend函数
id objc_msgSend(id self, SEL op, ...)
    • 调用 [super class]时,会转化成 objc_msgSendSuper函数
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
    • 第一个参数是 objc_super 这样一个结构体,其定义如下
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
    • 第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self
    • 第二个成员是记录当前类的父类是什么,告诉程序从父类中开始找方法,找到方法后,最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son
    • objc Runtime开源代码对- (Class)class方法的实现


    -(Class)class {
return object_getClass(self);
}



收起阅读 »

iOS 面试策略之算法基础1-3节

1. 基本数据结构数组数组是最基本的数据结构。在 Swift 中,以前 Objective-C 时代中将 NSMutableArray 和 NSArray 分开的做法,被统一到了唯一的数据结构 —— Array 。虽然看上去就一种数据结构,其实它的实现有三种:...
继续阅读 »

1. 基本数据结构


数组


数组是最基本的数据结构。在 Swift 中,以前 Objective-C 时代中将 NSMutableArray 和 NSArray 分开的做法,被统一到了唯一的数据结构 —— Array 。虽然看上去就一种数据结构,其实它的实现有三种:


  • ContiguousArray:效率最高,元素分配在连续的内存上。如果数组是值类型(栈上操作),则 Swift 会自动调用 Array 的这种实现;如果注重效率,推荐声明这种类型,尤其是在大量元素是类时,这样做效果会很好。
  • Array:会自动桥接到 Objective-C 中的 NSArray 上,如果是值类型,其性能与 ContiguousArray 无差别。
  • ArraySlice:它不是一个新的数组,只是一个片段,在内存上与原数组享用同一区域。


下面是数组最基本的一些运用。


// 声明一个不可修改的数组
let nums = [1, 2, 3]
let nums = [Int](repeating: 0, count: 5)

// 声明一个可以修改的数组
var nums = [3, 1, 2]
// 增加一个元素
nums.append(4)
// 对原数组进行升序排序
nums.sort()
// 对原数组进行降序排序
nums.sort(by: >)
// 将原数组除了最后一个以外的所有元素赋值给另一个数组
// 注意:nums[0..<nums.count - 1] 返回的是 ArraySlice,不是 Array
let anotherNums = Array(nums[0 ..< nums.count - 1])


不要小看这些简单的操作:数组可以依靠它们实现更多的数据结构。Swift 虽然不像 Java 中有现成的队列和栈,但我们完全可以用数组配合最简单的操作实现这些数据结构,下面就是用数组实现栈的示例代码。


// 用数组实现栈
struct Stack<Element> {
private var stack: [Element]
var isEmpty: Bool { return stack.isEmpty }
var peek: AnyObject? { return stack.last }

init() {
stack = [Element]()
}

mutating func push(_ element: Element) {
stack.append(object)
}

mutating func pop() -> Element? {
return stack.popLast()
}
}

// 初始化一个栈
let stack = Stack<String>()


最后特别强调一个操作:reserveCapacity()。它用于为原数组预留空间,防止数组在增加和删除元素时反复申请内存空间或是创建新数组,特别适用于创建和 removeAll() 时候进行调用,为整段代码起到提高性能的作用。


字典和集合


字典和集合(这里专指HashSet)经常被使用的原因在于,查找数据的时间复杂度为 O(1)。

一般字典和集合要求它们的 Key 都必须遵守 Hashable 协议,Cocoa 中的基本数据类型都

满足这一点;自定义的 class 需要实现 Hashable,而又因为 Hashable 是对 Equable 的扩展,

所以还要重载 == 运算符。


下面是关于字典和集合的一些实用操作:


let primeNums: Set = [3, 5, 7, 11, 13]
let oddNums: Set = [1, 3, 5, 7, 9]

// 交集、并集、差集
let primeAndOddNum = primeNums.intersection(oddNums)
let primeOrOddNum = primeNums.union(oddNums)
let oddNotPrimNum = oddNums.subtracting(primeNums)

// 用字典和高阶函数计算字符串中每个字符的出现频率,结果 [“h”:1, “e”:1, “l”:2, “o”:1]
Dictionary("hello".map { ($0, 1) }, uniquingKeysWith: +)


集合和字典在实战中经常与数组配合使用,请看下面这道算法题:


给一个整型数组和一个目标值,判断数组中是否有两个数字之和等于目标值


这道题是传说中经典的 “2Sum”,我们已经有一个数组记为 nums,也有一个目标值记为 target,最后要返回一个 Bool 值。


最粗暴的方法就是每次选中一个数,然后遍历整个数组,判断是否有另一个数使两者之和为 target。这种做法时间复杂度为 O(n^2)。


采用集合可以优化时间复杂度。在遍历数组的过程中,用集合每次保存当前值。假如集合中已经有了目标值减去当前值,则证明在之前的遍历中一定有一个数与当前值之和等于目标值。这种做法时间复杂度为 O(n),代码如下。


func twoSum(nums: [Int], _ target: Int) -> Bool {
var set = Set<Int>()

for num in nums {
if set.contains(target - num) {
return true
}

set.insert(num)
}

return false
}


如果把题目稍微修改下,变为


给定一个整型数组中有且仅有两个数字之和等于目标值,求两个数字在数组中的序号


思路与上题基本类似,但是为了方便拿到序列号,我们采用字典,时间复杂度依然是 O(n)。代码如下。


func twoSum(nums: [Int], _ target: Int) -> [Int] {
var dict = [Int: Int]()

for (i, num) in nums.enumerated() {
if let lastIndex = dict[target - num] {
return [lastIndex, i]
} else {
dict[num] = i
}
}

fatalError("No valid output!")
}


字符串和字符


字符串在算法实战中极其常见。在 Swift 中,字符串不同于其他语言(包括 Objective-C),它是值类型而非引用类型,它是多个字符构成的序列(并非数组)。首先还是列举一下字符串的通常用法。


// 字符串和数字之间的转换
let str = "3"
let num = Int(str)
let number = 3
let string = String(num)

// 字符串长度
let len = str.count

// 访问字符串中的单个字符,时间复杂度为O(1)
let char = str[str.index(str.startIndex, offsetBy: n)]

// 修改字符串
str.remove(at: n)
str.append("c")
str += "hello world"

// 检测字符串是否是由数字构成
func isStrNum(str: String) -> Bool {
return Int(str) != nil
}

// 将字符串按字母排序(不考虑大小写)
func sortStr(str: String) -> String {
return String(str.sorted())
}

// 判断字符是否为字母
char.isLetter

// 判断字符是否为数字
char.isNumber

// 得到字符的 ASCII 数值
char.asciiValue


关于字符串,我们来一起看一道以前的 Google 面试题。


给一个字符串,将其按照单词顺序进行反转。比如说 s 是 "the sky is blue",

那么反转就是 "blue is sky the"。


这道题目一看好简单,不就是反转字符串的翻版吗?这种方法有以下两个问题


  • 每个单词长度不一样
  • 空格需要特殊处理
    这样一来代码写起来会很繁琐而且容易出错。不如我们先实现一个字符串翻转的方法。


fileprivate func reverse<T>(_ chars: inout [T], _ start: Int, _ end: Int) {
var start = start, end = end

while start < end {
swap(&chars, start, end)
start += 1
end -= 1
}
}

fileprivate func swap<T>(_ chars: inout [T], _ p: Int, _ q: Int) {
(chars[p], chars[q]) = (chars[q], chars[p])
}


有了这个方法,我们就可以实行下面两种字符串翻转:


  • 整个字符串翻转,"the sky is blue" -> "eulb si yks eht"
  • 每个单词作为一个字符串单独翻转,"eulb si yks eht" -> "blue is sky the"
    整体思路有了,我们就可以解决这道问题了


func reverseWords(s: String?) -> String? {
guard let s = s else {
return nil
}

var chars = Array(s), start = 0
reverse(&chars, 0, chars.count - 1)

for i in 0 ..< chars.count {
if i == chars.count - 1 || chars[i + 1] == " " {
reverse(&chars, start, i)
start = i + 2
}
}

return String(chars)
}


时间复杂度还是 O(n),整体思路和代码简单很多。


总结


在 Swift 中,数组、字符串、集合以及字典是最基本的数据结构,但是围绕这些数据结构的问题层出不穷。而在日常开发中,它们使用起来也非常高效(栈上运行)和安全(无需顾虑线程问题),因为他们都是值类型。


2. 链表


本节我们一起来探讨用 Swift 如何实现链表以及链表相关的技巧。


基本概念


对于链表的概念,实在是基本概念太多,这里不做赘述。我们直接来实现链表节点。


class ListNode { 
var val: Int
var next: ListNode?

init(_ val: Int) {
self.val = val
}
}


有了节点,就可以实现链表了。


class LinkedList {
var head: ListNode?
var tail: ListNode?

// 头插法
func appendToHead(_ val: Int) {
let node = ListNode(val)

if let _ = head {
node.next = head
} else {
tail = node
}

head = node
}

// 头插法
func appendToTail(_ val: Int) {
let node = ListNode(val)

if let _ = tail {
tail!.next = node
} else {
head = node
}

tail = node
}
}


有了上面的基本操作,我们来看如何解决复杂的问题。


Dummy 节点和尾插法


话不多说,我们直接先来看下面一道题目。


给一个链表和一个值 x,要求将链表中所有小于 x 的值放到左边,所有大于等于 x 的值放到右边。原链表的节点顺序不能变。

例:1->5->3->2->4->2,给定x = 3。则我们要返回1->2->2->5->3->4


直觉告诉我们,这题要先处理左边(比 x 小的节点),然后再处理右边(比 x 大的节点),最后再把左右两边拼起来。


思路有了,再把题目抽象一下,就是要实现这样一个函数:


func partition(_ head: ListNode?, _ x: Int) -> ListNode? {}


即我们有给定链表的头节点,有给定的x值,要求返回新链表的头结点。接下来我们要想:怎么处理左边?怎么处理右边?处理完后怎么拼接?


先来看怎么处理左边。我们不妨把这个题目先变简单一点:


给一个链表和一个值 x,要求只保留链表中所有小于 x 的值,原链表的节点顺序不能变。


例:1->5->3->2->4->2,给定x = 3。则我们要返回 1->2->2


我们只要采用尾插法,遍历链表,将小于 x 值的节点接入新的链表即可。代码如下:


func getLeftList(_ head: ListNode?, _ x: Int) -> ListNode? { 
let dummy = ListNode(0)
var pre = dummy, node = head

while node != nil {
if node!.val < x {
pre.next = node
pre = node!
}
node = node!.next
}

// 防止构成环
pre.next = nil
return dummy.next
}


注意,上面的代码我们引入了 Dummy 节点,它的作用就是作为一个虚拟的头前结点。我们引入它的原因是我们不知道要返回的新链表的头结点是哪一个,它有可能是原链表的第一个节点,可能在原链表的中间,也可能在最后,甚至可能不存在(nil)。而 Dummy 节点的引入可以巧妙的涵盖所有以上情况,我们可以用 dummy.next 方便得返回最终需要的头结点。


现在我们解决了左边,右边也是同样处理。接着只要让左边的尾节点指向右边的头结点即可。全部代码如下:


func partition(_ head: ListNode?, _ x: Int) -> ListNode? {
// 引入Dummy节点
let prevDummy = ListNode(0), postDummy = ListNode(0)
var prev = prevDummy, post = postDummy

var node = head

// 用尾插法处理左边和右边
while node != nil {
if node!.val < x {
prev.next = node
prev = node!
} else {
post.next = node
post = node!
}
node = node!.next
}

// 防止构成环
post.next = nil
// 左右拼接
prev.next = postDummy.next

return prevDummy.next
}


注意这句 post.next = nil,这是为了防止链表循环指向构成环,是必须的但是很容易忽略的一步。

刚才我们提到了环,那么怎么检测链表中是否有环存在呢?



快行指针


笔者理解快行指针,就是两个指针访问链表,一个在前一个在后,或者一个移动快另一个移动慢,这就是快行指针。来看一道简单的面试题:


如何检测一个链表中是否有环?


答案是用两个指针同时访问链表,其中一个的速度是另一个的 2 倍,如果他们相等了,那么这个链表就有环了,这就是快行指针的实际使用。代码如下:


func hasCycle(_ head: ListNode?) -> Bool { 
var slow = head
var fast = head

while fast != nil && fast!.next != nil {
slow = slow!.next
fast = fast!.next!.next

if slow === fast {
return true
}
}

return false
}


再举一个快行指针一前一后的例子,看下面这道题。


删除链表中倒数第 n 个节点。例:1->2->3->4->5,n = 2。返回1->2->3->5。

注意:给定 n 的长度小于等于链表的长度。


解题思路依然是快行指针,这次两个指针移动速度相同。但是一开始,第一个指针(指向头结点之前)就落后第二个指针 n 个节点。接着两者同时移动,当第二个移动到尾节点时,第一个节点的下一个节点就是我们要删除的节点。代码如下:


func removeNthFromEnd(head: ListNode?, _ n: Int) -> ListNode? {
guard let head = head else {
return nil
}

let dummy = ListNode(0)
dummy.next = head
var prev: ListNode? = dummy
var post: ListNode? = dummy

// 设置后一个节点初始位置
for _ in 0 ..< n {
if post == nil {
break
}
post = post!.next
}

// 同时移动前后节点
while post != nil && post!.next != nil {
prev = prev!.next
post = post!.next
}

// 删除节点
prev!.next = prev!.next!.next

return dummy.next
}


这里还用到了 Dummy 节点,因为有可能我们要删除的是头结点。


总结


这次我们用 Swift 实现了链表的基本结构,并且实战了链表的几个技巧。在结尾处,我还想强调一下 Swift 处理链表问题的两个细节问题:


  • 一定要注意头结点可能就是 nil。所以给定链表,我们要看清楚 head 是不是 optional,在判断是不是要处理这种边界条件。
  • 注意每个节点的 next 可能是 nil。如果不为 nil,请用"!"修饰变量。在赋值的时候,也请注意"!"将 optional 节点传给非 optional 节点的情况。


3. 栈和队列


这期我们来讨论一下栈和队列。在 Swift 中,没有内设的栈和队列,很多扩展库中使用 Generic Type 来实现栈或是队列。正规的做法使用链表来实现,这样可以保证加入和删除的时间复杂度是 O(1)。然而笔者觉得最实用的实现方法是使用数组,因为 Swift 没有现成的链表,而数组又有很多的 API 可以直接使用,非常方便。


基本概念


对于栈来说,我们需要了解以下几点:


  • 栈是后进先出的结构。你可以理解成有好几个盘子要垒成一叠,哪个盘子最后叠上去,下次使用的时候它就最先被抽出去。
  • 在 iOS 开发中,如果你要在你的 App 中添加撤销操作(比如删除图片,恢复删除图片),那么栈是首选数据结构
  • 无论在面试还是写 App 中,只关注栈的这几个基本操作:push, pop, isEmpty, peek, size。


protocol Stack {
/// 持有的元素类型
associatedtype Element

/// 是否为空
var isEmpty: Bool { get }
/// 栈的大小
var size: Int { get }
/// 栈顶元素
var peek: Element? { get }

/// 进栈
mutating func push(_ newElement: Element)
/// 出栈
mutating func pop() -> Element?
}

struct IntegerStack: Stack {
typealias Element = Int

var isEmpty: Bool { return stack.isEmpty }
var size: Int { return stack.count }
var peek: Element? { return stack.last }

private var stack = [Element]()

mutating func push(_ newElement: Element) {
stack.append(newElement)
}

mutating func pop() -> Element? {
return stack.popLast()
}
}


对于队列来说,我们需要了解以下几点:


  • 队列是先进先出的结构。这个正好就像现实生活中排队买票,谁先来排队,谁先买到票。
  • iOS 开发中多线程的 GCD 和 NSOperationQueue 就是基于队列实现的。
  • 关于队列我们只关注这几个操作:enqueue, dequeue, isEmpty, peek, size。


protocol Queue {
/// 持有的元素类型
associatedtype Element

/// 是否为空
var isEmpty: Bool { get }
/// 队列的大小
var size: Int { get }
/// 队首元素
var peek: Element? { get }

/// 入队
mutating func enqueue(_ newElement: Element)
/// 出队
mutating func dequeue() -> Element?
}

struct IntegerQueue: Queue {
typealias Element = Int

var isEmpty: Bool { return left.isEmpty && right.isEmpty }
var size: Int { return left.count + right.count }
var peek: Element? { return left.isEmpty ? right.first : left.last }

private var left = [Element]()
private var right = [Element]()

mutating func enqueue(_ newElement: Element) {
right.append(newElement)
}

mutating func dequeue() -> Element? {
if left.isEmpty {
left = right.reversed()
right.removeAll()
}
return left.popLast()
}
}


栈和队列互相转化


处理栈和队列问题,最经典的一个思路就是使用两个栈/队列来解决问题。也就是说在原栈/队列的基础上,我们用一个协助栈/队列来帮助我们简化算法,这是一种空间换时间的思路。下面是示例代码:


// 用栈实现队列
struct MyQueue {
var stackA: Stack
var stackB: Stack

var isEmpty: Bool {
return stackA.isEmpty
}

var peek: Any? {
get {
shift()
return stackB.peek
}
}

var size: Int {
get {
return stackA.size + stackB.size
}
}

init() {
stackA = Stack()
stackB = Stack()
}

func enqueue(object: Any) {
stackA.push(object);
}

func dequeue() -> Any? {
shift()
return stackB.pop();
}

fileprivate func shift() {
if stackB.isEmpty {
while !stackA.isEmpty {
stackB.push(stackA.pop()!);
}
}
}
}

// 用队列实现栈
struct MyStack {
var queueA: Queue
var queueB: Queue

init() {
queueA = Queue()
queueB = Queue()
}

var isEmpty: Bool {
return queueA.isEmpty
}

var peek: Any? {
get {
if isEmpty {
return nil
}

shift()
let peekObj = queueA.peek
queueB.enqueue(queueA.dequeue()!)
swap()
return peekObj
}
}

var size: Int {
return queueA.size
}

func push(object: Any) {
queueA.enqueue(object)
}

func pop() -> Any? {
if isEmpty {
return nil
}

shift()
let popObject = queueA.dequeue()
swap()
return popObject
}

private func shift() {
while queueA.size > 1 {
queueB.enqueue(queueA.dequeue()!)
}
}

private func swap() {
(queueA, queueB) = (queueB, queueA)
}
}


上面两种实现方法都是使用两个相同的数据结构,然后将元素由其中一个转向另一个,从而形成一种完全不同的数据。


面试题实战


给一个文件的绝对路径,将其简化。举个例子,路径是 "/home/",简化后为 "/home";路径是"/a/./b/../../c/",简化后为 "/c"。


这是一道 Facebook 的面试题。这道题目其实就是平常在终端里面敲的 cd、pwd 等基本命令所得到的路径。


根据常识,我们知道以下规则:


  • “. ” 代表当前路径。比如 “ /a/. ” 实际上就是 “/a”,无论输入多少个 “ . ” 都返回当前目录
  • “..”代表上一级目录。比如 “/a/b/.. ” 实际上就是 “ /a”,也就是说先进入 “a” 目录,再进入其下的 “b” 目录,再返回 “b” 目录的上一层,也就是 “a” 目录。


然后针对以上信息,我们可以得出以下思路:


  1. 首先输入是个 String,代表路径。输出要求也是 String, 同样代表路径;
  2. 我们可以把 input 根据 “/” 符号去拆分,比如 "/a/b/./../d/" 就拆成了一个String数组["a", "b", ".", "..", "d"];
  1. 创立一个栈然后遍历拆分后的 String 数组,对于一般 String ,直接加入到栈中,对于 ".." 那我们就对栈做 pop 操作,其他情况不错处理。


思路有了,代码也就有了


func simplifyPath(path: String) -> String {
// 用数组来实现栈的功能
var pathStack = [String]()
// 拆分原路径
let paths = path.split(separatedBy: "/")

for path in paths {
// 对于 "." 我们直接跳过
guard path != "." else {
continue
}
// 对于 ".." 我们使用pop操作
if path == ".." {
if (!pathStack.isEmpty) {
pathStack.removeLast()
}
// 对于太注意空数组的特殊情况
} else if path != "" {
pathStack.append(path)
}
}
// 将栈中的内容转化为优化后的新路径
return "/" + pathStack.joined(separator: "/")
}


上面代码除了完成了基本思路,还考虑了大量的特殊情况、异常情况。这也是硅谷面试考察的一个方面:面试者思路的严谨,对边界条件的充分考虑,以及代码的风格规范。


总结


在 Swift 中,栈和队列是比较特殊的数据结构,笔者认为最实用的实现和运用方法是利用数组。虽然它们本身比较抽象,却是很多复杂数据结构和 iOS 开发中的功能模块的基础。这也是一个工程师进阶之路理应熟练掌握的两种数据结构。

收起阅读 »

iOS 面试简单准备

1.简历的准备在面试中,我发现很多人都不能写好一份求职简历,所以我们首先谈谈如何写一份针对互联网公司的求职简历。1.简洁的艺术互联网公司和传统企业有着很大的区别,通常情况下,创新和效率是互联网公司比较追求的公司文化,所以体现在简历上,就是超过一页的简历通常会被...
继续阅读 »

1.简历的准备


在面试中,我发现很多人都不能写好一份求职简历,所以我们首先谈谈如何写一份针对互联网公司的求职简历。


1.简洁的艺术


互联网公司和传统企业有着很大的区别,通常情况下,创新和效率是互联网公司比较追求的公司文化,所以体现在简历上,就是超过一页的简历通常会被认为不够专业。


更麻烦的是,多数超过一页的简历很可能在 HR 手中就被过滤掉了。因为 HR 每天会收到大量的简历,一般情况下每份简历在手中的停留时间也就 10 秒钟左右。而超过一页的简历会需要更多的时间去寻找简历中的有价值部分,对于 HR 来说,她更倾向于认为这种人通常是不靠谱的,因为写个简历都不懂行规,为什么还要给他面试机会呢?


那么我们应该如何精简简历呢? 简单说来就是一个字:删!


删掉不必要的自我介绍信息。很多求职者会将自己在学校所学的课程罗列上去,例如:C 语言,数据结构,数学分析⋯⋯好家伙,一写就是几十门,还放在简历的最上面,就怕面试官看不见。对于这类信息,一个字:删!面试官不关心你上了哪些课程,而且在全中国,大家上的课程也都大同小异,所以没必要写出来。


删除不必要的工作或实习、实践经历。如果你找一份程序员的工作,那么你参加了奥运会的志愿者活动,并且拿到了奖励或者你参加学校的辩论队,获得了最佳辩手这些经历通常是不相关的。诸如此类的还有你帮导师代课,讲了和工作不相关的某某专业课,或者你在学生会工作等等。删除不相关的工作、实习或实践内容可以保证你的简历干净。当然,如果你实在没得可写,比如你是应届生,一点实习经历都没有,那可以适当写一两条,保证你能写够一页的简历,但是那两条也要注意是强调你的团队合作能力或者执行力之类的技能,因为这些才是面试官感兴趣的。


删除不必要的证书。最多写个 4、6 级的证书,什么教师资格证,中高级程序员证,还有国内的各种什么认证,都是没有人关心的。


删除不必要的细节。作为 iOS 开发的面试官,很多求职者在介绍自己的 iOS 项目经历的时候,介绍了这个工程用的工作环境是 Mac OS,使用的机器是 Mac Mini,编译器是 Xcode,能够运行在 iOS 什么版本的环境。还有一些人,把这个项目用到的开源库都写上啦,什么 AFNetworking, CocoaPods 啥的。这些其实都不是重点,请删掉。后面我会讲,你应该如何介绍你的 iOS 项目经历。


自我评价,这个部分是应届生最喜欢写的,各种有没有的优点都写上,例如:


性格开朗、稳重、有活力,待人热情、真诚;工作认真负责,积极主动,能吃苦耐劳,勇于承受压力,勇于创新;有很强的组织能力和团队协作精神,具有较强的适应能力;纪律性强,工作积极配合;意志坚强,具有较强的无私奉献精神。对待工作认真负责,善于沟通、协调有较强的组织能力与团队精神;活泼开朗、乐观上进、有爱心并善于施教并行;上进心强、勤于学习能不断提高自身的能力与综合素质。


这些内容在面试的时候不太好考查,都可以删掉。通常如果有 HR 面的话,HR 自然会考查一些你的沟通,抗压,性格等软实力。


我相信,不管你是刚毕业的学生,还是工作十年的老手,你都可以把你的简历精简到一页 A4 纸上。记住,简洁是一种美,一种效率,也是一种艺术。


2.重要的信息写在最前面


将你觉得最吸引人的地方写在最前面。如果你有牛逼公司的实习,那就把实习经历写在最前面,如果你在一个牛逼的实验室里面做科研,就把研究成果和论文写出来,如果你有获得过比较牛逼的比赛名次(例如 Google code jam, ACM 比赛之类),写上绝对吸引眼球。


所以,每个人的简历的介绍顺序应该都是不一样的,不要在网上下载一个模板,然后就一项一项地填:教育经历,实习经历,得奖经历,个人爱好,这样的简历毫无吸引力,也无法突出你的特点。


除了你的个人特点是重要信息外,你的手机号、邮箱、毕业院校、专业以及毕业时间这些也都是非常重要的,一定要写在简历最上面。


3.不要简单地罗列工作经历


不要简单地说你开发了某某 iOS 客户端。这样简单的罗列你的作品集并不能让面试官很好地了解你的能力,当然,真正在面试时面试官可能会仔细询问,但是一份好的简历,应该省去一些面试官额外询问你的工作细节的时间。


具体的做法是:详细的描述你对于某某 iOS 客户端的贡献。主要包括:你参与了多少比例功能的开发? 你解决了哪些开发中的有挑战的问题? 你是不是技术负责人?


而且,通过你反思这些贡献,你也可以达到自我审视,如果你发现这个项目你根本什么有价值的贡献都没做,就打了打酱油,那你最好不要写在简历上,否则当面试官在面试时问起时,你会很难回答,最终让他发现你的这个项目经历根本一文不值时,肯定会给一个负面的印象。


4.不要写任何虚假或夸大的信息


刚刚毕业的学生都喜欢写精通 Java,精通 C/C++,其实代码可能写了不到 1 万行。我觉得你要精通某个语言,至少得写 50 万行这个语言的代码才行,而且要对语言的各种内部机制和原理有了解。那些宣称精通 Java 的同学,连 Java 如何做内存回收,如何做泛型支持,如何做自动 boxing 和 unboxing 的都不知道,真不知道为什么要写精通二字。


任何夸大或虚假的信息,在面试时被发现,会造成极差的面试印象。所以你如果对某个知识一知半解,要么就写 “使用过” 某某,要么就干脆不写。如果你简历实在太单薄,没办法写上了一些自己打酱油的项目,被问起来怎么办? 请看看下面的故事:


我面试过一个同学,他在面试时非常诚实。我问他一些简历上的东西,他如果不会,就会老实说,这个我只是使用了一下,确实不清楚细节。对于一些没有技术含量的项目,他也会老实说,这个项目他做的工作比较少,主要是别人在做。最后他还会补充说,“我自认为自己数据结构和算法还不错,要不你问我这方面的知识吧。”


这倒是一个不错的办法,对于一个没有项目经验,但是聪明并且数据结构和算法基础知识扎实的应届生,其实我们是非常愿意培养的。很多人以为公司面试是看经验,希望招进来就能干活,其实不是的,至少我们现在以及我以前在网易招人,面试的是对方的潜力,潜力越大,可塑性好,未来进步得也更快;一些资质平庸,却经验稍微丰富一点的开发者,相比聪明好学的面试者,后劲是不足的。


总之,不要写任何虚假或夸大的信息,即使你最终骗得过面试官,进了某公司,如果能力不够,在最初的试用期内,也很可能因为能力不足而被开掉。


5.留下更多信息


刚刚说到,简历最好写够一张 A4 纸即可,那么你如果想留下更多可供面试官参考的信息怎么办呢?其实你可以附上更多的参考链接,这样如果面试官对你感兴趣,自然会仔细去查阅这些链接。对于 iOS 面试来说,GitHub 上面的开源项目地址、博客地址都是不错的参考信息。如果你在微博上也频繁讨论技术,也可以附上微博地址。


我特别建议大家如果有精力,可以好好维护一下自己的博客或者 GitHub 上的开源代码。因为如果你打算把这些写到简历上,让面试官去上面仔细评价你的水平,你就应该对上面的内容做到足够认真的准备。否则,本来面试完面试官还挺感兴趣的,结果一看你的博客和开源代码,评价立刻降低,就得不偿失了。


6.不要附加任何可能带来负面印象的信息


任何与招聘工作无关的东西,尽量不要提。有些信息提了可能有加分,也可能有减分,取决于具体的面试官。下面我罗列一下我认为是减分的信息。


1)个人照片


不要在简历中附加个人照片。个人长相属于与工作能力不相关的信息,也许你觉得你长得很帅,那你怎么知道你的样子不和面试官的情敌长得一样? 也许你长得很漂亮,那么你怎么知道 HR 是否被你长得一样的小三把男朋友抢了? 我说得有点极端,那人们对于长相的评价标准确实千差万别,萝卜青菜各有所爱,加上可能有一些潜在的极端情况,所以没必要附加这部分信息。这属于加了可能有加分,也可能有减分的情况。


2)有风险的爱好


不要写各种奇怪的爱好。喜欢打游戏、抽烟、喝酒,这类可能带来负面印象的爱好最好不要写。的确有些公司会有这种一起联机玩游戏或者喝酒的文化,不过除非你明确清楚对于目标公司,写上会是加分项,否则还是不写为妙。


3)使用 PDF 格式


不要使用 Word 格式的简历,要使用 PDF 的格式。我在招 iOS 程序员时,好多人的简历都是 Word 格式的,我都怀疑这些人是否有 Mac 电脑。因为 Mac 下的 office 那么难用,公司好多人机器上都没有 Mac 版 office。我真怀疑这些人真是的想投简历么? PDF 格式的简历通常能展现出简历的专业性。


4)QQ号码邮箱


不要使用 QQ 号开头的 QQ 邮箱,例如 12345@qq.com ,邮箱的事情我之前简单说过,有些人很在乎这个,有些人觉得无所谓,我个人对用数字开头的 QQ 邮箱的求职者不会有加分,但是对使用 Gmail 邮箱的求职者有加分。因为这涉及到个人的工作效率,使用 Gmail 的人通常会使用邮件组,过滤器,IMAP 协议,标签,这些都有助于提高工作效率。如果你非要使用 QQ 邮箱,也应该申请一个有意义的邮箱名,例如 tangqiaoboy@qq.com 。


7.职业培训信息


不要写参加过某某培训公司的 iOS 培训,特别是那种一、两个月的速成培训。这对于我和身边很多面试官来说,绝对是负分。


这个道理似乎有点奇怪,因为我们从小都是由老师教授新知识的。我自己也实验过,掌握同样的高中课本上的知识,自己自学的速度通常比老师讲授的速度要慢一倍的时间。即一个知识点,如果你自己要看 2 小时的书才能理解的话,有好的老师给你讲解的话,只需要一个小时就够了。所以,我一直希望在学习各种东西的时候都能去听一些课程,因为我认为这样节省了我学习的时间。


但是这个道理在程序员这个领域行不通,为什么这么说呢?原因有两点:


  1. 计算机编程相关的知识更新速度很快。同时,国内的 IT 类资料的翻译质量相当差,原创的优秀书籍也很少。所以,我们通常需要靠阅读英文才能掌握最新的资料。拿 iOS 来说,每年 WWDC 的资料都非常重要,而这些内容涉及版权,国内培训机构很难快速整理成教材。
  2. 计算机编程知识需要较多的专业知识积累和实践。学校的老师更多只能做入门性的教学工作。
    如果一个培训机构有一个老师,他强到能够通过自己做一些项目来积累很多专业知识和实践,并且不断地从国外资料上学习最新的技术。那么这个人在企业里面会比在国内的培训机构更有施展自己能力的空间。国内的培训机构因为受众面的原因,基本上还是培养那种初级的程序员新手,所以对老师的新技术学习速度要求不会那么高,自然老师也不会花那么时间在新技术研究上。但是企业就不一样了,企业需要不停地利用新技术来增强自己的产品竞争力,所以对于 IT 企业来说,产品的竞争就是人才的竞争,所以给优秀的人能够开出很高的薪水。
    所以,我们不能期望从 IT 类培训机构中学习到最新的技术,一切只能通过我们自学。当然,自学之后在同行之间相互交流,对于我们的技术成长也是很有用的。小结



上图是本节讨论的总结,在简历准备上,我们需要考虑简历的简洁性等各种注意事项。


2.寻找机会


1.寻找内推机会

其实,最好的面试机会都不是公开渠道的。好的机会都隐藏于各种内部推荐之中。通过内部推荐,你可以更加了解目标工作的团队和内容,另外内部推荐通常也可以跳过简历筛选环节,直接参加笔试或面试。我所在的猿辅导公司为内推设立了非常高的奖金激励,因为我们发现,综合各种渠道来看,内推的渠道的人才的简历质量最高,面试通过率最高的。


所以,如果你是学生,找找你在各大公司的师兄师姐内推;如果你已经工作了,你可以找前同事或者通过一些社交活动认识的技术同行内推。


大部分情况下,如果在目标公司你完全没有认识的人,你也可以找机会来认识几个。比如你可以通过微博、知乎、Twitter、GitHub 来结交新的朋友。然后双方聊天如果愉快的话,我相信内推这种举手之劳的事情对方不会拒绝的。


如果你都工作 5 年以上,还是没有建立足够好的社交圈子帮助你内推,那可能你需要做很多的社交活动交一些朋友。


2.其它常见的渠道

内推之外,其它的公开招聘渠道通常都要差一些。现在也有一些专门针对互联网行业的招聘网站,例如拉勾、100offer 这类,它们也是不错的渠道,可以找到相关的招聘信息。


但因为这类公开渠道简历投放数量巨大,通常 HR 那边就会比较严格地筛选简历,拿我们公司来说,通常在这些渠道看 20 份简历,才会有 1 份愿意约面的简历。而且 HR 会只挑比较好的学校或者公司的候选人,也不排除还有例如笔试这种更多的面试流程。但是面试经验都是慢慢积累的,建议你也可以尝试这些渠道。


3.面试流程


1.流程简述


就我所知,大部分的 iOS 公司的面试流程都大同小异。我们先简述一下大体的流程,然后再详细讨论。


在面试的刚开始,面试官通常会要求你做一个简短的自我介绍。然后面试官可能会和你聊聊你过去的实习项目或者工作内容。接着面试官可能会问你一些具体的技术问题,有经验的面试官会尽量找一些和你过去工作相关的技术问题。最后,有些公司会选择让你当场写写代码,Facebook 会让你在白板上写代码,国内的更多是让你在 A4 纸上写。有一些公司也会问一些系统设计方面的问题,考查你的整体架构能力。在面试快要结束时,通常面试官还会问你有没有别的什么问题。


以上这些流程,不同公司可能会跳过某些环节。比如有一些公司就不会考察当场在白板或 A4 纸上写代码。有些公司可能跳过问简历的环节直接写代码,特别是校园招聘的时候,因为应届生通常项目经验较少。面试流程图如下所示:



2.自我介绍


自我介绍通常是面试中最简单、最好准备的环节。


一个好的自我介绍应该结合公司的招聘职位来做定制。比如公司有硬件的背景,就应该介绍一下在硬件上的经验或者兴趣;公司如果注重算法能力,则介绍自己的算法练习;公司如果注重团队合作,那么你介绍一下自己的社会活动都是可以的。


一个好的自我介绍应该提前写下来,并且背熟。因为候选人通常的紧张感都是来自于面试刚开始的几分钟,如果你刚开始的几分钟讲的结结巴巴,那么这种负面情绪会加剧你面试时的紧张感,从而影响你正常发挥。如果你提前把自我介绍准备得特别流利,那么开始几分钟的紧张感过去之后,你很可能就会很快进入状态,而忘记紧张这个事情了。


即使做到了以上这些仍然是不够的,候选者常见的问题还包括:


  • 太简短
  • 没有重点
  • 太拖沓
  • 不熟练


我们在展开讨论上面这些问题之前,我们可以站在面试官的立场考虑一下:如果你是面试官,你为什么要让候选人做自我介绍?你希望在自我介绍环节考察哪些信息?


在我看来,自我介绍环节相当重要,因为:


  • 首先它考察了候选人的表达能力。大部分的程序员表达能力可能都一般,但是如果连自我介绍都说不清楚,通常就说明表达沟通能力稍微有点问题了。面试官通过这个环节可以初步考察到候选人的表达能力。
  • 它同样也考察了候选人对于面试的重视程度。一般情况下,表达再差的程序员,也可以通过事先拟稿的方式,把自我介绍内容背下来。如果一个人自我介绍太差,说明他不但表达差,而且不重视这次面试。
  • 最后,自我介绍对之后的面试环节起到了支撑作用。因为自我介绍中通常会涉及自己的项目经历,自己擅长的技术等。这些都很可能吸引面试官追问下去。好的候选人会通过自我介绍 “引导” 面试官问到自己擅长的领域知识。如果面试官最后对问题答案很满意,通过面试的几率就会大大增加。


所以我如果是面试官,我希望能得到一个清晰流畅的自我介绍。下面我们来看看候选人在面试中的常见问题。


1)太简短


一个好的自我介绍大概是 3~5 分钟。过短的自我介绍没法让面试官了解你的大致情况,也不足以看出来你的基本表达能力。


如果你发现自己没法说出足够时间的自我介绍。可以考虑在介绍中加入:自己的简单的求学经历,项目经历,项目中有亮点的地方,参与或研究过的一些开源项目,写过的博客,其它兴趣爱好,自己对新工作的期望和目标公司的理解。


我相信每个人经过准备,都可以做到一个 5 分钟长度的自我介绍。


2)没有重点


突破了时间的问题,接下来就需要掌握介绍的重点。通常一个技术面试,技术相关的介绍才是重点。所以求学经历,兴趣爱好之类的内容可以简单提到即可。


对于一个工作过的开发者,你过去做的项目哪个最有挑战,最能展示出你的水平其实自己应该是最清楚的。所以大家可以花时间把这个内容稍微强调一下。


当然你也没必要介绍得太细致,面试官如果感兴趣,自然会在之后的面试过程中和你讨论。


3)太拖沓


有些工作了几年的人,做过的项目差不多有个 3~5 个,面试的时候就忍不住一个一个说。单不说这么多项目在自我介绍环节不够介绍。就是之后的详细讨论环节,面试官也不可能讨论完你的所有项目经历。


所以千万不要做这种 “罗列经历” 的事情,你要做的就是挑一个或者最多两个项目,介绍一下项目大致的背景和你解决的主要问题即可。至于具体的解决过程,可以不必介绍。


4)不熟练


即便你的内容完全合适,时间长度完全合理,你也需要保证一个流利的陈述过程。适当在面试前多排练几次,所有人都可以做到一个流利的自我介绍。


还有一点非常神奇,就是一个人在做一件事情的时候,通常都是开始的前以及刚开始几分钟特别紧张。比如考试,演讲或者面试,通常这几分钟之后,人们进入 “状态” 了,就会忘记紧张了。


将自己的自我介绍背下来,可以保证一个流利顺畅的面试开局,这可以极大舒缓候选人的紧张情绪。相反,一开始自我介绍就结结巴巴,会加剧候选人的紧张情绪,而技术面试如果不能冷静的话,是很难在写代码环节保证逻辑清晰正确的。


所以,请大家务必把这个小环节重视起来,做出一个完美的开局。


3.项目介绍


自我介绍之后,就轮到讨论更加具体的内容环节了,通常面试官都会根据自我介绍或者你的简历,选一个他感兴趣的项目来详细讨论。


这个时候,大家务必需要提前整理出自己参与的项目的具体挑战,以及自己做得比较好的地方。切忌不要说:“这个项目很简单,没什么挑战,那个项目也很简单,没什么好说的”。再简单的事情,都可以做到极致的,就看你有没有一个追求完美的心。


比如你的项目可能在你看来就是摆放几个 iOS 控件。但是,这些控件各自有什么使用上的技巧,有什么优化技巧?其实如果专心研究,还是有很多可以学习的。拿 UITableView 来说,一个人如果能够做到把它的滑动流程度优化好,是非常不容易的。这里面涉及网络资源的异步加载、图片资源的缓存、后台线程的渲染、CALayer 层的优化等等。


这其实也要求我们做工作要精益求精,不求甚解。所以一场成功的面试最最本质上,看得还是自己平时的积累。如果自己平时只是糊弄工作,那么面试时就很容易被看穿。


在这一点上,我奉劝大家在自己的简历上一定要老实。不要在建简历上弄虚作假,把自己没有做过的项目写在里面。


顺便我在这里也教一下大家如何面试别人。如果你是面试官,考察简历的真假最简单的方法就是问细节。一个项目的细节如果问得很深入,候选人有没有做过很容易可以看出来。


举个例子,如果候选人说他在某公司就职期间做了某某项目。你就可以问他:


  • 这个工作具体的产品需求是什么样的?
  • 大概做了多长时间?
  • 整体的软件架构是什么样的?
  • 涉及哪些人合作?几个开发和测试?
  • 项目的时间排期是怎么定的?
  • 你主要负责的部分和合作的人?
  • 项目进行中有没有遇到什么问题?
  • 这个项目最后最大的收获是什么?遗憾是什么?
  • 项目最困难的一个需求是什么?具体什么实现的?


面试官如果做过类似项目,还可以问问通常这个项目常见的坑,看看候选人是什么解决的。


4.写代码


编程能力,说到底还是一个实践的能力,所以说大部分公司都会考察当场写代码。我面试过上百人,见到过很多候选人在自我介绍和项目讨论时都滔滔不绝,侃侃而谈,感觉非常好。但是一到写代码环节就怂了,要么写出来各种逻辑问题和细节问题没处理好,要么就是根本写不出来。


由于人最终招进来就是干活写代码的,所以如果候选人当场写代码表现很差的话,基本上面试就挂掉了。


程序员这个行业,说到底就是一个翻译的工作,把产品经理的产品文档和设计师的 UI 设计,翻译成计算机能够理解的形式,这个形式通常就是一行一行的源码。


当面试官考察你写代码的时候,他其实在考察:


  • 你对语言的熟悉程度。如果候选人连常见的变量定义和系统函数都不熟悉,说明他肯定经验还是非常欠缺。
  • 你对逻辑的处理能力。产品经理关注的是用户场景和核心需求,而程序员关注的是逻辑边界和异常情况。程序的 bug 往往就是边界和特殊情况没有处理好。虽然说写出没有 bug 的程序几乎不可能,但是逻辑清晰的程序员能够把思路理清楚,减少 bug 发生的概率。
  • 设计和架构能力。好的代码需要保证易于维护和修改。这里面涉及很多知识,从简单的 “单一职责” 原则,到复杂的 “好的组合优于继承” 原则,其中设计模式相关的知识最多。写代码的时候多少还是能够看出这方面的能力。另外有些公司,例如 Facebook,会有专门的系统设计(System Design)面试环节,专注于考察设计能力。


5.系统设计


有一些公司喜欢考查一些系统设计的问题,简单来说,就是让你解决一个具体的业务需求,看看你是否能够将业务逻辑梳理清楚,并且拆解成各个模块,设计好模块间的关系。举几个例子,面试官可能让你:


  • 设计一个类似微博的信息流应用。
  • 设计一个本地数据缓存架构。
  • 设计一个埋点分析系统。
  • 设计一个直播答题系统。
  • 设计一个多端的数据同步系统。
  • 设计一个动态补丁的方案。


这些系统的设计都非常考查一个人知识的全面性。通常情况下,如果一个人只知道 iOS 开发的知识,是很难做出相关的设计的。为了能够解决这些系统设计题,我们首先需要有足够的知识面,适度了解一下 Android 端、Web 端以及服务器端的各种技术方案背后的原理。你可以不写别的平台的代码,但是一定要理解它们在技术上能做到什么,不能做到什么。你也不必过于担心,面试官在考查的时候,还是会重点考查 iOS 相关的部分。


我们将在下一小节,展开讨论如何准备和回答系统设计题。


6.提问


提问环节通常在面试结束前,取决于前面的部分是否按时结束,有些时候前面的环节占用了太多时间,也可能没有提问环节了。在后面的章节,我们会展开讨论一下如何提问。



收起阅读 »

腾讯抖音iOS岗位三面面经

1.进程和线程的区别2.死锁的原因3.介绍虚拟内存4.常见排序算法,排序算法稳定的意思,快排的复杂度什么时候退化,基本有序用什么5.TCP可靠性6.http+https算法Z字遍历二叉树,归并排序后面说因为我不会java和安卓,会帮忙转推到iOS的组(面试的这...
继续阅读 »

1.进程和线程的区别


2.死锁的原因


3.介绍虚拟内存


4.常见排序算法,排序算法稳定的意思,快排的复杂度什么时候退化,基本有序用什么


5.TCP可靠性


6.http+https


算法


Z字遍历二叉树,归并排序


后面说因为我不会java和安卓,会帮忙转推到iOS的组(面试的这个组是java客户端)


腾讯PCG iOS一面(1h)


1.聊项目,聊了很久,一开始没有意会面试官想知道什么,最后说是想知道我这么做比起从客户端自己去实现的区别(这个项目是小米实习时候的项目,做的浏览器内核,页面翻译功能,


基本每一个客户端应用都会有一个类似于浏览器内核的东西,对页面进行渲染,呈现,也可以叫渲染引擎,学前端的肯定知道这个东西,他主要是解释html,css,js的。


我做的这个页面翻译功能可以不经过内核直接由客户端工程师用安卓客户端实现整套逻辑,所以这么问我了)


2.实现string类,实现构造,析构,里面加一个kmp


3.介绍智能指针,智能指针保存引用计数的变量存在哪里,引用计数是否线程安全


4.算法:两个只有0和1的数字序列,只能0  1互换,每次当前位互换都会使后面的也换掉(比如,011000,换第二位,成了000111),计算从一个变到另一个需要几步操作。


5.https,验证公钥有效的方法,为什么非对称对称并用


腾讯PCG iOS二面 (40min)


1.算法:


合并排序链表


2.static关键字的作用


3.const关键字的作用


4.成员初始化列表的作用


5.指针和引用的区别


6.又是很久的项目,怎么去学习浏览器内核(chromium内核的代码量有几千万行,而且写的很难懂,用了大量的设计模式,作为一个菜鸡真的很痛苦)


,怎么去调试项目中遇到的问题(这里主要是一个ipc接口没用对),你觉得人家google的是怎么去调的,你为什么和人家做法不一样?


腾讯PCG iOS三面(2h)


1.还是聊了很久项目(已经麻了,做过的东西一定要能说出口)


2.浏览器呈现一个页面经过了哪几步(DOM树,layoutobject树,browser进程绘制)


3.C++多态的实现


4.DNS解析,递归与迭代的区别


5.chromium用的渲染引擎是什么,这个渲染引擎对应的js解释引擎是什么(blink和v8,前几个问题表现有些差,这会在问一些1+1的问题了,哭)


6.平时怎么学习技术的,看过哪些书,有过哪些输出(我把实习时写的项目wiki给截了个图)


然后反问,打开牛客让我写了个代码,他不知道去哪了,我自己在这写,写了一个多钟头,你以为这是道很难的题吗?no,是我那会确实很菜,哈哈


题目是,给一个字符串插入最少的字符,让这个字符串变成回文


腾讯hr面 (40min)


1.有哪些缺点


2.投了哪些,为什么不投阿里头条(实习忙的我面你们都要面不过来了)


3.如何选择offer


4.家是哪的,为什么愿意去深圳


每一个问题都不是简单的答完就完事了,他会跟着问很多


然后反问时我问问题给我说了20分钟


抖音 iOS一面 (1h20min)


上来闲聊了一会


1.算法:


字符串大数相加


写完问我有没有需要优化的地方(内存可以优化一下)


2.string类赋值运算符是深拷贝还是浅拷贝


3.算法:


根据前序和中序输出二叉树的后续遍历


4.C++ deque底层,deque有没有重载[]


5.为什么要内存对齐,内存对齐的规则


6.算法:


上台阶,加了个条件,这次上两级,下次就只能上一级


7.反问+闲聊


抖音 iOS二面 (1h)


十分钟不到的项目


1.进程和线程的区别和联系


2.线程共享哪些内存空间


3.进程内存模型


4.进程间通信方式


5. 虚拟内存,为什么要有虚拟内存,虚拟内存如何映射到物理内存


后面还挖了一些操作系统的问题,记不太清了


5.TCP为什么四次挥手


6.https客户端验证公钥的方法


7.描述并写一下LRU


8.说一下怎么学chromium的,怎么上手项目的


9.C++内存分配,写了一段代码,看里面申请了哪部分内存,申请了多少,代码有什么问题


10.代码里面的内存泄漏怎么解决,智能指针的引用计数怎么实现,那些成员函数会影响到引用计数


11.代码里面有无线程安全问题,线程安全问题的是否会导致程序崩溃,为什么


12.C++虚函数的实现原理,纯虚函数


13.C++引用和指针的区别,引用能否为空


抖音 iOS三面 (1h)


1.lambda表达式,它应用表达式外变量的方式和区别


2.decltype的作用,他和auto有什么不同


3.C++的所有智能指针介绍一下


4.C++thread里面的锁,条件变量,讲一下怎么用他们实现生产者消费者模型


5.C++20有什么新东西(我就知道支持了协程,然后他说我就想问你协程,然后我说,其实我具体不了解,丢人了)


6.右值引用是什么,移动构造函数有什么好处


7.操作系统微内核宏内核(懵)


8.进程间通信的共享内存,如何保证安全性(信号量),结合epoll讲讲共享内存


9.TCP协议切片(懵)


10.TCP协议的流量控制机制,滑窗为0时,怎么办


11.算法


合并K个排序链表


12.合并K个排序数组,讲思路,我说归并,他说,传输参数是数组,不是vector,你如何判断数组的大小



收起阅读 »

iOS 整理出一份高级iOS面试题

1、NSArray与NSSet的区别?NSArray内存中存储地址连续,而NSSet不连续NSSet效率高,内部使用hash查找;NSArray查找需要遍历NSSet通过anyObject访问元素,NSArray通过下标访问2、NSHashTable与NSMa...
继续阅读 »

1、NSArray与NSSet的区别?


  • NSArray内存中存储地址连续,而NSSet不连续
  • NSSet效率高,内部使用hash查找;NSArray查找需要遍历
  • NSSet通过anyObject访问元素,NSArray通过下标访问


2、NSHashTable与NSMapTable?


  • NSHashTable是NSSet的通用版本,对元素弱引用,可变类型;可以在访问成员时copy
  • NSMapTable是NSDictionary的通用版本,对元素弱引用,可变类型;可以在访问成员时copy


(注:NSHashTable与NSSet的区别:NSHashTable可以通过option设置元素弱引用/copyin,只有可变类型。但是添加对象的时候NSHashTable耗费时间是NSSet的两倍。

NSMapTable与NSDictionary的区别:同上)


3、属性关键字assign、retain、weak、copy


  • assign:用于基本数据类型和结构体。如果修饰对象的话,当销毁时,属性值不会自动置nil,可能造成野指针。
  • weak:对象引用计数为0时,属性值也会自动置nil
  • retain:强引用类型,ARC下相当于strong,但block不能用retain修饰,因为等同于assign不安全。
  • strong:强引用类型,修饰block时相当于copy。


4、weak属性如何自动置nil的?


  • Runtime会对weak属性进行内存布局,构建hash表:以weak属性对象内存地址为key,weak属性值(weak自身地址)为value。当对象引用计数为0 dealloc时,会将weak属性值自动置nil。


5、Block的循环引用、内部修改外部变量、三种block


  • block强引用self,self强引用block
  • 内部修改外部变量:block不允许修改外部变量的值,这里的外部变量指的是栈中指针的内存地址。__block的作用是只要观察到变量被block使用,就将外部变量在栈中的内存地址放到堆中。
  • 三种block:NSGlobalBlack(全局)、NSStackBlock(栈block)、NSMallocBlock(堆block)


6、KVO底层实现原理?手动触发KVO?swift如何实现KVO?


  • KVO原理:当观察一个对象时,runtime会动态创建继承自该对象的类,并重写被观察对象的setter方法,重写的setter方法会负责在调用原setter方法前后通知所有观察对象值得更改,最后会把该对象的isa指针指向这个创建的子类,对象就变成子类的实例。
  • 如何手动触发KVO:在setter方法里,手动实现NSObject两个方法:willChangeValueForKey、didChangeValueForKey
  • swift的kvo:继承自NSObject的类,或者直接willset/didset实现。


7、categroy为什么不能添加属性?怎么实现添加?与Extension的区别?category覆盖原类方法?多个category调用顺序


  • Runtime初始化时categroy的内存布局已经确定,没有ivar,所以默认不能添加属性。
  • 使用runtime的关联对象,并重写setter和getter方法。
  • Extenstion编译期创建,可以添加成员变量ivar,一般用作隐藏类的信息。必须要有类的源码才可以添加,如NSString就不能创建Extension。
  • category方法会在runtime初始化的时候copy到原来前面,调用分类方法的时候直接返回,不再调用原类。如何保持原类也调用(https://www.jianshu.com/p/40e28c9f9da5)。
  • 多个category的调用顺序按照:Build Phases ->Complie Source 中的编译顺序。


8、load方法和initialize方法的异同。——主要说一下执行时间,各自用途,没实现子类的方法会不会调用父类的?

load initialize 调用时机 app启动后,runtime初始化的时候 第一个方法调用前调用 调用顺序 父类->本类->分类 父类->本类(如果有分类直接调用分类,本类不会调用) 没实现子类的方法会不会调用父类的 否 是 是否沿用父类实现 否 是




9、对 runtime 的理解。——主要是方法调用时如何查找缓存,如何找到方法,找不到方法时怎么转发,对象的内存布局


OC中向对象发送消息时,runtime会根据对象的isa指针找到对象所属的类,然后在该类的方法列表和父类的方法列表中寻找方法执行。如果在最顶层父类中没找到方法执行,就会进行消息转发:Method resoution(实现方法)、fast forwarding(转发给其他对象)、normal forwarding(完整消息转发。可以转发给多个对象)


10、runtime 中,SEL和IMP的区别?


每个类对象都有一个方法列表,方法列表存储方法名、方法实现、参数类型,SEL是方法名(编号),IMP指向方法实现的首地址


11、autoreleasepool的原理和使用场景?


  • 若干个autoreleasepoolpage组成的双向链表的栈结构,objc_autoreleasepoolpush、objc_autoreleasepoolpop、objc_autorelease
  • 使用场景:多次创建临时变量导致内存上涨时,需要延迟释放
  • autoreleasepoolpage的内存结构:4k存储大小



12、Autorelase对象什么时候释放


在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。


13、Runloop与线程的关系?Runloop的mode? Runloop的作用?内部机制?


  • 每一个线程都有一个runloop,主线程的runloop默认启动。
  • mode:主要用来指定事件在运行时循环的优先级
  • 作用:保持程序的持续运行、随时处理各种事件、节省cpu资源(没事件休息释放资源)、渲染屏幕UI


14、iOS中使用的锁、死锁的发生与避免


  • @synchronized、信号量、NSLock等
  • 死锁:多个线程同时访问同一资源,造成循环等待。GCD使用异步线程、并行队列


15、NSOperation和GCD的区别


  • GCD底层使用C语言编写高效、NSOperation是对GCD的面向对象的封装。对于特殊需求,如取消任务、设置任务优先级、任务状态监听,NSOperation使用起来更加方便。
  • NSOperation可以设置依赖关系,而GCD只能通过dispatch_barrier_async实现
  • NSOperation可以通过KVO观察当前operation执行状态(执行/取消)
  • NSOperation可以设置自身优先级(queuePriority)。GCD只能设置队列优先级(DISPATCH_QUEUE_PRIORITY_DEFAULT),无法在执行的block中设置优先级
  • NSOperation可以自定义operation如NSInvationOperation/NSBlockOperation,而GCD执行任务可以自定义封装但没有那么高的代码复用度
  • GCD高效,NSOperation开销相对高


16、oc与js交互


  • 拦截url
  • JavaScriptCore(只适用于UIWebView)
  • WKScriptMessageHandler(只适用于WKWebView)
  • WebViewJavaScriptBridge(第三方框架)


17、swift相比OC有什么优势?


18、struct、Class的区别


  • class可以继承,struct不可以
  • class是引用类型,struct是值类型
  • struct在function里修改property时需要mutating关键字修饰


19、访问控制关键字(public、open、private、filePrivate、internal)


  • public与open:public在module内部中,class和func都可以被访问/重载/继承,外部只能访问;而open都可以
  • private与filePrivate:private修饰class/func,表示只能在当前class源文件/func内部使用,外部不可以被继承和访问;而filePrivate表示只能在当前swift源文件内访问
  • internal:在整个模块或者app内都可以访问,默认访问级别,可写可不写


20、OC与Swift混编


  • OC调用swift:import "工程名-swift.h” @objc 
  • swift调用oc:桥接文件


21、map、filter、reduce?map与flapmap的区别?


  • map:数组中每个元素都经过某个方法转换,最后返回新的数组(xx.map({$0 * $0}))
  • flatmap:同map类似,区别在flatmap返回的数组不存在nil,并且会把optional解包;而且还可以把嵌套的数组打开变成一个([[1,2],[2,3,4],[5,6]] ->[1,2,2,3,4,5,6])
  • filter:用户筛选元素(xxx.filter({$0 > 25}),筛选出大于25的元素组成新数组)
  • reduce:把数组元素组合计算为一个值,并接收初始值()




22、guard与defer


  • guard用于提前处理错误数据,else退出程序,提高代码可读性
  • defer延迟执行,回收资源。多个defer反序执行,嵌套defer先执行外层,后执行内层


23、try、try?与try!


  • try:手动捕捉异常
  • try?:系统帮我们处理,出现异常返回nil;没有异常返回对应的对象
  • try!:直接告诉系统,该方法没有异常。如果出现异常程序会crash


24、@autoclosure:把一个表达式自动封装成闭包


25、throws与rethrows:throws另一个throws时,将前者改为rethrows


26、App启动优化策略?main函数执行前后怎么优化


  • 启动时间 = pre-main耗时+main耗时
  • pre-main阶段优化:
  • 删除无用代码
  • 抽象重复代码
  • +load方法做的事情延迟到initialize中,或者+load的事情不宜花费太多时间
  • 减少不必要的framework,或者优化已有framework
  • Main阶段优化
  • didFinishLauchingwithOptions里代码延后执行
  • 首次启动渲染的页面优化


27、crash防护?


  • unrecognized selector crash
  • KVO crash
  • NSNotification crash
  • NSTimer crash
  • Container crash(数组越界,插nil等)
  • NSString crash (字符串操作的crash)
  • Bad Access crash (野指针)
  • UI not on Main Thread Crash (非主线程刷UI (机制待改善))


28、内存泄露问题?


主要集中在循环引用问题中,如block、NSTime、perform selector引用计数问题。


29、UI卡顿优化?


30、架构&设计模式


  • MVC设计模式介绍
  • MVVM介绍、MVC与MVVM的区别?
  • ReactiveCocoa的热信号与冷信号
  • 缓存架构设计LRU方案
  • SDWebImage源码,如何实现解码
  • AFNetWorking源码分析
  • 组件化的实施,中间件的设计
  • 哈希表的实现原理?如何解决冲突


31、数据结构&算法


  • 快速排序、归并排序
  • 二维数组查找(每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数)
  • 二叉树的遍历:判断二叉树的层数
  • 单链表判断环


32、计算机基础


  1. http与https?socket编程?tcp、udp?get与post?
  2. tcp三次握手与四次握手
  1. 进程与线程的区别



收起阅读 »

iOS面试基础知识 (五)

混编技术移动开发已经进入大前端时代。对于混编技术,笔者一般在面试中也会问,通常会问h5混编、rn、weex、flutter等相关方面的问题,以考察面试者对于混编技术的了解程度。H5混编实现相对于rn、weex等混编技术,在App里面内嵌H5实现成本较低,所以目...
继续阅读 »

混编技术


移动开发已经进入大前端时代。对于混编技术,笔者一般在面试中也会问,通常会问h5混编、rn、weex、flutter等相关方面的问题,以考察面试者对于混编技术的了解程度。


H5混编实现


相对于rn、weex等混编技术,在App里面内嵌H5实现成本较低,所以目前市面上H5混编仍是主流,笔者在面试中一般会问H5与App怎么通信。概括来说,主要有如下集中方式:


伪协议实现


伪协议指的是自己自定义的url协议,通过webview的代理拦截到url的加载,识别出伪协议,然后调用native的方法。伪协议可以这样定义:AKJS://functionName?param1=value1&param2=value2。 其中AKJS代表我们自己定义的协议,functionName代表要调用的App方法,?后面代表传入的参数。

一、UIWebView通过UIWebViewDelegate的代理方法-webView: shouldStartLoadWithRequest:navigationType:进行伪协议拦截。

二、WKWebView通过WKNavigationDelegate代理方法实现- webView:decidePolicyForNavigationAction:decisionHandler:进行伪协议拦截。

此种实现方式优点是简单。

缺点有:


  • 由于url长度大小有限制,导致传参大小有限制,比如h5如果要传一个图片的base64字符串过来,这种方式就无能为力了。
  • 需要在代理拦截方法里面写一系列if else处理,难以维护。
  • 如果App要兼容UIWebView和WKWebView,需要有两套实现,难以维护。


JSContext


为了解决伪协议实现的缺点,我们可以往webview里面注入OC对象,不过这种方案只能用于UIWebView中。此种方式的实现步骤如下:

一、在webViewDidFinishLoad方法中通过JSContext注入JS对象


self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
self.jsContext[@"AK_JSBridge"] = self.bridgeAdapter; //往JS中注入OC对象


二、OC对象实现JSExport协议,这样JS就可以调用OC对象的方法了


@interface AKBridgeAdapter : NSOject< JSExport >
- (void)getUID; // 获取用户ID


此种方案的优点是JS可以直接调用对象的方法,通过提供对象这种方式,代码优雅;缺点是只能用于UIWebView、不能用于WKWebView。


WKScriptMessageHandler


WKWebView可以通过提供实现了WKScriptMessageHandler协议的类来实现JS调用OC,实现步骤如下:

一、往webview注入OC对象。


[self.configuration.userContentController addScriptMessageHandler:self.adapter name:@"AK_JSBridge"]


二、实现- userContentController:didReceiveScriptMessage:获取方法调用名和参数


- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.body isKindOfClass:[NSDictionary class]]) {
NSDictionary *dicMessage = message.body;

NSString *funcName = [dicMessage stringForKey:@"funcName"];
NSString *parameter = [dicMessage stringForKey:@"parameter"];
//进行逻辑处理
}
}


此种方案的优点是实现简单,缺点是不支持UIWebView。


第三方库WKWebViewJavascriptBridge


该库是iOS使用最广泛的JSBridge库,该库通过伪协议+JS消息队列实现了JS与OC交互,此种方案兼容UIWebView和WKWebView。


RN、Weex、Flutter混编技术


RN(React Native)是facebook开发的跨三端(iOS、Android、H5)开源框架,目前在业界使用最广泛;Weex是阿里开源的类似RN的大前端开发框架,国内有些公司在使用;Flutter是Google开发的,作为后旗之秀,目前越来越流行。

笔者一般在面试中会问一下这类框架是怎么实现页面渲染,怎么实现调用OC的,以考察面试者是否了解框架实现原理。


组件化


任何一个对技术有追求的团队,都会做组件化,组件化的目标是模块解耦、代码复用。


组件代码管理方式


目前业内一般采用pod私有库的方式来管理自己的组件。


组件通信方式


MGJRouter


MGJRouter通过注册url的方式来实现方法注册和调用


[MGJRouter registerURLPattern:@"mgj://category/travel" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameters[MGJRouterParameterUserInfo]:%@", routerParameters[MGJRouterParameterUserInfo]);
// @{@"user_id": @1900}
}];

[MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];


该种方案的缺点有:


  • url定义由于是字符串,有可能造成重复。
  • 参数传入不能直接传model,而是需要传字典,如果方法实现方修改一个字段的类型但没有通知调用方,调用方无法直接知道,有可能导致崩溃。
  • 通过字典传参不直观,调用方需要知道字段的名字才能获取字段值,如果字段名不定义为宏,到处拷贝字段名造成难以维护。


CTMediator


CTMediator通过CTMediator的类别来实现方法调用。

一、组件提供方实现Target、Action。


@interface Target_A : NSObject

- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params;

@end

- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
// 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}


二、组件提供方实现CTMediator类别暴露接口给使用方。


@interface CTMediator (CTMediatorModuleAActions)

- (UIViewController *)CTMediator_viewControllerForDetail;

@end

- (UIViewController *)CTMediator_viewControllerForDetail
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativFetchDetailViewController
params:@{@"key":@"value"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界选择是push还是present
return viewController;
} else {
// 这里处理异常场景,具体如何处理取决于产品
return [[UIViewController alloc] init];
}
}


此种方案的优点是通过Targrt-Action实现了组件之间的解耦,通过暴露方法给组件使用方,避免了url直接传递字典带来的问题。

缺点是:


  • CTMediator类别实现由于需要通过performTarget方式来实现,需要写一堆方法名、方法参数名字字符串,影响阅读;
  • 没有组件管理器概念,组件直接的互相调用都是通过直接引用CTMediator类别来实现,没有实现真正的解耦。


BeeHive


BeeHive通过url来实现页面路由,通过Protocol来实现方法调用。

一、注册service


[[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]];


二、调用service


id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];

// use homeVc do invocation


笔者推荐使用BeeHive这种方式来做组件化,基于Protocol(面向接口)的编程方式能让组件提供方清晰地提供接口声明给使用方;能充分利用编辑器特性,比如如果接口删除了一个参数,能通过编译器编不过来告诉调用方接口发生了变化。

收起阅读 »

iOS面试基础知识 (四)

网络相关做移动开发,除了写UI,大部分的工作就是跟后台做接口联调了,所以网络相关的知识在面试当中是相当重要且必不可少的。Get与Post区别笔者在面试中会经常问这个问题,发现有挺多面试者回答得不好。很多人不知道Get与Post网络请求参数放在哪里。Get请求参...
继续阅读 »

网络相关


做移动开发,除了写UI,大部分的工作就是跟后台做接口联调了,所以网络相关的知识在面试当中是相当重要且必不可少的。


Get与Post区别


笔者在面试中会经常问这个问题,发现有挺多面试者回答得不好。很多人不知道Get与Post网络请求参数放在哪里。

Get请求参数是以kv方式拼在url后面的,虽然http协议对url的长度没有限制,但是浏览器和服务器一般都限制长度;Post请求参数是放在body里面的,对长度没什么限制。


https原理


https与http区别


https是在http的基础上加上ssl形成的协议,http传输数据是明文的,https则是以对称加密的方式传输数据。


https证书校验过程


https采用对称加密传输数据,对称加密需要的密钥由客户端生成,通过非对称加密算法加密传输给后台。具体步骤如下:

1、客户端向服务器发起HTTPS请求,连接到服务器的443端口。

2、服务器有一个用来做非对称加密的密钥对,即公钥和私钥,服务器端保存着私钥,服务器将自己的公钥发送给客户。

3、客户端收到服务器的公钥之后,会对公钥进行检查,验证其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续。严格的说,这里应该是验证服务器发送的数字证书的合法性,如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,我们将该密钥称之为client key,然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了。

4、客户端会发起HTTPS中的第二个HTTP请求,将加密之后的客户端密钥发送给服务器。

5、服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文。

6、后续客户端和服务器基于client key进行对称加密传输数据。


网络参数签名、加密实现方式


除了用https协议传输数据,有些对数据安全要求比较高的App比如金融类App还会对参数进行签名和加密,这样可以防止网络请求参数被篡改以及敏感业务数据泄露


网络参数签名


为了防止网络请求被篡改,一般会对请求参数进行hash,一般会有一个sign字段表示签名。


假定客户端请求参数dic如下:
{
"name":"akon",
"city":"shenzhen",
}


那么如何生成sign字段呢?

一般通用的做法是把字典按照key的字母升序排序然后拼接起来,然后再进行sha256,再md5。


  • 把字典按照key的字母排序拼接生成字符串str = "city=shenzhen&name=akon"。
  • 对str先进行sha256然后再进行md5生成sign。
    值得注意的是,为了增加破解的难度,我们可以在生成的str前面、后面加入一段我们App特有的字符串,然后对str hash可以采用base64、sha256,md5混合来做。


网络参数加密方式


为了效率,我们一般会采用对称加密加密数据,DES,3DES,AES这些方式都可以。既然要用对称加密,那就涉及到对称加密的密钥怎么生成,有如下方式:


  • 最简单的方式,代码写死密钥。密钥可以用base64或者抑或算法进行简单的加密,用的时候再解密,这种方式比裸写密钥更安全。
  • 后台下发密钥。后台可以在登录的时候下发这个密钥,客户端保存这个密钥后续用来做加密。由于客户端要保存这个密钥,所以还是存在泄露的风险。
  • 仿照https证书校验过程,客户端生成对称加密的密钥clientKey,对参数进行加密,然后用非对称加密对clientKey进行加密生成cryptKey传给后台;后台获取到cryptKey解析出clientKey,然后再用clientKey解密出请求参数。这种方式最安全,推荐使用。


AFNetworking实现原理


作为iOS使用最广泛的第三方网络库,AFNetworking基本上是面试必问的。笔者面试都会问,通过AF的一些问题,可以了解面试者是否熟练使用AF,以及是否阅读过AF的源代码。


AF的设计架构图


如果面试者能把AF的分层架构图清晰地画出来,那至少证明面试者有阅读过AF的源码。


AF关于证书校验是哪个类实现的?有哪几种证书校验方式?


AFSecurityPolicy用来做证书校验的。有三种校验方式:


  • AFSSLPinningModeNone 客户端不进行证书校验,完全信任服务端。
  • AFSSLPinningModePublicKey 客户端对证书进行公钥校验。
  • AFSSLPinningModeCertificate 客户端对整个证书进行校验。


AF请求参数编码、响应参数解码分别是哪两个类实现的?支持什么方式编码,解码?


  • AFHTTPRequestSerializer、AFHTTPResponseSerializer分别用来做编码和解码。
  • 编码方式有url query类型、 json、plist方式。
  • 解码支持NSData、json、xml、image类型。


关于AF如果再深入点可以问问具体实现细节,可以通过细节进一步考察面试者的内功。


SDWebImage实现原理


iOS下载图片基本都用SDWebImage,这个库笔者面试基本都会问。


下载流程


一、先去内存缓存找,找到了直接返回UIImage,否则走第二步;

二、去磁盘缓存里面找,找到了直接返回UIImage,否则走第三步;

三、网络下载,下载完成后存入本地磁盘和内存缓存,然后返回UIImage给调用方。


url生成key的算法是什么?


  • 内存缓存key是url
  • 磁盘缓存key是对url进行md5生成的。


清缓存时机


  • 对于内存缓存,在下载图片加载图片到内存时、内存收到警告时候进行清理。
  • 对于磁盘缓存,在App退出、进后台清理。


网络防劫持策略


H5防劫持


黑客可以通过劫持URL,注入JS代码来劫持H5,可以通过黑名单机制来解决这类问题。


DNS防劫持


DNS的过程其实是域名替换成IP的过程,这个过程如果被黑客劫持,黑客可以返回自己的IP给客户端,从而劫持App。可以通过HTTP DNS方案来解决这个问题。


网络优化


网络优化的核心点是减少网络请求次数和数据传输量。策略有很多,列举一些常用的手段:


合并接口


有些接口可以合并就合并,把几个接口合并成一个接口,可以省去每个接口建立连接的时间以及每个请求传输的http请求头和响应头。


采用pb等省流量传输协议


我们可以采用xml、json、pb等格式传输数据。

这三种方式数据量大小和性能pb>json>xml。


webp


采用webp图片可以节省客户端和服务端的带宽。


采用tcp而不是http


http是基于tcp的应用层协议,相比tcp,http多出来一个几百字节的请求头和响应头,并且每次通信都要建立连接,效率比不上tcp。


同运营商、就近接入


可以根据用户手机的运营商返回相应机房的服务器给客户端,比如联通返回联通的服务器;

可以根据用户所处区域返回相应的服务器给客户端,比如深圳返回深圳机房的服务器。

收起阅读 »

iOS面试基础知识 (三)

iOS
多线程多线程创建方式iOS创建多线程方式主要有NSThread、NSOperation、GCD,这三种方式创建多线程的优缺点如下:NSThreadNSThread 封装了一个线程,通过它可以方便的创建一个线程。NSThread 线程之间的并发控制,是需要我们自...
继续阅读 »

多线程


多线程创建方式


iOS创建多线程方式主要有NSThread、NSOperation、GCD,这三种方式创建多线程的优缺点如下:


NSThread


  • NSThread 封装了一个线程,通过它可以方便的创建一个线程。NSThread 线程之间的并发控制,是需要我们自己来控制的。它的缺点是需要我们自己维护线程的生命周期、线程之间同步等,优点是轻量,灵活。


NSOperation


  • NSOperation 是一个抽象类,它封装了线程的实现细节,不需要自己管理线程的生命周期和线程的同步等,需要和 NSOperationQueue 一起使用。使用 NSOperation ,你可以方便地控制线程,比如取消线程、暂停线程、设置线程的优先级、设置线程的依赖。NSOperation常用于下载库的实现,比如SDWebImage的实现就用到了NSOperation。


GCD


  • GCD(Grand Central Dispatch) 是 Apple 开发的一个多核编程的解决方法。GCD 是一个可以替代 NSThread 的很高效和强大的技术。在平常开发过程中,我们用的最多的就是GCD。哦,对了,NSOperation是基于GCD实现的。


多线程同步


多线程情况下访问共享资源需要进行线程同步,线程同步一般都用锁实现。从操作系统层面,锁的实现有临界区、事件、互斥量、信号量等。这里讲一下iOS中多线程同步的方式。


atomic


属性加上atomic关键字,编译器会自动给该属性生成代码用以多线程访问同步,它并不能保证使用属性的过程是线程安全的。一般我们在定义属性的时候用nonatomic,避免性能损失。


@synchronized


@synchronized指令是一个对象锁,用起来非常简单。使用obj为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程1和线程2中的@synchronized后面的obj不相同,则不会互斥。@synchronized其实是对pthread_mutex递归锁的封装。

@synchronized优点是我们不需要在代码中显式的创建锁对象,使用简单; 缺点是@synchronized会隐式的添加一个异常处理程序,该异常处理程序会在异常抛出的时候自动的释放互斥锁,从而带来额外开销。


NSLock


最简单的锁,调用lock获取锁,unlock释放锁。如果其它线程已经调用lock获取了锁,当前线程调用lock方法会阻塞当前线程,直到其它线程调用unlock释放锁为止。NSLock使用简单,在项目中用的最多。


NSRecursiveLock


递归锁主要用来解决同一个线程频繁获取同一个锁而不造成死锁的问题。注意lock和unlock调用必须配对。


NSConditionLock


条件锁,可以设置自定义条件来获取锁。比如生产者消费者模型可以用条件锁来实现。


NSCondition


条件,操作系统中信号量的实现,方法- (void)wait和- (BOOL)waitUntilDate:(NSDate *)limit用来等待锁直至锁有信号;方法- (void)signal和- (void)broadcast使condition有信号,通知等待condition的线程,变成非阻塞状态。


dispatch_semaphore_t


信号量的实现,可以实现控制GCD队列任务的最大并发量,类似于NSOperationQueue的maxConcurrentOperationCount属性。


pthread_mutex


mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。使用pthread_mutex_init创建锁,使用pthread_mutex_lock和pthread_mutex_unlock加锁和解锁。注意:mutex可以通过PTHREAD_MUTEX_RECURSIVE创建递归锁,防止重复获取锁导致死锁


 //创建锁,注意:mutex可以通过PTHREAD_MUTEX_RECURSIVE创建递归锁,防止重复获取锁导致死锁
pthread_mutexattr_t recursiveAttr;
pthread_mutexattr_init(&recursiveAttr);
pthread_mutexattr_settype(&recursiveAttr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(self.mutex, &recursiveAttr);
pthread_mutexattr_destroy(&recursiveAttr);

pthread_mutex_lock(&self.mutex)
//访问共享数据代码
pthread_mutex_unlock(&self.mutex)


OSSpinLock


OSSpinLock 是自旋锁,等待锁的线程会处于忙等状态。一直占用着 CPU。自旋锁就好比写了个 while,whil(被加锁了) ; 不断的忙等,重复这样。OSSpinLock是不安全的锁(会造成优先级反转),什么是优先级反转,举个例子:

有线程1和线程2,线程1的优先级比较高,那么cpu分配给线程1的时间就比较多,自旋锁可能发生优先级反转问题。如果优先级比较低的线程2先加锁了,紧接着线程1进来了,发现已经被加锁了,那么线程1忙等,while(未解锁); 不断的等待,由于线程1的优先级比较高,CPU就一直分配之间给线程1,就没有时间分配给线程2,就有可能导致线程2的代码就没有办法往下走,就会造成线程2没有办法解锁,所以这个锁就不安全了。

建议不要使用OSSpinLock,用os_unfair_lock来代替。


//初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//加锁
OSSpinLockLock(&lock);
//解锁
OSSpinLockUnlock(&lock);


os_unfair_lock


os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等


//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);


性能


性能从高到低排序

1、os_unfair_lock

2、OSSpinLock

3、dispatch_semaphore

4、pthread_mutex

5、NSLock

6、NSCondition

7、pthread_mutex(recursive)

8、NSRecursiveLock

9、NSConditionLock

10、@synchronized


JSON Model互转


项目中JSON Model转换方式


平常开发过程中,经常需要进行JSON与Model互转,尤其是接口数据转换。我们可以手动解析,也可以用MJExtension、YYModel这些第三方库,用第三方库最大的好处他可以自动给你转换并且处理类型不匹配等异常情况,从而避免崩溃。


MJExtension实现原理


假定后台返回的字典dic为:
{
"name":"akon",
"address":"shenzhen",
}

我们自定义了一个类UserModel
@interface UserModel : NSObject

@property (nonatomic, strong)NSString* name;
@property (nonatomic, strong)NSString* address;

@end


  • MJExtension是如何做属性映射的?
    MJExtension在遍历dic属性时,比如遍历到name属性时,先去缓存里查找这个类是否有这个属性,有就赋值akon。没有就遍历UserModel的属性列表,把这个类的属性列表加入到缓存中,查看这个类有没有定义name属性,如果有,就把akon赋给这个属性,否则不赋值。
  • MJExtension是如何给属性赋值的?
    利用KVC机制,在查找到UserModel有name的属性,使用[self setValue:@"akon" forKey:@"name"]进行赋值。
  • 如何获取类的属性列表?
    通过class_copyPropertyList方法
  • 如何遍历成员变量列表?
    通过class_copyIvarList方法


数据存储方式


iOS常见数据存储方式及使用场景


iOS中可以采用NSUserDefaults、Archive、plist、数据库等方式等来存储数据,以上存储方式使用的业务场景如下:


  • NSUserDefaults一般用来存储一些简单的App配置。比如存储用户姓名、uid这类轻量的数据。
  • Archive可以用来存储model,如果一个model要用Archive存储,需要实现NSCoding协议。
  • plist存储方式。像NSString、NSDictionary等类都可以直接存调用writeToFile:atomically:方法存储到plist文件中。
    -数据库存储方式。大量的数据存储,比如消息列表、网络数据缓存,需要采用数据库存储。可以用FMDB、CoreData、WCDB、YYCache来进行数据库存储。建议使用WCDB来进行数据库存储,因为WCDB是一个支持orm,支持加密,多线程安全的高性能数据库。


数据库操作


笔者在面试中,一般会问下面试者数据库的操作,以此开考察一下面试者对于数据库操作的熟练程度。


  • 考察常用crud语句书写。
    创建表、给表增加字段、插入、删除、更新、查询SQL怎么写。尤其是查询操作,可以考察order by, group by ,distinct, where匹配以及联表查询等技巧。
  • SQL语句优化技巧。如索引、事务等常用优化技巧。
  • 怎么分库、分表?
  • FMDB或者WCDB(orm型)实现原理。
  • 怎么实现数据库版本迁移?
收起阅读 »

iOS面试基础知识 (二)

iOS
一、类别OC不像C++等高级语言能直接继承多个类,不过OC可以使用类别和协议来实现多继承。1、类别加载时机在App加载时,Runtime会把Category的实例方法、协议以及属性添加到类上;把Category的类方法添加到类的metaclass上。2、类别添...
继续阅读 »

一、类别


OC不像C++等高级语言能直接继承多个类,不过OC可以使用类别和协议来实现多继承。


1、类别加载时机


在App加载时,Runtime会把Category的实例方法、协议以及属性添加到类上;把Category的类方法添加到类的metaclass上。


2、类别添加属性、方法


1)在类别中不能直接以@property的方式定义属性,OC不会主动给类别属性生成setter和getter方法;需要通过objc_setAssociatedObject来实现。


@interface TestClass(ak)

@property(nonatomic,copy) NSString *name;

@end

@implementation TestClass (ak)

- (void)setName:(NSString *)name{

objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY);
}

- (NSString*)name{
NSString *nameObject = objc_getAssociatedObject(self, "name");
return nameObject;
}


2)类别同名方法覆盖问题


  • 如果类别和主类都有名叫funA的方法,那么在类别加载完成之后,类的方法列表里会有两个funA;
  • 类别的方法被放到了新方法列表的前面,而主类的方法被放到了新方法列表的后面,这就造成了类别方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会停止查找,殊不知后面可能还有一样名字的方法;
  • 如果多个类别定义了同名方法funA,具体调用哪个类别的实现由编译顺序决定,后编译的类别的实现将被调用。
  • 在日常开发过程中,类别方法重名轻则造成调用不正确,重则造成crash,我们可以通过给类别方法名加前缀避免方法重名。


关于类别更深入的解析可以参见美团的技术文章深入理解Objective-C:Category


二、协议


定义


iOS中的协议类似于Java、C++中的接口类,协议在OC中可以用来实现多继承和代理。


方法声明


协议中的方法可以声明为@required(要求实现,如果没有实现,会发出警告,但编译不报错)或者@optional(不要求实现,不实现也不会有警告)。

笔者经常会问面试者如下两个问题:

-怎么判断一个类是否实现了某个协议?很多人不知道可以通过conformsToProtocol来判断。

-假如你要求业务方实现一个delegate,你怎么判断业务方有没有实现dalegate的某个方法?很多人不知道可以通过respondsToSelector来判断。


三、通知中心


iOS中的通知中心实际上是观察者模式的一种实现。


postNotification是同步调用还是异步调用?


同步调用。当调用addObserver方法监听通知,然后调用postNotification抛通知,postNotification会在当前线程遍历所有的观察者,然后依次调用观察者的监听方法,调用完成后才会去执行postNotification后面的代码。


如何实现异步监听通知?


通过addObserverForName:object:queue:usingBlock来实现异步通知。


四、KVC


KVC查找顺序


1)调用setValue:forKey时候,比如[obj setValue:@"akon" forKey:@"key"]时候,会按照key,iskey,key,iskey的顺序搜索成员并进行赋值操作。如果都没找到,系统会调用该对象的setValue:forUndefinedKey方法,该方法默认是抛出异常。

2)当调用valueForKey:@"key"的代码时,KVC对key的搜索方式不同于setValue"akon" forKey:@"key",其搜索方式如下:


  • 首先按get, is的顺序查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型,会将其包装成一个NSNumber对象。
  • 如果没有找到,KVC则会查找countOf、objectInAtIndex或AtIndexes格式的方法。如果countOf方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调
    用这个代理集合的方法,就会以countOf,objectInAtIndex或AtIndexes这几个方法组合的形式调用。还有一个可选的get:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
    -如果上面的方法没有找到,那么会同时查找countOf,enumeratorOf,memberOf格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf,enumeratorOf,memberOf组合的形式调用。
  • 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按,is,,is的顺序搜索成员变量名。
  • 如果还没找到,直接调用该对象的valueForUndefinedKey:方法,该方法默认是抛出异常。


KVC防崩溃


我们经常会使用KVC来设置属性和获取属性,但是如果对象没有按照KVC的规则声明该属性,则会造成crash,怎么全局通用地防止这类崩溃呢?

可以通过写一个NSObject分类来防崩溃。


@interface NSObject(AKPreventKVCCrash)

@end

@ implementation NSObject(AKPreventKVCCrash)

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
}

- (id)valueForUndefinedKey:(NSString *)key{

return nil;
}

@end


五、KVO


定义


KVO(Key-Value Observing),键值观察。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。


注册、移除KVO


通过如下两个方案来注册、移除KVO


- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;


通过observeValueForKeyPath来获取值的变化。


- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context


我们可以通过facebook开源库KVOController方便地进行KVO。


KVO实现


苹果官方文档对KVO实现介绍如下:


Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.


即当一个类型为 ObjectA 的对象,被添加了观察后,系统会生成一个派生类 NSKVONotifying_ObjectA 类,并将对象的isa指针指向新的类,也就是说这个对象的类型发生了变化。因此在向ObjectA对象发送消息时候,实际上是发送到了派生类对象的方法。由于编译器对派生类的方法进行了 override,并添加了通知代码,因此会向注册的对象发送通知。注意派生类只重写注册了观察者的属性方法。


关于kvc和kvo更深入的详解参考iOS KVC和KVO详解


六、autorelasepool


用处


在 ARC 下,我们不需要手动管理内存,可以完全不知道 autorelease 的存在,就可以正确管理好内存,因为 Runloop 在每个 Runloop Circle 中会自动创建和释放Autorelease Pool。

当我们需要创建和销毁大量的对象时,使用手动创建的 autoreleasepool 可以有效的避免内存峰值的出现。因为如果不手动创建的话,外层系统创建的 pool 会在整个 Runloop Circle 结束之后才进行 drain,手动创建的话,会在 block 结束之后就进行 drain 操作,比如下面例子:


for (int i = 0; i < 100000; i++)
{
@autoreleasepool
{
NSString* string = @"akon";
NSArray* array = [string componentsSeparatedByString:string];
}
}


比如SDWebImage中这段代码,由于encodedDataWithImage会把image解码成data,可能造成内存暴涨,所以加autoreleasepool避免内存暴涨


 @autoreleasepool {
NSData *data = imageData;
if (!data && image) {
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
SDImageFormat format;
if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
}
[self _storeImageDataToDisk:data forKey:key];
}


Runloop中自动释放池创建和释放时机


  • 系统在 Runloop 中创建的 autoreleaspool 会在 Runloop 一个 event 结束时进行释放操作。
  • 我们手动创建的 autoreleasepool 会在 block 执行完成之后进行 drain 操作。需要注意的是:
    当 block 以异常结束时,pool 不会被 drain
    Pool 的 drain 操作会把所有标记为 autorelease 的对象的引用计数减一,但是并不意味着这个对象一定会被释放掉,我们可以在 autorelease pool 中手动 retain 对象,以延长它的生命周期(在 MRC 中)。


收起阅读 »

iOS面试基础知识 (一)

iOS
iOS面试基础知识 (一)一、Runtime原理Runtime是iOS核心运行机制之一,iOS App加载库、加载类、执行方法调用,全靠Runtime,这一块的知识个人认为是最基础的,基本面试必问。1、Runtime消息发送机制1)iOS调用一个方法时,实际上...
继续阅读 »

iOS面试基础知识 (一)


一、Runtime原理


Runtime是iOS核心运行机制之一,iOS App加载库、加载类、执行方法调用,全靠Runtime,这一块的知识个人认为是最基础的,基本面试必问。


1、Runtime消息发送机制


1)iOS调用一个方法时,实际上会调用objc_msgSend(receiver, selector, arg1, arg2, ...),该方法第一个参数是消息接收者,第二个参数是方法名,剩下的参数是方法参数;

2)iOS调用一个方法时,会先去该类的方法缓存列表里面查找是否有该方法,如果有直接调用,否则走第3)步;

3)去该类的方法列表里面找,找到直接调用,把方法加入缓存列表;否则走第4)步;

4)沿着该类的继承链继续查找,找到直接调用,把方法加入缓存列表;否则消息转发流程;

很多面试者大体知道这个流程,但是有关细节不是特别清楚。


  • 问他/她objc_msgSend第一个参数、第二个参数、剩下的参数分别代表什么,不知道;
  • 很多人只知道去方法列表里面查找,不知道还有个方法缓存列表。
    通过这些细节,可以了解一个人是否真正掌握了原理,而不是死记硬背。


2、Runtime消息转发机制


如果在消息发送阶段没有找到方法,iOS会走消息转发流程,流程图如下所示:


1)动态消息解析。检查是否重写了resolveInstanceMethod 方法,如果返回YES则可以通过class_addMethod 动态添加方法来处理消息,否则走第2)步;

2)消息target转发。forwardingTargetForSelector 用于指定哪个对象来响应消息。如果返回nil 则走第3)步;

3)消息转发。这步调用 methodSignatureForSelector 进行方法签名,这可以将函数的参数类型和返回值封装。如果返回 nil 执行第四步;否则返回 methodSignature,则进入 forwardInvocation ,在这里可以修改实现方法,修改响应对象等,如果方法调用成功,则结束。否则执行第4)步;

4)报错 unrecognized selector sent to instance。

很多人知道这四步,但是笔者一般会问:


  • 怎么在项目里全局解决"unrecognized selector sent to instance"这类crash?本人发现很多人回答不出来,说明面试者肯定是在死记硬背,你都知道因为消息转发那三步都没处理才会报错,为什么不知道在消息转发里面处理呢?
  • 如果面试者知道可以在消息转发里面处理,防止崩溃,再问下面试者,你项目中是在哪一步处理的,看看其是否有真正实践过?


二、load与initialize


1、load与initialize调用时机


+load在main函数之前被Runtime调用,+initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。


2、load与initialize在分类、继承链的调用顺序


  • load方法的调用顺序为:
    子类的 +load 方法会在它的所有父类的 +load 方法之后执行,而分类的 +load 方法会在它的主类的 +load 方法之后执行。
    如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。
  • initialize的调用顺序为:
    +initialize 方法的调用与普通方法的调用是一样的,走的都是消息发送的流程。如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖。
  • 怎么确保在load和initialize的调用只执行一次
    由于load和initialize可能会调用多次,所以在这两个方法里面做的初始化操作需要保证只初始化一次,用dispatch_once来控制


笔者在面试过程中发现很多人对于load与initialize在分类、继承链的调用顺序不清楚。对怎么保证初始化安全也不清楚


三、RunLoop原理


RunLoop苹果原理图



图中展现了 Runloop 在线程中的作用:从 input source 和 timer source 接受事件,然后在线程中处理事件。


1、RunLoop与线程关系


  • 一个线程是有一个RunLoop还是多个RunLoop? 一个;
  • 怎么启动RunLoop?主线程的RunLoop自动就开启了,子线程的RunLoop通过Run方法启动。


2、Input Source 和 Timer Source


两个都是 Runloop 事件的来源,其中 Input Source 又可以分为三类


  • Port-Based Sources,系统底层的 Port 事件,例如 CFSocketRef ,在应用层基本用不到;
  • Custom Input Sources,用户手动创建的 Source;
  • Cocoa Perform Selector Sources, Cocoa 提供的 performSelector 系列方法,也是一种事件源;
    Timer Source指定时器事件,该事件的优先级是最低的。
    本人一般会问定时器事件的优先级是怎么样的,大部分人回答不出来。


3、解决NSTimer事件在列表滚动时不执行问题


因为定时器默认是运行在NSDefaultRunLoopMode,在列表滚动时候,主线程会切换到UITrackingRunLoopMode,导致定时器回调得不到执行。

有两种解决方案:


  • 指定NSTimer运行于 NSRunLoopCommonModes下。
  • 在子线程创建和处理Timer事件,然后在主线程更新 UI。


四、事件分发机制及响应者链


1、事件分发机制


iOS 检测到手指触摸 (Touch) 操作时会将其打包成一个 UIEvent 对象,并放入当前活动Application的事件队列,UIApplication 会从事件队列中取出触摸事件并传递给单例的 UIWindow 来处理,UIWindow 对象首先会使用 hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为 hit-test view。

hitTest:withEvent:方法的处理流程如下:


  • 首先调用当前视图的 pointInside:withEvent: 方法判断触摸点是否在当前视图内;
  • 若返回 NO, 则 hitTest:withEvent: 返回 nil,若返回 YES, 则向当前视图的所有子视图 (subviews) 发送 hitTest:withEvent: 消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图(后加入的先遍历),直到有子视图返回非空对象或者全部子视图遍历完毕;
  • 若第一次有子视图返回非空对象,则 hitTest:withEvent: 方法返回此对象,处理结束;
  • 如所有子视图都返回空,则 hitTest:withEvent: 方法返回自身 (self)。
    流程图如下:


2、响应者链原理


iOS的事件分发机制是为了找到第一响应者,事件的处理机制叫做响应者链原理。

所有事件响应的类都是 UIResponder 的子类,响应者链是一个由不同对象组成的层次结构,其中的每个对象将依次获得响应事件消息的机会。当发生事件时,事件首先被发送给第一响应者,第一响应者往往是事件发生的视图,也就是用户触摸屏幕的地方。事件将沿着响应者链一直向下传递,直到被接受并做出处理。一般来说,第一响应者是个视图对象或者其子类对象,当其被触摸后事件被交由它处理,如果它不处理,就传递给它的父视图(superview)对象(如果存在)处理,如果没有父视图,事件就会被传递给它的视图控制器对象 ViewController(如果存在),接下来会沿着顶层视图(top view)到窗口(UIWindow 对象)再到程序(UIApplication 对象)。如果整个过程都没有响应这个事件,该事件就被丢弃。一般情况下,在响应者链中只要由对象处理事件,事件就停止传递。

一个典型的事件响应路线如下:

First Responser --> 父视图-->The Window --> The Application --> nil(丢弃)

我们可以通过 [responder nextResponder] 找到当前 responder 的下一个 responder,持续这个过程到最后会找到 UIApplication 对象。


五、内存泄露检测与循环引用


1、造成内存泄露原因


  • 在用C/C++时,创建对象后未销毁,比如调用malloc后不free、调用new后不delete;
  • 调用CoreFoundation里面的C方法后创建对对象后不释放。比如调用CGImageCreate不调用CGImageRelease;
  • 循环引用。当对象A和对象B互相持有的时候,就会产生循环引用。常见产生循环引用的场景有在VC的cellForRowAtIndexPath方法中cell block引用self。


2、常见循环引用及解决方案


1) 在VC的cellForRowAtIndexPath方法中cell的block直接引用self或者直接以_形式引用属性造成循环引用。


 cell.clickBlock = ^{
self.name = @"akon";
};

cell.clickBlock = ^{
_name = @"akon";
};


解决方案:把self改成weakSelf;


__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
weakSelf.name = @"akon";
};


2)在cell的block中直接引用VC的成员变量造成循环引用。


//假设 _age为VC的成员变量
@interface TestVC(){

int _age;

}
cell.clickBlock = ^{
_age = 18;
};


解决方案有两种:


  • 用weak-strong dance


__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf->age = 18;
};


  • 把成员变量改成属性


//假设 _age为VC的成员变量
@interface TestVC()

@property(nonatomic, assign)int age;

@end

__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
weakSelf.age = 18;
};


3)delegate属性声明为strong,造成循环引用。


@interface TestView : UIView

@property(nonatomic, strong)id<TestViewDelegate> delegate;

@end

@interface TestVC()<TestViewDelegate>

@property (nonatomic, strong)TestView* testView;

@end

testView.delegate = self; //造成循环引用


解决方案:delegate声明为weak


@interface TestView : UIView

@property(nonatomic, weak)id<TestViewDelegate> delegate;

@end


4)在block里面调用super,造成循环引用。


cell.clickBlock = ^{
[super goback]; //造成循环应用
};


解决方案,封装goback调用


__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
[weakSelf _callSuperBack];
};

- (void) _callSuperBack{
[self goback];
}


5)block声明为strong

解决方案:声明为copy

6)NSTimer使用后不invalidate造成循环引用。

解决方案:


  • NSTimer用完后invalidate;
  • NSTimer分类封装


+ (NSTimer *)ak_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)(void))block
repeats:(BOOL)repeats{

return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ak_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}

+ (void)ak_blockInvoke:(NSTimer*)timer{

void (^block)(void) = timer.userInfo;
if (block) {
block();
}
}

--



3、怎么检测循环引用


  • 静态代码分析。 通过Xcode->Product->Anaylze分析结果来处理;
  • 动态分析。用MLeaksFinder(只能检测OC泄露)或者Instrument或者OOMDetector(能检测OC与C++泄露)。


六、VC生命周期


考察viewDidLoad、viewWillAppear、ViewDidAppear等方法的执行顺序。

假设现在有一个 AViewController(简称 Avc) 和 BViewController (简称 Bvc),通过 navigationController 的push 实现 Avc 到 Bvc 的跳转,调用顺序如下:

1、A viewDidLoad 

2、A viewWillAppear 

3、A viewDidAppear 

4、B viewDidLoad 

5、A viewWillDisappear 

6、B viewWillAppear 

7、A viewDidDisappear 

8、B viewDidAppear

如果再从 Bvc 跳回 Avc,调用顺序如下:

1、B viewWillDisappear 

2、A viewWillAppear 

3、B viewDidDisappear 

4、A viewDidAppear

收起阅读 »

iOS 简单模拟服务器如何解析客户端传来的表单数据及图片格式数据并本地保存

iOS
废话开篇:在日常开发中经常会有上传表单及图片到服务器场景,这里有两种实现方式:一、单独封装一个图片文件格式存储代码,服务器对 Response 返回值里面返回服务器图片路径,再通过其他接口绑定服务器图片路径;二、表单及图片文件直接提交。那么...
继续阅读 »


废话开篇:在日常开发中经常会有上传表单及图片到服务器场景,这里有两种实现方式:一、单独封装一个图片文件格式存储代码,服务器对 Response 返回值里面返回服务器图片路径,再通过其他接口绑定服务器图片路径;二、表单及图片文件直接提交。那么,其实说是两种方式,其实归根到底就是一种:数据传输与接收。那么,下面就在 OC 上简单模拟服务器如何解析客户端传来的表单数据及图片格式数据

以前文章地址:

# iOS 简单模拟 https 证书信任逻辑

# iOS 基于 CocoaHTTPServer 搭建手机内部服务器,实现 http 及 https 访问、传输数据

基于上述文章继续进行本次的 模拟服务器如何解析客户端传来的表单数据及图片格式数据

效果如下:

屏幕录制2021-11-18 下午4.17.38.gif

前言说明:

这里简单说一下 AFNetwork 下是如何同时进行数据参数提交及文件上传的。这里只是简单的说一下思路:

先上一段简单的 AF 请求代码

    AFHTTPSessionManager * m = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"https://10.10.60.20"]];

NSDictionary * dic = @{@"title":@"中国万岁",@"name":@"中国人"};

    [m POST:@"https://10.10.60.20:12345/doPost" parameters:dic headers:@{} constructingBodyWithBlock:^(**id**<AFMultipartFormData>  _Nonnull formData) {

        NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0]; // 获取当前时间0秒后的时间

        NSTimeInterval time = [date timeIntervalSince1970]*1000;// *1000 是精确到毫秒(13位),不乘就是精确到秒(10位)

        NSString *timeString = [NSString stringWithFormat:@"iOS%.0f", time];

        UIImage * image = [UIImage imageNamed:@"sea"];

        NSData *data = UIImageJPEGRepresentation(image, 0.5f);

        [formData appendPartWithFileData:data name:@"file" fileName:[NSString stringWithFormat:@"%@.jpg",timeString] mimeType:@"image/jpg"];

        } progress:^(NSProgress * _Nonnull uploadProgress) {          

        } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {

        } failure:^(NSURLSessionDataTask * **_Nullable** task, NSError * _Nonnull error) {   

        }
     ];

1、网络请求参数的传入

这里代码无需过多解释,dic 就是要传输的请求参数,那么,在这个参数完成之后,其实 AFNetworking 就对参数进行了存储,并且在后面的图片上传的时候用拼接的 NSData 的方式进行数据拼接。

2、图片数据获取及 NSData 拼接

AF 调用下面的方法进行了请求数据的拼接。

[formData appendPartWithFileData:data name:@"file" fileName:[NSString stringWithFormat:@"%@.jpg",timeString] mimeType:@"image/jpg"];

3、基于第二步骤,创建多个数据读取对象,通过 Stream 进行 NSData 的依次读取,因为 AF 下的 POST 请求会跟一个 Stream 进行绑定

[self.request setHTTPBodyStream:self.bodyStream];

那么,在开启的发送请求前,AF 又重写了 Stream 下

- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)length

方法。进而可以在 Stream 读取的过程中对多个文件 data 进行拼接,最终将整个数据进行一次传输。

4、注意事项:(1)AF 会在 header 里面进行数据总长度的标定,这样服务器在最先拿到 header 时便可以知晓此次传输的数据总长度。(2)AF 会随机生成一个 boundary 也放到 header 里面,这个参数的目的就是将请求中不通的 参数文件进行边界划分,这样,服务器在解析的时候就知道了哪些 data 是一个完整的数据。当然,AF 也会标定一下传输类型在 header 里,比如:Content-Type

好了,上述其实只是一个铺垫,来看一下最终如何总 data 里解析出请求参数及图片文件

步骤一、基于 CocoaHTTPServer 搭建完的本地 OC 服务器进行数据解析

对于如何搭建的请参考上面的文章链接

这里要处理的就是下面的这个方法,客户端传过来的数据都会在这个方法里执行,因为一个系统的 Stream 一次性读取最大数是有限制的,所以,对于大文件上传的过程,此方法会走多次。

- (void)processBodyData:(NSData *)postDataChunk;

思路:因为服务器收到所有的 data 里完整的参数数据都是用换行符来分割的,那么通过对 "\r\n" 换行符进行切割,那么,两个换行符之间的数据就是一个完整的参数。

- (void)parseData:(NSData *)postDataChunk
{
//这里记录图片文件 data 在数据接收总 data 里的初始位置索引
    int fileDataStartIndex = 0;
    //换行符\r\n
    UInt16 separatorBytes = 0X0A0D;
    NSData * separatorData = [NSData dataWithBytes:&separatorBytes length:2];
    int l = (int)[separatorData length];
//遍历接收的数据,找到所有以 0A0D 分割的完整 data 数据
    for (int i = 0; i < [postDataChunk length] - l; i++) {
//以换行符长度为单位依次排查、寻找
        NSRange searchRange = {i,l};
        //是换行符
        if ([[postDataChunk subdataWithRange:searchRange] isEqualToData:separatorData]) {
            
            //获取换行符之间的data的位置
            NSRange newDataRange = {self.dataStartIndex,i - self.dataStartIndex};
            self.dataStartIndex = i + l;
//这里先进性请求参数的筛选,文件data保存位置偏后,那么,一开始就需要 self.paramReceiveComplete 标识来标定是否排查到文件 data 了
            if (self.paramReceiveComplete) {
                fileDataStartIndex = i + l;
                continue;
            }

            //跳过换行符
            i += (l-1);
//获取换行符之间的完整数据格式
            NSData * newData = [postDataChunk subdataWithRange:newDataRange];
//判断是否为空
            if ([newData length]) {
//获取文本信息
                NSString *content = [[NSString alloc] initWithData:newData encoding:NSUTF8StringEncoding];
//替换所有的换行特殊字符
                content = [content stringByReplacingOccurrencesOfString:@"\r\n" withString:@""];
//这里注意的是边界信息 Boundary ,也就是 AF 给钉里面的数据不解析
                if (content.length && ![content containsString:@"--Boundary"]) {
//如果解析到文件,那么 content 里会包含 name="file" 的标识,用此标识进行数据格式的判断
                    if ([content containsString:@"name=\"file\""]){
//读到文件了
                        self.currentParserType = @"file";
                    } else {
//请求参数
                        self.currentParserType = @"text/plain";
                    }

                    //表单数据解析
                    if ([self.currentParserType containsString:@"text/plain"]){
//content 里面包含 form-data,说明是数据参数说明,里面会包含 key 值
                        if ([content containsString:@"form-data"]) {
                            NSString * key = [content componentsSeparatedByString:@"name="].lastObject;
                            key = [key stringByReplacingOccurrencesOfString:@"\"" withString:@""];
//这里临时保存了key值,在后面解析到 value 的时候进行数据绑定
                            self.currentParamKey = key;
                        } else {
//解析到了 value 用 self.currentParamKey 进行绑定
                            if (self.currentParamKey && content) {
                                [self.receiveParamDic setValue:content forKey:self.currentParamKey];
                            }
                        }
                    } else {
                        //开始文件处理,标定一下,因为由于文件大小的影响,此方法会走多次,那么,在一开始标定后,下一次再进来就直接进行文件数据的拼接
                        self.paramReceiveComplete = YES;
                    }
                }
            }
        }
    }

//文件的写入(其实这里不是很严谨,因为请求参数较小的原因,所以,即便是第一次执行此方法,里面也会有文件 data 开始读取的情况)
    NSRange fileDataRange = {fileDataStartIndex,postDataChunk.length - fileDataStartIndex};
    NSData * fileData = [postDataChunk subdataWithRange:fileDataRange];
    [self.outputStream write:[fileData bytes] maxLength:fileData.length];

}

步骤二、数据写入沙盒

声明一个 NSOutputStream 对象

@property (nonatomic,strong) NSOutputStream * outputStream;

CocoaHTTPServer -> HTTPConnection 类是不进行常规化的 init 的,所以,初始化 outputStream 这里用懒加载的形式。

- (NSOutputStream *)outputStream

{

    if (!_outputStream) {
        NSString * cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
        NSString * filePath = [cachePath stringByAppendingPathComponent:@"wsl.png"];
        NSLog(@"filePath = %@",filePath);
        _outputStream = [[NSOutputStream alloc] initToFileAtPath:filePath append:**YES**];
        [_outputStream open];
    }
    return _outputStream;
}

进行文件写入沙盒操作:

    NSRange fileDataRange = {fileDataStartIndex,postDataChunk.length - fileDataStartIndex};
    NSData * fileData = [postDataChunk subdataWithRange:fileDataRange];
    [self.outputStream write:[fileData bytes] maxLength:fileData.length];

在处理完数据后关闭流

- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{
    [self.outputStream close];
    self.outputStream = nil;
}

步骤三、查看运行结果

先看是否获取了请求的参数:

image.png

在看图片是否保存完成,通过打印模拟器的沙盒路径,直接 前往文件夹 即可找到沙盒文件

image.png

可以看到,这里保存图片也成功了。

image.png

这里说明一下:

遵循 MultipartFormDataParserDelegate 协议也可以直接获取文件的 data ,直接去读,再去存即可。但是它没有暴露给外界数据请求的 key 而只有 value,但是如果仅作为文件的传输还是很方便的。

如下:

遵循代理协议

image.png

声明 MultipartFormDataParser 对象

image.png

MultipartFormDataParser 对象进行数据解析

image.png

进行文件数据解析代理执行

image.png

其实 CocoaHTTPServer 封装的解析工具类实现原理亦是如此。

好了,简单模拟服务器如何解析客户端传来的表单数据及图片格式数据并本地保存 功能就实现完了,代码拙劣,大神勿笑。

收起阅读 »

Metal 框架之渲染管线渲染图元

iOS
「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」概述在 《 Metal 框架之使用 Metal 来绘制视图内容 》中,介绍了如何设置 MTKView 对象并使用渲染通道更改视图的内容,实现了将背景色渲染为视图的内容。本示...
继续阅读 »


「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

概述

在 《 Metal 框架之使用 Metal 来绘制视图内容 》中,介绍了如何设置 MTKView 对象并使用渲染通道更改视图的内容,实现了将背景色渲染为视图的内容。本示例将介绍如何配置渲染管道,作为渲染通道的一部分,在视图中绘制一个简单的 2D 彩色三角形。该示例为每个顶点提供位置和颜色,渲染管道使用该数据,在指定的顶点颜色之间插入颜色值来渲染三角形。

在本示例中,将介绍如何编写顶点和片元函数、如何创建渲染管道状态对象,以及最后对绘图命令进行编码。

triangle’s vertices.png

理解 Metal 渲染管线

渲染管线处理绘图命令并将数据写入渲染通道的目标中。一个完整地渲染管线有许多阶段组成,一些阶段需要使用着色器进行编程,而一些阶段则需要配置固定的功能件。本示例的管线主要包含三个阶段:顶点阶段、光栅化阶段和片元阶段。其中,顶点阶段和片元阶段是可编程的,这可以使用 Metal Shading Language (MSL) 来编写函数,而光栅化阶段则是不可编程的,直接使用固有功能件来配置。

render piple.png

渲染从绘图命令开始,其中包括顶点个数和要渲染的图元类型。如下是本例子的绘图命令:


// Draw the triangle.

[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle

                  vertexStart:0

                  vertexCount:3];


顶点阶段会处理每个顶点的数据。当顶点经过顶点阶段处理后,渲染管线会对图元光栅化处理,以此来确定渲染目标中的哪些像素位于图元的边界内(即图元可以转化成的像素)。片元阶段是要确定渲染目标的像素值。

自定义渲染管线

顶点函数为单个顶点生成数据,片元函数为单个片元生成数据,可以通过编写函数来指定它们的工作方式。我们可以依据希望管道完成什么功能以及如何完成来配置管道的各个阶段。

决定将哪些数据传递到渲染管道以及将哪些数据传递到管道的后期阶段,通常可以在三个地方执行此操作:

  • 管道的输入,由 App 提供并传递到顶点阶段。

  • 顶点阶段的输出,它被传递到光栅化阶段。

  • 片元阶段的输入,由 App 提供或由光栅化阶段生成。

在本示例中,管道的输入数据包括顶点的位置及其颜色。为了演示顶点函数中执行的转换类型,输入坐标在自定义坐标空间中定义,以距视图中心的像素为单位进行测量。这些坐标需要转换成 Metal 的坐标系。

声明一个 AAPLVertex 结构,使用 SIMD 向量类型来保存位置和颜色数据。


typedef struct

{

    vector_float2 position;

    vector_float4 color;

} AAPLVertex;


SIMD 类型在 Metal Shading Language 中很常见,相应的需要在 App 中使用 simd 库。 SIMD 类型包含特定数据类型的多个通道,因此将位置声明为 vector_float2 意味着它包含两个 32 位浮点值(x 和 y 坐标)。颜色使用 vector_float4 存储,因此它们有四个通道:红色、绿色、蓝色和 alpha。

在 App 中,输入数据使用常量数组指定:


static const AAPLVertex triangleVertices[] =

{

    // 2D positions,    RGBA colors

    { {  250,  -250 }, { 1, 0, 0, 1 } },

    { { -250,  -250 }, { 0, 1, 0, 1 } },

    { {    0,   250 }, { 0, 0, 1, 1 } },

};


顶点阶段为顶点生成数据,需要提供颜色和变换的位置。使用 SIMD 类型声明一个包含位置和颜色值的 RasterizerData 结构。


struct RasterizerData

{

    // The [[position]] attribute of this member indicates that this value

    // is the clip space position of the vertex when this structure is

    // returned from the vertex function.

    float4 position [[position]];



    // Since this member does not have a special attribute, the rasterizer

    // interpolates its value with the values of the other triangle vertices

    // and then passes the interpolated value to the fragment shader for each

    // fragment in the triangle.

    float4 color;

};


输出位置(在下面详细描述)必须定义为 vector_float4 类型。颜色在输入数据结构中声明。

需要告诉 Metal 光栅化数据中的哪个字段提供位置数据,因为 Metal 不会对结构中的字段强制执行任何特定的命名约定。使用 [[position]] 属性限定符来标记位置字段,使用它来保存该字段输出位置。

fragment 函数只是将光栅化阶段的数据传递给后面的阶段,因此它不需要任何额外的参数。

定义顶点函数

需要使用 vertex 关键字来定义顶点函数,包含入参和出参。


vertex RasterizerData

vertexShader(uint vertexID [[vertex_id]],

             constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],

             constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])


第一个参数 vertexID 使用 [[vertex_id]] 属性限定符来修饰,它是 Metal 关键字。当执行渲染命令时,GPU 会多次调用顶点函数,为每个顶点生成一个唯一值。

第二个参数 vertices 是一个包含顶点数据的数组,使用之前定义的 AAPLVertex 结构。

要将位置转换为 Metal 的坐标,该函数需要绘制三角形的视口的大小(以像素为单位),因此需要将其存储在 viewportSizePointer 参数中。

第二个和第三个参数使用 [[buffer(n)]] 属性限定符来修饰。默认情况下,Metal 自动为每个参数分配参数表中的插槽。当使用 [[buffer(n)]] 限定符修饰缓冲区参数时,明确地告诉 Metal 要使用哪个插槽。显式声明插槽可以方便的修改着色器代码,而无需更改 App 代码。

编写顶点函数

 编写的顶点函数必须生成输出结构的两个字段,使用 vertexID 参数索引顶点数组并读取顶点的输入数据,还需要获取视口尺寸。


float2 pixelSpacePosition = vertices[vertexID].position.xy;

// Get the viewport size and cast to float.

vector_float2 viewportSize = vector_float2(*viewportSizePointer);
复制代码

顶点函数必须提供裁剪空间坐标中的位置数据,这些位置数据是 3D 的点,使用四维齐次向量 (x,y,z,w) 来表示。光栅化阶段获取输出位置,并将 x、y 和 z 坐标除以 w 以生成归一化设备坐标中的 3D 点。归一化设备坐标与视口大小无关。

NDC_ coordinates.png

归一化设备坐标使用左手坐标系来映射视口中的位置。图元被裁剪到这个坐标系中的一个裁剪框上,然后被光栅化。剪切框的左下角位于 (-1.0,-1.0) 坐标处,右上角位于 (1.0,1.0) 处。正 z 值指向远离相机(指向屏幕)。z 坐标的可见部分在 0.0(近剪裁平面)和 1.0(远剪裁平面)之间。

下图是将输入坐标系转换为归一化的设备坐标系。

ndc转换.png

因为这是一个二维应用,不需要齐次坐标,所以先给输出坐标写一个默认值,w值设置为1.0,其他坐标设置为0.0。这意味顶点函数在该坐标空间中生成的 (x,y) 已经在归一化设备坐标空间中了。将输入位置除以1/2视口大小就生成归一化的设备坐标。由于此计算是使用 SIMD 类型执行的,因此可以使用一行代码同时计算两个通道,执行除法并将结果放在输出位置的 x 和 y 通道中。


out.position = vector_float4(0.0, 0.0, 0.0, 1.0);

out.position.xy = pixelSpacePosition / (viewportSize / 2.0);


最后,将颜色值赋给 out.color 作为返回值。


out.color = vertices[vertexID].color;


编写片元函数

片元阶段对渲染目标可以做修改处理。光栅化器确定渲染目标的哪些像素被图元覆盖,仅处于三角形片元中的那些像素才会被渲染。

光栅化阶段.png

片元函数处理光栅化后的位置信息,并计算每个渲染目标的输出值。这些片元值由管道中的后续阶段处理,最终写入渲染目标。

本示例中的片元着色器接收与顶点着色器的输出中声明的相同参数。使用 fragment 关键字声明片元函数。它只有一个输入参数,与顶点阶段提供的 RasterizerData 结构相同。添加 [[stage_in]] 属性限定符以指示此参数由光栅化器生成。


fragment float4 fragmentShader(RasterizerData in [[stage_in]])


如果片元函数写入多个渲染目标,则必须为每个渲染目标声明一个变量。由于此示例只有一个渲染目标,因此可以直接指定一个浮点向量作为函数的输出,此输出是要写入渲染目标的颜色。

光栅化阶段计算每个片元参数的值并用它们调用片元函数。光栅化阶段将其颜色参数计算为三角形顶点处颜色的混合,片元离顶点越近,顶点对最终颜色的贡献就越大。

颜色插值.png

将内插颜色作为函数的输出返回。


return in.color;


创建渲染管线状态对象

完成着色器函数编写后,需要创建一个渲染管道,通过 MTLLibrary 为每个着色器函数指定一个 MTLFunction 对象。


id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];


id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];

id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];


接下来,创建一个 MTLRenderPipelineState 对象,使用 MTLRenderPipelineDescriptor 来配置管线。


MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];

pipelineStateDescriptor.label = @"Simple Pipeline";

pipelineStateDescriptor.vertexFunction = vertexFunction;

pipelineStateDescriptor.fragmentFunction = fragmentFunction;

pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;



_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor

                                                        
error:&error];




除了指定顶点和片元函数之外,还可以指定渲染目标的像素格式。像素格式 (MTLPixelFormat) 定义了像素数据的内存布局。对于简单格式,此定义包括每个像素的字节数、存储在像素中的数据通道数以及这些通道的位布局。渲染管线状态必须使用与渲染通道指定的像素格式兼容的像素格式才能够正确渲染,由于此示例只有一个渲染目标并且它由视图提供,因此将视图的像素格式复制到渲染管道描述符中。

使用 Metal 创建渲染管道状态对象时,渲染管线需要转换片元函数的输出像素格式为渲染目标的像素格式。如果要针对不同的像素格式,则需要创建不同的管道状态对象,可以在不同像素格式的多个管道中使用相同的着色器。

设置视口

有了管道的渲染管道状态对象后,就可以使用渲染命令编码器来渲染三角形了。首先,需要设置视口来告诉 Metal 要绘制到渲染目标的哪个部分。


// Set the region of the drawable to draw into.

[renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, 0.0, 1.0 }];


设置渲染管线状态

为渲染管线指定渲染管线状态对象。


[renderEncoder setRenderPipelineState:_pipelineState];


将参数数据发送到顶点函数

通常使用缓冲区 (MTLBuffer) 将数据传递给着色器。但是,当只需要向顶点函数传递少量数据时,可以将数据直接复制到命令缓冲区中。

该示例将两个参数的数据复制到命令缓冲区中,顶点数据是从定义的数组复制而来的,视口数据是从设置视口的同一变量中复制的,片元函数仅使用从光栅化器接收的数据,因此没有传递参数。


[renderEncoder setVertexBytes:triangleVertices

                       length:sizeof(triangleVertices)

                      atIndex:AAPLVertexInputIndexVertices];



[renderEncoder setVertexBytes:&_viewportSize

                       length:sizeof(_viewportSize)

                      atIndex:AAPLVertexInputIndexViewportSize];


编码绘图命令

指定图元的种类、起始索引和顶点数。当三角形被渲染时,vertex 函数被调用,参数 vertexID 的值分别为 0、1 和 2。


// Draw the triangle.

[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle

                  vertexStart:0

                  vertexCount:3];


与使用 Metal 绘制到屏幕一样,需要结束编码过程并提交命令缓冲区。不同之处是,可以使用相同的一组步骤对更多渲染命令进行编码。按照指定的顺序来执行命令,生成最终渲染的图像。 (为了性能,GPU 可以并行处理命令甚至部分命令,只要最终结果是按顺序渲染的就行。)

颜色插值

在此示例中,颜色值是在三角形内部插值计算出来的。有时希望由一个顶点生成一个值并在整个图元中保持不变,这需要在顶点函数的输出上指定 flat 属性限定符来执行此操作。示例项目中,通过在颜色字段中添加 [[flat]] 限定符来实现此功能。


float4 color [[flat]];


渲染管线使用三角形的第一个顶点(称为激发顶点)的颜色值,并忽略其他两个顶点的颜色。还可以混合使用 flat 着色和内插值,只需在顶点函数的输出上添加或删除 flat 限定符即可。

总结

本文介绍了如何配置渲染管道,如何编写顶点和片元函数、如何创建渲染管道状态对象,以及最后对绘图命令进行编码,最终在视图中绘制一个简单的 2D 彩色三角形。

本文示例代码下载

收起阅读 »