注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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 彩色三角形。

本文示例代码下载

收起阅读 »

来聊聊 关于SwiftUI State的一些细节

本文转载自:onevcat.com/2021/01/swi…,本文转载出于传递更多信息之目的,版权归原作者或者来源机构所有。@State 基础在 SwiftUI 中,我们使用 @State 进行私有状态管理,并驱动 View&nb...
继续阅读 »


本文转载自:onevcat.com/2021/01/swi…,本文转载出于传递更多信息之目的,版权归原作者或者来源机构所有。

@State 基础

在 SwiftUI 中,我们使用 @State 进行私有状态管理,并驱动 View 的显示,这是基础中的基础。比如,下面的 ContentView 将在点击加号按钮时将显示的数字 +1:

struct ContentView: View {
@State private var value = 99
var body: some View {
VStack(alignment: .leading) {
Text("Number: (value)")
Button("+") { value += 1 }
}
}
}

当我们想要将这个状态值传递给下层子 View 的时候,直接在子 View 中声明一个变量就可以了。下面的 View 在表现上来说完全一致:

struct DetailView: View {
let number: Int
var body: some View {
Text("Number: (number)")
}
}

struct ContentView: View {
@State private var value = 99
var body: some View {
VStack(alignment: .leading) {
DetailView(number: value)
Button("+") { value += 1 }
}
}
}

在 ContentView 中的 @State value 发生改变时,ContentView.body 被重新求值,DetailView 将被重新创建,包含新数字的 Text 被重新渲染。一切都很顺利。

子 View 中自己的 @State

如果我们希望的不完全是这种被动的传递,而是希望 DetailView 也拥有这个传入的状态值,并且可以自己对这个值进行管理的话,一种方法是在让 DetailView 持有自己的 @State,然后通过初始化方法把值传递进去:

struct DetailView0: View {
@State var number: Int
var body: some View {
HStack {
Text("0: (number)")
Button("+") { number += 1 }
}
}
}

// ContentView
@State private var value = 99
var body: some View {
// ...
DetailView0(number: value)
}

这种方法能够奏效,但是违背了 @State 文档中关于这个属性标签的说明:

… declare your state properties as private, to prevent clients of your view from accessing them.

如果一个 @State 无法被标记为 private 的话,一定是哪里出了问题。一种很朴素的想法是,将 @State 声明为 private,然后使用合适的 init 方法来设置它。更多的时候,我们可能需要初始化方法来解决另一个更“现实”的问题:那就是使用合适的初始化方法,来对传递进来的 value 进行一些处理。比如,如果我们想要实现一个可以对任何传进来的数据在显示前就进行 +1 处理的 View:

struct DetailView1: View {
@State private var number: Int

init(number: Int) {
self.number = number + 1
}
//
}

但这会给出一个编译错误!

Variable ‘self.number’ used before being initialized

在最新的 Xcode 中,上面的方法已经不会报错了:对于初始化方法中类型匹配的情况,Swift 编译时会将其映射到内部底层存储的值,并完成设置。 不过,对于类型不匹配的情况,这个映射依然暂时不成立。比如下面的 var number: Int? 和输入参数的 number: Int就是一个例子。因此,我决定 还是把下面的讨论再保留一段时间。

一开始你可能对这个错误一头雾水。我们会在本文后面的部分再来看这个错误的原因。现在先把它放在一边,想办法让编译通过。最简单的方式就是把 number 声明为 Int?

struct DetailView1: View {
@State private var number: Int?

init(number: Int) {
self.number = number + 1
}

var body: some View {
HStack {
Text("1: (number ?? 0)")
Button("+") { number = (number ?? 0) + 1 }
}
}
}

// ContentView
@State private var value = 99
var body: some View {
// ...
DetailView1(number: value)
}

问答时间,你觉得 DetailView1 中的 Text 显示的会是什么呢?是 0,还是 100?

如果你回答的是 100 的话,恭喜,你答错掉“坑”里了。比较“出人意料”,虽然我们在 init中设置了 self.number = 100,但在 body 被第一次求值时,number 的值是 nil,因此 0会被显示在屏幕上。

@State 内部

问题出在 @State 上:SwiftUI 通过 property wrapper 简化并模拟了普通的变量读写,但是我们必须始终牢记,@State Int 并不等同于 Int,它根本就不是一个传统意义的存储属性。这个 property wrapper 做的事情大体上说有三件:

  1. 为底层的存储变量 State<Int> 这个 struct 提供了一组 getter 和 setter,这个 State struct 中保存了 Int 的具体数字。
  2. 在 body 首次求值前,将 State<Int> 关联到当前 View 上,为它在堆中对应当前 View 分配一个存储位置。
  3. 为 @State 修饰的变量设置观察,当值改变时,触发新一次的 body 求值,并刷新屏幕。

我们可以看到的 State 的 public 的部分只有几个初始化方法和 property wrapper 的标准的 value:

struct State<Value> : DynamicProperty {
init(wrappedValue value: Value)
init(initialValue value: Value)
var wrappedValue: Value { get nonmutating set }
var projectedValue: Binding<Value> { get }
}

不过,通过打印和 dump State 的值,很容易知道它的几个私有变量。进一步地,可以大致猜测相对更完整和“私密”的 State 结构如下:

struct State<Value> : DynamicProperty {
var _value: Value
var _location: StoredLocation<Value>?

var _graph: ViewGraph?

var wrappedValue: Value {
get { _value }
set {
updateValue(newValue)
}
}

// 发生在 init 后,body 求值前。
func _linkToGraph(graph: ViewGraph) {
if _location == nil {
_location = graph.getLocation(self)
}
if _location == nil {
_location = graph.createAndStore(self)
}
_graph = graph
}

func _renderView(_ value: Value) {
if let graph = _graph {
// 有效的 State 值
_value = value
graph.triggerRender(self)
}
}
}

SwiftUI 使用 meta data 来在 View 中寻找 State 变量,并将用来渲染的 ViewGraph 注入到 State 中。当 State 发生改变时,调用这个 Graph 来刷新界面。关于 State 渲染部分的原理,超出了本文的讨论范围。有机会在后面的博客再进一步探索。

对于 @State 的声明,会在当前 View 中带来一个自动生成的私有存储属性,来存储真实的 State struct 值。比如上面的 DetailView1,由于 @State number 的存在,实际上相当于:

struct DetailView1: View {
@State private var number: Int?
private var _number: State<Int?> // 自动生成
// ...
}

这为我们解释了为什么刚才直接声明 @State var number: Int 无法编译:

struct DetailView1: View {
@State private var number: Int

init(number: Int) {
self.number = number + 1
}
//
}

Int? 的声明在初始化时会默认赋值为 nil,让 _number 完成初始化 (它的值为 State<Optional<Int>>(_value: nil, _location: nil));而非 Optional 的 number 则需要明确的初始化值,否则在调用 self.number 的时候,底层 _number 是没有完成初始化的。

于是“为什么 init 中的设置无效”的问题也迎刃而解了。对于 @State 的设置,只有在 View 被添加到 graph 中以后 (也就是首次 body 被求值前) 才有效。

当前 SwiftUI 的版本中,自动生成的存储变量使用的是在 State 变量名前加下划线的方式。这也是一个代码风格的提示:我们在自己选择变量名时,虽然部分语言使用下划线来表示类型中的私有变量,但在 SwiftUI 中,最好是避免使用 _name 这样的名字,因为它有可能会被系统生成的代码占用 (类似的情况也发生在其他一些 property wrapper 中,比如 Binding 等)。

几种可选方案

在知道了 State struct 的工作原理后,为了达到最初的“在 init 中对传入数据进行一些操作”这个目的,会有几种选择。

首先是直接操作 _number

struct DetailView2: View {
@State private var number: Int

init(number: Int) {
_number = State(wrappedValue: number + 1)
}

var body: some View {
return HStack {
Text("2: (number)")
Button("+") { number += 1 }
}
}
}

因为现在我们直接插手介入了 _number 的初始化,所以它在被添加到 View 之前,就有了正确的初始值 100。不过,因为 _number 显然并不存在于任何文档中,这么做带来的风险是这个行为今后随时可能失效。

另一种可行方案是,将 init 中获取的 number 值先暂存,然后在 @State number 可用时 (也就是在 body ) 中,再进行赋值:

struct DetailView3: View {
@State private var number: Int?
private var tempNumber: Int

init(number: Int) {
self.tempNumber = number + 1
}

var body: some View {
DispatchQueue.main.async {
if (number == nil) {
number = tempNumber
}
}
return HStack {
Text("3: (number ?? 0)")
Button("+") { number = (number ?? 0) + 1 }
}
}
}

不过,这样的做法也并不是很合理。State 文档中明确指出:

You should only access a state property from inside the view’s body, or from methods called by it.

虽然 DetailView3 可以按照预期工作,但通过 DispatchQueue.main.async 中来访问和更改 state,是不是推荐的做法,还是存疑的。另外,由于实际上 body 有可能被多次求值,所以这部分代码会多次运行,你必须考虑它在 body 被重新求值时的正确性 (比如我们需要加入 number == nil 判断,才能避免重复设值)。在造成浪费的同时,这也增加了维护的难度。

对于这种方法,一个更好的设置初值的地方是在 onAppear 中:

struct DetailView4: View {
@State private var number: Int = 0
private var tempNumber: Int

init(number: Int) {
self.tempNumber = number + 1
}

var body: some View {
HStack {
Text("4: (number)")
Button("+") { number += 1 }
}.onAppear {
number = tempNumber
}
}
}

虽然 ContentView中每次 body 被求值时,DetailView4.init 都会将 tempNumber 设置为最新的传入值,但是 DetailView4.body 中的 onAppear 只在最初出现在屏幕上时被调用一次。在拥有一定初始化逻辑的同时,避免了多次设置。

如果一定要从外部给 @State 一个初始值,这种方式是笔者比较推荐的方式:从外部在 initializer 中直接对 @State 直接进行初始化, 是反模式的做法:一方面它事实上违背了 @State 应该是纯私有状态这一假设,另一方面由于 SwiftUI 中 View 只是一个“虚拟”的结构,而非真实的渲染 对象,即使表现为同一个视图,它在别的 view 的 body 中是可能被重复多次创建的。在初始化方法中做 @State 赋值,很可能导致已经改变的现有状态 被意外覆盖,这往往不是我们想要的结果。

State, Binding, StateObject, ObservedObject

@StateObject 的情况和 @State 很类似:View 都拥有对这个状态的所有权,它们不会随着新的 View init 而重新初始化。这个行为和 Binding 以及 ObservedObject 是正好相反的:使用 Binding 和 ObservedObject 的话,意味着 View 不会负责底层的存储,开发者需要自行决定和维护“非所有”状态的声明周期。

当然,如果 DetailView 不需要自己拥有且独立管理的状态,而是想要直接使用 ContentView中的值,且将这个值的更改反馈回去的话,使用标准的 @Bining 是毫无疑问的:

struct DetailView5: View {
@Binding var number: Int
var body: some View {
HStack {
Text("5: (number)")
Button("+") { number += 1 }
}
}
}

状态重设

对于文中的情景,想要对本地的 State (或者 StateObject) 在初始化时进行操作,最合适的方式还是通过在 .onAppear 里赋值来完成。如果想要在初次设置后,再次将父 view 的值“同步”到子 view 中去,可以选择使用 id modifier 来将子 view 上的已有状态清除掉。在一些场景下,这也会非常有用:

struct ContentView: View {
@State private var value = 99

var identifier: String {
value < 105 ? "id1" : "id2"
}

var body: some View {
VStack(alignment: .leading) {
DetailView(number: value)
Button("+") { value += 1 }
Divider()
DetailView4(number: value)
.id(identifier)
}
}

被 id modifier 修饰后,每次 body 求值时,DetailView4 将会检查是否具有相同的 identifier。如果出现不一致,在 graph 中的原来的 DetailView4 将被废弃,所有状态将被清除,并被重新创建。这样一来,最新的 value 值将被重新通过初始化方法设置到 DetailView4.tempNumber。而这个新 View 的 onAppear 也会被触发,最终把处理后的输入值再次显示出来。

总结

对于 @State 来说,严格遵循文档所预想的使用方式,避免在 body 以外的地方获取和设置它的值,会避免不少麻烦。正确理解 @State 的工作方式和各个变化发生的时机,能让我们在迷茫时找到正确的分析方向,并最终对这些行为给出合理的解释和预测。

iOS相关资料下载

收起阅读 »

详细分析iOS启动页广告

iOS
最近公司有个需求,需要添加启动页广告,查了不少资料,基本上有2种说法。一种是实时展示广告,另外一种是先保存,下次再展示本地的。对于这两种说法,仔细了研究下,有可取之处,也有一些小缺点。下面就和大家慢慢探讨下。1.先下载后展示方案先说下我采用的方案,APP首次启...
继续阅读 »


最近公司有个需求,需要添加启动页广告,查了不少资料,基本上有2种说法。一种是实时展示广告,另外一种是先保存,下次再展示本地的。对于这两种说法,仔细了研究下,有可取之处,也有一些小缺点。下面就和大家慢慢探讨下。

1.先下载后展示方案

先说下我采用的方案,APP首次启动,加载引导页,然后进入首页,这第一次不展示启动页广告。可以选择在didFinishLaunchingWithOptions里面,先网络请求广告,判断本地是有已经存储了相同的广告信息,如果是,则不用理会。不是,则存储到本地上。

等下次进来,可以判断是否有本地存储的广告信息,有则直接展示,没有就直接进入首页。

优点:启动流程流畅,无影响,不会影响用户启动体验。

缺点:广告不是实时的。例如本地广告已经下架了,这时候启动还加载本地的是不是就出问题了。对于这点,我觉得还是要看公司实际运营情况来确定,如果有后台返回的有效期,就能避免这种情况。

想了下,无伤大雅,影响也不是很大。采用这种方式感觉也不错。

2.实时展示方案

这个方案,有研究过,也是一种不错的做法。APP启动,直接网络请求广告,我们直接跳到广告页,这里也分成2种情况。

一种情况,先加载本地固定的广告,1S内有广告数据返回,倒计时开启,直接展示广告,没有广告,或者网络请求失败,直接结束倒计时,进入首页。

另外一种情况,和一开始说的先下载后展示的有点雷同,这时候就是先加载本地下载的广告,1S内有广告数据返回,倒计时开启,直接展示广告,并把广告下载到本地。如果网络请求失败,就倒计时本地下载的广告,如果没有广告,也是直接结束倒计时,进入首页。

优点:实时更新启动广告,保证每次都是最新的。

缺点:广告可以会延迟展示,用户体验可能会差点。

还是想了下,其实感觉都行,毕竟要看注重点在哪里。用户体验嘛,对于我来说,肯定是能不展示倒计时是更好的,直接进入首页。但既然有启动页这广告东西,我觉得展示也行,不要太频繁就好,不要弄得每次打开都有。这只是我的一个小小期望而已。

3.多Windows实现

对于实现这个启动广告功能,又有两种做法,其中一种是利用多windows来实现。

我们在didFinishLaunchingWithOptions里面,先添加2个window。

      // 多window实现,相当于又2个window,1个在下面,1个在上面
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.tintColor = .darkGray;
let nav1 = UINavigationController(rootViewController: ViewController())
window?.rootViewController = nav1
window?.makeKeyAndVisible()

// self.splashWindow = UIWindow(frame: CGRect(x: 0, y: 100, width: 300, height: 500))
self.splashWindow = UIWindow(frame: UIScreen.main.bounds)
let splashVC = SplashViewViewController()
let nav = UINavigationController(rootViewController: splashVC)
splashWindow?.rootViewController = nav
splashWindow?.makeKeyAndVisible()

splashWindow是展示广告的,window是展示首页的,window在splashwindow的下面,所以我们先看到的上面是展示广告的,这种做法的好处是在倒计时广告的时候,首页其实已经在请求加载页面了,等倒计时结束,这时候首页也已经加载好了。

4.单window实现

单window的话,无非就是看rootViewController是哪个页面,我们直接由广告页,变成首页就好。这里无非要注意的就是过渡的动画。这里看自己想怎样的效果了。

这种单window用法,我们常见的有登录页,首页互相切换,还有引导页和首页切换等等,实现起来倒是不难。也打算细说了。

5.效果图

按照国际惯例,提供一下GitHubDemo:github.com/wenweijia/S…

6.总结

对于方案提出了2个,都是文字类的,听起来的确是文绉绉的,本来想弄个流程图的,有点懒,也比较忙,后面有时间再补吧

主要是抛砖引玉,还是想看下各位大佬的看法,例如有没有更好的方案,哪些方案需要完善一下,欢迎留意,谢谢!

收起阅读 »

Swift系列 -- 可选类型

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」前言好记性不如烂笔头,学习过后还是要总结输出才能更有利于对知识的消化吸收。因此对于Swift的学习作了一个系列总结:Swift中的函数盘点本篇作为Swift学习总结的第二篇文章,主要探...
继续阅读 »


前言

好记性不如烂笔头,学习过后还是要总结输出才能更有利于对知识的消化吸收。因此对于Swift的学习作了一个系列总结:

本篇作为Swift学习总结的第二篇文章,主要探索的是关于Swift可选项的内容。

还记得在使用OC开发时,对于一个对象而没有初始化时,其默认值为nil,并且可以在后续的操作中将该对象重新赋值为nil。因此我们经常会遇到因为对象为nil造成的程序错误,例如向数组中插入了nil造成闪退。

但是Swift是一门类型安全语言,Swift的类型是不允许变量值为nil的,而不论是引用类型,还是值类型。但是,在实际的开发中,确实存在变量可能为nil的情况,因此Swfit中还提供了一种特殊的类型 -- 可选类型,用于处理这种情况,下面就一起探索下可选项的相关内容。

一、可选类型的本质

Swift的类型安全是指,定义一个变量时,给定了类型,那么就不能再将其它类型的值赋给该变量。当然,这也不是说Swift中定义变量时,必须显式的指定变量类型。Swift还有类型推断,即根据给定的值,自动确定变量类型。如下代码所示:

let a:Int
a = "123" => Cannot assign value of type 'String' to type 'Int'

var b = 20 // 赋值为20,类型推断为Int
b = "swift" => Cannot assign value of type 'String' to type 'Int'

var c:Int = nil => 'nil' cannot initialize specified type 'Int'
var str: String = nil => 'nil' cannot initialize specified type 'String'


在上述事例代码中,a显式指定了类型为Intb通过类型推断也被指定为Int,将String赋值给这两个变量时都报错Cannot assign value of type 'String' to type 'Int' 。

对于cstr两个变量,将其初始值赋值为nil,结果会报错nil无法为指定类型赋初始化值。需要注意的一点是,变量c虽然为Int类型,但是其初始值并不为0,即 var c:Int 和 var c:Int = 0 并不等价

不过在实际的开发过程中,我们经常会遇到无法确定一个变量是否有值的情况,比如在一个相机App中,当我们获取当前摄像头时,不能确定摄像头是否正在被其它App使用,或者摄像头硬件本身有什么问题,因此无法确定是否可以获取成功,那么此时我们就可能得到一个nil值。此时,就需要有一种类型可以接收nil,又可以接收正常的值。在Swift中,用以实现这种类型的就是可选类型

Swift的可选类型的定义方式为类型+?,具体代码如下:Xnip2021-11-17_10-57-25.png可选类型的变量可以给定一个对应类型的初始值,若不给定,则其默认值为nil

当可选类型有具体的值时,与其对应的类型也是有区别的,不能做等价处理,以Int为例:Xnip2021-11-17_14-14-51.pnga为可选类型的Int,b为普通的Int类型,虽然值都为20,但是a的类型打印出来是Optional(20)。在LLDB中po一下查看ab,结果如下:Xnip2021-11-17_14-32-30.png可以发现,b是一个纯粹的Int值20,而a是在20外面包了一层,这一点类似于一个盒子,如下图所示:Xnip2021-11-17_14-51-56.png

图中红色部分表示存储的值,如果可选类型中存储有值,则盒子中存储具体的值,本例中为Int值2,如果可选类型为nil,则盒子为空。

那么如果是多重可选项呢?即可选项能否包裹一层可选项呢?代码如下:

let result:Int?? = 20

通过LLDB调试,可以看到其实际结构如下所示:

Xnip2021-11-18_17-55-34.png

画图可表示为:

Xnip2021-11-18_17-52-31.png

通过LLDB打印出来可以看到可选类型是由一个Optional包裹的类型,那么Optional是什么呢?其实Optional是一个枚举类型,可以发现其定义如下所示:Xnip2021-11-18_23-34-47.png因此如下图所示的代码是等价的:Xnip2021-11-18_23-40-20.pngOptional通过泛型来指定其要包装的类型,并且Optional遵守了ExpressibleByNilLiteral协议,遵守该协议的枚举、结构体或类初始化时允许值为nil。

Optional枚举内部包含nonesome两个case,如果值为nil,则属于none,有值的话则包装为some,由此也可看出Swift枚举的强大。

二、强制解包

既然可选类型是将对应类型的值包在一个盒子中,那么是否可以将可选类型的值赋值给对应类型的变量呢?可以简单做个测试,结果如下:Xnip2021-11-17_17-02-06.png答案显然是否定的,编译器在编译时就会报错Value of optional type 'Int?' must be unwrapped to a value of type 'Int',Int?必须解包成一个 Int类型。

Swift中可选类型的强制解包使用一个!即可,代码如下所示:

let a:Int? = 20
var b:Int = 20
b = a!

代码第三行 b = a!中,可选类型Int a即解包为了Int,并赋值给b。当然a依然是一个可选类型,其值依然为20。

强制解包需要注意以下几点:

  • 1、强制解包后,对于原可选变量的值没有影响,其依然为可选类型
  • 2、值为nil的可选类型,强制解包会发生闪退,因此在使用强制解包时,需要确定可选类型中的值不为nil

与强制解包一起的还有一种类型,隐式解包的可选项,代码表现为类型 + !。例子如下:

let result:Int! = 20
let realInt:Int = result

可以发现result可以直接赋值给一个Int的realInt,因为隐式解包的可选项会隐式的将变量解包,而不会有明显的感知。不过需要明确的一点是,隐式解包依然是可选项,如果不是确定变量会一直有值,使用需要谨慎。

三、可选项绑定

使用可选项强制解包时,为例防止值为nil的闪退,我们可能会像下面这样写代码:

   let a:Int? = 20
   var b:Int

if a != nil {
b = a!
}

相对于直接解包,这样写安全性确实提高了一些,不过Swift提供了一种更加优雅的方式来解决这一问题,即可选项绑定,代码如下:

let a:Int? = 20
var b:Int
if let value = a {
b = value
} else {
print("a的值为nil")
}

如同代码中if后面的条件所示,可选项绑定的语法是let value = 可选项变量。可选项绑定使用在条件判断等地方,如果可选项变量为nil,则条件为false,如果可选项变量不为nil,则会自动解包并赋值给value,只是value的作用域仅限if条件后的{},不能用在else后的{}。

如果有多个可选项绑定,中间需要用,隔开,而不能使用&&,如图所示:

Xnip2021-11-17_18-49-11.png

Xnip2021-11-17_18-51-01.png

四、空合并运算符

Swift中还提供了空运算符 ??,其定义为如下代码

public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?

public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T

空合并运算符是一个二元运算符,假定有两个变量 a 和 b,使用空合并运算符方式为 a ?? b,使用时有以下注意事项:

  • a??b,如果a为nil,则返回b,否则返回a自身
  • a需要为可选项,否则虽然编译器不会报错,但是没有意义
  • b可以是可选项,也可以不是可选项
  • 不管a、b是否都是可选项,两者存储的类型要对应,例如Int?<=> Int 或 Int?<=> Int?
  • 如果b不是可选项,a的值不为nil,则在返回 a 时,会自动解包,事实上b决定了返回值是否解包

4.1 空合并运算符使用举例

以下为空合并运算符的几个例子,假定a、b存储皆为Int值:

  • a为nil,b为Int值2
let a:Int? = nil

let b:Int = 2

let result = a ?? b // result为b的值,且为Int类型

  • a为nil,b为可选类型Int值2
let a:Int? = nil

let b:Int? = 2

let result = a ?? b // result为b的值 Optional(2)

  • a不为nil,b为Int类型
let a:Int? = 3

let b:Int = 2

let result = a ?? b // result为a的值,并且已经解包为3

  • a不为nil,b为可选Int类型
let a:Int? = 3

let b:Int? = 2

let result = a ?? b // result为a的值,并且依然为Optional(3)

还有多个空合并运算符连接使用的情况,如下代码:

let a:Int? = 2
let b:Int? = nil
let c:Int = 4

let result = a ?? b ?? c // result值为2,是 a 解包后的值

如例所示,当多个??连接使用时,决定result值的依然是最后一个变量c,前面 a??b 得到了可选Int?值2,因为c为int类型,所以得到解包后的Int值2

4.2 空合并运算符与可选项绑定

??还可以与可选项绑定结合在一起使用,如下代码所示:

  • 类似于 a != nil || b != nil
let a:Int? = 2
let b:Int? = nil

if let result = a ?? b { // 只要a和b中有一个不为空,就可以进入该条件判断
let c = result
    print(c)
}

  • 类似于 a != nil && b != nil
let a:Int? = 2
let b:Int? = nil

if let c = a, let d = b { // 只有a和b都不为nil时,才会进入条件判断
    print(c)
    print(d)
}

通过上述两种方式,可以更加精简的进行多个可选项的nil值判断,并且在条件为真的情况下可以自动解包,直接使用解包后的值。

五、guard语句

guard语句与if语句类似,都是条件判断语句,其语法规则为:

guard 条件 else {
// 执行代码
}

不过与if语句不同的是,guard语句是条件为false时,进入{}执行代码。如下面的例子所示:

let a:Int? = 20

guard a != nil else {
print("a的值为nil")
return
}
print("a的值为\(a!)")

并且guard语句的代码块中,必须有return或者抛出异常,否则编译器会报错如下:Xnip2021-11-18_15-51-34.png

在最初接触到guard语句时,可能想已经有了if语句,为什么还需要guard呢?并且guard实现的功能,if也可以实现,会觉得其有些多余。但是经过一段时间开发,对两者做出对比后,可以发现在一定程度上,guard表达的语义更加明确,代码的可读性更高。例如,上面的代码改成if语句如下:

if a == nil {
print("a的值为nil")
return
}

print("a的值为\(a!)")

对比两段代码,语义上我们符合我们预期的值 a != nil,如果使用if语句是要判断a==nil,使用guard就判断a != nil,不符合就return即可,因此guard更加适合做容错判断。

六、总结

  • Swift可选类型的本质是Optional枚举,其包含nonesome两个case,none表示当前变量值为nil,some表示当前变量值不为nil
  • Swift可选项不能直接赋值给其包装的类型所对应的变量,应该在解包后赋值,但是需要注意的是,要在保证有值的情况下强制解包,否则会Crash
  • 对于可选类型的判空处理,可以使用可选项绑定来做,这样更加优雅

与OC不同,Swift更加注重安全性,尤其是对于nil的处理上,Swift更加的严谨,虽然在刚接触时会有些不适应,但是在开发过程中却可以省去很多对nil的容错处理,由此也可以看出Swift的强大与设计精妙。以上即为对于Swift中的可选类型的总结,欢迎大家指正。

收起阅读 »

iOS App上架技能:不更新版本的情况下删除App Store非主语言的方法、app上架后的事项(ASO及ASA)

iOS App上架技能:不更新版本的情况下删除App Store非主语言的方法、app上架后的事项(ASO及ASA)这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战。前言iOS上架前的准备:kunnan.blog.csdn.net/a...
继续阅读 »

iOS App上架技能:不更新版本的情况下删除App Store非主语言的方法、app上架后的事项(ASO及ASA)

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

前言

  • iOS上架前的准备:kunnan.blog.csdn.net/article/det…
  • 上架技巧(不更新版本的情况下删除App Store非主语言的方法)
  • 常见上架问题及解决方案(上传ipa包被吃掉、已上架app在AppStore搜不到)
  • app上架后的事项(ASO、ASA)

I、AppStore 上架技巧

1.1 上传构建版本

archive之后通过 Xcode、macOS 版 Transporter 或 altool 上传构建版本

help.apple.com/app-store-c…

  • Xcode 上传 在这里插入图片描述
  • Transporter 在这里插入图片描述
  • 通过 altool 上传您 App 的二进制文件

您可以使用 xcrun(包含在 Xcode 中)来调用 altool,该命令行工具用于公证、验证并上传您 App 的二进制文件至 App Store。在“终端”的命令行中指定以下命令之一:

$ xcrun altool --validate-app -f file -t platform -u username [-p password] [--output-format xml]
$ xcrun altool --upload-app -f file -t platform -u username [-p password] [—output-format xml]

【注】如果您使用自动构建系统,则可以将公证过程集成到现有构建脚本中。Xcode 中的 altool 和 stapler 命令行工具可将您的软件上传至 Apple 公证服务,并将生成的凭证附加到您的可执行文件中。altool 位于:/Applications/Xcode.app/Contents/Developer/usr/bin/altool。

有关更多信息,请参见《altool 指南》

help.apple.com/asc/appsalt…

1.2 不更新版本的情况下删除App Store非主语言的方法

1、由于AppStore缓存原因导致已上架app在AppStore上搜不到的解决方案2、不更新版本的情况下删除App Store非主语言的方法(应用场景:马甲包)

blog.csdn.net/z929118967/…

1.3 对开发权限和上架权限进行分离管理

在大公司通常苹果开发账号归数据中心人管,如果没有专门测试的开发者账号,只能在公司开发者下面添加一个新用户用于测试开发;选择对应职能即可。

在这里插入图片描述 通过添加开发职能账号,方便其他开发者知道app的审核状态。 当然你也可以采用邮件转发来同步信息(当发件人是>no_reply@email.apple.com时,就转发给特定人员 ) 在这里插入图片描述

具体流程举例

苹果版本升级先发邮件给市场管理部邮箱scglb@xxx.com,由对应人员走oa申请流程,审批完成后开发同事邮件发送审批截图+具体事宜给总部研发对应同事,然后总部这边就操作后面的上架流程(打包+上架)。

II、常见上架问题及解决方案

2.1 iOS app因蓝牙功能隐蔽而导致上架被拒绝的解决方案

相关的公众号文章:https://mp.weixin.qq.com/s?__biz=MzI0MjU5MzU5Ng==&mid=2247484133&idx=1&sn=1d50f59ea026c1b4a9d540c9c1222695&chksm=e978b8b6de0f31a0bbcff38495e858d4db16a854828c1c80719df820826d8405f93b3662ef29&mpshare=1&scene=1&srcid=0114rQ5AKSFyy8QxZoG4Jrmf&sharer_sharetime=1610606706852&sharer_shareid=38c24777c9b84b8b44c56026b3aa9bd7&version=3.0.36.2330&platform=mac#rd

2.2 info.plist 的权限配置问题导致的app被吃掉了

如果上传ipa包之后,app被吃掉了,大部分是权限问题。

 <key>NSAppleMusicUsageDescription</key>
 <string>App需要您的同意,才能访问媒体资料库</string>
 <key>NSBluetoothPeripheralUsageDescription</key>
 <string>App需要您的同意,才能访问蓝牙</string>
 <key>NSCalendarsUsageDescription</key>
 <string>App需要您的同意,才能访问日历</string>
 <key>NSCameraUsageDescription</key>
 <string>App需要您的同意,才能访问相机</string>
 <key>NSLocationAlwaysUsageDescription</key>
 <string>App需要您的同意,才能始终访问位置</string>
 <key>NSLocationUsageDescription</key>
 <string>App需要您的同意,才能访问位置</string>
 <key>NSLocationWhenInUseUsageDescription</key>
 <string>App需要您的同意,才能在使用期间访问位置</string>
 <key>NSMicrophoneUsageDescription</key>
 <string>App需要您的同意,才能访问麦克风</string>
 <key>NSPhotoLibraryAddUsageDescription</key>
 <string>To save the conversion results to the phone, you need to open the album permissions.</string>
 <key>NSPhotoLibraryUsageDescription</key>
 <string>To save the conversion results to the phone, you need to open the album permissions.</string>
 <key>NSRemindersUsageDescription</key>
 <string>App需要您的同意,才能访问提醒事项</string>

  • other
 <key>NSAppleMusicUsageDescription</key>
 <string></string>
 <key>NSCalendarsUsageDescription</key>
 <string></string>
 <key>NSCameraUsageDescription</key>
 <string>是否允许此App使用你的相机?</string>
 <key>NSContactsUsageDescription</key>
 <string>是否允许此App访问你的通讯录?</string>
 <key>NSLocationWhenInUseUsageDescription</key>
 <string></string>
 <key>NSMicrophoneUsageDescription</key>
 <string>是否允许此App使用你的麦克风?</string>
 <key>NSPhotoLibraryUsageDescription</key>
 <string>是否允许此App访问你的媒体资料库?</string>
 <key>NSRemindersUsageDescription</key>
 <string></string>

III 、app上架之后的事项

3.1 ASO

blog.csdn.net/z929118967/…

3.2 管理符号表

  • 上传app上线版本的dSYMs文件到bugly,用于后续的app日志文件符号化

3.3 管理代码分支

blog.csdn.net/z929118967/…

3.4 申请iOS App上线爱思助手应用市场

iOS App如何在爱思助手应用市场上架?

blog.csdn.net/z929118967/…

3.5 Apple search ads(ASA)

searchads.apple.com/cn/

时隔五年,ASA(Apple Search Ads,即苹果搜索广告)终于上线中国大陆地区的App Store。 在这里插入图片描述

使用 Apple Search Ads Advanced,你可以在两个位置展示你的 app:

1、一个是“搜索”标签广告,在用户搜索前展示; 2、另一个是搜索结果顶部广告,在用户搜索时展示。

ITC后台和苹果广告这两者是两个不同的体系,两个账号是不同的,单独的一个苹果广告账号可以给多个App进行投放

如果公司下有多个开发者账号,可将这些账号的包授权给同一个投放账号,这样这个投放账号就可以投放不同主体的App。

Q1.目前ASA账户充值是预充值还是后付呢?

现在是要预充值的,因为苹果可能会随时根据你的消耗情况进行扣款。扣款条件主要是分两种情况,分别是满500美金或者7天扣一次,当这两个条件哪个先触达了就按哪个来。

Q2.公司注册的个人小号没有营业执照,这个号下面的App应该怎么推广?

按目前苹果在国内市场的政策来看,要使用苹果广告都需要营业执照,所以这样的小号大概率是没办法推广的。

see also

(高校学生于教育商店选购新款 iPad /Mac 可享受优惠)【修订版】

mp.weixin.qq.com/s/rkRMVUoYK…

更多内容请关注#小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向

收起阅读 »

Swift组件化如何解耦

组件化如何解耦把同一模块的代码放到一起代码是两个模块的代码,不能放在同一模块的怎么办。问题1很简单,就是从代码层面做好按模块分开。 如A模块的代码全部放到A模块里面,然后要对外的时候,A模块放出对外的接口给其他模块调用。 比如日志模块,他能够独立成一个模块,他...
继续阅读 »

组件化如何解耦

  1. 把同一模块的代码放到一起

  2. 代码是两个模块的代码,不能放在同一模块的怎么办。

问题1很简单,就是从代码层面做好按模块分开。 如A模块的代码全部放到A模块里面,然后要对外的时候,A模块放出对外的接口给其他模块调用。 比如日志模块,他能够独立成一个模块,他不依赖别的模块,所以只需要把负责写日志等的代码放到一个日志模块里面,这样别人想要输出日志。就可以引入日志模块并用日志模块的接口输出日志就行。这里面没有耦合,也就不需要解耦。

问题2,比如A模块会用到B模块的方法,然后B模块又有可能用到A模块的代码,但是又不能把A和B合并为一个模块的时候怎么办。总不能A模块编译都编译不过,因为编译的时候会提示缺少B模块的方法。同样B模块也一样,缺少A模块,他编译都编译不了。

这里就需要对A模块做B模块的解耦,同样B模块也一样要做A模块的解耦。

关于解耦的方法,网上也有挺多。比较有代表性的两种如下:

  1. CTMediator的target-action模式

  2. BeeHive的用protocol实现模块间调用

这里参考BeeHive的思想,用swift实现了一个解耦的例子。

定义

  1. A模块
  2. B模块
  3. Interface模块(保存各个模块的对外接口,比如A或者B模块的对外接口,都放在这里)
  4. 主工程

先看最后的依赖图

image.png

A模块要调用B模块的接口,这里他不需要依赖B模块,他只需要依赖Interface模块,然后A模块要调用B模块的功能,他只需要调用B模块放到Interface模块的接口就行。

如何实现:

Interface模块里面有一个公共类,比如叫:ModuleInterface 然后他里面有两个方法, 一个注册函数是让别的模块把自己实例注册到这里来的 一个是获取函数,通过某个key,获取到对应的模块的实例

public class ModuleInterface {
public static let shared = ModuleInterface()
public var protocols: [String: BaseProtocol] = [:] // 维护一个字典
// 注册函数
public func registProtocol(by name: String, instance: BaseProtocol) {
self.protocols[name] = instance
}
// 获取实例
public func getProtocol(by name: String) -> BaseProtocol? {
return self.protocols[name]
}
}

这里的核心是维护一个字典,通过对应的key,找到对应的实例。

然后B模块的公开接口也放在这个Interface模块里面,如:

extension ModuleInterface: BProtocol {
// b对外的接口
public func getBModuleValue(b: String, callback: ((Int)->Void)) -> Int {
if let pro = self.getProtocol(by: "BProtocol") as? BProtocol {
return pro.getBModuleValue(b: b, callback: callback)
} else {
print("no found BProtocol instance")
callback(0)
return 0
}
}
}

这里有两个技巧

  1. 使用extension ModuleInterface: BProtocol, 这样可以把BProtocol里面定义的方法,实现到ModuleInterface类里面,这样别的模块调用的时候,统一用ModuleInterface来调用就行,入口简单
  2. 使用self.getProtocol(by: "BProtocol") as? BProtocol,通过转类型的方式得到BProtocol的实例,就可以调用B模块的方法了。而且是运行时检查,这样也解决了,没有引用B模块也能编译通过。

如上:这样每个模块只需要在初始化的时候把自己的实例添加到这个字典里面去。然后想调用其他模块的时候,只需要从这个字典拿出对应模块的实例,再去调用别的模块就行。

然后A模块要想使用B模块的getBModuleValue的方法时,他只需要引入Interface模块,然后从ModuleInterface里面去调用如下:

let a = ModuleInterface.shared.getBModuleValue(b: "a call b") { value in
print("==callBModule=result==", value)
}

整个代码实现非常简单。

具体pod的代码如下:

A模块的podspec的定义
s.dependency 'Interface'

A模块的podfile的定义
use_frameworks!

platform :ios, '9.0'

target 'AModule_Example' do
pod 'AModule', :path => '../'
pod 'Interface', :path => '../../Interface'
end

B模块的podspec的定义
s.dependency 'Interface'
B模块的podfile的定义
use_frameworks!

platform :ios, '9.0'

target 'BModule_Example' do
pod 'BModule', :path => '../'
pod 'Interface', :path => '../../Interface'
end

主工程Demo的podfile的定义
use_frameworks!

platform :ios, '9.0'

target 'Demo' do
pod 'AModule', :path => '../AModule'
pod 'BModule', :path => '../BModule'
pod 'Interface', :path => '../Interface'
end

具体代码看例子: github.com/yxh265/Modu…

收起阅读 »

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

iOS
拒绝编译等待 - 动态研发模式 ARK作者:字节跳动终端技术——徐纪光背景iOS 业界研发模式多为 CocoaPods + Xcode + Git 的多仓组件化开发模型。为追求极致的研发体验、提升研发效率,对该研发模式进行了大量优化,但目前遇到了以下瓶颈,亟需...
继续阅读 »

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

作者:字节跳动终端技术——徐纪光

背景

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: github.com/kuperxu/Kwa…

[2] MBox: mp.weixin.qq.com/s/5_IlQPWnC…

字节跳动终端技术团队(Client Infrastructure)是大前端基础技术的全球化研发团队(分别在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),负责整个字节跳动的大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率;支持的产品包括但不限于抖音、今日头条、西瓜视频、飞书、懂车帝等,在移动端、Web、Desktop等各终端都有深入研究。

火山引擎应用开发套件MARS是字节跳动终端技术团队过去九年在抖音、今日头条、西瓜视频、飞书、懂车帝等 App 的研发实践成果,面向移动研发、前端开发、QA、 运维、产品经理、项目经理以及运营角色,提供一站式整体研发解决方案,助力企业研发模式升级,降低企业研发综合成本。

收起阅读 »

iOS使用addChildViewController

iOS
「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战」。iOS早在iOS5的时候为了解耦、更加清晰的处理页面View的逻辑,UIViewController提供了addChildViewController方法,将ViewControl...
继续阅读 »

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

iOS早在iOS5的时候为了解耦、更加清晰的处理页面View的逻辑,UIViewController提供了addChildViewController方法,将ViewController作为容器处理视图控制器的切换,将比较复杂的UI使用子ViewController来管理。

iOS5.0之前只能在ViewControllerview中不断的通过addSubView添加subViewVCview视图层级中。这样使得主ViewController中的内容越来越混乱,代码越来越多,subView的管理越来越困难。

iOS5.0之后按照MVC的原则,每个ViewController只需要管理一个view视图层次结构,因此我们可以使用childViewController来拆分开发中比较复杂的View。并且此时的childViewController拥有了与父ViewController同步的声明周期。

项目中使用:

在我们项目的APP首页的实现中使用到了,首页内容展示位推荐分类菜单,以及每个菜单下的内容展示,不同的分类下的内容view展示的UI多样化。

相关方法:

///子视图控制器数组
@property(nonatomic,readonly) NSArray *childViewControllers

///向父VC中添加子VC
- (void)addChildViewController:(UIViewController *)childController

///将子VC从父VC中移除
- (void) removeFromParentViewController

///fromViewController 当前显示在父视图控制器中的子视图控制器
///toViewController 将要显示的姿势图控制器
///duration 动画时间
/// options 动画效果(渐变,从下往上等等,具体查看API)
///animations 转换过程中得动画
///completion 转换完成
- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion

///当向父VC添加子VC之后,该方法会自动调用;
- (void)willMoveToParentViewController:(UIViewController *)parent

///从父VC移除子VC之后,该方法会自动调用
- (void)didMoveToParentViewController:(UIViewController *)parent

如何使用?:

  • 如果在view上添加的只是简单的控件的话,那么使用addSubView添加到父ViewController上;
  • 如果子视图是比较复杂的视图集合,功能丰富,就选择使用addChildViewController来添加新的子ViewController,但也需要通过addSubview将子ViewControllerview添加到父视图的视图层级中;
  • iOS5之后使用addChildViewController时的原则,我们在使用addSubview的时候,同时调用addChildViewController方法将subView对应的viewController也加到当前viewController的管理中;
  • 对于那些不需要显示的subView,只需通过addChildViewControllersubVC添加到父控制器中,需要显示时再调用transitionFromViewController方法将其显示出来;
  • 当收到系统的 Memory Warning 的时候,系统也会自动把当前没有显示的 subview 销毁掉 掉,以节省内存;
  • 优点:
  1. 使页面逻辑更加清晰明了,遵循MVC模式,每个View对应相应的ViewController;
  2. 当存在不需显示的view时,将不会被加载,减少内尺使用;
  3. 当收到内存警告时,会将没有加载出的view率先释放,优化了程序的内存释放机制;

系统方法解释:

  • addChildViewController

[A父视图控制器 addChildViewController:B子视图控制器]在视图控制器A中添加了子视图控制器B.调用这个方法时如果子视图控制器已经有父视图控制器了,那么调用该方法会先把子视图控制器从之前的父视图控制器中移除,然后再添加到当前的视图控制器上作为子视图控制器。

注意:调用addChildViewController后会自动调用willMoveToParentViewController:superVC方法;

  • removeFromParentViewController

将子视图控制器从父视图控制器中移除,移除之后将自动调用didMoveToParentViewController

注意:调用removeFromParentViewControlle后会调用didMoveToParentViewController:nil方法

  • willMoveToParentViewController

当一个视图控制器从视图控制器容器中被添加或者被删除之前,该方法被调用parent:父视图控制器,如果没有父视图控制器,将为nil;当调用removeFromParentViewController方法是必须先手动调用该方法,且parent参数为nil。

  • didMoveToParentViewController

当从一个视图控制容器中添加或者移除viewController后,该方法被调用;当调用addChildViewController方法时必须手动调用该方法,且parent参数为父控制器。

代码展示:

  • 添加子VC
//自动调用,可以省略 
//[childVC willMoveToParentViewController: superVC];
[superVC addChildViewController:childVC];
[superVC.view addSubview:childVC.view];
[childVC didMoveToParentViewController:superVC];

  • 删除子VC
[childVC willMoveToParentViewController];
[childVC removeFromParentViewController];
//自动调用,可以省略
//[childVC didMoveToParentViewController:nil];

  • 切换子VC
[self addChildViewController:newController];
[self transitionFromViewController:oldController toViewController:newController duration:1.5f options:UIViewAnimationOptionCurveEaseOut animations:^{

} completion:^(BOOL finished) {
if (finished) {
[newController didMoveToParentViewController:self];
[oldController willMoveToParentViewController:nil];
[oldController removeFromParentViewController];
self.currentVC = newController;
}
else{
self.currentVC = oldController;
}
}];

总结:

  1. addChildViewController向父视图控制器中添加子视图控制器时,添加之后自动调用willMoveToParentViewController,需要手动调用didMoveToParentViewController
  2. removeFromParentViewController将子视图控制器从父视图控制器中移除,移除之后自动调用didMoveToParentViewController: nil参数为nil,需要在移除前手动调用willMoveToParentViewController
  3. transitionFromViewController:toViewController在调用这个方法之前先调用[fromViewController willMoveToParentViewController:nil]然后在completion后调用[toViewController didMoveToParentViewController:self]方法;
  4. 在切换子视图控制器显示的时候需要保证切换的子视图控制器已经被添加到父视图控制器中;
  5. 当某个子视图控制器将从父视图控制器中删除时,parent参数为nil,即:[将被删除的VC willMoveToParentViewController:nil];
  6. 当某个子试图控制器将加入到父视图控制器时,parent参数为父视图控制器,即:[将被加入的VC didMoveToParentViewController:superVC];

参考文献

blog.csdn.net/yongyinmg/a…


作者:麻蕊老师
链接:https://juejin.cn/post/7031466347410718727

收起阅读 »

「设计模式」iOS 中的适配器模式 Adapter

iOS
1. 生活中的适配器 提到适配器,最先想到什么?莫过于 电源适配器 了,日常使用的电脑、手机等电子设备都会有个电源适配器,作用是将插座里输出的高压交流电转换为电子设备所需的低压直流电。另外,世界各地区除了标准电压不同以外,大部分电源插头形状也不同,所以还有一类...
继续阅读 »

适配器模式.png


1. 生活中的适配器


提到适配器,最先想到什么?莫过于 电源适配器 了,日常使用的电脑、手机等电子设备都会有个电源适配器,作用是将插座里输出的高压交流电转换为电子设备所需的低压直流电。另外,世界各地区除了标准电压不同以外,大部分电源插头形状也不同,所以还有一类适配器,用于连接插头,例如香港的标准插座是三角方头的,就需要一个适配器来连接转换。


概括起来,适配器的功能是让原本不能一起工作的多个设备在不改变自身行为的前提下能一起工作。发散一下的话,笔记本电脑上各种接口使用的扩展坞、在国外旅游可能用到的语言翻译器、各种显卡 / 声卡 / 硬盘的驱动程序等等都可以理解为适配器。


2. 适配器模式


2.1 适配器模式定义


在《Head First 设计模式》中的定义如下:



适配器模式:将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。



适配器模式中主要有三个角色:



  • Target 目标接口 / 对象

  • Adaptee 被适配的对象

  • Adapter 适配器


即:通过适配器 Adapter 将被适配对象 Adaptee 包装成支持目标接口 Target 的对象,使原来 Target 能够完成的任务现在通过适配器包装后的对象也能支持,所以适配器也称作包装器 Wrapper


例如国标三角插座一般是三角扁头的,而港版电源适配器是三角方头的,在内地就不好使,需要弄一个转换器,把港版电源适配器插在转换器上再把转换器插在国标插座上,就可以正常工作了。上面的国标插座就对应为 Target 目标接口角色,港版三角方头插头是被适配的对象,额外的专用适配器将国标三角扁头转换港版三角方头。


2.2 适配器的类型


按照实现适配器的方式可以分为两种类型:类适配器(继承)  和 对象(组合)适配器。类图如下:


适配器模式类图pure.png


类适配器通过继承,也就是子类化,然后在子类中实现目标接口。在支持多重继承的语言中(C++、Python),类适配器同时继承父类以及 Target,由于在 Objective-C 以及 Java 这类语言不支持多重继承,所以目标接口一般为 协议 Protocol / 接口 Interface


对象适配器通过组合 - 将被适配对象作为适配器的属性,在实现目标接口相关方法中根据需要访问被适配对象。


类适配器 vs 对象适配器



























类适配器对象适配器
实现方式继承组合
作用范围仅被适配者类被适配者类及其子类
其他+ 易于重载,必要时可以覆盖被适配者的行为。+ 结构上更简单,不需要额外属性指向被适配者。+ 可以选择将部分工作委托给被适配者,更具弹性。 - 需要额外属性指向被适配者。

2.3 适配器的优缺点



  • 使用者(客户)与接口绑定,而不是与实现绑定,实现解耦。

  • 让没有关联的类能一起工作,不侵入原有代码,隔离原系统的影响。

  • 过多使用适配器会导致代码结构混乱(任何模式过度使用都会有问题吧:)


2.4 适配器应用场景



  • 面对遗留代码,期望项目统一使用新特性同时兼容已有类。→ eg.《Head First 设计模式》ch 7. 关于迭代器与枚举的示例。

  • 扩展新功能,方便接入新的第三方库。→ eg. 《人人都懂设计模式》电子阅读器中通过适配第三方 PDF 解析库来扩展支持 PDF 阅读。


3. iOS 中的适配器模式


在 iOS 系统上,苹果一般通过协议(可以理解为 Target 为接口)来实现适配器。例如常用的 UITableViewDataSourceUITableViewDelegate,将一个原本不能为 UITableView 提供数据 / 响应相关事件的类包装成数据源 / 代理,显然这是类适配器。详细实践介绍参考 Raywenderlich


3.1 属性包装器 @propertyWrapper


在 Swift 中当需要为属性添加相同的逻辑代码时使用属性包装器会大大减少工作量。属性包装器可以应用于结构体、枚举或者类。


如 Swift 官方文档中的示例,期望整型属性值始终小于 12,可以定义如下 TwelveOrLess 属性包装器:


// 定义 *TwelveOrLess* 属性包装器
@propertyWrapper
struct TwelveOrLess {
// 私有存储属性 number
private var number = 0
// 包装值
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}

// 使用 *TwelveOrLess* 来定义一个小矩形,长宽都小于等于一定值。
struct SmallRectangle {
@TwelveOrLess var height: Int
@TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height) // 打印 "0"

rectangle.height = 10
print(rectangle.height) // 打印 "10"

rectangle.height = 24
print(rectangle.height) // 打印 "12"


*以下为个人理解,不一定正确。


可以将 @propertWrapper 也理解为一个协议,这个协议要求对象实现包装属性的 Set/Get 方法:


protocol PropertyWrapperProtocol {
var wrappedValue : Int { set get }
}


即通过 TwelveOrLess 实现 PropertyWrapperProtocol 协议,来实现一个适配器,这个适配器的作用是返回一个限定范围内的数,如果尝试设置超过预设最大值的数,也只会保存为最大值。类比于电源适配器将输入的高电压适配器低电压。当然 Swift 中的属性适配器更强大也更灵活,参考 Swift GG 翻译文档 - 属性包装器


3.2 应用代理适配器 UIApplicationDelegateAdaptor


iOS 14 中新增了 UIApplicationDelegateAdaptor 用于包装原来 UIKit 中的应用代理UIApplicationDelegateNSApplicationDelegateAdaptor for AppKit、WKExtensionDelegateAdaptor for WatchKit),以便在 SwiftUI 中访问应用代理。


@propertyWrapper struct UIApplicationDelegateAdaptor<DelegateType> where DelegateType : NSObject, DelegateType : UIApplicationDelegate


从 @propertyWrapper 可以看出实际上是属性包装器的一个具体应用场景。通过泛型 DelegateType 传入一个 NSObject 类型且遵循 UIApplicationDelegate 协议的对象,猜测内部一些操作是通过转交给这个代理对象来执行的,显然是一个 对象适配器。这样使用(参考 HackingWithSwiftstackoverflow: swiftui-app-life-cycle-ios14-where-to-put-appdelegate-code):


class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("do something")
return true
}
}

@main
struct testApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

var body: some Scene {
WindowGroup {
ContentView()
}
}
}


有其他 iOS 相关的适配器应用实例欢迎分享讨论。


以上就是目前学习总结的 适配器模式 相关知识了。(有些地方配合图示理解更直观,似乎还差点什么,过些日子一起补上示例代码:)


参考



  1. Raywenderlich - How To Use the Adapter Pattern. 包含一个完整的例子演示如何利用协议(数据源&委托代理)实现通用水平滚动视图。

  2. Bloodline - iOS中的设计模式 - 适配器(Adapter) 介绍挺全面的。

  3. 《Head First 设计模式》ch 7. 适配器模式与外观模式。①用插座作为示例解析适配器;②面向对象适配器小节中的 ‘现有系统 → 适配器 -) 厂商类’ 例子比较形象;③示例:Java 中通过 EnumerationIterator 枚举迭代(适配)器遵循新的 迭代器 Iterator 接口来替代早期的 枚举 Enumeration(除了判断是否还有元素及访问下一个元素,迭代器还支持移除元素) 。

  4. 《人人都懂设计模式 - 从生活中领悟设计模式》第 13 章。①中国古建筑中的榫卯结构例子,不同榫头与榫槽配合工作;②示例:一个支持 .txt 及 .epub 格式的电子阅读器项目,通过适配器适配第三方 PDF 解析库支持 PDF 阅读。


扩展



  • 一般一个适配器只包装一个被适配对象,有没有一个适配器‘包装’多个被适配对象的场景?有!那就是 外观 / 门面模式 Facade Pattern

  • 有的电源适配器除了改变插头形状/电流电压外,还会提供一些额外功能,例如状态指示灯、扩展 USB 接口等,这类特性通过 装饰者模式 Decorator Pattern 实现。(装饰者主要 添加特性,适配器主要 转换接口

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

5 个让 Swift 更优雅的扩展——Pt.1

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战引言作为开发者,应该编写具有高可维护性和可扩展性的代码。我们可以通过扩展原有的功能,写出更易读,更简洁的代码。下面就介绍 5 个日常开发中非常实用的扩展。1. 自定义下标来安全访问数组我想...
继续阅读 »

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


引言

作为开发者,应该编写具有高可维护性和可扩展性的代码。我们可以通过扩展原有的功能,写出更易读,更简洁的代码。

下面就介绍 5 个日常开发中非常实用的扩展。

1. 自定义下标来安全访问数组

我想每个开发人员都至少经历过一次index-out-of-bounds的报错。就是数组越界,这个大家都懂,就不过多介绍了。下面是个数组越界的例子:

let values = ["A", "B", "C"]
values[0] // A
values[1] // B
values[2] // C
values[3] // Fatal error: Index out of range

既然是下标超过了数组的大小,那我们在取值之前,先检查下标是否超过数组大小。让我们来看下面的几种方案:

  • 通过 if 来判断下标
if 2 < values.count {
values[2] // "C"
}
if 3 < values.count {
values[3] // 不会走到这里
}

虽然也可以,但显的就很重复繁琐,每次取值之前都要判断一遍下标。

  • 定义公共函数

既然每次都要检查下标,那就把检查下标的逻辑放在一个函数里

func getValue<T>(in elements: [T], at index: Int) -> T? {
guard index >= 0 && index < elements.count else {
return nil
}
return elements[index]
}

let values = ["A", "B", "C"]
getValue(in: values, at: 2) // "C"
getValue(in: values, at: 3) // nil

不仅使用泛型支持了任何类型的元素,当数组越界时,还很贴心的返回了 nil,防止崩溃。

虽然很贴心,但每次取值都要把原数组传进去,显的就很冗余。

  • extension

既然每次都要传入数组很冗余,那就把数组的参数给去掉。我们知道 Swift 一个很强大的特性就是 extension,我们给 Array定义个 extension,并把这个函数添加进去。

extension Array {
func getValue(at index: Int) -> Element? {
guard index >= 0 && index < self.count else {
return nil
}
return self[index]
}
}

let values = ["A", "B", "C"]
values.getValue(at: 2) // "C"
values.getValue(at: 3) // nil

  • subscript

虽然看起来好很多了,但可不可以像原生的取值一样, 一个[]就搞定了呢?of course!

extension Array {
subscript (safe index: Int) -> Element? {
guard index >= 0 && index < self.count else {
return nil
}
return self[index]
}
}

values[safe: 2] // "C"
values[safe: 3] // nil

自定义的[safe: 2]和原生的 [2]非常的接近了。但自定义的提供了数据越界保护机制。

  • 应用到 Collection

既然这么棒,岂能数组一人独享,我们把它应用到所有 Collection 协议。看起来是不是很优雅~😉

extension Collection {
public subscript (safe index: Self.Index) -> Iterator.Element? {
(startIndex ..< endIndex).contains(index) ? self[index] : nil
}
}


2. 平等的处理 nil 和空字符串

在处理可选值时,我们通常需要将它们与 nil 进行比较进行空检查。当为 nil 时,我们会提供一个默认值让程序继续执行。比如下面这个例子:

func unwrap(value: String?) -> String {
return value ?? "default value"
}

unwrap(value: "foo") // foo
unwrap(value: nil) // default value

但是还有种情况就是空字符串,有时,我们需要把空字符串当做 nil 的情况来处理。此时,不仅要坚持 nil,还要检查空字符串的情况

func unwrap(value: String?) -> String {
let defaultValue = "default value"
guard let value = value else {
return defaultValue
}
if value.isEmpty {
return defaultValue
}
return value
}

unwrap(value: "foo") // foo
unwrap(value: "") // default value
unwrap(value: nil) // default value

虽然也能解决问题,但依然看起来很臃肿,我们把他简化一下:

func unwrapCompressed(value val: String?) -> String {
return val != nil && !val!.isEmpty ? val! : "default value"
}

unwrapCompressed(value: "foo") // foo
unwrapCompressed(value: "") // default value
unwrapCompressed(value: nil) // default value

虽然简化了很多,但不易读,可维护性略差。

可以把空字符串先转化为 nil,再进行处理,这样就和处理 nil 的情况一致了。

public extension String {
var nilIfEmpty: String? {
self.isEmpty ? nil : self
}
}

let foo: String? = nil

if let value = foo?.nilIfEmpty {
print(value) //不会调用
}

if let value = "".nilIfEmpty {
print(value) //不会调用
}

if let value = "ABC".nilIfEmpty {
print(value) //ABC
}


总结

这里先介绍 5 个常用扩展中的其中 2 个,剩下 3 个且听下回分解啦~

  • 给集合增加扩展,防止取值越界造成崩溃
  • 给字符串增加扩展,让空字符串变为 nil

如果觉得对你有帮助,不妨在项目中试试吧~

链接:https://juejin.cn/post/7026271045652840461

收起阅读 »

iOS App - 从编译到运行

iOS
在iOS开发中,app是被直接编译成机器码后在CPU上运行的,而不是使用解释器编译成字节码再运行。从app的编译到运行的过程中,要经过编译、链接、启动几个步骤。而在iOS中,编译阶段分为前端和后端,前端使用Apple开发的Clang,后端使用LLVM。 编译...
继续阅读 »

在iOS开发中,app是被直接编译成机器码后在CPU上运行的,而不是使用解释器编译成字节码再运行。从app的编译到运行的过程中,要经过编译、链接、启动几个步骤。而在iOS中,编译阶段分为前端和后端,前端使用Apple开发的Clang,后端使用LLVM。



编译


编译过程


编译过程主要有



  • 预处理

  • 词法分析

  • 语法分析

  • 静态分析

  • 中间代码生成

  • 汇编生成

  • 链接生成可执行文件


预处理


在预处理的阶段中,编译器Clang首先预处理我们代码,做一些比如将宏替换到代码中、删除注释、处理预编译命令等工作


词法分析


在此阶段词法分析器读入预处理过的代码字节流,将其中的字符处理成有意义的词素序列,对于每个词素产生词法单元并标记位置,处理完成后进入下一步。这个过程主要是为了在下一步生成语法树做基础工作。


语法分析


这一步中使用在词法分析中生成的词法单元,抽象生成一个语法树(AST,Abstract syntax tree)。抽象语法树上的每个节点也标记了它在源代码的位置。抽象语法树的遍历比起源代码块很多,这一步主要是为了后面的静态分析。
抽象语法树AST


静态分析 | 中间代码生成


将源代码转化为抽象语法树后,编译器就可以遍历整个树来做静态分析。**常见的类型检查、语法错误、方法未定义等都是在静态分析中发现并处理的,当然静态分析能做的事情还有非常多。**在静态分析结束后,编译器会生成IR。IR是整个编译链接系统的中间产物,是一种比较接近机器码的形式,但他与平台无关,通过IR可以生成多个平台的机器码。IR是在iOS编译系统中,前端Clang和后端LLVM的分界点。Clang的任务在生成IR后结束,将IR交付给LLVM后LLVM开始工作。


汇编生成


在获得到IR后,LLVM可以根据优化策略对IR进行一些优化,如尾递归优化、循环优化、全局变量优化。在优化完成后,LLVM会调用汇编生成器将IR转化成汇编代码。此时,生成产物就是.o文件了(二进制文件)。
在生成二进制文件后,我们可以通过二进制重排的方式对我们的编译产物进行更进一步的优化,已达到缩小编译产物大小、优化启动速度等目的


链接


在将源代码编译成.o文件后,就开始链接。链接其实就是一个打包的过程,将编译出的所有.o文件和一些如dylib,.a,tbd文件链接起来,一起合并生成一个Mach-o文件。到这里,编译过程全部结束,可执行文件mach-o已生成。在链接前,符号是未跟内存地址、寄存器绑定的,尤其是一些被定义在其他模块的符号。而在链接阶段,链接器完成了上述工作,进行了除动态库符号外的符号绑定,同时将这些目标文件链接成一个可执行文件


Mach-o文件结构


-w313



  • Header

    • Header 包含该二进制文件的一般信息 字节顺序、架构类型、加载指令的数量等。 使得可以快速确认一些信息,比如当前文件用于32位还是64位,对应的处理器是什么、文件类型是什么



  • Load Commands

    • 是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等。这一段紧跟Header,加载Mach-O文件时会使用这里的数据来确定内存的分布



  • Data

    • Data 通常是对象文件中最大的部分,包含Segement的具体数据,如静态C字符串,带参数/不带参数的OC方法,带参数/不带参数的C函数。当运行一个可执行文件时,虚拟内存 (virtual memory) 系统将 segment 映射到进程的地址空间上。

    • Segment __PAGEZERO 规定进程地址空间的前多少空间不可读写

    • Segment __TEXT 包含可执行的二进制代码

    • Segment __DATA 包含了将被更改的数据

    • Segment __LINKEDIT 包含了方法和变量的元数据,代码签名等信息。




静态链接


编译主要分为静态链接动态链接。在编译器阶段进行的是静态链接,也就是在上文中提到的过程。这一阶段是将在前面生成的各种目标文件和各种库(or module in swift)链接起来,生成一个可执行文件mach-o。




运行


装载


一个程序从可执行文件到运行,基本都要经过装载和动态库链接两个阶段。由于在可执行文件生成前已经完成了静态库链接,所以在装载时所有的源代码和静态库已经完成了装载,而动态库链接则需要下文提到的动态链接来完成。


可执行文件,或者说程序,是一个静态的概念,而进程是一个动态的概念。每个程序在运行起来后,他对应的进程都会拥有独立的地址空间,而这个地址空间是由计算机硬件(CPU的位数)决定的,当然,进程只是以为自己拥有计算机整个的地址空间,实际上他是与其他的进程共享计算机的内存(虚拟化)


装载,就是把硬盘上的可执行文件映射到虚拟内存上的过程。


装载的过程,也可以当作是进程建立的过程,一般来说有以下几个步骤。



  • 创建一个独立的虚拟地址空间

  • 读取可执行文件头,建立虚拟地址空间与可执行文件之间的映射关系。(将可执行文件中的相对地址与虚拟地址空间的地址进行绑定)

  • 将CPU的指令寄存器设为可执行文件的入口地址,交与CPU启动运行


动态链接


静态链接是链接静态库,需要链接进Mach-o文件中,如果需要更新就需要重新编译一次,所以无法动态更新和加载。而动态链接是使用dyld动态加载动态库,可以实现动态地加载和更新。并且其他的进程、框架链接的都是同一个动态库,节省了内存。


iOS中我们常用的一些如UIKitFoundation等框架都是使用动态链接的,而为了节省内存,系统将这些库放在动态库共享缓存区(Dyld shared cache)


mach-o文件中,属于动态库的符号会被标记为未定义,但他们的名字与路径会被记录下来。在运行时dyld会通过dlopendlsym导入动态库,并通过记录的路径找到对应的动态库,通过记录的名字找到对应的地址,进行符号与地址的绑定。


dlopen会将动态库映射到进程的虚拟地址空间中,由于载入的动态库中可能也会存在未定义的符号,也就是说该动态库还依赖了其他的动态库,这时会触发更多的动态库被载入,但dlopen可以决定是立刻载入这些依赖库还是延后载入。


dlopen打开动态库后返回的是引用的指针,dlsym的作用就是通过dlopen返回的动态库指针和函数符号,得到函数的地址然后使用。


动态链接解决了静态链接内存占用过多只要有库修改就要重新编译打包的缺点,但同时也引入了新的问题。



  • 结构复杂,动态链接将重定位推迟到运行时进行。

  • 引入了安全问题,这也是我们能够进行PLT HOOK的基础

  • 性能问题


而提到动态库链接,在iOS领域就必须提到我们的dyld


dyld - Dynamic Link Editor



dyld是苹果开发的动态链接器,是苹果系统的一个重要组成部分。它负责mach-o文件的动态库链接和程序的启动。相关代码已开源




  • 启动流程


main方法前的调用栈


启动工程,在_objc_init处设置一个symbolic breakpoint,Xcode会帮我们在main方法执行前设置断点。进入lldb后使用bt命令,我们就可以看到_objc_init方法前的调用栈。


可以看到,dyld是最先被启动的。_dyld_start后,首先调用的是dyldbootstrap命名空间里的start函数,dyld:bootstrap意义为dyld进行自举工作。由于动态链接器本身也是一个共享对象,那么它自己也需要重定向工作。那么为了避免循环重定向的问题,动态链接器相对于其他的共享对象需要有一些特性。第一个就是它不可以依赖于其他的共享对象,第二个是它的重定向工作可以由自己完成。这种具有一定限制条件的启动代码称为自举(bootstrap)


由于dyld比较复杂,在这里就先不详细展开,留待另一篇文章中细讲。启动的大体流程为



  • dyld 开始将程序二进制文件初始化

  • 交由 ImageLoader 读取image,其中包含了我们的类、方法等各种符号

  • 由于 runtime 向 dyld 绑定了回调,当image 加载到内存后,dyld会通知runtime进行处理

  • runtime接手后调用map_images做解析和处理,接下来load_images中调用call_load_methods方法,遍历所有加载进来的Class,按继承层级依次调用Class的 +load 方法和其 Category 的 +load 方法


所以动态链接器的工作流程为



  1. 动态链接器自举 (动态链接器的地址在可执行文件的.interp段) ->

  2. 装载共享对象(在这个步骤合并生成全局符号表)->

  3. 重定位(遍历可执行文件和每个共享对象的重定位表将GOT/PLT中需要重定位的位置进行修正)->

  4. 初始化(执行共享对象.init段中的代码,进程的.init段由程序初始化代码执行)->

  5. 将控制权交还给程序的入口


写在最后


在写这篇的过程中系统地学习了一下app从编译到运行的过程。在编译阶段,静态链接动态链接这种编译原理相关的知识很重要,有时间可以读一下编译原理那本书。运行阶段,dyld在main函数执行前做了非常多工作,其实现也很复杂,待仔细学习后再写一篇聚焦于dyld的笔记。


链接:https://juejin.cn/post/7030435738944536607
来源:稀土掘金
收起阅读 »

重要!后面几个月,iOS开发需要注意的3件事情

iOS
这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战前言这里非常感谢@恋猫de小郭,大佬的一篇文章让我醍醐灌顶。通过大佬的这篇文章对开发者而言《个人信息保护法》更新究竟是什么?如何应对适配?,我回过头,老老实实看了苹果开发的一些新闻,有一些...
继续阅读 »


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

前言

这里非常感谢@恋猫de小郭,大佬的一篇文章让我醍醐灌顶。

通过大佬的这篇文章对开发者而言《个人信息保护法》更新究竟是什么?如何应对适配?,我回过头,老老实实看了苹果开发的一些新闻,有一些非常重要的信息。

如果你从事iOS开发,抑或针对Flutter开发,有需要上架App Store,我建议各位都来了解这3件事情。

自2022年1月31日起,需在app内提供帐户删除的功能

截屏2021-11-12 15.31.56.png

撇开11月1日颁布的《中华人民共和国个人信息保护法》,早上10月6日,Apple就发布了新闻说明:

需要在App中提供账户删除的功能,并且对于在2022年1月31日开始提交的App生效。

这说明了一个什么事呢?

如果你有一个App,后面会持续迭代,那么这个账户删除功能必须加上!!!

这事最好和项目、技术、后台一起讨论一下,我个人认为在App端无非多了一个交互,多了一个接口,多了一个逻辑,但是这个删除对后端来说,可能需要删除的东西就多了。

另外,从这字面上看,这应该是一个硬删除吧(软删除大家都懂的)。

自2022年4月起,必须使用Xcode 13和iOS 15 SDK构建App,提交至App Store

截屏2021-11-12 15.42.35.png


说简单点:

说白了,这是一波强行的让你升级Xcode的做法,没办法,在此道上混,就只能这么走。

迟早都要升级Xcode13,早点升级早踩坑。


另外需要注意的是,有些项目可能会在Xcode12上运行的很好,但是在Xcode13上一运行就报错,需要提前做好准备,我手上就有一个这样的项目。

年末假期接受app提交

截屏2021-11-12 15.56.06.png

大家都知道的,由于11月和12月都有西方的传统节日,所以一般情况下,在一些时间点提交App到App Store审核会异常缓慢。

今年Apple自己内卷了一把,在年末的假期也接受app提交了,虽然不知道具体速度如何,不过这也算是迎合国内的市场需求吧。

因为有些app就是在年末或者过节的时候迭代的非常频繁。毕竟大家都有剁手嘛~😁

参考文档

“需在 app 内提供帐户删除”的要求将于 1 月 31 日生效

将iOS和iPadOS app提交至App Store

年末假期接受app提交

macOS Monterey 与以下电脑兼容

总结

今天就想讲这么几件事情,其中第一件和第二件事情在我看来还是挺重要的,大家自己也掂量一下呗。

一周有空去Apple Developer网站看看新闻,有些对开发还是非常重要的。

Apple Developer新闻与更新

收起阅读 »

iOS App 的最佳架构,存在么?

iOS
iOS App 的最佳架构,存在么?本文翻译自 The best architecture for the iOS app, does it even exist?,建议参考原文阅读,也可查看这里前一段时间,我偶然发现了有关 iOS 体系结构模式的文...
继续阅读 »

iOS App 的最佳架构,存在么?

本文翻译自 The best architecture for the iOS app, does it even exist?,建议参考原文阅读,也可查看这里

前一段时间,我偶然发现了有关 iOS 体系结构模式的文章,标题颇具挑衅性:“唯一可行的 iOS 架构”。标题中问题的答案实际上是 MVC。简而言之,MVC 是 iOS 应用程序唯一可行的也是最好的架构。

该文章的主要思想是人们只是以错误的方式去理解 MVC。该 ViewController 实际上是表示层的一部分,而 Model 部分则代表整个 Domain Model,而不仅仅是某些数据实体。总的来说,我同意那个帖子的想法,但是如果我同意那个帖子的每一个陈述,我就不会写这篇文章了,不是吗?

我注意到作者基本上没有涉及格式良好的应用程序体系结构的一个非常重要的方面:使用单元测试(UT)覆盖了应用程序业务逻辑(BL)。对我而言,这是明智的应用程序体系结构的最重要因素之一。如果无法提取应用程序 BL 并以足够的覆盖范围实现 UT,那么这种架构简直糟透了。

此外,如果一个应用没有 UT,那么证明上述观点是不可行的,因此其架构最有可能出现问题。您可以向自己保证,在你从紧张工作中的片刻休息时间可以轻松实现UT,或者仅仅是因为您在 XCode 项目中拥有专用的“Tests”目标以及一些模板 UT。相信我,但这不过是一种幻想。我坚信,如果单元测试未随功能一起实施或在交付后不久就将无法实施。

以该声明为公理,应用程序体系结构必须提供将 BL 与 UI 表示分离并使其可测试的功能。很明显,由于 UIViewController 子类对生命周期的依赖性,因此它不是该角色的最佳候选人。这意味着负责 BL 的类必须位于 UIViewController 和 Services 之间,或者换句话说,位于 View 和 Domain Model 之间。

值得注意的是,这里的 Services 是指负责联网,与数据库、传感器、蓝牙、钥匙串、第三方服务等进行通信的逻辑。换句话说,是应用中多个位置、页面的共享部分。而图上的业务逻辑部分仅对应于一个页面或一个由视图控制器表示的页面组件。在开头提到的有关 MVC 的文章中,作者将 BL 和 Domain Model 部分结合在一起,同时接受 UIViewController 是表示逻辑(即视图)的一部分。

现在,当确定了将表示和业务逻辑分离的需要时,让我们考虑一下这两个部分如何相互通信。 这就是那些著名的架构模式出现的地方。

MVP

在 MVP 模式中,Presenter 和 View 通过协议相互链接。Presenter 被注入了 View 协议的实例,反之亦然,View 的协议必须具有足够的接口才能在 UI 中呈现原始数据,而Presenter 的协议必须具有传输从用户或系统接收到的事件(如触摸,手势,摇动等)的接口。UIViewController 子类在此处表示 View 部分,而 Presenter 类不能依赖UIKit(例如,有时需要导入 UIKit 才能对 UIImage 等数据类进行操作)。

在 iOS 上,由于 UIViewController 生命周期的工作方式,它必须具有对 Presenter 实例的强引用,而最后一个必须是弱引用,以避免循环引用。此配置使人联想到委托模式。 在大多数情况下,UIViewController 可能具有对 Presenter 的直接类型化引用,但在某些情况下,最后一个角色也可以通过协议注入到第一个中。如果 presentation 用于不同的业务逻辑,这可能会很有用。Presenter 到 UIViewController 的链接必须通过协议才能进行模拟,并用 UT 覆盖。在 Service 部分,我不会做太多具体说明,但是为了测试 Presenter,还必须将其与协议一起注入。

有关 MVP 的更多详细信息以及基本示例,请参见此处1

MVVM

在 MVVM 模式中,表示和业务部分使用响应性绑定相互通信,它们分别称为 View 和 ViewModel。在 iOS 中,通常会使用 ReactiveCocoa,RxSwift 或现代的 Combine 框架进行响应性绑定,它们通常位于 ViewModel 类中,并且也由 ViewController 通过协议使用。在与 Services 或 Domain Model 进行通信的一部分中,MVP 并没有太大的区别,但人们可能更喜欢在这里使用绑定或响应性事件。与前面的模式一样,必须在协议中注入依赖项,以便在 UT 中模拟它们。

可以在此处2找到有关 MVVM 的更多详细信息以及基本示例。

MVVM+Router

这里独立的主题是路由。在 iOS 中,以模态方式显示新屏幕或推送到导航堆栈是通过 UIViewController 子类来实现的。但是,这些操作可能是 BL 的一部分,并且可能会被 UT 覆盖,例如如果发生特定事件,则必须关闭屏幕。在这种情况下,将应用程序逻辑的这一部分分为一个称为 Router 的类是有意义的。因此,模式变为 MVP+R 或 MVVM+R。在某些来源中,您可能会发现此部分分别命名为 Coordinator 和 MVVP+C 或 MVVM+C。尽管协调器可能具有除路由之外的其他一些逻辑,但我更喜欢在概念上将它们等同。ViewModel 和 Router 之间的链接必须通过协议,并且最后一个必须仅负责屏幕操作,所有 BL 必须仍然集中在第一个中。因此,Router 不是 UT 的主题。

具有 MVVM+R 架构模式实现的示例项目可以在我的 GitHub3 上找到。

其它

VIPER iOS 体系结构模式是 MVVM+R 的扩展,其中 ViewModel 分为两部分:Interactor 和 Presenter。第一个负责与实体(即域模型)的通信。第二部分准备要在视图中呈现的模型类。老实说,我从未使用过这种模式,因为对我而言,它似乎过于分散和复杂。 MVVM+R 关注点分离对我而言总是足够的。

在 MVVM+R 中,每个模块(屏幕)必须至少显示 3 个类:ViewController,ViewModel 和 Router。并且必须有一个实例化所有这些部分并将它们彼此链接的位置,即模块构建的关键。最合适的位置是 Router,因为它没有与 iOS UIViewController 生命周期耦合,并且必须知道如何显示页面才能正确关闭它。但是,在某些情况下,将这一部分移到名为 Builder 的单独的类中会更方便,这就是 RIB(Uber的架构模式)中发生的情况。ViewModel 重命名为 Interactor,其余部分保持不变。这种模式具有 Uber 引入的一些更有趣的想法和技术,您可以在 RIB Wiki 上阅读。但是,我在 RIBs 代码库中发现的最实用的东西是 XCode 模板,当在项目中引入新的 RIBlet 时,它可以帮助避免样板编码。这些模板也可以很容易地用于 MVVM+R 类。为 Uber 的 iOS 工程师👏。

最后简单聊聊关于 iOS 上的单向数据流架构模式。如果看一下以上模式的方案,它们在组件之间都具有双向连接。在 Redux 中不是这种情况。这种架构模式最初是从 Web 迁发到移动应用的,尤其是 React 框架。在 ReSwift 框架中,此概念是 iOS 开发中最受欢迎的实现。我不会详细介绍,因为尚未在生产应用中使用此架构。但是,很明显,从 Web 开发进入 iOS 的人们发现这种架构模式最为直观和熟悉。

结论

什么才是最好的应用程序架构始终是一个热门的主题,所以现在我更倾向于约翰·桑德尔在他的最近一次演讲中提出的想法:

最好的架构是您和您的团队共同创建的架构,通过将标准模式和技术与系统设计相结合来适合您的项目。

参考

[1]https://medium.com/@saad.eloulladi/ios-swift-mvp-architecture-pattern-a2b0c2d310a3
[2]https://medium.com/flawless-app-stories/practical-mvvm-rxswift-a330db6aa693
[3]https://github.com/OlexandrStepanov/MVVM-RouterDemo

链接:https://juejin.cn/post/6844904067374776334

收起阅读 »

iOS-应用程序的加载

iOS
资料准备: 1、dyld源码下载opensource.apple.com/ 2、libdispatch源码下载opensource.apple.com/ 3、libSystem源码下载opensource.apple.com/ 前情提要: 在探索分析app启动...
继续阅读 »

资料准备:


1、dyld源码下载opensource.apple.com/

2、libdispatch源码下载opensource.apple.com/

3、libSystem源码下载opensource.apple.com/


前情提要:


在探索分析app启动之前,我们需要先了解iOS中App代码的编译过程以及动态库和静态库。


编译过程


1预编译:处理代码中的#开头的预编译指令,比如删除#define并展开宏定义,将#include包含的文件插入到该指令位置等(即替换宏,删除注释,展开头文件,产生.i文件)

2编译:对预编译处理过的文件进行词法分析、语法分析和语义分析,并进行源代码优化,然后生成汇编代码(即将.i文件转换为汇编语言,产生.s文件)

3汇编:通过汇编器将汇编代码转换为机器可以执行的指令,并生成目标文件.o文件

4链接:将目标文件链接成可执行文件.这一过程中,链接器将不同的目标文件链接起来,因为不同的目标文件之间可能有相互引用的变量或调用的函数,如我们经常调用Foundation框架和UIKit框架中的方法和变量,但是这些框架跟我们的代码并不在一个目标文件中,这就需要链接器将它们与我们自己的代码链接起来


流程如下



Foundation和UIKit这种可以共享代码、实现代码的复用统称为库——它是可执行代码的二进制文件,可以被操作系统写入内存,它又分为静态库和动态库



静态库


链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
.a.lib、非系统framework都是静态库


动态库


链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。如.dylib.framework都是动态库


dyld:


简介


dyld(The dynamic link editor)是苹果的动态链接器,负责程序的链接及加载工作,是苹果操作系统的重要组成部分,存在于MacOS系统的(/usr/lib/dyld)目录下.在应用被编译打包成可执行文件格式的Mach-O文件之后 ,交由dyld负责链接,加载程序。

整体流程如下


image.png


dyld_shared_cache


由于不止一个程序需要使用UIKit系统动态库,所以不可能在每个程序加载时都去加载所有的系统动态库.为了优化程序启动速度和利用动态库缓存,苹果从iOS3.1之后,将所有系统库(私有与公有)编译成一个大的缓存文件,这就是dyld_shared_cache,该缓存文件存在iOS系统下的/System/Library/Caches/com.apple.dyld/目录下


dyld加载流程:


load方法处加一个断点,点击函数调用栈/使用LLDB——bt指令打印,都能看到最初的起点_dyld_start


_dyld_start


可以看到_dyld_start是汇编写的,从注释中可以看出dyldbootstrap::start方法就是最开始的start方法。


image.png


dyldbootstrap::start


dyldbootstrap::start其实C++的语法,其中dyldbootstrap代表命名空间,start则是这个命名空间中的方法。


image.png


image.png
可以看到start这个方法的核心是dyld::main


dyld::main


_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue) {

代码省略......

/// 环境变量的配置
// Grab the cdHash of the main executable from the environment
/// 从环境变量中获取主要可执行文件的cdHash
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
unsigned bufferLenUsed;
if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
mainExecutableCDHash = mainExecutableCDHashBuffer;
}
/// 根据Mach-O头部获取当前运行的架构信息
getHostInfo(mainExecutableMH, mainExecutableSlide);

代码省略......

/// 检查共享缓存是否开启,在iOS中必须开启
// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache(mainExecutableSlide);
#else
/// 检查共享缓存是否映射到了共享区域
mapSharedCache(mainExecutableSlide);
#endif

代码省略......

/// 加载可执行文件,并生成一个ImageLoder实例对象
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

代码省略......

/// 加载所有DYLD_INSERT_LIBRARIES指定的库
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}

代码省略......

/// link主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);

代码省略......

/// link动态库
// link any inserted libraries
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
if ( gLinkContext.allowInterposing ) {
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing(gLinkContext);
}
}
}

代码省略......

sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
uint64_t bindMainExecutableEndTime = mach_absolute_time();
ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
gLinkContext.notifyBatch(dyld_image_state_bound, false);

// Bind and notify for the inserted images now interposing has been registered
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true, nullptr);
}
}

代码省略......

/// 弱符号绑定
// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);

代码省略......

/// 执行初始化方法
// run all initializers
initializeMainExecutable();

代码省略......

/// 寻找m目标可执行文件ru入口并执行
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();

}#### 1 环境变量配置


通过以上代码分析大体流程如下


1 环境变量配置



  • 平台,版本,路径,主机信息的确定

  • 从环境变量中获取主要可执行文件的cdHash

  • checkEnvironmentVariables(envp)检查设置环境变量

  • defaultUninitializedFallbackPaths(envp)DYLD_FALLBACK为空时设置默认值

  • getHostInfo(mainExecutableMH, mainExecutableSlide)获取程序架构


image.png
image.png
image.png


Xcode设置了这两个环境变量参数,在App启动时就会打印相关参数、环境变量信息
如下


image.png


image.png


2 共享缓存



  • checkSharedRegionDisable检查是否开启共享缓存(在iOS中必须开启)

  • mapSharedCache加载共享缓存库,其中调用loadDyldCache函数有这么几种情况:

    • 仅加载到当前进程mapCachePrivate(模拟器仅支持加载到当前进程)

    • 共享缓存是第一次被加载,就去做加载操作mapCacheSystemWide

    • 共享缓存不是第一次被加载,那么就不做任何处理




image.png


image.png


3 主程序的初始化


调用instantiateFromLoadedImage函数实例化了一个ImageLoader对象
image.png
通过instantiateMainExecutable方法创建ImageLoader实例对象
image.png
这里主要是为主可执行文件创建映像,返回一个ImageLoader类型的image对象,即主程序.其中sniffLoadCommands函数会获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验
image.png


4 插入动态库


遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载,通过该环境变量我们可以注入自定义的一些动态库代码从而完成安全攻防,loadInsertedDylib内部会从DYLD_ROOT_PATHLD_LIBRARY_PATHDYLD_FRAMEWORK_PATH等路径查找dylib并且检查代码签名,无效则直接抛出异常
image.png
image.png


5 link主程序
image.png


5 link动态库
image.png


6 弱符号绑定
image.png


7 执行初始化方法


image.png
initializeMainExecutable源码主要是循环遍历,都会执行runInitializers方法
image.png
runInitializers(cons其核心代码是processInitializers函数的调用
image.png
processInitializers函数对镜像列表调用recursiveInitialization函数进行递归实例化
image.png
recursiveInitialization函数其作用获取到镜像的初始化,核心方法有两个notifySingledoInitialization
image.png
notifySingle函数,其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());这句
image.png
sNotifyObjCInit只有赋值操作
image.png
registerObjCNotifiers发现在_dyld_objc_notify_register进行了调用,这个函数只在运行时提供给objc使用
image.png
objc4源码中查找_dyld_objc_notify_register,发现在_objc_init源码中调用了该方法,并传入了参数,所以sNotifyObjCInit的赋值的就是objc中的load_images所以综上所述,notifySingle是一个回调函数
image.png
load_images中可以看到call_load_methods方法调用
image.png
call_load_methods方法其核心是通过do-while循环调用call_class_loads方法
image.png
call_class_loads这里调用的load方法就是类的load方法
image.png
至此也证实了load_images调用了所有的load函数。


doInitialization中调用了两个核心方法doImageInitdoModInitFunctions
image.png
doImageInit其核心主要是for循环加载方法的调用,这里需要注意的一点是libSystem的初始化必须先运行
image.png
doModInitFunctions中加载了所有Cxx文件,这里需要注意的一点是libSystem的初始化必须先运行
image.png
通过doImageInitdoModInitFunctions方法知道libSystem初始化必须先运行,这里也和堆栈信息相互验证一致性
image.png
libSystem库中的初始化函数libSystem_initializer中调用了libdispatch_init函数
image.png
libdispatch_init方法中调用了_os_object_init函数
image.png
_os_object_init方法中调用了_objc_init函数
image.png
结合上面的分析,从初始化_objc_init注册的_dyld_objc_notify_register的参数2,即load_images,到sNotifySingle --> sNotifyObjCInie=参数2sNotifyObjcInit()调用,形成了一个闭环


8 寻找主程序入口
image.png
dyld汇编源码实现
image.png


dyld加载流程


image.png




链接:https://juejin.cn/post/7030253046210773029

收起阅读 »

谈谈iOS项目的多环境配置

iOS
在项目中配置多环境,需要了解的三个芝士点: Project: 包含了项目所有的代码,资源文件,所有信息。 Target: 对指定代码和资源文件的具体构建方式。 Scheme: 对指定Target的环境配置。 配置多环境的三种方案 多Target 先复制一...
继续阅读 »

在项目中配置多环境,需要了解的三个芝士点:



  • Project: 包含了项目所有的代码,资源文件,所有信息。

  • Target: 对指定代码和资源文件的具体构建方式。

  • Scheme: 对指定Target的环境配置。


配置多环境的三种方案


多Target



  1. 先复制一份一样的Target


image.png



  1. 对其进行重新命名,此时对于项目会增加一个新的info.plist文件


image.png



  1. 设置其对应的info.plist文件


image.png
4. 对于这个新的Target修改其对应BundleID


image.png



  1. 设置宏定义来实现多环境配置



  • Objc:在Objc中通过在Preprocessor Macros中配置宏定义


image.png



  • Swift:在Swift中通过在Other Swfit Flags中增加配置


image.png


总结
通过多Target方案会有两个缺点,第一每生成一个Target都会产生一个Info.plist文件,会比较冗余,第二就是比较麻烦,因为每次都会要设置宏定义,故不建议采纳。


通过Scheme实现多环境配置



  1. 添加新的Configuration


image.png



  1. 增加新的Scheme


image.png



  1. schemeBuild Configuration一一对应


image.png



  1. 新增定义设置(这里以区分不同环境需要访问的域名来举例)


image.png


image.png



  1. Info.plist中新增访问接口


image.png



  1. 在项目中进行访问


image.png


image.png


image.png


image.png


可以看到实现了不同的scheme访问了不同的值,实现了多环境配置,不过这个方案依然不够方便,因为有些Build Settings里针对不同环境需要做不同设置,这样还是不够方便。


xcconfig


1.在项目中创建自己的xcconfig文件,这里分别创建debugreleaserc对应的文件


image.png


2.在ProjectConfigurations进行对应


image.png


3.在xcconfig文件中进行配置(同样以不同环境的域名为例子)


image.png


image.png


image.png


4.在plist文件中提供接口
image.png


5.运行程序发现报错


image.png


这里涉及使用pod,如果另外创建xcconfig文件会导致这个错误,如果不涉及pod则不会报错,来看下控制台的报错


image.png


6.引入pods工程下的xcconfig相关文件
仅举例debug.xcconfig文件,其余操作均如下


image.png


7.选中不同的scheme运行,即可实现多环境配置


image.png


image.png


image.png


注意
在自己创建的xcconfig进行设置一些Build Settings里的参数时,可能会覆盖掉pods里的设置,这时需要加上关键字$(inherited),这样就会继承pods文件中的设置。


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

iOS autorelease与自动释放池

iOS
autorelease、autorelease pool以及原理 autorelease与MRC、ARC autorelease:在MRC下,内存管理允许有三个操作,分别是release,retain,autorelease。release会使对象的引用计数...
继续阅读 »

autorelease、autorelease pool以及原理


autorelease与MRC、ARC



  • autorelease:在MRC下,内存管理允许有三个操作,分别是release,retain,autoreleaserelease会使对象的引用计数立刻-1,retain使对象的引用计数立刻+1,autorelease也会让对象的引用计数-1,但不是立刻-1.调用autorelease的对象会被加入到autorelease pool中,在合适的时间autorelease pool向对象调用release,也就是说,对象被延迟释放了。

  • 而在ARC下,Apple禁止了手动调用autorelease方法。使用@autoreleaseblock创建自动释放池后,runtime会自动向在block中的对象加上autorelease


autorelease pool



A thread's autorelease pool is a stack of pointers. Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary. A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released. The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary. Thread-local storage points to the hot page, where newly autoreleased objects are stored.



以上是objc-781源码中NSObject.mm对于自动释放池的定义。从定义里面可以得知,自动释放池实际上是一个存放了指针的栈,栈中的指针有两类,一类是等待释放的对象指针,一类是名为POOL_BOUNDARY的哨兵指针。释放池之间以链表的形式相连,一个Page通常是4096个字节的大小(虚拟内存中的一页)。而前面提到的POOL_BOUNDARY哨兵指针的作用就是标示每个池子的尾端。


当在MRC中调用autorelease方法或者在ARC中将对象编写在@autoreleaseblock中,对象将会被注册到自动释放池中,当合适的时机到来自动释放池将会向这些对象调用release方法,以释放对象。



The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.



以上是Developer Documentation中Apple对于autorelease pool的一个介绍。可以看到在主线程中,每一个事件循环Runloop的开始,Appkit框架都会为程序创建一个自动释放池,并且在每次Runloop结束时释放所有在池中的对象。
需要注意的是,在这里Apple提到了一点:如果程序中临时创建了大量的autorelease对象,那么更好的做法是开发者自行新增一个释放池来最小化内存峰值的发生。


原理


First of all,最简单的代码


我们先来写出最常见的@autorelease代码。
-w461
当我们在Xcode中建立一个macOS项目时,通常模版中的main函数就包含了这样一段类似的代码。其中的@autorelease pool就是在ARC环境下使用自动释放池的API。


OC to C++


在终端中使用clang -rewrite-objc main.m命令将main.m文件转换成C++代码文件。
转换出来的代码会很多,我们挑重点的看。


-w511
在C++代码的main函数中,@autoreleasepool{}已经被转换成了如上代码。我们可以看到熟悉的objc_msgSeng,这是OC的灵魂-消息发送。
同时,@autoreleasepool{}变成了__AtAutoreleasePool,看来自动释放池的真实结构就是这个。我们再找一下它的定义在哪里。


通过搜索关键字,我们找到了它的定义语句。
-w543


可以看到,__AtAutoreleasePool结构体中定义了一个构造函数和一个析构函数,并调用了objc_autoreleasePoolPush()objc_autoreleasePoolPop()两个函数。


objc源码


这一步,我们到objc4-781代码中找上述两个方法的实现。
-w397
可以看到,这两个方法实际上是调用了AutoreleasePoolPage类的push()pop()方法,也就是说,自动释放池在runtime中的实际结构其实是AutoreleasePoolPage,这就是它的最终面目了。


-w596


AutoreleasePoolPage类继承于AutoreleasePoolPageData


-w766


从这里我们可以看到,autoreleasepool 是与线程一一对应的。同时线程池之间以双向链表相连。


这里引用网上一位同学分享的内存分布图
AutoreleasePoolPage


接着我们来看一下几个关键方法的具体实现。


autorelease


首先是autorelease方法。调用该方法会将对象加入到自动释放池中。
autorelease


第一行和第二行代码分别对传入参数做了一些检验,从第二行代码可以见到,如果传入的对象是TaggedPointer类型的,比如由小于等于9个字符的字面量字符串创建的NSString,将会有其他的处理操作。


autoreleaseFast-w465


该方法是autorelease方法的关键方法。可以看到第一行通过hotPage()方法拿到一个最近使用过的Page,然后来到流程控制。



  • 如果获取到了该hotPage并且Page还没有满,那么将对象加入到该Page中;

  • 如果Page满了,则调用autoreleaseFullPage方法创建一个新的page,将对象加入到新创建的page后并将新建立的page与通过hotPage()获取到的page相连接。

  • 如果没有获取到hotpage,那么将会调用autoreleaseNoPage方法建立并初始化自动释放池。


AutoreleasePoolPage::push()


在前面我们提到将对象加入到自动释放池时首先调用objc_autoreleasePoolPush方法,而该方法只起到了调用AutoreleasePoolPage::push()方法的作用。
-w688


其中,if-else的if分支是当出错时Debug会执行的流程,正常将会执行else分支里的代码。而autoreleaseFast()方法的实现在上一小段中已给出,这里传入的POOL_BOUNDARY就是哨兵对象。当创建一个自动释放池时,会调用该push()函数。


_objc_autoreleasePoolPrint()


使用_objc_autoreleasePoolPrint();方法可以打印出目前自动释放池中的对象,当然在使用前要先extern void _objc_autoreleasePoolPrint(void);.
该方法会调用AutoreleasePoolPage::printAll();打印出自动释放池中的相关信息。


-w553


从打印出的信息来看,自动释放池确实是跟线程一一对应的,并且在创建时会将一个哨兵对象加入到池中,这与我们在上文的代码分析结果相互映证。


写在最后


关于autoreleaseautoreleasePool的原理和代码的分析大概就是这些,当然还有很多具体实现是本文没有提到的,有兴趣的读者也可以自行到objc4-781的源码里找到NSObject.mm文件更加详细地研究。
总得来说,自动释放池机制延迟了对象的生命周期,并且可以为开发者自动释放需要被释放的对象,减少了内存泄漏发生的可能。


作者:tino又想吃肉了
链接:https://juejin.cn/post/7030358196149551112
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS 简单模拟 https 证书信任逻辑

iOS
废话开篇:https 证书是什么?如何进行认证呢?带着这些疑问来简单的实现一下验证过程简单的了解一下 https 在数据传输前的一些操作,如图:这里总结一下上面的流程图关键的步骤:1、认证网络请求的安全性:服务器会在建立真正的数据传输之前返回一个公钥数字证书。...
继续阅读 »

废话开篇:https 证书是什么?如何进行认证呢?带着这些疑问来简单的实现一下验证过程

简单的了解一下 https 在数据传输前的一些操作,如图:

image.png

这里总结一下上面的流程图关键的步骤:

1、认证网络请求的安全性

服务器会在建立真正的数据传输之前返回一个公钥数字证书。这里客户端需要在 URLSession 进行认证挑战方法回调里进行判断然后确定是否要继续进行请求。代理方法如下:

- (void)URLSession:(NSURLSession *)session

              task:(NSURLSessionTask *)task

didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge

 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler

可以这样理解,URLSession 做 https 网络请求的时候其实会把请求鉴权的权限通过代理的方法给暴露出来,是否信任并继续建立连接可以按照特定规则去执行(如自签证书),只有 https 请求会走代理方法,http 则不进行回调,这也是为什么 iOS系统 为什么提倡使用 https 的原因。

2、认证通过,通过公私钥非对称加密方式对最后的对称加密密钥进行加、解密:

这话听起来有点绕,基于第一步的公钥数字证书信任,那么,生成一个用于请求数据对称加密的密钥(对称加密更快),用这个公钥进行非对称加密,在由服务器的私钥进行解密,得到这个密钥,那么,真正建立的数据传输就以此密钥进行加、解密。

下面,模拟一下如何进行的公钥证书受信

创建 公钥.der 及 证书.cer 文件

在终端依次输入如下命令:


//生成私钥
openssl genrsa -out private_key.pem 1024

//获取 证书.cer
openssl req -new -key private_key.pem -out rsaCertReq.csr

openssl x509 -req -days 3650 -in rsaCertReq.csr -signkey private_key.pem -out rsaCert.crt

//将 .crt 格式证书转换为 .cer 格式证书,后面iOS程序里需要 .cer格式证书
openssl x509 -in rsaCert.crt -out rsaCert.cer -outform der

//获得 公钥.der
openssl x509 -outform der -in rsaCert.crt -out public_key.der



过程中会有一些简单信息输入,这里没有特别的要求,文件创建后目录如图:

image.png

把 .cer 格式证书 和 公钥.der 格式证书 全部拖到工程里:

image.png

下面输出一段代码,用 .cer 证书去验证 公钥.der 是否可信。


- (void)trustIsVaild

{
//获取工程下所有cer证书(https 网络请求鉴权必需证书)
    NSArray *paths = [[NSBundle mainBundle] pathsForResourcesOfType:@"cer" inDirectory:@"."];

//保存工程内的所有 cer 证书(并在后面设置为鉴权锚点)
    NSMutableArray *pinnedCertificates = [NSMutableArray array];

    for (NSString *path in paths) {

        NSData *certificateData = [NSData dataWithContentsOfFile:path];

        [pinnedCertificates addObject:( __bridge_transfer id)SecCertificateCreateWithData(NULL, ( __bridge CFDataRef)certificateData)];

    }

//获取工程下的公钥数字证书(在https网络请求认证挑战中由服务器返回)
    NSString * publicKeyPath = [[NSBundle mainBundle] pathForResource:@"public_key" ofType:@"der"];

    NSData *derData = [[NSData alloc] initWithContentsOfFile:publicKeyPath];

//证书资源
    SecCertificateRef myCertificate = SecCertificateCreateWithData(kCFAllocatorDefault, ( __bridge CFDataRef)derData);

//验证政策设置
    SecPolicyRef myPolicy = SecPolicyCreateBasicX509();

    SecTrustRef myTrust;

//SecTrust 赋值
    OSStatus status = SecTrustCreateWithCertificates(myCertificate,myPolicy,&myTrust);

    if (status == noErr) {
//设置证书锚点(这里的意思就是如果鉴权到指定的证书是有效的,那么,就信任此公钥数字签名,这里如果不设置,那么就会一直找向根证书,由于工程里的公钥数字证书是自签的,所以,一定不会受信)
        SecTrustSetAnchorCertificates(myTrust, ( __bridge CFArrayRef)pinnedCertificates);

        SecTrustResultType result;

        if (SecTrustEvaluate(myTrust, &result) == 0) {

//kSecTrustResultUnspecified 隐式信任
//kSecTrustResultProceed 可继续进行
            if ((result == kSecTrustResultUnspecified || result == kSecTrustResultProceed)) {

                NSLog(@"受信任的证书");

            } else {

                NSLog(@"未受信任的证书");

            }

        } else {

            NSLog(@"未受信任的证书初始化操作失败");

        }

    }

}

运行如下:

image.png

顺便输出一下不设置 证书锚点 控制台内容:

if (status == noErr) {
//不设置锚点
//SecTrustSetAnchorCertificates(myTrust, (__bridge CFArrayRef)pinnedCertificates);

        SecTrustResultType result;

        if (SecTrustEvaluate(myTrust, &result) == 0) {

            if ((result == kSecTrustResultUnspecified || result == kSecTrustResultProceed)) {

                NSLog(@"受信任的证书");

            } else {

                NSLog(@"未受信任的证书");
            }
        } else {

            NSLog(@"未受信任的证书初始化操作失败");
        }
    }

image.png

到这里,公钥证书如果受信,那么,下一步就规定一个 对称加密 session key 用这个公钥加密,发送到服务器,然后用对应的私钥解密,供以后的数据传输进行 对称加密 操作。

所以,移动端在做自定义证书鉴权的时候就需要存储服务器生成的 .cer 证书文件!

AFNetworking 下的鉴权方式处理相对复杂,因为 URLSession 的认证挑战回调是允许程序员全部无条件开启的,所以,AFNetworking 在默认鉴权行为的基础上添加了几种自定义鉴权方式:

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {

    AFSSLPinningModeNone,//无条件开启

    AFSSLPinningModePublicKey,//认证公钥内容

    AFSSLPinningModeCertificate,//认证证书

};

而且,在此之前 AFNetworking 通过

@property (readwrite, nonatomic, copy) AFURLSessionTaskAuthenticationChallengeBlock authenticationChallengeHandler;

暴露给外界闭包进行自定义鉴权逻辑及处理结果。

- (void)URLSession:(NSURLSession *)session

              task:(NSURLSessionTask *)task

didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
BOOL evaluateServerTrust = NO;
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *credential = nil;

//AFNetworking 暴露给程序员自定义处理入口
if (self.authenticationChallengeHandler) {
id result = self.authenticationChallengeHandler(....);
... (解析处理结果)
}
...(证书认证处理代码)

//最后调用 completionHandler 继续执行操作
    if (completionHandler) {

        completionHandler(disposition, credential);

    }
}

disposition: 可以设置继续鉴权挑战(NSURLSessionAuthChallengeUseCredential) 或者中断鉴权挑战(NSURLSessionAuthChallengeCancelAuthenticationChallenge

credential: 如果证书认证通过则直接进行赋值,

credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];

否则为 nil

这里只是简单的梳理一下证书信任逻辑,就不再赘述 AFNetworking 源码部分。代码拙劣,大神勿笑。


作者:头疼脑胀的代码搬运工
链接:https://juejin.cn/post/7030345610704191501
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

京东七鲜一面总结

iOS
京东七鲜一面总结1. http 链接到断开的过程?第一步:TCP建立连接:三次握手HTTP 是应用层协议,他的工作还需要数据层协议的支持,最常与它搭配的就是 TCP 协议(应用层、数据层是 OSI 七层模型中的,以后有机会会说到的)。TCP...
继续阅读 »

京东七鲜一面总结

1. http 链接到断开的过程?

第一步:TCP建立连接:三次握手

HTTP 是应用层协议,他的工作还需要数据层协议的支持,最常与它搭配的就是 TCP 协议(应用层、数据层是 OSI 七层模型中的,以后有机会会说到的)。TCP 协议称为数据传输协议,是可靠传输,面向连接的,并且面向字节流的。

面向连接:通信之前先建立连接,确保双方在线。 可靠传输:在网络正常的情况下,数据不会丢失。 面向字节流:传输灵活,但是 TCP 的传输存在粘包问题,没有明显的数据约定。

在正式发送请求之前,需要先建立 TCP 连接。建立 TCP 连接的过程简单地来说就是客户端和服务端之间发送三次消息来确保连接的建立,这个过程称为三次握手

第二步:浏览器发送请求命令

TCP 连接建立完成后,客户端就可以向服务端发送请求报文来请求了

请求报文分为请求行、请求头、空行、请求体,服务端通过请求行和请求头中的内容获取客户端的信息,通过请求体中的数据获取客户端的传递过来的数据。

第三步:应答响应

在接收到客户端发来的请求报文并确认完毕之后。服务端会向客户端发送响应报文

响应报文是有状态行、响应头、空行和响应体组成,服务端通过状态行和响应头告诉客户端请求的状态和如何对数据处理等信息,真正的数据则在响应体中传输给客户端。

第四步:断开 TCP 连接

当请求完成后,还需要断开 tcp 连接,断开的过程

断开的过程简单地说就算客户端和服务端之间发送四次信息来确保连接的断开,所以称为四次挥手。

延伸:

一、单向请求 HTTP 请求是单向的,是只能由客户端发起请求,由服务端响应的请求-响应模式。(如果你需要双向请求,可以用 socket)

二、基于 TCP 协议 HTTP 是应用层协议,所以其数据传输部分是基于 TCP 协议实现的。

三、无状态 HTTP 请求是无状态的,即没有记忆功能,不能获取之前请求或响应的内容。起初这种简单的模式,能够加快处理速度,保证协议的稳定,但是随着应用的发展,这种无状态的模式会使我们的业务实现变得麻烦,比如说需要保存用户的登录状态,就得专门使用数据库来实现。于是乎,为了实现状态的保持,引入了 Cookie 技术来管理状态。

四、无连接 HTTP 协议不能保存连接状态,每次连接只处理一个请求,用完即断,从而达到节约传输时间、提高并发性。在 TCP 连接断开之后,客户端和服务端就像陌生人一样,下次再发送请求,就得重新建立连接了。有时候,当我们需要发送一段频繁的请求时,这种无连接的状态反而会耗费更多的请求时间(因为建立和断开连接本身也需要时间),于是乎,HTTP1.1 中提出了持久连接的概念,可以在请求头中设置 Connection: keep-alive 来实现。

2. 深拷贝、浅拷贝

深拷贝、浅拷贝实例说明?

深拷贝:是对对象本身的拷贝; 浅拷贝:是对指针的拷贝;

在 oc 中父类的指针可以指向子类的对象,这是多态的一个特性 声明一个 NSString 对象,让它指向一个 NSMutableString 对象,这一点是完全可以的,因为 NSMutableString 的父类就是 NSString。NSMutableString 是一个可以改变的对象,如果我们用 strong 修饰,NSString 对象强引用了 NSMutableString 对象。假如我们在其他的地方修改了这个 NSMutableString 对象,那么 NSString 的值会随之改变。

关于copy修饰相关

1、对 NSString 进行 copy -> 这是一个浅拷贝,但是因为是不可变对象,后期值也不会改变;

2、对 NSString 进行 mutableCopy -> 这是一个深拷贝,但是拷贝出来的是一个可变的对象 NSMutableString;

3、对 NSMutableString 进行 copy -> 这是一个深拷贝,拷贝出来一个不可变的对象;

4、对 NSmutableString 进行 mutableCopy -> 这是一个深拷贝,拷贝出来一个可变的对象;

总结:

对对象进行 mutableCopy,不管对象是可变的还是不可变的都是深拷贝,并且拷贝出来的对象都是可变的;

对对象进行 copy,copy 出来的都是不可变的。

对于系统的非容器类对象,我们可以认为,如果对一不可变对象复制,copy 是指针复制(浅拷贝)和 mutableCopy 就是对象复制(深拷贝)。如果是对可变对象复制,都是深拷贝,但是 copy 返回的对象是不可变的。

指 NSArrayNSDictionary 等。对于容器类本身,上面讨论的结论也是适用的,需要探讨的是复制后容器内对象的变化。

//copy返回不可变对象,mutablecopy返回可变对象
NSArray *array1 = [NSArray arrayWithObjects:@"a",@"b",@"c",nil];
NSArray *arrayCopy1 = [array1 copy];
//arrayCopy1是和array同一个NSArray对象(指向相同的对象),包括array里面的元素也是指向相同的指针
NSLog(@"array1 retain count: %d",[array1 retainCount]);
NSLog(@"array1 retain count: %d",[arrayCopy1 retainCount]);
NSMutableArray *mArrayCopy1 = [array1 mutableCopy];

mArrayCopy1 是 array1 的可变副本,指向的对象和 array1 不同,但是其中的元素和 array1 中的元素指向的是同一个对象。

mArrayCopy1 还可以修改自己的对象 [mArrayCopy1 addObject:@"de"];

[mArrayCopy1 removeObjectAtIndex:0]; array1 和 arrayCopy1 是指针复制,而 mArrayCopy1 是对象复制,mArrayCopy1 还可以改变期内的元素:删除或添加。但是注意的是,容器内的元素内容都是指针复制。

NSArray *mArray1 = [NSArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];
NSArray *mArrayCopy2 = [mArray1 copy];
NSMutableArray *mArrayMCopy1 = [mArray1 mutableCopy];
NSMutableString *testString = [mArray1 objectAtIndex:0];
[testString appendString:@" tail"];
NSLog(@"%@-%@-%@",mArray1,mArrayMCopy1,mArrayCopy2);
结果:mArray1,mArrayMCopy1,mArrayCopy2三个数组的首元素都发生了变化!

补充:来自开发者留言

面试时我有时会问说说 copy 和 mutableCopy,候选人几乎 100% 说你是说深拷贝和浅拷贝啊 ....,我会说不是!

> 下面用 NSArrayNSMutableArray 举例,因为 NSStringNSMutableString 并不会再引用其它对象,因此不足以说明问题。

1、NSArray 等类型的 copy 实际并没有 copy,或者最多只能说 copy 了引用,因为 copy 方法只返回了 self,这是对内存的优化;

2、而 NSMutableArray 的 copy 确实 copy 了,得到的是新的 NSArray 对象,但并不是所谓的深拷贝,因为它只浅浅地 copy 了一个 NSArray,其中的内容仍然是 NSMutableArray 的内容,可以用 == 直接判等;

3、NSArray 和 NSMutableArray 的 mutableCopy 与 2 相似,只是结果是个 NSMutableArray

4、以上说法一般只适用于 Foundation 提供的一些类型,很多时候并不适用于自己写的类 —— 思考一下你自己写的类是怎么实现 NSCopying 协议的?有实现 NSMutableCopying 协议吗?

所以 ObjC 并没有所谓的深拷贝,要想实现真正的深拷贝,基本上只能依赖序列化+反序列化,这样得到的结果才是深到见底的深拷贝。

如果你说道理大家都懂,深拷贝、浅拷贝只是一种叫法而已,那我只能说你太不严谨了,官方文档从来没这么教过;而且这种说法也不利于初学者理解,以及再学习其它语言时触类旁通,比如 Java。

所以建议严谨一点可以叫引用拷贝和浅拷贝,深拷贝很少用到;或者非要两个互为反义词,可以叫真拷贝和假拷贝。

3. load 和 initialize 区别

load 方法和 initialize 方法区别,以及在子类、父类、分类中调用顺序?

+(void)load

1、+load 方法加载顺序:父类> 子类> 分类 (load 方法都会加载)注意:(如果分类中有 AB,顺序要看 AB 加入工程中顺序) ,可能结果:( 父类> 子类> 分类A> 分类B ) 或者( 父类> 子类> 分类B> 分类A )

2、+load 方法不会被覆盖(比如有父类,子类,分类A,分类B,这四个 load 方法都会加载)。

3、+load 方法调用在 main函数前

+(void)initialize

1、分类 (子类没有 initialize 方法,父类存在或者没有 1initialize 方法)

2、分类> 子类 (多个分类就看编译顺序,只有存在一个)

3、父类> 子类 (分类没有 initialize 方法)

4、父类 (子类,分类都没有 initialize 方法)

总结 +initialize:

1、当调用子类的 + initialize 方法时候,先调用父类的,如果父类有分类, 那么分类的 + initialize 会覆盖掉父类的

2、分类的 + initialize 会覆盖掉父类的

3、子类的 + initialize 不会覆盖分类的

4、父类的 + initialize 不一定会调用, 因为有可能父类的分类重写了它

5、发生在main函数后。

4. 同名方法调用顺序

同名方法在子类、父类、分类的调用顺序?

load,initialize方法调用源码分析

注意+load 方法是根据方法地址直接调用,并不是经过 objc_msgSend 函数调用(通过 isa 和 superclass 找方法),所以不会存在方法覆盖的问题。

5. 事件响应链

事件响应链(同一个控制器有三个view,如何判断是否拥有相同的父视图)

iOS 系统检测到手指触摸( Touch )操作时会将其打包成一个 UIEvent 对象,并放入当前活动 Application 的事件队列,单例的 UIApplication 会从事件队列中取出触摸事件并传递给单例的 UIWindow 来处理,UIWindow 对象首先会使用 hitTest:withEvent: 方法寻找此次 Touch 操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为 hit-test view

UIAppliction --> UIWiondw -->递归找到最适合处理事件的控件-->控件调用 touches 方法-->判断是否实现 touches 方法-->没有实现默认会将事件传递给上一个响应者-->找到上一个响应者。

UIResponder 是所有响应对象的基类,在 UIResponder 类中定义了处理上述各种事件的接口。我们熟悉的 UIApplication、 UIViewController、 UIWindow 和所有继承自 UIView 的 UIKit 类都直接或间接的继承自 UIResponder,所以它们的实例都是可以构成响应者链的响应者对象。

//如何获取父视图
UIResponder *nextResponder = gView.nextResponder;
NSMutableString *p = [NSMutableString stringWithString:@"--"];
while (nextResponder) {
NSLog(@"%@%@", p, NSStringFromClass([nextResponder class]));
[p appendString:@"--"];
nextResponder = nextResponder.nextResponder;
}

如果有父视图则 nextResponder 指向父视图如果是控制器根视图则指向控制器;

控制器如果在导航控制器中则指向导航控制器的相关显示视图最后指向导航控制器;

如果是根控制器则指向 UIWindow

UIWindow 的 nexResponder 指向 UIApplication 最后指向 AppDelegate

6.TCP丢包

TCP 会不会丢包?该怎么处理?网络断开会断开链接还是一直等待,如果一直网络断开呢?

TCP 在不可靠的网络上实现可靠的传输,必然会有丢包。TCP 是一个“”协议,一个详细的包将会被 TCP 拆分为好几个包上传,也是将会把小的封裝成大的上传,这就是说 TCP 粘包和拆包难题。

TCP丢包总结

7.自动释放池

自动释放池创建和释放的时机,在子线程是什么时候创建释放的?

默认主线程的运行循环(runloop)是开启的,子线程的运行循环(runloop)默认是不开启的,也就意味着子线程中不会创建 autoreleasepool,所以需要我们自己在子线程中创建一个自动释放池。(子线程里面使用的类方法都是 autorelease,就会没有池子可释放,也就意味着后面没有办法进行释放,造成内存泄漏。)

在主线程中如果产生事件那么 runloop 才回去创建 autoreleasepool,通过这个道理我们就知道为什么子线程中不会创建自动释放池了,因为子线程的 runloop 默认是关闭的,所以他不会自动创建 autoreleasepool,需要我们手动添加。

如果你生成一个子线程的时候,要在线程开始执行的时候,尽快创建一个自动释放池,否则会内存泄露。因为子线程无法访问主线程的自动释放池。

8.计算机编译流程

源文件: 载入.h.m.cpp 等文件

预处理: 替换宏,删除注释,展开头文件,产生 .i 文件

编译: 将 .i 文件转换为汇编语言,产生 .s 文件

汇编: 将汇编文件转换为机器码文件,产生 .o 文件

链接: 对 .o 文件中引用其他库的地方进行引用,生成最后的可执行文件

dyld加载流程

收起阅读 »

Swift 中的函数盘点

iOS
Swift中的函数盘点「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」前言Swift已经被越来越多的公司使用起来,因此Swift的学习也应该提上日程了。本篇就先探索Swift中的函数,主要包括以下几个方面:Swift函数定义Swif...
继续阅读 »

Swift中的函数盘点

前言

Swift已经被越来越多的公司使用起来,因此Swift的学习也应该提上日程了。本篇就先探索Swift中的函数,主要包括以下几个方面:

  • Swift函数定义
  • Swift函数参数与返回值
  • Swift函数重载
  • 内敛函数优化
  • 函数类型、嵌套函数

一、Swift函数定义

函数的定义包含函数名、函数体、参数及返回值,定义了函数会做什么、接收什么以及返回什么。函数名前要加上 func 关键字修饰。如下为一个完整的函数定义事例:

func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}

  • 函数名: greet
  • 参数:圆括号中(person: String)即为参数,person为参数名,String为类型
  • 返回值:使用一个 -> 来明确函数的返回值,在该事例中定义了一个 String类型的返回值

二、函数返回值与参数

2.1 函数返回值

从返回值的角度看,函数可以分为有返回值无返回值两种。无返回值的函数可以有如下三种定义方式:

func testA() -> Void {
}

func testB() -> () {
}

func testC() {
}

let a = testA()
let b = testB()
let c = testC()


打印 a、b、c 可以发现,三者的类型均为(),即空元组。在 Void 的定义处也可以发现,Swift中 Void 就是空元组。Xnip2021-11-10_18-27-32.png也就是说上面三种方式是等价的,都表示无返回值的情况,不过从代码简洁程度上来说,最后一种更方便使用。

还有一种函数有返回值的情况,如同第一节中所述的函数定义方式,即为一种返回值为String的函数。在Swift中,函数的返回值可以隐式返回,如果函数体中只有一句返回代码,则可以省略return关键字。如下代码所示,两种写法是等价的:

func testD() -> String {
    return "正常返回"
}
func testE() -> String {
    "隐式返回"
}

Swift中还可以通过元组实现多个返回值的情况,如下所示:

func compute(a:Int, b: Int) -> (sum: Int, difference: Int) {
    return (a+b, a-b);
}

compute函数返回一个元组,包含了求和与求差,实现了返回多个值的情况。

2.2 函数参数

与OC不同的是,Swift中函数的参数是let修饰的,参数值是不支持修改的。如下图所示,可以证明。Xnip2021-11-10_22-30-01.png

2.2.1 函数标签

Swift的函数参数除了形参外,还包含一个参数标签。形参在函数内部使用,使得函数体中使用没有歧义,而函数标签用于在函数调用时使用,其目的是增加可读性。函数标签是可以省略的,使用_表示即可,需要注意的是,_与不设置函数标签是不一样的,如下图所示:Xnip2021-11-10_23-04-29.pngXnip2021-11-10_23-05-00.png当使用_时,调用函数不会显示函数标签,而不设置函数标签会把形参作为函数标签。

2.2.2 函数默认参数值

Swift可以给函数参数设置默认值,设置了默认值的参数,在函数调用时可以不传参Xnip2021-11-10_23-30-40.png由于参数 a 有了默认值 8,所以在调用时只传参 b 就可以。同样的,如果参数均有默认值,则在调用函数时,都可以不传值。Xnip2021-11-10_23-35-15.png如图所示,由于两个参数均有默认值,在调用时都不传值,就像调用了一个无参函数一样。

Swift中设置函数参数默认值可以不按照顺序,因为Swift中有函数标签,不会造成歧义。而在C++中,则必须要按照从右往左的顺序依次设置,两者对比如下:Xnip2021-11-10_23-46-29.pngXnip2021-11-10_23-44-45.png下面一张图是C++的调用,没有按照顺序设置默认值,直接报错缺失b的默认值,而Swift中则不会。但是,如果Swift函数参数都隐藏了函数标签,则无法识别是给哪个参数,只能按照从右往左的方向赋值,这样就会照成报错,如下图所示:Xnip2021-11-10_23-55-41.png在调用函数时,直接报错缺失第二个参数。因此,在Swift中,如果省略了函数参数标签,要保证所有的函数参数都有值,或者都可以得到赋值。

2.2.3 可变参数

与OC的NSLog参数一样,Swift函数也提供了可变参数,其定义方式是 参数名:类型...,可以参照系统的print函数定义:Xnip2021-11-11_09-39-17.pngprint函数的第一个参数即为可变参数,参数类型为Any,可以接受任意类型,输入时以,分割即可。

可变参数需要注意的一点是,在紧随其后的一个参数不能省略参数标签,如下图所示:Xnip2021-11-11_09-47-15.png

参数b也是一个Any类型,如果省略了参数标签,则在调用函数时就没有了标签区分,仅凭,编译器无法确定该将参数赋值给item还是b,因此会报错。

可变参数本质上是一个数组,可以在函数内部使用参数,查看其类型如下:Xnip2021-11-11_09-35-56.png可以看到 item 实际上是一个 Any 类型的数组。

2.2.4 inout修饰的参数

在OC和C中,我们可以通过指针传参,以达到在函数内部修改函数外部实参的值的目的。在Swift中,也提供了类似的方法,不过需要使用inout修饰一下参数,具体使用方式如下:

Xnip2021-11-11_11-19-31.png

number的值本来为10,经过inoutFunc函数调用,结果变为了20。那么 inout 是如何改变了外部实参的值的呢?有种说法是与OC一样,采用了指针传值的方式改变;还有说法是 inout 在底层是一个函数,将其修饰的函数内部的值通过这个函数重新赋值外部实参。针对这两种说法,我们可以通过汇编来验证下,本次使用的是真机调试,因此使用的是ARM下的汇编。

将上图中12行22行的断点打开,并打开XCode的汇编调试 Debug -> Debug Workflow -> Always show Disassembly。运行工程,首先进入22行的断点:Xnip2021-11-11_11-29-56.png图中红框处为 inoutFunc 函数的调用处,在上面28行可以发现一行代码 ldr x0, [sp, #0x10],这句代码的意思是,将[sp, #0x10]的值赋值给 x0 寄存器,[sp, #0x10]表示 sp+#0x10的地址,也就是说 x0 寄存器现在存储的是一个地址,通过 register read x0 命令可知改地址为 x0 = 0x000000016dbf9a80

单步调试进入 inoutFunc 函数,得到如下代码:

Xnip2021-11-11_11-36-36.png

执行到第4行,再次读取 x0 寄存器得到了相同的值x0 = 0x000000016dbf9a80,此时通过 x/4gx 读取内存地址0x000000016dbf9a80的值,得到结果如下:

Xnip2021-11-11_11-39-01.png

红框中的值 0x000000000000000a 换算成十进制正是 10。走到第6行汇编代码,将x0存储的地址所指向的内容存到x8寄存器,然后将值加10,就此完成对外部实参值的改变。在viewDidLoad中调用inoutFunc后并没有对于number的重新赋值,也证实了inout是通过地址传递改变外部实参的值。

使用inout需要注意两点:

  • 1、inout只能传入可以被多次赋值的,即不能传入常量和字面量
  • 2、inout不能修饰可变参数

三、函数重载

函数重载指的是函数名相同,但是参数名称不同 || 参数类型不同 || 参数个数不同 || 参数标签不同。需要注意的是,函数重载(overload)与函数重写(override)是两个概念,函数重写涉及到继承关系,而函数重载不涉及继承关系。另外,在OC中没有函数或方法的重载,只有重写。以下是几个函数重载的例子:

Xnip2021-11-11_14-28-28.png

可以看到,四个函数的方法名称相同,但是参数不同,实际上并不会报错,这就是方法重载。

不过方法重载也有需要注意的地方:

  • 方法重载与函数返回值无关,即函数名及参数完全相同的情况下,如果返回值不同,不构成函数重载,编译器会报错。

Xnip2021-11-11_14-35-59.png

如图所示,在调用方法时,编译器不知道该调用哪个函数,因此会报二义性错误。

  • 方法重载与默认参数值的情况

Xnip2021-11-11_14-38-50.png

从图中可以发现,由于第二个函数给参数c设置了默认值,在调用时形式上与第一个函数一样,不过编译器在此并不会报错,猜想是因为第二个函数还有一种test(a: , b: , c: )的调用形式。

四、inline内联函数

内联函数,其实是指开启了编译器内联优化后,编译器会将某些函数优化处理,该优化会将函数体抽离出来直接调用,而不会给这个函数再开辟栈空间。

func test() {
    print("test123")
}
test()
复制代码

如以上函数所示,调用test()时,需要为其开辟栈空间,而其内部只调用了一个print函数,所以在开启内联优化的情况下,可能会直接调用print函数。

开启内联优化的方式如下图:Xnip2021-11-11_15-15-38.pngDebug模式下默认不开启优化,Release模式下默认是开启的。为了测试内联优化的现象,这里先将Debug模式开启优化,之后在test()调用处打断点,再运行工程会发现,直接打印了test123,然后在test函数内部打断点,进入汇编如下:Xnip2021-11-11_14-58-28.png全局搜索发现没有test函数的调用,而是直接调用了print函数。

不过内联优化,也不是对所有函数都会进行优化,以下几点不会优化:

  • 函数体代码比较多
  • 函数存在递归调用
  • 函数包含动态派发,例如类与子类的多态调用

内联函数还有内联参数控制@inline(never) 和 @inline(__always)

  • 使用@inline(never)修饰,即使开启了编译器优化,也不会内联
  • 使用@inline(__always)修饰,开启编译器优化后,即使函数体代码很长也会内联,但是递归和动态派发依然不会优化

五、函数类型

每一个函数都可以符合一种函数类型,例如:

func test() {
    print("test123")
}

对应 () -> ()

func compute(a:Int = 8, b: Int = 9) -> Int {
    return a+b;
}

对应 (Int, Int) -> Int

复制代码

上述代码中,() -> () 和 (Int, Int) -> Int都表示一种函数类型。可以发现函数类型是不需要参数名的,直接标明参数类型即可。

函数类型也可以用作函数的参数和返回值,使用函数类型作为返回值的函数被称为高阶函数,例如:

// 函数类型作为参数
func testFunc(action:(Int) -> Int) {
    var result = action(2)
    print(result)
}

func action(a:Int) -> Int {
    return a
}
testFunc(action: action(a:))

// 函数类型作为返回值
func action(a:Int) -> Int {
    return a
}
func testFunc() -> (Int) -> Int {
    return action(a:)
}
let fu = testFunc()
print(fu(3))

复制代码

六、嵌套函数

Swift中,可以在函数内部定义函数,被称为嵌套函数,如下代码所示:

func forward(_ forward: Bool) -> (Int) -> Int {
    func next(_ input: Int) -> Int {
        input + 1
    }
    func previous(_ input: Int) -> Int {
        input - 1
    }

    return forward ? next : previous
}
复制代码

像上面这样在函数内部定义其他的函数,其目的是为了将函数内部的实现封装起来,外部只看到调用了 forward,而不需要知道其内部的实现逻辑,当然也不能直接调用内部的嵌套函数。

总结

相对于OC,Swift中主要增加了以下几点:

  • 参数标签
  • 函数重载
  • 嵌套函数

整体而言,个人感觉Swift的函数使用起来更加方便,参数标签使得代码可读性更强。以上即为本篇关于Swift函数的总结,如有不足之处,欢迎大家指正。

收起阅读 »

iOS 自定义通知声音

iOS
iOS 自定义通知声音场景在消息推送里面播放自定义生成的声音解决方案生成自定义声音文件后,必须要写入到【/Library/Sounds/】才能进行播放///往声音目录/Library/Sounds/写入音频文件 - (void)writeMusicDataWi...
继续阅读 »

iOS 自定义通知声音

场景

在消息推送里面播放自定义生成的声音

解决方案

  1. 生成自定义声音文件后,必须要写入到【/Library/Sounds/】才能进行播放
///往声音目录/Library/Sounds/写入音频文件
- (void)writeMusicDataWithUrl:(NSString*)filePath
callback:(void(^)(BOOL success,NSString * fileName))blockCallback{
NSString *bundlePath = filePath;
NSString *libPath = [NSHomeDirectory() stringByAppendingString:@"/Library/Sounds/"];

NSFileManager *manager = [NSFileManager defaultManager];
if (![manager fileExistsAtPath:libPath]) {
NSError *error;
[manager createDirectoryAtPath:libPath withIntermediateDirectories:YES attributes:nil error:&error];
}

NSData *data = [NSData dataWithContentsOfFile:bundlePath];

BOOL flag = [data writeToFile:[libPath stringByAppendingString:[filePath lastPathComponent]] atomically:YES];
if (flag) {
NSLog(@"文件写成功");
if (blockCallback) {
blockCallback(YES,[filePath lastPathComponent]);
}
}else{
NSLog(@"文件写失败");
if (blockCallback) {
blockCallback(NO,nil);
}
}
}

  1. 在【UNMutableNotificationContent】的【sound】参数中写入文件名
///!!!!:推送语音播报
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];

UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; //标题
content.sound = [UNNotificationSound soundNamed:fileName];

content.body = @"语音播报";// 本地推送一定要有内容,即body不能为空。

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
if (@available(iOS 15.0, *)) {
content.interruptionLevel = UNNotificationInterruptionLevelTimeSensitive;//会使手机亮屏且会播放声音;可能会在免打扰模式(焦点模式)下展示
// @"{\"aps\":{\"interruption-level\":\"time-sensitive\"}}";
// @"{\"aps\":{\"interruption-level\":\"active\"}}";
content.body = @"语音播报";// 本地推送一定要有内容,即body不能为空。
}
#endif
// repeats,是否重复,如果重复的话时间必须大于60s,要不会报错
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.01 repeats:NO];
/* */
//添加通知的标识符,可以用于移除,更新等搡作
NSString * identifier = [[NSUUID UUID] UUIDString];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
[center addNotificationRequest:request withCompletionHandler:^(NSError *_Nullable error) {
completed();
}];

参考: http://www.jianshu.com/p/a6eba8cfb… blog.csdn.net/LANGZI77585…

https://juejin.cn/post/7029245981149364255

收起阅读 »

iOS内购详解

iOS
iOS内购详解概述iOS内购是指苹果 App Store 的应用内购买,即In-App Purchase,简称IAP(以下本文关于内购都简称为IAP),是苹果为 App 内购买虚拟商品或服务提供的一套交易系统。为什么我们需要掌握IAP这套流程呢,因为App S...
继续阅读 »

iOS内购详解

概述

iOS内购是指苹果 App Store 的应用内购买,即In-App Purchase,简称IAP(以下本文关于内购都简称为IAP),是苹果为 App 内购买虚拟商品或服务提供的一套交易系统。为什么我们需要掌握IAP这套流程呢,因为App Store审核指南规定:

如果您想要在 app 内解锁特性或功能 (解锁方式有:订阅、游戏内货币、游戏关卡、优质内容的访问
限或解锁完整版等),则必须使用 App 内购买项目。App 不得使用自身机制来解锁内容或功能,
如许可证密钥、增强现实标记、二维码等。App 及其元数据不得包含按钮、外部链接或其他行动号
召用语,以指引用户使用非 App 内购买项目机制进行购买。
复制代码

这段话的大概意思就是APP内的虚拟商品或服务,必须使用 IAP 进行购买支付,不允许使用支付宝、微信支付等其它第三方支付方式(包括Apple Pay),也不允许以任何方式(包括跳出App、提示文案等)引导用户通过应用外部渠道购买。如果违反此规定,apple审核人员不会让你的APP上架!!!

内购前准备

APP内集成IAP代码之前需要先去开发账号的ITunes Connect进行以下三步操作:

1,后台填写银行账户信息

2,配置商品信息,包括产品ID,产品价格等

3,配置用于测试IAP支付功能的沙箱账户。

填写银行账户信息一般交由产品管理人员负责,开发者不需要关注,开发者需要关注的是第二步和第三步。

配置内购商品

IAP 是一套商品交易系统,而非简单的支付系统,每一个购买项目都需要在开发者后台的Itunes Connect后台为 App 创建一个对应的商品,提交给苹果审核通过后,购买项目才会生效。内购商品有四种类型:

  • 消耗型项目:只可使用一次的产品,使用之后即失效,必须再次购买,如:游戏币、一次性虚拟道具等
  • 非消耗型项目:只需购买一次,不会过期或随着使用而减少的产品。如:电子书
  • 自动续期订阅:允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期,如:Apple Music这类按月订阅的商品(有些鸡贼的开发者以此收割对IAP商品不熟悉的用户,参考App Store“流氓”软件)
  • 非续期订阅:允许用户购买有时限性服务的产品,此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期

配置商品信息需要注意产品ID和产品价格

1,产品 ID 具有唯一性,建议使用项目的 Bundle Identidier 作为前缀后面拼接自定义的唯一的商品名或者 ID(字母、数字),这里有个坑:一旦新建一个内购商品,它的产品ID将永远被占用,即使该商品已经被删除,已创建的内购商品除了产品 ID 之外的所有信息都可以修改,如果删除了一个内购商品,将无法再创建一个相同产品 ID 的商品,也意味着该产品 ID 永久失效!!!

2,在创建IAP项目的时候,需要设定价格,产品价格只能从苹果提供的价格等级去选择,这个价格等级是固定的,同一价格等级会对应各个国家的货币,比如等级1对应1美元、6元人民币,等级2对应2美元、12元人民币……最高等级87对应999.99美元、6498元人民币。另外可能是为了照顾某些货币区的开发者和用户,还有一些特殊的等级,比如备用等级A对应1美元、1元人民币,备用等级B对应1美元、3元人民币这样。除此之外,IAP项目不能定一个9.9元人民币这样不符合任何等级的价格。详细价格等级表可以看苹果的官方价格等级文档

苹果的价格等级表通常是不会调整的,但也不排除在某些货币汇率发生巨大变化的情况下,对该货币的定价进行调整,调整前苹果会发邮件通知开发者。

3,商品分成

App Store上的付费App和App内购,苹果与开发者默认是3/7分成。但实际上,在某些地区苹果与开发者分成之前需要先扣除交易税,开发者的实际分成不一定是70%。从2015年10月开始,苹果对中国地区的App Store购买扣除了2%的交易税,对于中国区帐号购买的IAP,开发者的实际分成在68%~69%之间。而且中国以外不同地区的交易税标准也存在差异,如苹果的官方价格等级文档

,如果需要严格计算实际收入,可能需要把这个部分也考虑进来。

针对不同地区的内购,内购价格和对应的开发者实际收入在苹果的价格等级表中有详细列举。

另外,根据苹果在2016年6月的新规则,针对Auto-Renewable Subscription类型的IAP,如果用户购买的订阅时间超过1年,那么从第二年开始,开发者可以获得85%的分成。详情可查看苹果的订阅产品价格说明

沙箱账户

新的内购产品上线之前,测试人员一般需要对内购产品进行测试,但是内购涉及到钱,所以苹果为内购测试提供了 沙箱测试账号 的功能,Apple Pay 推出之后 沙箱测试账号`也可以用于 Apple Pay 支付的测试,沙箱测试账号 简单理解就是:只能用于内购和 Apple Pay 测试功能的 Apple ID,它并不是真实的 Apple ID。

填写沙箱测试账号信息需要注意以下几点:

  • 电子邮件不能是别人已经注册过 AppleID 的邮箱
  • 电子邮箱可以不是真实的邮箱,但是必须符合邮箱格式
  • App Store 地区的选择,测试的时候弹出的提示框以及结算的价格会按照沙箱账号选择的地区来,建议测试的时候新建几个不同地区的账号进行测试!!!

沙箱账号测试的使用:

  • 首先沙箱测试账号必须在真机环境下进行测试,并且是 adhoc 证书或者 develop 证书签名的安装包,沙盒账号不支持直接从 App Store 下载的安装包
  • 去真机的 App Store 退出真实的 Apple ID 账号,退出之后并不需要在App Store 里面登录沙箱测试账号
  • 然后去 App 里面测试购买商品,会弹出登录框,选择 使用现有的 Apple ID,然后登录沙箱测试账号,登录成功之后会弹出购买提示框,点击 购买,然后会弹出提示框完成购买。

内购流程

IAP的支付流程分为客户端和服务端,客户端的工作如下:

  • 获取内购产品列表(从App内读取或从自己服务器读取),向用户展示内购列表
  • 用户选择某个内购产品后,先请求可用的内购产品的本地化信息列表,此次调用Apple的StoreKit库的代码
  • 得到内购产品的本地化信息后,根据用户选择的内购产品的ID得到内购产品
  • 根据内购产品发起IAP购买请求,收到购买完成的回调
  • 购买流程结束后, 向服务器发起验证凭证以及支付结果的请求
  • 自己的服务器将支付结果信息返回给前端并发放虚拟产品

前端支付流程图如下:

------------------------------ IAPManager.h -----------------------------
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef enum {
IAPPurchSuccess = 0, // 购买成功
IAPPurchFailed = 1, // 购买失败
IAPPurchCancel = 2, // 取消购买
IAPPurchVerFailed = 3, // 订单校验失败
IAPPurchVerSuccess = 4, // 订单校验成功
IAPPurchNotArrow = 5, // 不允许内购
}IAPPurchType;

typedef void (^IAPCompletionHandle)(IAPPurchType type,NSData *data);

@interface IAPManager : NSObject
+ (instancetype)shareIAPManager;
- (void)startPurchaseWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle;
@end

NS_ASSUME_NONNULL_END



------------------------------ IAPManager.m -----------------------------

#import "IAPManager.h"
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

@interface IAPManager()<SKPaymentTransactionObserver,SKProductsRequestDelegate>{
NSString *_currentPurchasedID;
IAPCompletionHandle _iAPCompletionHandle;
}
@end

@implementation IAPManager

+ (instancetype)shareIAPManager{

static IAPManager *iAPManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
iAPManager = [[IAPManager alloc] init];
});
return iAPManager;
}
- (instancetype)init{
self = [super init];
if (self) {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}

- (void)dealloc{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}


- (void)startPurchaseWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle{
if (purchID) {
if ([SKPaymentQueue canMakePayments]) {
_currentPurchasedID = purchID;
_iAPCompletionHandle = handle;

//从App Store中检索关于指定产品列表的本地化信息
NSSet *nsset = [NSSet setWithArray:@[purchID]];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];
}else{
[self handleActionWithType:IAPPurchNotArrow data:nil];
}
}
}

- (void)handleActionWithType:(IAPPurchType)type data:(NSData *)data{
#if DEBUG
switch (type) {
case IAPPurchSuccess:
NSLog(@"购买成功");
break;
case IAPPurchFailed:
NSLog(@"购买失败");
break;
case IAPPurchCancel:
NSLog(@"用户取消购买");
break;
case IAPPurchVerFailed:
NSLog(@"订单校验失败");
break;
case IAPPurchVerSuccess:
NSLog(@"订单校验成功");
break;
case IAPPurchNotArrow:
NSLog(@"不允许程序内付费");
break;
default:
break;
}
#endif
if(_iAPCompletionHandle){
_iAPCompletionHandle(type,data);
}
}

- (void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction{
//交易验证
NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:recepitURL];

if(!receipt){
// 交易凭证为空验证失败
[self handleActionWithType:IAPPurchVerFailed data:nil];
return;
}
// 购买成功将交易凭证发送给服务端进行再次校验
[self handleActionWithType:IAPPurchSuccess data:receipt];

NSError *error;
NSDictionary *requestContents = @{
@"receipt-data": [receipt base64EncodedStringWithOptions:0]
};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];

if (!requestData) { // 交易凭证为空验证失败
[self handleActionWithType:IAPPurchVerFailed data:nil];
return;
}

NSString *serverString = @"https:xxxx";
NSURL *storeURL = [NSURL URLWithString:serverString];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];

[[NSURLSession sharedSession] dataTaskWithRequest:storeRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
// 无法连接服务器,购买校验失败
[self handleActionWithType:IAPPurchVerFailed data:nil];
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) {
// 服务器校验数据返回为空校验失败
[self handleActionWithType:IAPPurchVerFailed data:nil];
}

NSString *status = [NSString stringWithFormat:@"%@",jsonResponse[@"status"]];
if(status && [status isEqualToString:@"0"]){
[self handleActionWithType:IAPPurchVerSuccess data:nil];
} else {
[self handleActionWithType:IAPPurchVerFailed data:nil];
}
#if DEBUG
NSLog(@"----验证结果 %@",jsonResponse);
#endif
}
}];

// 验证成功与否都注销交易,否则会出现虚假凭证信息一直验证不通过,每次进程序都得输入苹果账号
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSArray *product = response.products;
if([product count] <= 0){
#if DEBUG
NSLog(@"--------------没有商品------------------");
#endif
return;
}

SKProduct *p = nil;
for(SKProduct *pro in product){
if([pro.productIdentifier isEqualToString:_currentPurchasedID]){
p = pro;
break;
}
}

#if DEBUG
NSLog(@"productID:%@", response.invalidProductIdentifiers);
NSLog(@"产品付费数量:%lu",(unsigned long)[product count]);
NSLog(@"产品描述:%@",[p description]);
NSLog(@"产品标题%@",[p localizedTitle]);
NSLog(@"产品本地化描述%@",[p localizedDescription]);
NSLog(@"产品价格:%@",[p price]);
NSLog(@"产品productIdentifier:%@",[p productIdentifier]);
#endif

SKPayment *payment = [SKPayment paymentWithProduct:p];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}

//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
#if DEBUG
NSLog(@"------------------从App Store中检索关于指定产品列表的本地化信息错误-----------------:%@", error);
#endif
}

- (void)requestDidFinish:(SKRequest *)request{
#if DEBUG
NSLog(@"------------requestDidFinish-----------------");
#endif
}

#pragma mark - SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
for (SKPaymentTransaction *tran in transactions) {
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
[self verifyPurchaseWithPaymentTransaction:tran];
break;
case SKPaymentTransactionStatePurchasing:
#if DEBUG
NSLog(@"商品添加进列表");
#endif
break;
case SKPaymentTransactionStateRestored:
#if DEBUG
NSLog(@"已经购买过商品");
#endif
// 消耗型不支持恢复购买
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:tran];
break;
default:
break;
}
}
}

// 交易失败
- (void)failedTransaction:(SKPaymentTransaction *)transaction{
if (transaction.error.code != SKErrorPaymentCancelled) {
[self handleActionWithType:IAPPurchFailed data:nil];
}else{
[self handleActionWithType:IAPPurchCancel data:nil];
}

[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
@end


/* 调用支付方法
- (void)purchaseWithProductID:(NSString *)productID{

[[IAPManager shareIAPManager] startPurchaseWithID:productID completeHandle:^(IAPPurchType type,NSData *data) {

}];
}
*/


服务端的工作:

  • 接收iOS端发过来的购买凭证,判断凭证是否已经存在或验证过,然后存储该凭证。将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。

恢复购买

内购有4种:消耗型项目,非消耗型,自动续期订阅,非续期订阅。 其中”非消耗型“和”自动续期订阅“需要提供恢复购买的功能,例如创建一个恢复按钮,不然审核很可能会被拒绝。

//调起苹果内购恢复接口
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];

“消耗型项目”和“非续期订阅”苹果不会提供恢复的接口,不要调用上述方法去恢复,否则有可能被拒!!!

“非续期订阅”也是跨设备同步的,所以原则上来说也需要提供恢复购买的功能,但需要依靠app自建的账户体系恢复,不能用上述苹果提供的接口。

内购掉单

掉单是用户付款买商品,钱扣了,商品却没到账。掉单一旦发生,用户通常会很生气地来找客服。然后客服只能找开发人员把商品给用户手动加上。显然,伤害用户的体验,特别是伤害付费用户的体验,是一件相当糟糕的事情。

掉单是如何产生的呢?这需要从IAP支付的技术流程说起。

IAP的支付流程:

1,发起支付

2,扣费成功

3,得到receipt(支付凭据)

4,去后台验证凭据获取商品交易状态

5,返回数据,验证成功前端刷新数据

  • 漏单情况一:

    2到3环节出问题属于苹果的问题,目前没做处理。

  • 漏单情况二:

3到4的时候出问题,比如断网。此时前端会把支付凭据持久化存储下来,如果期间用户卸载APP此单在前端就真漏了,如果没有协助,下次重新打开app进入购买页会先判断有无未成功的支付,有就提示用户,用户选择找回,重走4,5流程。这一步看产品需求怎么做,可以让用户自主选择是否恢复未成功的支付也可以前端默默恢复就行。

  • 漏单情况三:

4到5的时候出问题。此时后台其实已经成功,只是前端没获取到数据,当漏单处理,下次进入的时候先刷新数据即可。

内购注意事项

  • 交易凭据receipt判重

一般来说验证支付凭据(receipt)是否有效放后台去做,如果后台不做判重,同一个凭据就可以无数次验证通过,因为苹果也不判重,这就会导致前端可以凭此取到的一个支付凭据可以去后台无数次做校验!!!!,后台就会给前端发放无数次商品,但是用户只支付了一次钱,所以安全的做法是后台把验证通过的支付凭据做个记录,每次来新的凭据先判断是否已经使用过,防止多次发放商品。

参考

iOS 内购(In-App Purchase)总结

https://juejin.cn/post/7029252038252822564

收起阅读 »

iOS 教你如何像RN一样实时编译

工具类代码开源Github 一、效果 最终效果: 代码在保存之后,立马在模拟器上看到修改后的效果, 避免Command+R重新编译耗费时间的问题; 如果APP页面层级太深的话,传统调试要一步步点进到指定页面,使用该方案直接就能看到效果,所见即所得,👏👏👏 ...
继续阅读 »

工具类代码开源Github



一、效果


最终效果: 代码在保存之后,立马在模拟器上看到修改后的效果, 避免Command+R重新编译耗费时间的问题; 如果APP页面层级太深的话,传统调试要一步步点进到指定页面,使用该方案直接就能看到效果,所见即所得,👏👏👏



修改标题、修改背景色演示

二、背景


每次都被我们项目的编译速度整的快没脾气了,一直想着优化项目的编译速度。 想想之前做的RN项目的热部署效果真的很爽,不爽之余想到:他用个杂交品种能热部署,而我用苹果亲儿子没道理不行啊!能不能搞个runtime之类的跟新啊。
人有多大胆,地有多大产;不怕办不到,就怕想不到。终于找到了这个成吨减少工作量的方案。


超级简单,只有三步:
1、一个工具
2、选定项目目录
3、把一个文件放到项目中


无需其他任何配置,不对项目结构造成任何侵害。


三、一步步教你使用


1、工具下载 InjectionIII


InjectionIII 是我们需要用到个一个工具,不要因为要用一个工具而厌烦这个方案,它很简单。
它是免费的,app store 搜索:InjectionIII,Icon是 一个针筒。
也是开源的,


GitHub链接: github.com/johnno1962/…


App Store链接: https://itunes.apple.com/cn/app/injectioniii/id1380446739?mt=12


2、配置路径


打开InjectionIII工具,选择Open Project,选择你的代码所在的路径,然后点击Select Project Directory保存。


image.png


image.png


注意:InjectionIII 的File Watcher选项要保持选中状态。


3、导入配置文件


这步我简单写了一个配置文件,直接 GitHub下载 导入项目即可。
如果你比较反感下载文件也可以自己处理:
1.设置AppDelegate.m
打开你的源码,在AppDelegate.m的didFinishLaunchingWithOptions方法添加一行代码:


#if DEBUG
// iOS
[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection10.bundle"] load];
// tvOS
//[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/tvOSInjection.bundle"] load];
// macOS
//[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle"] load];
#endif


2.设置ViewController
在需要修改界面的ViewController添加方法- (void)injected,或者给ViewController类扩展添加方法- (void)injected。
所有修改控件的代码都写在这里面。


- (void)injected
{
//自定义修改...
//重新加载view
[self viewDidLoad];
}


4、启动项目,修改验证


在Xcode Command+R运行项目 ,看到Injection connected 提示即表示配置成功。
image.png


在需要修改的页面,修改控件UI,然后Command+S保存一下代码,立刻就在模拟器上显示修改的信息了。


工具使用中如有问题可以参考github上的过往经验,也欢迎留言我们一起讨论。
工具git地址:github.com/johnno1962/…


5、每个VC要使用的话,还需要去写injected,有点烦人,但是我们有方案


用runtime 给每个VC加个方法class_addMethod


依托InjectionIII的iOS热部署配置文件,无侵害,导入即用。


@implementation InjectionIIIHelper

#if DEBUG
/**
InjectionIII 热部署会调用的一个方法,
runtime给VC绑定上之后,每次部署完就重新viewDidLoad
*/

void injected (id self, SEL _cmd) {
//重新加载view
[self loadView];
[self viewDidLoad];
[self viewWillLayoutSubviews];
[self viewWillAppear:NO];
}

+ (void)load
{
//注册项目启动监听
__block id observer =
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
//更改bundlePath
[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection10.bundle"] load];
//[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle"] load];

[[NSNotificationCenter defaultCenter] removeObserver:observer];
}];

//给UIViewController 注册injected 方法
class_addMethod([UIViewController class], NSSelectorFromString(@"injected"), (IMP)injected, "v@:");

}
#endif
@end



iOS如何提高10倍以上编译速度


更多iOS提高开发效率插件Github


作者:Mr_Coder
链接:https://juejin.cn/post/6850037272415813645
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

使用RN笔记

一、学习说明 了解React和RN的基本语法; RN无法使用div、p、img都不能使用,只能使用RN固有的组件; 需要结合安卓的签名打包步骤,并使用RN提供的打包命令进行完整apk文件发布,最终发出来的就是Release版本的项目 webAPP开发方式: ...
继续阅读 »

一、学习说明



  1. 了解React和RN的基本语法;

  2. RN无法使用div、p、img都不能使用,只能使用RN固有的组件;

  3. 需要结合安卓的签名打包步骤,并使用RN提供的打包命令进行完整apk文件发布,最终发出来的就是Release版本的项目

  4. webAPP开发方式:



  • **H5+****:**需要做出一个完整的网站,然后在网站的基础上使用打包技术,其内部运行的还是网站,

  • **RN:**需要开发一个模板项目,这个模板不能运行到浏览器和手机中,完成后使用RN的打包命令后,把模板的代码翻译成原生的java代码,最终打包成原生手机app,只不过使用前端技术开发而已。


二、搭建开发环境



  1. http://www.react-native.cn/docs/enviro…(注:一定要仔细看文档的译注否则根本运行不了,根据文档的注释下载相应的包)

  2. 运行‘adb devices’的命令查看手机是否连接成功


三、遇到的问题


react-active-webview****直接使用会报**"RNCWebView" was not found in the UIManager.**



  • 解决办法:1.停止项目,cd ios目录运行npx pod install命令下载包

  • 包下完了运行npx react-active link react-native-webview 这时会提示连接ios 和android 成功

  • 重新编译项目 npx react-active run-android 后就可以正常使用了


在React Native开发的时候编译androidreact-native run-android莫名遇到以下的buildfailure:


:app:compileDebugAidl:app:compileDebugRenderscript:app:generateDebugBuildConfig:app:mergeDebugShaders UP-TO-DATE:app:compileDebugShaders UP-TO-DATE:app:generateDebugAssets UP-TO-DATE:app:mergeDebugAssets UP-TO-DATE:app:generateDebugResValues:app:generateDebugResources:app:mergeDebugResources:app:recordFilesBeforeBundleCommandDebug FAILED
复制代码

解决办法:cd android运行./gradlew --stop


react-native 其他请求都没有问题,但是文件上传会报错(‘Network request failed’)




  • 原因:Flipper Network构建initializeFlipper时出现的问题。




  • 解决:找到android/app/src/debug/java/com/**/ReactNativeFlipper.java文件注释43行 


    new NetworkingModule.CustomClientBuilder() {
    @Override
    public void apply(OkHttpClient.Builder builder) {
    // builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
    }
    });




打包时报错JVM内存不够



  • 打开gradle.properties文件 添加org.gradle.jvmargs=-Xmx4608M ,如果是真机测试可以注释。


打包时报错Execution failed for task ':xxxxx:verifyReleaseResources'



  • 是因为Android版本更新到了28,而第三方插件未及时更新,需要打开第三方包的android/build.gradle文件 将23修改成28


react-native-webView 交互




  • RN发送给HTML:


    RN页面首先绑定ref={webView => this.webView = webView} 通过this.webView.message.postMessage(data)来传递内容,html通过
    window.onload = function() {
    document.addEventListener('message', function(msg) {
    console.log(msg)
    });
    }来获取




  •  HTML发送给RN:


    RN页面首先绑定ref={webView => this.webView = webView} 通过webView自带的
    onMessage={(event)=>{
    const data = event.nativeEvent.data
    this._handleMessage(data);
    }}来获取
    HTML通过window.ReactNativeWebView.postMessage("h5 to rn") 来传递内容




四、常用命令和插件


 ./gradlew clean --stacktrace android清除缓存 


 ./gradlew assembleRelease --stacktrace android打包 


rm -rf node_modules && yarn cache clean 删除项目依赖包以及 yarn 缓存 


rm -rf ~/.rncache 清除 React-Native 缓存 


react-native-image-picker 上传图片


react-native-calendars 日历 


react-native-file-selector 文件管理


teaset ui组件


作者:qKK
链接:https://juejin.cn/post/6990561527375855623
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

ReactNative与iOS的交互

本文简要展示RN与iOS原生的交互功能。 1.1 RCTRootView初始化问题 /** * - Designated initializer - */ - (instancetype)initWithBridge:(RCTBridge *)bridge...
继续阅读 »

本文简要展示RN与iOS原生的交互功能。


1.1 RCTRootView初始化问题


/**
* - Designated initializer -
*/
- (instancetype)initWithBridge:(RCTBridge *)bridge
moduleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties NS_DESIGNATED_INITIALIZER;

/**
* - Convenience initializer -
* A bridge will be created internally.
* This initializer is intended to be used when the app has a single RCTRootView,
* otherwise create an `RCTBridge` and pass it in via `initWithBridge:moduleName:`
* to all the instances.
*/
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties
launchOptions:(NSDictionary *)launchOptions;


1、当Native APP内只有一处RN的入口时,可以使用initWithBundleURL,否则的话就要使用initWithBridge方法。

2、因为initWithBundleURL会在内部创建一个RCTBridge,当有多个RCTRootView入口时,就会存在多个RCTBridge,容易导致Native端与RN交互时多次响应,出现BUG。



1.2 创建自定义的RNBridgeManager



由于APP内有RN多入口的需求,所以共用一个RCTBridge



RNBridgeManager.h



#import <Foundation/Foundation.h>
#import <React/RCTBridge.h>

NS_ASSUME_NONNULL_BEGIN

@interface RNBridgeManager : RCTBridge
/**
RNBridgeManager单例
*/
+ (instancetype)sharedManager;

@end

NS_ASSUME_NONNULL_END


RNBridgeManager.m


#import "RNBridgeManager.h"

#import <React/RCTBundleURLProvider.h>

//dev模式下:RCTBridge required dispatch_sync to load RCTDevLoadingView Error Fix
#if RCT_DEV
#import <React/RCTDevLoadingView.h>
#endif
/**
自定义类,实现RCTBridgeDelegate
*/
@interface BridgeHandle : NSObject<RCTBridgeDelegate>

@end

@implementation BridgeHandle

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge{
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
}
@end


@implementation RNBridgeManager

+ (instancetype)sharedManager{
static RNBridgeManager *manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[RNBridgeManager alloc] initWithDelegate:[[BridgeHandle alloc] init] launchOptions:nil];
#if RCT_DEV
[manager moduleForClass:[RCTDevLoadingView class]];
#endif
});
return manager;
}


@end


1.3 Native进入RN页面


 RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:[RNBridgeManager sharedManager] moduleName:@"RNTest" initialProperties:nil];
UIViewController *vc = [[UIViewController alloc] init];
vc.view = rootView;
[self.navigationController pushViewController:vc animated:YES];

1.4 RN调用Native方法



  • 创建一个交互的类,实现<RCTBridgeModule>协议;

  • 固定格式:在.m的实现中,首先导出模块名字RCT_EXPORT_MODULE();RCT_EXPORT_MODULE接受字符串作为其Module的名称,如果不设置名称的话默认就使用类名作为Modul的名称;

  • 使用RCT_EXPORT_METHOD导出Native的方法;


1.4.1 比如我们导出Native端的SVProgressHUD提示方法:


RNInterractModule.h


#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

NS_ASSUME_NONNULL_BEGIN

@interface RNInterractModule : NSObject<RCTBridgeModule>

@end

NS_ASSUME_NONNULL_END


RNInterractModule.m


import "RNInterractModule.h"
#import "Util.h"
#import <SVProgressHUD.h>

@implementation RNInterractModule
////RCT_EXPORT_MODULE接受字符串作为其Module的名称,如果不设置名称的话默认就使用类名作为Modul的名称
RCT_EXPORT_MODULE();

//==============1、提示==============
RCT_EXPORT_METHOD(showInfo:(NSString *) info){
dispatch_sync(dispatch_get_main_queue(), ^{
[SVProgressHUD showInfoWithStatus:info];
});
}
@end


1.4.2 RN端调用导出的showInfo方法:


我们在RN端把Native的方法通过一个共同的utils工具类引入,如下



import { NativeModules } from 'react-native';

//导出Native端的方法
export const { showInfo} = NativeModules.RNInterractModule;

具体的RN页面使用时:


import { showInfo } from "../utils";

//通过Button点击事件触发
<Button
title='1、调用Native提示'
onPress={() => showInfo('我是原生端的提示!')}
/>

调用效果:

image


1.4.3 RN回调Native



RN文档显示,目前iOS端的回调还处于实验阶段



我们提供一个例子来模拟:目前的需求是做面包,RN端能提供面粉,但是不会做,Native端是有做面包的功能;所以我们需要先把面粉,传给Native端,Native加工好面包之后,再通过回调回传给RN端。


Native端提供方法


// 比如调用原生的方法处理图片、视频之类的,处理完成之后再把结果回传到RN页面里去
//TODO(RN文档显示,目前iOS端的回调还处于实验阶段)
RCT_EXPORT_METHOD(patCake:(NSString *)flour successBlock:(RCTResponseSenderBlock)successBlock errorBlock:(RCTResponseErrorBlock)errorBlock){
__weak __typeof(self)weakSelf = self;
dispatch_sync(dispatch_get_main_queue(), ^{
NSString *cake = [weakSelf patCake:flour];
//模拟成功、失败的block判断
if([flour isKindOfClass:[NSString class]]){
successBlock(@[@[cake]]);//此处参数需要放在数组里面
}else{
NSError *error = [NSError errorWithDomain:@"com.RNTest" code:-1 userInfo:@{@"message":@"类型不匹配"}];
errorBlock(error);
}
});
}


//使用RN端传递的参数字符串:"",调用Native端的做面包方法,加工成面包,再回传给RN
- (NSString *)patCake:(NSString *)flour{
NSString * cake = [NSString stringWithFormat:@"使用%@,做好了:🎂🍞🍞🍰🍰🍰",flour];
return cake;
}

RN端调用:


//首先工具类里先引入
export const { showInfo,patCake } = NativeModules.RNInterractModule;


//具体页面使用
<Button
title='4、回调:使用面粉做蛋糕'
onPress={() => patCake('1斤面粉',
(cake) => alert(cake),
(error) => alert('出错了' + error.message))}
/>

调用效果:


image


1.4.4 使用Promise回调


Native端提供方法



RCT_EXPORT_METHOD(callNameTointroduction:(NSString *)name resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock) reject){
__weak __typeof(self)weakSelf = self;
dispatch_sync(dispatch_get_main_queue(), ^{
if ([name isKindOfClass:NSString.class]) {
resolve([weakSelf introduction:name]);
}else{
NSError *error = [NSError errorWithDomain:@"com.RNTest" code:-1 userInfo:@{@"message":@"类型不匹配"}];
reject(@"class_error",@"Needs NSString Class",error);
}
});
}

- (NSString *)introduction:(NSString *)name{
return [NSString stringWithFormat:@"我的名字叫%@,今年18岁,喜欢运动、听歌...",name];
}

RN端调用:


//首先工具类里先引入
export const { showInfo,patCake, callNameTointroduction} = NativeModules.RNInterractModule;

//具体页面使用
<Button
title='5、Promise:点名自我介绍'
onPress={
async () => {
try {
let introduction = await callNameTointroduction('小明');
showInfo(introduction);
} catch (e) {
alert(e.message);
}
}
}
/>


调用效果:

image


1.5 Native端发送通知到RN


Native端继承RCTEventEmitter,实现发送RN通知类:


RNNotificationManager.h



#import <Foundation/Foundation.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTBridgeModule.h>

NS_ASSUME_NONNULL_BEGIN

@interface RNNotificationManager : RCTEventEmitter

+ (instancetype)sharedManager;

@end

NS_ASSUME_NONNULL_END


RNNotificationManager.m



#import "RNNotificationManager.h"

@implementation RNNotificationManager
{
BOOL hasListeners;
}


+ (instancetype)sharedManager{
static RNNotificationManager *manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
manager = [[self alloc] init];
});
return manager;
}

- (instancetype)init{
self = [super init];
if (self) {
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center removeObserver:self];
[center addObserver:self selector:@selector(handleEventNotification:) name:@"kRNNotification_Login" object:nil];
[center addObserver:self selector:@selector(handleEventNotification:) name:@"kRNNotification_Logout" object:nil];
};
return self;
}


RCT_EXPORT_MODULE()

- (NSArray<NSString *> *)supportedEvents{
return @[
@"kRNNotification_Login",
@"kRNNotification_Logout"
];
}
//优化无监听处理的事件
//在添加第一个监听函数时触发
- (void)startObserving{
//setup any upstream listenerse or background tasks as necessary
hasListeners = YES;
NSLog(@"----------->startObserving");
}

//will be called when this mdules's last listener is removed,or on dealloc.
- (void)stopObserving{
//remove upstream listeners,stop unnecessary background tasks.
hasListeners = NO;
NSLog(@"----------->stopObserving");
}

+ (BOOL)requiresMainQueueSetup{
return YES;
}

- (void)handleEventNotification:(NSNotification *)notification{
if (!hasListeners) {
return;
}

NSString *name = notification.name;
NSLog(@"通知名字-------->%@",name);
[self sendEventWithName:name body:notification.userInfo];

}

@end


RN端注册监听:


//utils工具类中导出
export const NativeEmitterModuleIOS = new NativeEventEmitter(NativeModules.RNNotificationManager);


//具体页面使用
import { NativeEmitterModuleIOS } from "../utils";

export default class ActivityScene extends Component {

constructor(props) {
super(props);
this.subscription = null;
this.state = {
loginInfo: '当前未登录',
};
}

updateLoginInfoText = (reminder) => {
this.setState({loginInfo: reminder.message})
};

//添加监听
componentWillMount() {
this.subscription = NativeEmitterModuleIOS.addListener('kRNNotification_Login', this.updateLoginInfoText);

}
//移除监听
componentWillUnmount() {
console.log('ActivityScene--------->', '移除通知');
this.subscription.remove();
}
render() {
return (
<View style={{flex: 1, backgroundColor: 'white'}}>
<Button
title='3、RN Push到Native 发送通知页面'
onPress={() => pushNative(RNEmitter)}
/>
<Text style={{fontSize: 20, color: 'red', textAlign: 'center',marginTop:50}}>{this.state.loginInfo}</Text>
</View>
);
}
}

效果展示:


image


1.6 完整Demo(包含iOS & Android)


RN-NativeTest


作者:BulldogX
链接:https://juejin.cn/post/6844903744061046791
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

iOS NerdyUI and Cupcake

iOS
NerdyUI 使用小技巧前言首先本文并不是完整的使用说明,不会对每个属性的用法都面面俱到。如果您想了解更多信息,可以到对应的头文件中查看。这里列出了一些在实际项目中可能会用到的小技巧以及注意事项,希望能对您有所帮助。如果看完觉得有用,麻烦点个赞。如果觉得值得...
继续阅读 »

NerdyUI 使用小技巧

前言

首先本文并不是完整的使用说明,不会对每个属性的用法都面面俱到。如果您想了解更多信息,可以到对应的头文件中查看。这里列出了一些在实际项目中可能会用到的小技巧以及注意事项,希望能对您有所帮助。如果看完觉得有用,麻烦点个赞。如果觉得值得一试,麻烦到 github 给个星,让我有继续写下去的动力。下一篇将解释 NerdyUI 实现上的一些小技巧,敬请期待。

如果您还不知道 NerdyUI 是什么,请先移步这里

Str

  1. .a() 可用来拼接字符串,.ap() 可用来拼接路径。它们能接受的参数跟 Str() 一样。传 nil 的话则什么事都不做,很适合用来拼接多个字符串。

     @"1".a(@"2").a(3).a(nil).a(4.0f).a(@5).a(@"%d", 6);    //@"123456"
    Str(province).a(city).a(district).a(address); //不用担心有的变量可能为 nil
  2. .subFrom() 和 .subTo() 用来截取子串,你可以传一个索引或字符串。

     @"hello".subFrom(2);         //"llo"
    @"hello".subFrom(@"l"); //"llo"
    @"hello".subTo(2); //"he"
    @"hello".subTo(@"ll"); //"he"
  3. .subMatch() 和 .subReplace() 可用正则表达式来查找和替换子串。

     @"pi: 3.13".subMatch(@"[0-9.]+");               //"3.13"
    @"pi: 3.13".subReplace(@"[0-9.]+", @"3.14"); //"pi: 3.14"

AttStr

  1. AttStr() 可以把多个 NSString、NSAttributedString 和 UIImage 拼接成一个 NSAttributedString。后面设置的属性默认会覆盖前面设置的相同属性,可以使用 .ifNotExists 来避免这种情况。

     .color(@"red").color(@"blue");                //蓝色
    .color(@"red").ifNotExists.color(@"blue"); //红色

    AttStr(
    @"small text, ",
    AttStr(@"large text, ").fnt(@40),
    AttStr(@"red small text, ").color(@"red"),
    Img(@"moose"),
    @"small text"
    ).ifNotExists.fnt(20);
  2. NSAttributedString 里能包含图片这个事实打开了无限的可能,很多之前要用用多个 Label 和 ImageView 才能实现的 UI 用 AttStr 可以很轻易的搞定。

     AttStr(@"A hat ", Img(@"hat"), @" and a moose", Img(@"moose");
  3. AttStr 的属性默认会应用到整个字符串,你可以用 .range()、 .match()、 .matchNumber、 .matchURL.matchHashTag 和 .matchNameTag 等来缩小范围。

     id str = @"Hello @Tim_123";

    AttStr(str).color(@"blue"); //整个字符串都为蓝色
    AttStr(str).range(0, 5).color(@"blue"); //"Hello" 为蓝色
    AttStr(str).match(@"Tim").color(@"blue"); //"Tim" 为蓝色
    AttStr(str).matchNumber.color(@"blue"); //"123" 为蓝色
    AttStr(str).matchNameTag.color(@"blue"); //"@Time_123" 为蓝色

    AttStr(str).range(0, 3).range(-3, 3).match(@"@").color(@"blue");
    //"Hel", "@", "123" 为蓝色

    .match() 可以使用正则表达式,负数的 range 表示从尾部往前数。.range() 和 .match() 可连续使用,表示同时选取多个子串。

  4. 使用 .lineGap() 可以设置行间距。但你应该很少会用到,因为 Label 也有一个 .lineGap() 快捷属性。.linkForLabel 只适用于 Label,不适用于其他视图。

Img

  1. 给 Img() 传色值的话会返回一个 1x1 大小的图片,这在大部分情况貌似都没什么用。除了 Button 的 .bgImg() 和 .highBgImg(),因为 Button 的 backgroundImage 会自动拉伸占满整个视图。

     Img(@"red").resize(100, 100);        //100x100 大小的红色图片
  2. .stretchable 会返回一个可拉伸的图片,拉伸位置在图片中心点。如果你想更具体的控制可拉伸区域,可以使用 .tileInsets() 和 .stretchInsets()

     Img(@"button-bg").stretchable;    //等于 Img(@"#button-bg");
    Img(@"pattern").tileInsets(0); //平铺图片
  3. .templates 和 UIView 的 .tint() 配合可以用来给图片上色。

    ImageView.img(Img(@"moose").templates).tint(@"red");

Color

  1. 你可以用 .opacity() 来修改 Color 的 alpha 值:

     Color(@"red").opacity(0.5);        //等于 Color(@"red,0.5");
  2. 你可以用 .brighten().darken().saturate().desaturate() 和 .hueOffset() 等来修改颜色。

     View.wh(100, 100).bgColor(@"#289DCE").onClick(^(UIView *v) {
    v.bgColor(v.backgroundColor.darken(0.2)); //模拟点击变暗效果
    });

Screen

  1. 你可以用 Screen.sizeScreen.width 和 Screen.height 来访问屏幕大小。Screen 还有一个比较有用的属性是 Screen.onePixel, 它始终返回一个像素的大小而不管是在什么设备上。比如设计师可能要求 App 里的分割线都是一个像素的大小,那么你就可以这么用:

     Style(@"separator").wh(Screen.width, Screen.onePixel).bgColor(@"#d9d9d9");
    ...
    id s1 = View.styles(@"separator");
    id s2 = View.styles(@"separator").x(15).w(Screen.width - 30);

View

  1. 如果你想设置一个视图的大小,可以用.wh(50, 50)。但如果你想让一个它的等于另一个视图的大小呢,你可以这么写 .wh(otherView.w, otherView.h), 或者更简单一点 .wh(otherView.wh), 这是因为 .wh() 既可以接受两个 CGFloat, 也可以接受一个 CGSize。.xy().cxy().maxXY() 和 .xywh() 也与此类似,比如 .cxy(otherView.center).xywh(otherView.frame) 和 .xywh(otherView.xy, 50, 50)等等。

  2. 当你想给一个视图设置 border 时,你可只传一个宽度 .border(2), 或者同时带上一个颜色 .border(2, @"red")。如果你已经有一个 UIColor 对象,那么也可以直接传这个对象 .border(2, borderColor),这对于 .tint().color() 和 .bgColor() 等也适用。

  3. 使用 .borderRadius() 会自动把 masksToBounds 设为 YES(如果没有设置阴影的话)。shadow() 默认向下投影,它有几种形式:

     .shadow(0.6);            //shadowOpacity
    .shadow(0.6, 2); //shadowOpacity + shadowRadius
    .shadow(0.3, 3, 3, 3); //shadowOpacity + shadowRadius + shadowOffsetXY
  4. .onClick() 可以用来给任意视图添加一个单击手势,如果这个视图是一个 UIButton,则它使用的是 Button 的 UIControlEventTouchUpInside 事件。使用 onClick 时还会自动把 userInteractionEnabled 设为 YES,毕竟当你给一个 UILabel 或者 UIImageView 添加单击事件时,你想让它们可以点击。

    你可以传一个 block 来作为回调方法,最简单的形式就是 .onClick(^{ ... })。 onClick 已经自动对 self 做了 weakify 处理,虽然标准做法是要在 block 里对 self再做个强引用,防止它提前释放。但大部分情况下你都不需要这么做,因为很多时候 self 对应的都是当前视图的父视图或者它所在的 ViewController,而它们是不会提前释放的。如果你还是不放心,那么你可以这么写:

     .onClick(^{ typeof(self) strongSelf = self; ... });

    如果需要在 block 里访问当前视图,你不能这么写:

     UIView *box = View.onClick(^{
    box.bgColor(@"blue"); //box为nil,因为此时onClick还没返回
    });

    正确写法应该是:

     UIView *box = View.onClick(^(UIView *box) {
    box.bgColor(@"blue"); //使用的是 block 参数
    });

    如果回调代码比较多,或者你更喜欢传统的 target-action 方式,那么你可以这么用:

     .onClick(@"boxDidTap")        //target 默认为 self,action 为字符串,请小心拼写
    .onClick(@"boxDidTap:") //如果你需要当前视图作为参数的话

    这里提到的 .onClick() 的用法同样适用于 .onChange().onFinish() 和 .onLink()等。

  5. 把一个视图添加到另一个视图里有三种方式:

     parentView.addChild(view1, view2, view3, ...);    //使用 addChild 添加多个子视图
    view1.addTo(parentView); //使用 addTo 加到父视图里
    view1.embedIn(parentView); //使用 embedIn 加到父视图里,会同时添加上下左右的约束

    .embedIn() 可以有额外的参数,用来设置距离父视图上下左右的偏移量:

     .embedIn(parentView, 10, 20, 30, 40);    //上:10, 左:20, 下:30, 右:40
    .embedIn(parentView, 10, 20, 30); //上:10,左右:20,下:30
    .embedIn(parentView, 10, 20); //上下:10, 左右:20
    .embedIn(parentView, 10); //上下左右:10
    .embedIn(parentView); //上下左右:0

    这中用法跟 HTML 里的 Margin 和 Padding 类似。如果有某些方向你不想加约束的话,你可以用 NERNull 代替:

     .embedIn(parentView, 10, 20, NERNull, NERNull);    //上:10,左:20
    .embedIn(parentView, 10, NERNull); //上下:10

    .embedIn()这种可变参数的用法同时也适用于 .insets(),后面会说到。

  6. 如果你习惯于手动布局,那么你可能会经常用到 .fitSize、 .fitWidth 和 .fitHeight 来改变视图的大小,用 .flexibleLeft、 .flexibleRight ... .flexibleWH等来设置 autoresizingMask。

    如果你习惯使用 AutoLayout, 则 .fixWidth().fixHeight()、 .fixWH()、 .makeCons()、 remakeCons() 和 updateCons() 等会是你的好朋友。.fixWidth() 等3个内部使用了 .remakeCons() 来设置宽高约束,所以你可以重复使用它们而不用担心会引起约束冲突。

Label

  1. 你可以用 .str() 来设置 text 或者 attributedText。同时你还可以直接传内置类型,省去了转换为字符串的过程:.str(1024)

  2. .fnt() 和 .color() 可以直接传 UIFont 或 UIColor 对象。

  3. .highColor() 可以用来设置 highlighted 状态下的字体颜色,比如 Cell 被选中时。

  4. 允许多行可以用 .lines(0) 或者 .multiline

  5. Label 链接的默认颜色是蓝色,你可以改成其他颜色:

     AttStr(@"hello world").match(@"world").linkForLabel.color(@"red");    //红色链接

    链接选中的样式也可以修改:

     //修改单个 Label 的样式
    label.nerLinkSelectedBorderRadius = 0;
    label.nerLinkSelectedColor = [UIColor blueColor];
    //全局修改
    [UILabel setDefaultLinkSelectedBackgroundColor:[UIColor blueColor] corderRadius:0];

    因为 UILabel 默认是不接受事件的,你必须使用 .touchEnabled 或者 .onLink() 才能点击链接。因为 .onLink() 也会把 userInteractionEnabled 设为 YES。

ImageView

  1. .img() 还会自动把当前视图的大小设置为图片的大小(如果你没设置过 frame 的话)。

     id iv1 = ImageView.img(@"cat");                //iv1 的大小等于图片的大小
    id iv2 = ImageView.wh(50,50).img(@"cat"); //iv2 的大小等于(50,50)

    .img() 和 .highImg() 还可以接受图片数组。

  2. 你可以用 .aspectFit.aspectFill 和 .centerMode 来设置 contentMode。

Button

  1. Button 标题默认为一行,可以使用 .multiline 来让它支持多行显示。

     Button.str(@"hello\nhow are you").multiline;
  2. Button 的 .bgImg() 和 .highBgImg() 非常的灵活和好用。

     .bgImg(@"btn-normal").highBgImg(@"btn-high");      //使用图片
    .bgImg(@"#btn-normal").highBgImg(@"#btn-high"); //使用可拉伸的图片
    .bgImg(@"red").highBgImg(@"blue"); //使用颜色

    之所以用 .bgImg() 而不是 .bgColor() 来设置按钮背景颜色是因为后者在 Cell 选中时会被清空。.bgImg() 跟 .img() 一样会把当前视图的大小设置为图片的大小(如果你没设置过 frame 的话)。

  3. 因为 UIButton 里带有一个 UILabel 和 一个 UIImageView,很适合用来创建这样的 UI:“一个图标后面跟着一段文字” 或者 “一段文字后面跟着一个图标”,并且图标和文字都可点击。

     //评论图标后跟着评论数
    .img(@"comment_icon").str(commentCount).gap(10);
    //"查看更多"后跟着向右箭头
    .img(@"disclosure_arrow").str(@"查看更多").gap(10).reversed;

    使用 .gap() 可在 image 和 title 之间加上一些间隙。使用 .reversed 可以调换 image 和 title 的位置。

  4. 有的时候你可能想在按钮内容和边框之间留一点空间,那么可以使用 .insets()

     .str(@"Done").insets(5, 10).fitSize;          //宽高跟着 title 的变化而变化
    .str(@"Done").insets(5, 10); //autolayout version
    .str(@"Done").h(45).insets(0, 10).fitWidth; //高度固定,宽度变化
    .str(@"Done").fixHeight(45).insets(0, 10); //autolayout version

    .insets() 还有一个妙用就是当按钮的背景图片带有阴影时,title 的显示位置会不太对,这时候就可以用 .insets() 来调整。 它能接受的参数跟 .embedIn() 的可变参数一样。

  5. 组合的使用 .borderRadius()、 .border()、 .color()、 .highColor()、 .bgImg()、 .highBgImg() 、.insets() 以及 AttStr() 等,可以创建出各种各样的按钮。

Constarints

  1. 一个完整的 NSLayoutConstraint 必须包含这个公式里的全部要素:

      view1.attr1 [= , >= , <=] view2.attr2 * multiplier + constant;

    所以当您使用 .makeCons() 来创建约束时,也必须包含这些要素:

      //让当前视图的左边和上边等于父视图的左边和上边
    make.left.equal.view(superview).left.multipliers(1).constants(0);
    make.top.equal.view(superview).top.multipliers(1).constants(0);

    //让当前视图的大小等于 view2 的大小
    make.width.equal.view(view2).width.multipliers(1).constants(0);
    make.height.equal.view(view2).height.multipliers(1).constants(0);

    可以看到要写不少代码,幸好这里面很多属性都有默认值,我们可以一步步的精简它们:

      //1. 如果有多个约束同时涉及到 view1 和 view2,则可以把它们合并在一起
    make.left.top.equal.view(superview).left.top.multipliers(1, 1).constants(0, 0);
    make.width.height.equal.view(view2).width.height.multipliers(1, 1).constants(0, 0);

    //2. 如果 multipliers 和 constants 的参数都是一样的,则可以把它们合并成一个
    make.left.top.equal.view(superview).left.top.multipliers(1).constants(0);
    make.width.height.equal.view(view2).width.height.multipliers(1).constants(0);

    //3. 如果 attr1 和 attr2 是一样的,则可以省略 attr2
    make.left.top.equal.view(superview).multipliers(1).constants(0);
    make.width.height.equal.view(view2).multipliers(1).constants(0);

    //4. multipliers 的默认值是 1, constants 的默认值是 0,所以它们也可以省略掉
    make.left.top.equal.view(superview);
    make.width.height.equal.view(view2);

    //5. 同时设置 width 和 height 的话可以用 size 来表示
    make.left.top.equal.view(superview);
    make.size.equal.view(view2);

    //6. relation 默认为 equal,所以也可以省略掉(坏处是可读性会降低)
    make.left.top.view(superview);
    make.size.view(view2);

    //7. 如果没指定 view2,则默认为父视图
    make.left.top; //虽然很奇怪,但你可以这么写。不过这时候会有警告,因为我们没用到返回值。
    make.size.view(view2);

    //8. 为了消除警告,可以使用 End() 结尾
    make.left.top.End();
    make.size.view(view2);

    //或者用 And 把它们拼接在一起
    make.left.top.And.size.view(view2);

    可以看到到最后变得非常的精简,但可读性也变得很差了。这就需要各位自己权衡了。

  2. 前面说过如果没有指定 view2, 则默认为父视图。这其实有一个例外,就是涉及到 width 和 height 时:

     make.size.equal.constants(100, 200);

    make.width.constants(100);
    make.height.equal.width.End(); //这里的 equal 不能省略,否则就意义不明了

    这里设置的都是当前视图的大小。如果想让它们相对于其他视图,则需要显示的指定:

     make.width.height.equal.view(view2).height.width.multipliers(0.5);
  3. .priority() 可用来设置优先级。.identifier() 可用来设置标识。

  4. 使用 .makeCons().remakeCons() 和 .updateCons() 前必须把当前视图加到父视图里。

     .addTo(superView).makeCons(^{});

TextField / TextView

  1. 你可以用 .hint() 来设置 placeholder, .maxLength() 来限制输入长度。这两个对 UITextField 和 UITextView 来说几乎是标配,奇怪的是系统默认只支持设置 UITextField 的 placeholder。

     .hint(@"Enter your name");      //使用默认的大小和颜色

    id att = AttStr(@"Enter your name").fnt(15).color(@"#999");
    .hint(att); //使用自定义的大小和颜色
  2. .onChange() 会在文本改变时回调,.onFinish() 会在点击键盘上的 return button 时回调。.insets() 的用法跟 UIButton 一样。UITextView 一个不一样的地方在于它默认是有 insets 的,如果你不想要,可以用 .insets(0) 来清空。

  3. 你可以用 .becomeFocus 来获取输入焦点。

HorStack / VerStack

  1. HorStack() 默认的对齐方式是 centerAlignment,VerStack() 默认的对齐方式是 leftAlignment。它们的用法类似于 UIStackView 及 Android 的 LinearLayout。

  2. 如果你设置了 Stack 的宽高约束,那么当 Stack 里子视图的宽度总和或高度总和小于 Stack 本身的宽或高时,有个子视图将会被拉伸。当 Stack 里子视图的宽度总和或高度总和大于 Stack 本身的宽或高时,有个子视图将会被压缩。对于使用 intrinsicContentSize 的子视图来说,你可以通过 .horHugging()、 .verHugging()、 horResistance().verResistance()、 .lowHugging 和 .lowResistance 等来修改 contentHuggingPriority 和 contentCompressionResistancePriority 的值,进而控制哪个子视图可以被拉伸或压缩。对于第一种情况,你还可以使用 NERSpring, 它相当于一个弹簧,会占用尽可能多的空间,这样所有的子视图都不会被拉伸。

  3. 如果你没有设置 StackView 的宽高约束,那么它的大小会跟随着子视图的变化而变化。一般只有最外层的 StackView 我们会设置它的宽或高(不管是直接或者间接,比如 .embedIn 可能会间接的影响它的宽高)。

     //宽度等于父视图宽度,高度跟随子视图变化
    VerStack(view1, view2, view3).centerAlignment.gap(10).embedIn(self.view, 0, 0, NERNull, 0);

    //固定宽高,使用 NERSpring 来避免子视图被拉伸
    VerStack(view1, @10, view2, NERSpring, view3, @20, view4).wh(self.view.wh).addTo(self.view);

    虽然后一个例子我们设置的是frame,但因为 UIView 的 translatesAutoresizingMaskIntoConstraints 默认为 YES,所以也相当于设置了宽高约束。加到 Stack 里的子视图的 translatesAutoresizingMaskIntoConstraints 会被设为 NO,所以只有最外层的 Stack 可以用设置 frame 的方式来布局。

  4. .gap() 会在每个子视图之间添加相同的间隙。@(n) 会在两个子视图之间添加间隙,这就允许不同的子视图之间有不同的间隙。

  5. 可以通过 -addArrangedSubview:、 -insertArrangedSubview:atIndex:、 -removeArrangedSubview: 和 removeArrangedSubviewAtIndex: 来添加或删除子视图。如果想临时隐藏子视图,可以直接设置子视图的 hidden 属性,这是一个非常好用的功能。

Alert / ActionSheet

  1. 可以同时有多个 Action 按钮,其中 .action() 和 . destructiveAction() 必须传标题和回调 block, .cancelAction() 可以只传一个标题:

     Alert.action(@"Action1", ^{

    }).action(@"Action2", ^{

    }).action(@"Action3", ^{

    }).destructiveAction(@"Delete", ^{

    }).cancelAction(@"Cancel").show();
  2. .title().message() 和 .action() 有个隐藏的功能是可以传 NSAttributedString,这就表示它们的显示样式是可以修改的。不过这不是官方提供的功能,可能只在某一些版本的系统上有效,不推荐大家使用。

  3. 使用 .tint() 可以改变所有普通按钮的字体颜色,这是系统提供的功能。

  4. 最后必须调用 .show() 才能显示出来。

Style

  1. View(及其子类)、AttStr 和 Style 可同时使用一个或多个 Styles。对 Style 来说,就相当于继承: Style(@"headline").fnt(@20).color(@"#333");

     Style(@"round-border").borderRadius(8).border(1, @"red");

    AttStr(someString).styles(@"headline");
    Label.styles(@"headline round-border"); //使用空格作为分隔符,就像 CSS 一样

    id roundHeadline = Style().styles(@"headline round-border").bgColor(@"lightGray");
    Button.styles(roundHeadline);
  2. 全局 Style 一般在程序启动的时候设置,比如 -application:didFinishLaunchingWithOptions: 或者 +load 里。

最后

  1. 链式属性分为两种:一种带参数,比如 .color(@"red"),一种不带参数,比如 .centerAlignment。如果最后一个属性是不带参数的属性,且它的返回值没有赋值给一个变量,那么那么编译器将给出警告。你可以使用 .End() 来消除警告。

     UILabel *someLabel = ...;
    ...
    someLabel.str(newString).fitSize; //Warning: Property access result unused

    someLabel.str(newString).fitSize.End(); //no more warning
  2. 尽可能的使用 id,如果后续不需要再访问某个变量的属性,定义为 id 可以减少不少代码。

  3. 多考虑使用 NSAttributedString。因为 AttStr() 的存在,使得创建 NSAttributedString 变得非常简单。并且系统控件早就全面的支持 NSAttributedString 了。

  4. 学会使用 StackView 或 LinearLayout 的方式来思考问题,即同时对几个视图进行布局而不是对每个视图单独进行布局。

  5. 学会使用特殊字符和表情符号,有一些图标乍一看像是图片,但是其实是可以使用特殊字符或表情来表示的。Unicode 提供了非常多的特殊字符,像是 ⚽︎♠︎♣︎☁︎☃☆★⚾︎◼︎▶︎✔︎✖︎♚✎✿✪ 等等,最重要的一点是这些图标就像普通文字一样可以改变大小和颜色。

  6. 如果发现有一些属性没找到,请更新到最新版本。

收起阅读 »

iOS 蓝牙设备名称缓存问题总结

1. 问题背景当设备已经在 App 中连接成功后修改设备名称App 扫描到的设备名称仍然是之前的名称App 代码中获取名称的方式为(perpheral.name)2. 问题分析当 APP 为中心连接其他的蓝牙设备时。首次连接成功过后,iOS系统内会将该外设缓存...
继续阅读 »

1. 问题背景

  1. 当设备已经在 App 中连接成功后
  2. 修改设备名称
  3. App 扫描到的设备名称仍然是之前的名称
  4. App 代码中获取名称的方式为(perpheral.name)

2. 问题分析

当 APP 为中心连接其他的蓝牙设备时。

首次连接成功过后,iOS系统内会将该外设缓存记录下来。

下次重新搜索时,搜索到的蓝牙设备时,直接打印 (peripheral.name),得到的是之前缓存中的蓝牙名称。

如果此期间蓝牙设备更新了名称,(peripheral.name)这个参数并不会改变,所以需要换一种方式获取设备的名称,在广播数据包内有一个字段为 kCBAdvDataLocalName,可以实时获取当前设备名称。

3. 问题解决

下面给出OC 和 Swift 的解决方法:

OC

-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI{
NSString *localName = [advertisementData objectForKey:@"kCBAdvDataLocalName"];
}

Swift

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
let localName = advertisementData["kCBAdvDataLocalName"]
}
收起阅读 »

iOS 面试题 八股文 1.6

一、面试题 1、说说你认识的Swift是什么? Swift是苹果于2014年WWDC(苹果开发者大会)发布的新开发语言,可与Objective-C共同运行于MAC OS和iOS平台,用于搭建基于苹果平台的应用程序。 2、举例说明Swift里面有哪些...
继续阅读 »

一、面试题


1、说说你认识的Swift是什么?

Swift是苹果于2014年WWDC(苹果开发者大会)发布的新开发语言,可与Objective-C共同运行于MAC OS和iOS平台,用于搭建基于苹果平台的应用程序。


2、举例说明Swift里面有哪些是 Objective-C中没有的?

Swift引入了在Objective-C中没有的一些高级数据类型,例如tuples(元组),可以使你创建和传递一组数值。
wift还引入了可选项类型(Optionals),用于处理变量值不存在的情况。可选项的意思有两种:一是变量是存在的,
例如等于X,二是变量值根本不存在。Optionals类似于Objective-C中指向nil的指针,但是适用于所有的数据类型,而非仅仅局限于类,Optionals 相比于Objective-C中nil指针更加安全和简明,并且也是Swift诸多最强大功能的核心。


3、NSArray与NSSet的区别?

NSArray内存中存储地址连续,而NSSet不连续
NSSet效率高,内部使用hash查找;NSArray查找需要遍历
NSSet通过anyObject访问元素,NSArray通过下标访问


4、Swift比Objective-C有什么优势?

Swift全面优于Objective-C语言,性能是Objective-C的1.3倍,上手更加容易。


5、NSHashTable与NSMapTable?

NSHashTable是NSSet的通用版本,对元素弱引用,可变类型;可以在访问成员时copy
NSMapTable是NSDictionary的通用版本,对元素弱引用,可变类型;
可以在访问成员时copy
(注:NSHashTable与NSSet的区别:NSHashTable可以通过option设置元素弱引用/copyin,只有可变类 型。但是添加对象的时候NSHashTable耗费时间是NSSet的两倍。
NSMapTable与NSDictionary的区别:同上)


6、Swift 是一门安全语言吗?

Swift是一门类型安全的语言,Optionals就是代表。Swift能帮助你在类型安全的环境下工作,如果你的代码中需要使用String类型,Swift的安全机制能阻止你错误的将Int值传递过来,这使你在开发阶段就能及时发现并修正问题。


7、属性关键字assign、retain、weak、copy

assign:用于基本数据类型和结构体。如果修饰对象的话,当销毁时,属性值不会自动置nil,可能造成野指针。
weak:对象引用计数为0时,属性值也会自动置nil
retain:强引用类型,ARC下相当于strong,但block不能用retain修饰,因为等同于assign不安全。
strong:强引用类型,修饰block时相当于copy。


8、weak属性如何自动置nil的?

Runtime会对weak属性进行内存布局,构建hash表:以weak属性对象内存地址为key,weak属性值(weak自身地址)为value。当对象引用计数为0 dealloc时,会将weak属性值自动置nil。


9、Swift 是一门安全语言吗?

Swift是一门类型安全的语言,Optionals就是代表。Swift能帮助你在类型安全的环境下工作,如果你的代码中需要使用String类型,Swift的安全机制能阻止你错误的将Int值传递过来,这使你在开发阶段就能及时发现并修正问题。


10、内存泄露问题?

主要集中在循环引用问题中,如block、NSTime、perform selector引用计数问题。


11、Block的循环引用、内部修改外部变量、三种block

block强引用self,self强引用block
内部修改外部变量:block不允许修改外部变量的值,这里的外部变量指的是栈中指针的内存地址。
__block的作用是只要观察到变量被block使用,就将外部变量在栈中的内存地址放到堆中。
三种block:
NSGlobalBlack(全局)、
NSStackBlock(栈block)、
NSMallocBlock(堆block)


12、KVO底层实现原理?手动触发KVO?swift如何实现KVO?

KVO原理:当观察一个对象时,runtime会动态创建继承自该对象的类,并重写被观察对象的setter方法,重写的setter方法会负责在调用原setter方法前后通知所有观察对象值得更改,最后会把该对象的isa指针指向这个创建的子类,对象就变成子类的实例。
如何手动触发KVO:在setter方法里,手动实现NSObject两个方法:willChangeValueForKey、didChangeValueForKey
swift的kvo:继承自NSObject的类,或者直接willset/didset实现。


13、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 中的编译顺序。


14、load方法和initialize方法的异同。——主要说一下执行时间,各自用途,没实现子类的方法会不会调用父类的?

load initialize 调用时机 app启动后,runtime初始化的时候 第一个方法调用前调用 调用顺序 父类->本类->分类 父类->本类(如果有分类直接调用分类,本类不会调用) 没实现子类的方法会不会调用父类的 否 是 是否沿用父类实现 否 是

见图 1

15、对 runtime 的理解。——主要是方法调用时如何查找缓存,如何找到方法,找不到方法时怎么转发,对象的内存布局

OC中向对象发送消息时,runtime会根据对象的isa指针找到对象所属的类,然后在该类的方法列表和父类的方法列表中寻找方法执行。如果在最顶层父类中没找到方法执行,就会进行消息转发:Method resoution(实现方法)、fast forwarding(转发给其他对象)、normal forwarding(完整消息转发。可以转发给多个对象)

16、runtime 中,SEL和IMP的区别?

每个类对象都有一个方法列表,方法列表存储方法名、方法实现、参数类型,SEL是方法名(编号),IMP指向方法实现的首地址


17、autoreleasepool的原理和使用场景?

若干个autoreleasepoolpage组成的双向链表的栈结构,objc_autoreleasepoolpush、objc_autoreleasepoolpop、objc_autorelease
使用场景:多次创建临时变量导致内存上涨时,需要延迟释放
autoreleasepoolpage的内存结构:4k存储大小

见图 2


18、Autorelase对象什么时候释放?

在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。


19、Runloop与线程的关系?Runloop的mode? Runloop的作用?内部机制?

每一个线程都有一个runloop,主线程的runloop默认启动。
mode:主要用来指定事件在运行时循环的优先级
作用:保持程序的持续运行、随时处理各种事件、节省cpu资源(没事件休息释放资源)、渲染屏幕UI


20、iOS中使用的锁、死锁的发生与避免

@synchronized、信号量、NSLock等
死锁:多个线程同时访问同一资源,造成循环等待。GCD使用异步线程、并行队列


21、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开销相对高


22、App启动优化策略?main函数执行前后怎么优化

启动时间 = pre-main耗时+main耗时
pre-main阶段优化:
删除无用代码
抽象重复代码
+load方法做的事情延迟到initialize中,或者+load的事情不宜花费太多时间
减少不必要的framework,或者优化已有framework

Main阶段优化
didFinishLauchingwithOptions里代码延后执行
首次启动渲染的页面优化


23、Swift 支持面向过程编程吗?

它采用了 Objective-C 的命名参数以及动态对象模型,可以无缝对接到现有的 Cocoa 框架,并且可以兼容 Objective-C 代码,支持面向过程编程和面向对象编程



24、Swift 是一门安全语言吗?

Swift是一门类型安全的语言,Optionals就是代表。Swift能帮助你在类型安全的环境下工作,如果你的代码中需要使用String类型,Swift的安全机制能阻止你错误的将Int值传递过来,这使你在开发阶段就能及时发现并修正问题。


25、Swift中如何定义变量和常量?

使用let来声明常量,使用var来声明变量


26、oc与js交互

拦截url
JavaScriptCore(只适用于UIWebView)
WKScriptMessageHandler(只适用于WKWebView)
WebViewJavaScriptBridge(第三方框架)


27、Swift的内存管理是怎样的?

Swift 使用自动引用计数(Automatic Reference Counting, ARC)来简化内存管理


28、struct、Class的区别

class可以继承,struct不可以
class是引用类型,struct是值类型
struct在function里修改property时需要mutating关键字修饰


29、访问控制关键字(public、open、private、filePrivate、internal)

public与open:public在module内部中,class和func都可以被访问/重载/继承,外部只能访问;而open都可以
private与filePrivate:private修饰class/func,表示只能在当前class源文件/func内部使用,外部不可以被继承和访问;而filePrivate表示只能在当前swift源文件内访问
internal:在整个模块或者app内都可以访问,默认访问级别,可写可不写


30、OC与Swift混编

OC调用swift:import "工程名-swift.h” @objc
swift调用oc:桥接文件
31、用Swift定义一个数组和字典?
let emptyArray = String[]()
let emptyDictionary = Dictionary<String, Float>()
32、try、try?与try!
try:手动捕捉异常
try?:系统帮我们处理,出现异常返回nil;没有异常返回对应的对象
try!:直接告诉系统,该方法没有异常。如果出现异常程序会crash
33、guard与defer
guard用于提前处理错误数据,else退出程序,提高代码可读性
defer延迟执行,回收资源。多个defer反序执行,嵌套defer先执行外层,后执行内层
34、架构&设计模式
MVC设计模式介绍
MVVM介绍、MVC与MVVM的区别?
ReactiveCocoa的热信号与冷信号
缓存架构设计LRU方案
SDWebImage源码,如何实现解码
AFNetWorking源码分析
组件化的实施,中间件的设计
哈希表的实现原理?如何解决冲突
35、数据结构&算法
快速排序、归并排序
二维数组查找(每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数)
二叉树的遍历:判断二叉树的层数
单链表判断环
36、内存泄露问题?
主要集中在循环引用问题中,如block、NSTime、perform selector引用计数问题。
37、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 (机制待改善))





收起阅读 »

iOS 面试题 八股文 1.6

如何自定义下标获取 实现 subscript 即可, 如extension AnyList { subscript(index: Int) -> T{ return self.list[index] } subsc...
继续阅读 »

如何自定义下标获取


实现 subscript 即可, 如

extension AnyList {
subscript(index: Int) -> T{
return self.list[index]
}
subscript(indexString: String) -> T?{
guard let index = Int(indexString) else {
return nil
}
return self.list[index]
}
}


索引除了数字之外, 其他类型也是可以的


?? 的作用


可选值的默认值, 当可选值为nil 的时候, 会返回后面的值. 如

let someValue = optional1 ?? 0


lazy 的作用


懒加载, 当属性要使用的时候, 才去完成初始化

class LazyClass {
lazy var someLazyValue: Int = {
print("lazy init value")
return 1
}()
var someNormalValue: Int = {
print("normal init value")
return 2
}()
}
let lazyInstance = LazyClass()
print(lazyInstance.someNormalValue)
print(lazyInstance.someLazyValue)
// 打印输出
// normal init value
// 2
// lazy init value
// 1


一个类型表示选项,可以同时表示有几个选项选中(类似 UIViewAnimationOptions ),用什么类型表示


需要实现自 OptionSet, 一般使用 struct 实现. 由于 OptionSet 要求有一个不可失败的init(rawValue:) 构造器, 而 枚举无法做到这一点(枚举的原始值构造器是可失败的, 而且有些组合值, 是没办法用一个枚举值表示的)

struct SomeOption: OptionSet {
let rawValue: Int
static let option1 = SomeOption(rawValue: 1 << 0)
static let option2 = SomeOption(rawValue:1 << 1)
static let option3 = SomeOption(rawValue:1 << 2)
}
let options: SomeOption = [.option1, .option2]


inout 的作用


输入输出参数, 如:

func swap( a: inout Int, b: inout Int) {
let temp = a
a = b
b = temp
}
var a = 1
var b = 2
print(a, b)// 1 2
swap(a: &a, b: &b)
print(a, b)// 2 1


Error 如果要兼容 NSError 需要做什么操作


其实直接转换就可以, 例如 SomeError.someError as NSError 但是这样没有错误码, 描述等等, 如果想和 NSError 一样有这些东西, 只需要实现 LocalizedErrorCustomNSError 协议, 有些方法有默认实现, 可以略过, 如:

enum SomeError: Error, LocalizedError, CustomNSError {
case error1, error2
public var errorDescription: String? {
switch self {
case .error1:
return "error description error1"
case .error2:
return "error description error2"
}
}
var errorCode: Int {
switch self {
case .error1:
return 1
case .error2:
return 2
}
}
public static var errorDomain: String {
return "error domain SomeError"
}
public var errorUserInfo: [String : Any] {
switch self {
case .error1:
return ["info": "error1"]
case .error2:
return ["info": "error2"]
}
}
}
print(SomeError.error1 as NSError)
// Error Domain=error domain SomeError Code=1 "error description error1" UserInfo={info=error1}


下面的代码都用了哪些语法糖


[1, 2, 3].map{ $0 * 2 }

[1, 2, 3] 使用了, Array 实现的ExpressibleByArrayLiteral 协议, 用于接收数组的字面值

map{xxx} 使用了闭包作为作为最后一个参数时, 可以直接写在调用后面, 而且, 如果是唯一参数的话, 圆括号也可以省略

闭包没有声明函数参数, 返回值类型, 数量, 依靠的是闭包类型的自动推断

闭包中语句只有一句时, 自动将这一句的结果作为返回值

0 在没有声明参数列表的时候, 第一个参数名称为0, 后续参数以此类推


什么是高阶函数


一个函数如果可以以某一个函数作为参数, 或者是返回值, 那么这个函数就称之为高阶函数, 如 map, reduce, filter


如何解决引用循环



  1. 转换为值类型, 只有类会存在引用循环, 所以如果能不用类, 是可以解引用循环的,

  2. delegate 使用 weak 属性.

  3. 闭包中, 对有可能发生循环引用的对象, 使用 weak 或者 unowned, 修饰


下面的代码会不会崩溃,说出原因

var mutableArray = [1,2,3]
for _ in mutableArray {
mutableArray.removeLast()
}


不会, 原理不清楚, 就算是把 removeLast(), 换成 removeAll() ,这个循环也会执行三次, 估计是在一开始, for

in 就对 mutableArray 进行了一次值捕获, 而 Array 是一个值类型 , removeLast() 并不能修改捕获的值.


给集合中元素是字符串的类型增加一个扩展方法,应该怎么声明


使用 where 子句, 限制 Element 为 String

extension Array where Element == String {
var isStringElement:Bool {
return true
}
}
["1", "2"].isStringElement
//[1, 2].isStringElement// error


定义静态方法时关键字 static 和 class 有什么区别


static 定义的方法不可以被子类继承, class 则可以

class AnotherClass {
static func staticMethod(){}
class func classMethod(){}
}
class ChildOfAnotherClass: AnotherClass {
override class func classMethod(){}
//override static func staticMethod(){}// error
}


一个 Sequence 的索引是不是一定从 0 开始?


不一定, 两个 for in 并不能保证都是从 0 开始, 且输出结果一致, 官方文档如下



Repeated Access


The Sequence protocol makes no requirement on conforming types regarding

whether they will be destructively consumed by iteration. As a

consequence, don't assume that multiple for-in loops on a sequence

will either resume iteration or restart from the beginning:

for element in sequence {
if ... some condition { break }
}

for element in sequence {
// No defined behavior
}



有些同学还是不太理解, 我写了一个demo 当作参考

class Countdown: Sequence, IteratorProtocol {
var count: Int
init(count: Int) {
self.count = count
}
func next() -> Int? {
if count == 0 {
return nil
} else {
defer { count -= 1 }
return count
}
}
}

var countDown = Countdown(count: 5)
print("begin for in 1")
for c in countDown {
print(c)
}
print("end for in 1")
print("begin for in 2")
for c in countDown {
print(c)
}
print("end for in 2")


最后输出的结果是

begin for in 1
5
4
3
2
1
end for in 1
begin for in 2
end for in 2


很明显, 第二次没有输出任何结果, 原因就是在第二次for in 的时候, 并没有将count 重置.


数组都实现了哪些协议


MutableCollection, 实现了可修改的数组, 如 a[1] = 2

ExpressibleByArrayLiteral, 实现了数组可以从[1, 2, 3] 这种字面值初始化的能力

...


如何自定义模式匹配


这部分不太懂, 贴个链接吧

http://swifter.tips/pattern-match/


autoclosure 的作用


自动闭包, 会自动将某一个表达式封装为闭包. 如

func autoClosureFunction(_ closure: @autoclosure () -> Int) {
closure()
}
autoClosureFunction(1)


详细可参考http://swifter.tips/autoclosure/


编译选项 whole module optmization 优化了什么


编译器可以跨文件优化编译代码, 不局限于一个文件.

http://www.jianshu.com/p/8dbf2bb05a1c


下面代码中 mutating 的作用是什么

struct Person {
var name: String {
mutating get {
return store
}
}
}


让不可变对象无法访问 name 属性


如何让自定义对象支持字面量初始化


有几个协议, 分别是

ExpressibleByArrayLiteral 可以由数组形式初始化

ExpressibleByDictionaryLiteral 可以由字典形式初始化

ExpressibleByNilLiteral 可以由nil 值初始化

ExpressibleByIntegerLiteral 可以由整数值初始化

ExpressibleByFloatLiteral 可以由浮点数初始化

ExpressibleByBooleanLiteral 可以由布尔值初始化

ExpressibleByUnicodeScalarLiteral

ExpressibleByExtendedGraphemeClusterLiteral

ExpressibleByStringLiteral

这三种都是由字符串初始化, 上面两种包含有 Unicode 字符和特殊字符


dynamic framework 和 static framework 的区别是什么



静态库和动态库, 静态库是每一个程序单独打包一份, 而动态库则是多个程序之间共享



链接: https://www.jianshu.com/p/7c7f4b4e4efe

链接:https://www.jianshu.com/p/cc4a737ddc1d

链接:https://www.jianshu.com/p/23d99f434281

收起阅读 »

iOS 面试题 八股文 1.5

defer 使用场景 defer 语句块中的代码, 会在当前作用域结束前调用, 常用场景如异常退出后, 关闭数据库连接func someQuery() -> ([Result], [Result]){ let db = DBOpen("xxx")...
继续阅读 »

defer 使用场景


defer 语句块中的代码, 会在当前作用域结束前调用, 常用场景如异常退出后, 关闭数据库连接

func someQuery() -> ([Result], [Result]){
let db = DBOpen("xxx")
defer {
db.close()
}
guard results1 = db.query("query1") else {
return nil
}
guard results2 = db.query("query2") else {
return nil
}
return (results1, results2)
}


需要注意的是, 如果有多个 defer, 那么后加入的先执行

func someDeferFunction() {
defer {
print("\(#function)-end-1-1")
print("\(#function)-end-1-2")
}
defer {
print("\(#function)-end-2-1")
print("\(#function)-end-2-2")
}
if true {
defer {
print("if defer")
}
print("if end")
}
print("function end")
}
someDeferFunction()
// 输出
// if end
// if defer
// function end
// someDeferFunction()-end-2-1
// someDeferFunction()-end-2-2
// someDeferFunction()-end-1-1
// someDeferFunction()-end-1-2


String 与 NSString 的关系与区别


NSString 与 String 之间可以随意转换,

let someString = "123"
let someNSString = NSString(string: "n123")
let strintToNSString = someString as NSString
let nsstringToString = someNSString as String


String 是结构体, 值类型, NSString 是类, 引用类型.

通常, 没必要使用 NSString 类, 除非你要使用一些特有方法, 例如使用 pathExtension 属性


怎么获取一个 String 的长度


不考虑编码, 只是想知道字符的数量, 用characters.count

"hello".characters.count // 5
"你好".characters.count // 2
"こんにちは".characters.count // 5


如果想知道在某个编码下占多少字节, 可以用

"hello".lengthOfBytes(using: .ascii) // 5
"hello".lengthOfBytes(using: .unicode) // 10
"你好".lengthOfBytes(using: .unicode) // 4
"你好".lengthOfBytes(using: .utf8) // 6
"こんにちは".lengthOfBytes(using: .unicode) // 10
"こんにちは".lengthOfBytes(using: .utf8) // 15


如何截取 String 的某段字符串


swift 中, 有三个取子串函数,

substring:to , substring:from, substring:with.

let simpleString = "Hello, world"
simpleString.substring(to: simpleString.index(simpleString.startIndex, offsetBy: 5))
// hello
simpleString.substring(from: simpleString.index(simpleString.endIndex, offsetBy: -5))
// world
simpleString.substring(with: simpleString.index(simpleString.startIndex, offsetBy: 5) ..< simpleString.index(simpleString.endIndex, offsetBy: -5))
// ,


使用起来略微麻烦, 具体用法可以参考我的另一篇文章http://www.jianshu.com/p/b3231f9406e9


throws 和 rethrows 的用法与作用


throws 用在函数上, 表示这个函数会抛出错误.

有两种情况会抛出错误, 一种是直接使用 throw 抛出, 另一种是调用其他抛出异常的函数时, 直接使用 try xx 没有处理异常.

enum DivideError: Error {
case EqualZeroError;
}
func divide(_ a: Double, _ b: Double) throws -> Double {
guard b != Double(0) else {
throw DivideError.EqualZeroError
}
return a / b
}
func split(pieces: Int) throws -> Double {
return try divide(1, Double(pieces))
}


rethrows 与 throws 类似, 不过只适用于参数中有函数, 且函数会抛出异常的情况, rethrows 可以用 throws 替换, 反过来不行

func processNumber(a: Double, b: Double, function: (Double, Double) throws -> Double) rethrows -> Double {
return try function(a, b)
}


try? 和 try!是什么意思


这两个都用于处理可抛出异常的函数, 使用这两个关键字可以不用写 do catch.

区别在于, try? 在用于处理可抛出异常函数时, 如果函数抛出异常, 则返回 nil, 否则返回函数返回值的可选值, 如:

print(try? divide(2, 1))
// Optional(2.0)
print(try? divide(2, 0))
// nil


而 try! 则在函数抛出异常的时候崩溃, 否则则返会函数返回值, 相当于(try? xxx)!, 如:

print(try! divide(2, 1))
// 2.0
print(try! divide(2, 0))
// 崩溃


associatedtype 的作用


简单来说就是 protocol 使用的泛型

例如定义一个列表协议

protocol ListProtcol {
associatedtype Element
func push(_ element:Element)
func pop(_ element:Element) -> Element?
}


实现协议的时候, 可以使用 typealias 指定为特定的类型, 也可以自动推断, 如

class IntList: ListProtcol {
typealias Element = Int // 使用 typealias 指定为 Int
var list = [Element]()
func push(_ element: Element) {
self.list.append(element)
}
func pop(_ element: Element) -> Element? {
return self.list.popLast()
}
}
class DoubleList: ListProtcol {
var list = [Double]()
func push(_ element: Double) {// 自动推断
self.list.append(element)
}
func pop(_ element: Double) -> Double? {
return self.list.popLast()
}
}


使用泛型也可以

class AnyList<T>: ListProtcol {
var list = [T]()
func push(_ element: T) {
self.list.append(element)
}
func pop(_ element: T) -> T? {
return self.list.popLast()
}
}


可以使用 where 字句限定 Element 类型, 如:

extension ListProtcol where Element == Int {
func isInt() ->Bool {
return true
}
}


什么时候使用 final


final 用于限制继承和重写. 如果只是需要在某一个属性前加一个 final.

如果需要限制整个类无法被继承, 那么可以在类名之前加一个final


public 和 open 的区别


这两个都用于在模块中声明需要对外界暴露的函数, 区别在于, public 修饰的类, 在模块外无法继承, 而 open 则可以任意继承, 公开度来说, public < open


声明一个只有一个参数没有返回值闭包的别名


没有返回值也就是返回值为 Void

typealias SomeClosuerType = (String) -> (Void)
let someClosuer: SomeClosuerType = { (name: String) in
print("hello,", name)
}
someClosuer("world")
// hello, world


Self 的使用场景


Self 通常在协议中使用, 用来表示实现者或者实现者的子类类型.

例如, 定义一个复制的协议

protocol CopyProtocol {
func copy() -> Self
}


如果是结构体去实现, 要将Self 换为具体的类型

struct SomeStruct: CopyProtocol {
let value: Int
func copySelf() -> SomeStruct {
return SomeStruct(value: self.value)
}
}


如果是类去实现, 则有点复杂, 需要有一个 required 初始化方法, 具体可以看这里 http://swifter.tips/use-self/

class SomeCopyableClass: CopyProtocol {
func copySelf() -> Self {
return type(of: self).init()
}
required init(){}
}


dynamic 的作用


由于 swift 是一个静态语言, 所以没有 Objective-C 中的消息发送这些动态机制, dynamic 的作用就是让 swift 代码也能有 Objective-C 中的动态机制, 常用的地方就是 KVO 了, 如果要监控一个属性, 则必须要标记为 dynamic, 可以参考我的文章http://www.jianshu.com/p/ae26100b9edf


什么时候使用 @objc


@objc 用途是为了在 Objective-C 和 Swift 混编的时候, 能够正常调用 Swift 代码. 可以用于修饰类, 协议, 方法, 属性.

常用的地方是在定义 delegate 协议中, 会将协议中的部分方法声明为可选方法, 需要用到@objc

@objc protocol OptionalProtocol {
@objc optional func optionalFunc()
func normalFunc()
}
class OptionProtocolClass: OptionalProtocol {
func normalFunc() {
}
}
let someOptionalDelegate: OptionalProtocol = OptionProtocolClass()
someOptionalDelegate.optionalFunc?()


Optional(可选型) 是用什么实现的


Optional 是一个泛型枚举

大致定义如下:

enum Optional<Wrapped> {
case none
case some(Wrapped)
}


除了使用 let someValue: Int? = nil 之外, 还可以使用let optional1: Optional<Int> = nil 来定义


收起阅读 »

iOS 面试题 八股文 1.4

励志背下所有的八股文class 和 struct 的区别 class 为类, struct 为结构体, 类是引用类型, 结构体为值类型, 结构体不可以继承 不通过继承,代码复用(共享)的方式有哪些 扩展, 全局函数 Set 独有的方法有哪些?// 定义一个 s...
继续阅读 »

励志背下所有的八股文

class 和 struct 的区别


class 为类, struct 为结构体, 类是引用类型, 结构体为值类型, 结构体不可以继承


不通过继承,代码复用(共享)的方式有哪些


扩展, 全局函数


Set 独有的方法有哪些?

// 定义一个 set
let setA: Set<Int> = [1, 2, 3, 4, 4]// {1, 2, 3, 4}, 顺序可能不一致, 同一个元素只有一个值
let setB: Set<Int> = [1, 3, 5, 7, 9]// {1, 3, 5, 7, 9}
// 取并集 A | B
let setUnion = setA.union(setB)// {1, 2, 3, 4, 5, 7, 9}
// 取交集 A & B
let setIntersect = setA.intersection(setB)// {1, 3}
// 取差集 A - B
let setRevers = setA.subtracting(setB) // {2, 4}
// 取对称差集, A XOR B = A - B | B - A
let setXor = setA.symmetricDifference(setB) //{2, 4, 5, 7, 9}


实现一个 min 函数,返回两个元素较小的元素

func myMin<T: Comparable>(_ a: T, _ b: T) -> T {
return a < b ? a : b
}
myMin(1, 2)


map、filter、reduce 的作用


map 用于映射, 可以将一个列表转换为另一个列表

[1, 2, 3].map{"\($0)"}// 数字数组转换为字符串数组
["1", "2", "3"]


filter 用于过滤, 可以筛选出想要的元素

[1, 2, 3].filter{$0 % 2 == 0} // 筛选偶数
// [2]


reduce 合并

[1, 2, 3].reduce(""){$0 + "\($1)"}// 转换为字符串并拼接
// "123"


组合示例

(0 ..< 10).filter{$0 % 2 == 0}.map{"\($0)"}.reduce(""){$0 + $1}
// 02468


map 与 flatmap 的区别


flatmap 有两个实现函数实现,

public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]

这个方法, 中间的函数返回值为一个可选值, 而 flatmap 会丢掉那些返回值为 nil 的值

例如

["1", "@", "2", "3", "a"].flatMap{Int($0)}
// [1, 2, 3]
["1", "@", "2", "3", "a"].map{Int($0) ?? -1}
//[Optional(1), nil, Optional(2), Optional(3), nil]


另一个实现

public func flatMap<SegmentOfResult>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Iterator.Element] where SegmentOfResult : Sequence

中间的函数, 返回值为一个数组, 而这个 flapmap 返回的对象则是一个与自己元素类型相同的数组

func someFunc(_ array:[Int]) -> [Int] {
return array
}
[[1], [2, 3], [4, 5, 6]].map(someFunc)
// [[1], [2, 3], [4, 5, 6]]
[[1], [2, 3], [4, 5, 6]].flatMap(someFunc)
// [1, 2, 3, 4, 5, 6]


其实这个实现, 相当于是在使用 map 之后, 再将各个数组拼起来一样的

[[1], [2, 3], [4, 5, 6]].map(someFunc).reduce([Int]()) {$0 + $1}
// [1, 2, 3, 4, 5, 6]


什么是 copy on write时候


写时复制, 指的是 swift 中的值类型, 并不会在一开始赋值的时候就去复制, 只有在需要修改的时候, 才去复制.

这里有详细的说明

http://www.jianshu.com/p/7e8ba0659646


如何获取当前代码的函数名和行号


#file 用于获取当前文件文件名

#line 用于获取当前行号

#column 用于获取当前列编号

#function 用于获取当前函数名

以上这些都是特殊的字面量, 多用于调试输出日志

具体可以看这里 apple 文档

https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Expressions.html

这里有中文翻译

http://wiki.jikexueyuan.com/project/swift/chapter3/04_Expressions.html


如何声明一个只能被类 conform 的 protocol


声明协议的时候, 加一个 class 即可

protocol SomeClassProtocl: class {
func someFunction()
}


guard 使用场景


guard 和 if 类似, 不同的是, guard 总是有一个 else 语句, 如果表达式是假或者值绑定失败的时候, 会执行 else 语句, 且在 else 语句中一定要停止函数调用

例如

guard 1 + 1 == 2 else {
fatalError("something wrong")
}


常用使用场景为, 用户登录的时候, 验证用户是否有输入用户名密码等

guard let userName = self.userNameTextField.text,
let password = self.passwordTextField.text else {
return
}


收起阅读 »

iOS 面试题 八股文 1.3

82.找错题 试题1: void test1() { char string[10]; char* str1 = "0123456789"; strcpy( string, str1 ); } 试题2: void test2() { char string[1...
继续阅读 »


82.找错题
试题1:
void test1()
{
char string[10];
char* str1 = "0123456789";
strcpy( string, str1 );
}
试题2:
void test2()
{
char string[10], str1[10];
int i;
for(i=0; i<10; i++)
{
str1 = 'a';
}
strcpy( string, str1 );
}
试题3:
void test3(char* str1)
{
char string[10];
if( strlen( str1 ) <= 10 )
{
strcpy( string, str1 );
}
}
解答:
试题1字符串str1需要11个字节才能存放下(包括末尾的’\0’),而string只有10个字节的空间,strcpy会导致数组越界;
对试题2,如果面试者指出字符数组str1不能在数组内结束可以给3分;如果面试者指出strcpy(string, str1)调用使得从str1起复制到string内存起所复制的字节数具有不确定性可以给7分,在此基础上指出库函数strcpy工作方式的给10分;
对试题3,if(strlen(str1) <= 10)应改为if(strlen(str1) < 10),因为strlen的结果未统计’\0’所占用的1个字节。
剖析:
考查对基本功的掌握:
(1)字符串以’\0’结尾;
(2)对数组越界把握的敏感度;
(3)库函数strcpy的工作方式,如果编写一个标准strcpy函数的总分值为10,下面给出几个不同得分的答案:
2分
void strcpy( char *strDest, char *strSrc )
{
 while( (*strDest++ = * strSrc++) != ‘\0’ );
}
4分
void strcpy( char *strDest, const char *strSrc ) 
//将源字符串加const,表明其为输入参数,加2分
{
 while( (*strDest++ = * strSrc++) != ‘\0’ );
}
7分
void strcpy(char *strDest, const char *strSrc) 
{
//对源地址和目的地址加非0断言,加3分
assert( (strDest != NULL) && (strSrc != NULL) );
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
10分
//为了实现链式操作,将目的地址返回,加3分!
char * strcpy( char *strDest, const char *strSrc ) 
{
assert( (strDest != NULL) && (strSrc != NULL) );
char *address = strDest; 
while( (*strDest++ = * strSrc++) != ‘\0’ ); 
return address;
}
从2分到10分的几个答案我们可以清楚的看到,小小的strcpy竟然暗藏着这么多玄机,真不是盖的!需要多么扎实的基本功才能写一个完美的strcpy啊!
(4)对strlen的掌握,它没有包括字符串末尾的'\0'。
读者看了不同分值的strcpy版本,应该也可以写出一个10分的strlen函数了,完美的版本为: int strlen( const char *str ) //输入参数const
{
assert( strt != NULL ); //断言字符串地址非0
int len;
while( (*str++) != '\0' ) 

len++; 

return len;
}
试题4:
void GetMemory( char *p )
{
p = (char *) malloc( 100 );
}
void Test( void ) 
{
char *str = NULL;
GetMemory( str ); 
strcpy( str, "hello world" );
printf( str );
}
试题5:
char *GetMemory( void )

char p[] = "hello world"; 
return p; 
}
void Test( void )

char *str = NULL; 
str = GetMemory(); 
printf( str ); 
}
试题6:
void GetMemory( char **p, int num )
{
*p = (char *) malloc( num );
}
void Test( void )
{
char *str = NULL;
GetMemory( &str, 100 );
strcpy( str, "hello" ); 
printf( str ); 
}
试题7:
void Test( void )
{
char *str = (char *) malloc( 100 );
strcpy( str, "hello" );
free( str ); 
... //省略的其它语句
}
解答:
试题4传入中GetMemory( char *p )函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值,执行完
char *str = NULL;
GetMemory( str ); 
后的str仍然为NULL;
试题5中
char p[] = "hello world"; 
return p; 
的p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,其根源在于不理解变量的生存期。
试题6的GetMemory避免了试题4的问题,传入GetMemory的参数为字符串指针的指针,但是在GetMemory中执行申请内存及赋值语句
*p = (char *) malloc( num );
后未判断内存是否申请成功,应加上:
if ( *p == NULL )
{
...//进行申请内存失败处理
}
试题7存在与试题6同样的问题,在执行
char *str = (char *) malloc(100);
后未进行内存是否申请成功的判断;另外,在free(str)后未置str为空,导致可能变成一个“野”指针,应加上:
str = NULL;
试题6的Test函数中也未对malloc的内存进行释放。
剖析:
试题4~7考查面试者对内存操作的理解程度,基本功扎实的面试者一般都能正确的回答其中50~60的错误。但是要完全解答正确,却也绝非易事。
对内存操作的考查主要集中在:
(1)指针的理解;
(2)变量的生存期及作用范围;
(3)良好的动态内存申请和释放习惯。
再看看下面的一段程序有什么错误:
swap( int* p1,int* p2 )
{
int *p;
*p = *p1;
*p1 = *p2;
*p2 = *p;
}
在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃。在VC++中DEBUG运行时提示错误“Access Violation”。该程序应该改为:
swap( int* p1,int* p2 )
{
int p;
p = *p1;
*p1 = *p2;
*p2 = p;
}[img=12,12]file:///D:/鱼鱼软件/鱼鱼多媒体***本/temp/{56068A28-3D3B-4D8B-9F82-AC1C3E9B128C}_arc_d[1].gif[/img] 3.内功题
试题1:分别给出BOOL,int,float,指针变量 与“零值”比较的 if 语句(假设变量名为var)
解答:
BOOL型变量:if(!var)
int型变量: if(var==0)
float型变量:
const float EPSINON = 0.00001;
if ((x >= - EPSINON) && (x <= EPSINON)
指针变量:  if(var==NULL)
剖析:
考查对0值判断的“内功”,BOOL型变量的0判断完全可以写成if(var==0),而int型变量也可以写成if(!var),指针变量的判断也可以写成if(!var),上述写法虽然程序都能正确运行,但是未能清晰地表达程序的意思。 
一般的,如果想让if判断一个变量的“真”、“假”,应直接使用if(var)、if(!var),表明其为“逻辑”判断;如果用if判断一个数值型变量(short、int、long等),应该用if(var==0),表明是与0进行“数值”上的比较;而判断指针则适宜用if(var==NULL),这是一种很好的编程习惯。
浮点型变量并不精确,所以不可将float变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。如果写成if (x == 0.0),则判为错,得0分。
试题2:以下为Windows NT下的32位C++程序,请计算sizeof的值
void Func ( char str[100] )
{
sizeof( str ) = ?
}
void *p = malloc( 100 );
sizeof ( p ) = ?
解答:
sizeof( str ) = 4
sizeof ( p ) = 4
剖析:
Func ( char str[100] )函数中数组名作为函数形参时,在函数体内,数组名失去了本身的内涵,仅仅只是一个指针;在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。
数组名的本质如下:
(1)数组名指代一种数据结构,这种数据结构就是数组;
例如:
char str[10];
cout << sizeof(str) << endl;
输出结果为10,str指代数据结构char[10]。
(2)数组名可以转换为指向其指代实体的指针,而且是一个指针常量,不能作自增、自减等操作,不能被修改;
char str[10]; 
str++; //编译出错,提示str不是左值
(3)数组名作为函数形参时,沦为普通指针。
Windows NT 32位平台下,指针的长度(占用内存的大小)为4字节,故sizeof( str ) 、sizeof ( p ) 都为4。
试题3:写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个。另外,当你写下面的代码时会发生什么事?
least = MIN(*p++, b);
解答:
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
MIN(*p++, b)会产生宏的副作用
剖析:
这个面试题主要考查面试者对宏定义的使用,宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对“参数”进行的是一对一的替换。
程序员对宏定义的使用要非常小心,特别要注意两个问题:
(1)谨慎地将宏定义中的“参数”和整个宏用用括弧括起来。所以,严格地讲,下述解答:
#define MIN(A,B) (A) <= (B) ? (A) : (B)
#define MIN(A,B) (A <= B ? A : B )
都应判0分;
(2)防止宏的副作用。
宏定义#define MIN(A,B) ((A) <= (B) ? (A) : (B))对MIN(*p++, b)的作用结果是:
((*p++) <= (b) ? (*p++) : (*p++))
这个表达式会产生副作用,指针p会作三次++自增操作。
除此之外,另一个应该判0分的解答是:
#define MIN(A,B) ((A) <= (B) ? (A) : (B)); 
这个解答在宏定义的后面加“;”,显示编写者对宏的概念模糊不清,只能被无情地判0分并被面试官淘汰。
试题4:为什么标准头文件都有类似以下的结构? 
#ifndef __INCvxWorksh
#define __INCvxWorksh 
#ifdef __cplusplus
extern "C" {
#endif 
/*...*/ 
#ifdef __cplusplus
}
#endif 
#endif /* __INCvxWorksh */
解答:
头文件中的编译宏
#ifndef __INCvxWorksh
#define __INCvxWorksh
#endif 
的作用是防止被重复引用。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在symbol库中的名字与C语言的不同。例如,假设某个函数的原型为: 
void foo(int x, int y);
该函数被C编译器编译后在symbol库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。_foo_int_int这样的名字包含了函数名和函数参数数量及类型信息,C++就是考这种机制来实现函数重载的。
为了实现C和C++的混合编程,C++提供了C连接交换指定符号extern "C"来解决名字匹配问题,函数声明前加上extern "C"后,则编译器就会按照C语言的方式将该函数编译为_foo,这样C语言中就可以调用C++的函数了。[img=12,12]file:///D:/鱼鱼软件/鱼鱼多媒体***本/temp/{C74A38C4-432E-4799-B54D-73E2CD3C5206}_arc_d[1].gif[/img] 
试题5:编写一个函数,作用是把一个char组成的字符串循环右移n个。比如原来是“abcdefghi”如果n=2,移位后应该是“hiabcdefgh” 
函数头是这样的:
//pStr是指向以'\0'结尾的字符串的指针
//steps是要求移动的n
void LoopMove ( char * pStr, int steps )
{
//请填充...
}
解答:
正确解答1:
void LoopMove ( char *pStr, int steps )
{
int n = strlen( pStr ) - steps;
char tmp[MAX_LEN]; 
strcpy ( tmp, pStr + n ); 
strcpy ( tmp + steps, pStr); 
*( tmp + strlen ( pStr ) ) = '\0';
strcpy( pStr, tmp );
}
正确解答2:
void LoopMove ( char *pStr, int steps )
{
int n = strlen( pStr ) - steps;
char tmp[MAX_LEN]; 
memcpy( tmp, pStr + n, steps ); 
memcpy(pStr + steps, pStr, n ); 
memcpy(pStr, tmp, steps ); 
}
剖析:
这个试题主要考查面试者对标准库函数的熟练程度,在需要的时候引用库函数可以很大程度上简化程序编写的工作量。
最频繁被使用的库函数包括:
(1) strcpy
(2) memcpy
(3) memset

收起阅读 »

iOS 面试题 八股文 1.2

12 怎样防止指针的越界使用问题?    必须让指针指向一个有效的内存地址,  1 防止数组越界  2 防止向一块内存中拷贝过多的内容  3 防止使用空...
继续阅读 »


12 怎样防止指针的越界使用问题? 

  必须让指针指向一个有效的内存地址, 

1 防止数组越界 

2 防止向一块内存中拷贝过多的内容 

3 防止使用空指针 

4 防止改变const修改的指针 

5 防止改变指向静态存储区的内容 

6 防止两次释放一个指针 

7 防止使用野指针. 

 

 

13 指针的类型转换? 

指针转换通常是指针类型和void * 类型之前进行强制转换,从而与期望或返回void指针的函数进行正确的交接. 

63static有什么用途?(请至少说明两种)
            1.限制变量的作用域
            2.设置变量的存储域
            7. 引用与指针有什么区别?
            1) 引用必须被初始化,指针不必。
            2) 引用初始化以后不能被改变,指针可以改变所指的对象。
            2) 不存在指向空值的引用,但是存在指向空值的指针。
            8. 描述实时系统的基本特性
            在特定时间内完成特定的任务,实时性与可靠性

64全局变量和局部变量在内存中是否有区别?如果有,是什么区别?
            全局变量储存在静态数据库,局部变量在堆栈
            10. 什么是平衡二叉树
            左右子树都是平衡二叉树且左右子树的深度差值的绝对值不大于1

65堆栈溢出一般是由什么原因导致的?
            没有回收垃圾资源
            12. 什么函数不能声明为虚函数?
            constructor
            13. 冒泡排序算法的时间复杂度是什么?
            O(n^2)
            14. 写出float x 与“零值”比较的if语句。
            if(x>0.000001&&x<-0.000001)
            16. Internet采用哪种网络协议?该协议的主要层次结构?
            tcp/ip 应用层/传输层/网络层/数据链路层/物理层
            17. Internet物理地址和IP地址转换采用什么协议?
            ARP (Address Resolution Protocol)(地址解析協議)
            18.IP地址的编码分为哪俩部分?
            IP地址由两部分组成,网络号和主机号。不过是要和“子网掩码”按位与上之后才能区
            分哪些是网络位哪些是主机位。
            2.用户输入M,N值,从1至N开始顺序循环数数,每数到M输出该数值,直至全部输出。写
            出C程序。
            循环链表,用取余操作做
            3.不能做switch()的参数类型是:
            switch的参数不能为实型。
            華為
            1、局部变量能否和全局变量重名?
            答:能,局部会屏蔽全局。要用全局变量,需要使用"::"
            局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而
            不会用到全局变量。对于有些编译器而言,在同一个函数内可以定义多个同名的局部变
            量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那
            个循环体内
            2、如何引用一个已经定义过的全局变量?
            答:extern
            可以用引用头文件的方式,也可以用extern关键字,如果用引用头文件方式来引用某个
            在头文件中声明的全局变理,假定你将那个变写错了,那么在编译期间会报错,如果你
            用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在连接期
            间报错
            3、全局变量可不可以定义在可被多个.C文件包含的头文件中?为什么?
            答:可以,在不同的C文件中以static形式来声明同名全局变量。
            可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋
            初值,此时连接不会出错
            4、语句for( ;1 ;)有什么问题?它是什么意思?
            答:和while(1)相同。
            5、do……while和while……do有什么区别?
            答:前一个循环一遍再判断,后一个判断以后再循环

661.IP Phone的原理是什么?
            IPV6
            2.TCP/IP通信建立的过程怎样,端口有什么作用?
            三次握手,确定是哪个应用程序使用该协议
            3.1号信令和7号信令有什么区别,我国某前广泛使用的是那一种?
            4.列举5种以上的电话新业务?
            微软亚洲技术中心的面试题!!!
            1.进程和线程的差别。
            线程是指进程内的一个执行单元,也是进程内的可调度实体.
            与进程的区别:
            (1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
            (2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
            (3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属
            于进程的资源.
            (4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开
            销明显大于创建或撤消线程时的开销。
            2.测试方法
            人工测试:个人复查、抽查和会审
            机器测试:黑盒测试和白盒测试
            2.Heap与stack的差别。
            Heap是堆,stack是栈。
            Stack的空间由操作系统自动分配/释放,Heap上的空间手动分配/释放。
            Stack空间有限,Heap是很大的自由存储区
            C中的malloc函数分配的内存空间即在堆上,C++中对应的是new操作符。
            程序在编译期对变量和函数分配内存都在栈上进行,且程序运行过程中函数调用时参数的
            传递也在栈上进行
            3.Windows下的内存是如何管理的?
            4.介绍.Net和.Net的安全性。
            5.客户端如何访问.Net组件实现Web Service?
            6.C/C++编译器中虚表是如何完成的?
            7.谈谈COM的线程模型。然后讨论进程内/外组件的差别。
            8.谈谈IA32下的分页机制
            小页(4K)两级分页模式,大页(4M)一级
            9.给两个变量,如何找出一个带环单链表中是什么地方出现环的?
            一个递增一,一个递增二,他们指向同一个接点时就是环出现的地方
            10.在IA32中一共有多少种办法从用户态跳到内核态?
            通过调用门,从ring3到ring0,中断从ring3到ring0,进入vm86等等
            11.如果只想让程序有一个实例运行,不能运行两个。像winamp一样,只能开一个窗
            口,怎样实现?
            用内存映射或全局原子(互斥变量)、查找窗口句柄..
            FindWindow,互斥,写标志到文件或注册表,共享内存。

67如何截取键盘的响应,让所有的‘a’变成‘b’?

            键盘钩子SetWindowsHookEx
            13.Apartment在COM中有什么用?为什么要引入?
            14.存储过程是什么?有什么用?有什么优点?
            我的理解就是一堆sql的集合,可以建立非常复杂的查询,编译运行,所以运行一次后,
            以后再运行速度比单独执行SQL快很多
            15.Template有什么特点?什么时候用?
            16.谈谈Windows DNA结构的特点和优点。
            网络编程中设计并发服务器,使用多进程与多线程,请问有什么区别?
            1,进程:子进程是父进程的复制品。子进程获得父进程数据空间、堆和栈的复制品。
            2,线程:相对与进程而言,线程是一个更加接近与执行体的概念,它可以与同进程的其
            他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
            两者都可以提高程序的并发度,提高程序运行效率和响应时间。
            线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源管理和保护;而进程
            正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。
            思科

收起阅读 »

iOS 面试题 八股文 1.1

54多线程 多线程编程是防止主线程堵塞,增加运行效率等等的最佳方法。而原始的多线程方法存在很多的毛病,包括线程锁死等。在Cocoa中,Apple提供了NSOperation这个类,提供了一个优秀的多线程编程方法。 本次介绍NSOperation的子集,简易...
继续阅读 »


54多线程

多线程编程是防止主线程堵塞,增加运行效率等等的最佳方法。而原始的多线程方法存在很多的毛病,包括线程锁死等。在Cocoa中,Apple提供了NSOperation这个类,提供了一个优秀的多线程编程方法。

本次介绍NSOperation的子集,简易方法的NSInvocationOperation:

 

一个NSOperationQueue 操作队列,就相当于一个线程管理器,而非一个线程。因为你可以设置这个线程管理器内可以并行运行的的线程数量等等

55oc语法里的@perpoerty不用写@synzhesize了,自动填充了。并且的_name;

写方法时候不用提前声明。llvm 全局方法便利。

枚举类型。enum hello:Integer{  } 冒号后面直接可以跟类型,以前是:

enum hello{} 后面在指定为Integer .

桥接。ARC 自动release retain 的时候 CFString CFArray . Core Fountion. 加上桥接_brige  才能区分CFString 和NSString 而现在自动区分了,叫固定桥接。

 

下拉刷新封装好了。

UICollectionViewController. 可以把表格分成多列。

 

Social Framework(社交集成)

UIActivityViewController来询问用户的社交行为

 

缓存:就是存放在临时文件里,比如新浪微博请求的数据,和图片,下次请求看这里有没有值。

56Singleton(单例模式),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。 

代码如下: 

static ClassA *classA = nil;//静态的该类的实例 

+ (ClassA *)sharedManager 

{ 

@synchronized(self) { 

if (!classA) { 

classA = [[super allocWithZone:NULL]init]; 

return classA; 

} 

+ (id)allocWithZone:(NSZone *)zone { 

return [[self sharedManager] retain]; 

- (id)copyWithZone:(NSZone *)zone { 

return self; 

- (id)retain { 

return self; 

- (NSUIntger)retainCount { 

return NSUIntgerMax; 

- (oneway void)release { 

- (id)autorelease { 

return self; 

-(void)dealloc{ 

57请写一个C函数,若处理器是Big_endian的,则返回0;若是Little_endian的,则返回1 int checkCPU( ) {   

     {           

       union w      

            {        

                     int a;      

                     char b;         

             } c;             

            c.a = 1;    

        return  (c.b ==1);      

  } 

剖析:嵌入式系统开发者应该对Little-endian和Big-endian模式非常了解。采用Little-endian模式的CPU对操作数的存放方式是从低字节到高字节, Big-endian  模式的CPU对操作数的存放方式是从高字节到低字节。在弄清楚这个之前要弄清楚这个问题:字节从右到坐为从高到低! 假设从地址0x4000开始存放: 0x12345678,是也个32位四个字节的数据,最高字节是0x12,最低字节是0x78:在Little-endian模式CPU内存中的存放方式为: (高字节在高地址,低字节在低地址) 

内存地址0x4000 0x4001 0x4002 0x4003 

存放内容 0x78 0x56 0x34 0x12 

大端机则相反。 

 

有的处理器系统采用了小端方式进行数据存放,如Intel的奔腾。有的处理器系统采用了大端方式进行数据存放,如IBM半导体和Freescale的PowerPC处理器。不仅对于处理器,一些外设的设计中也存在着使用大端或者小端进行数据存放的选择。因此在一个处理器系统中,有可能存在大端和小端模式同时存在的现象。这一现象为系统的软硬件设计带来了不小的麻烦,这要求系统设计工程师,必须深入理解大端和小端模式的差别。大端与小端模式的差别体现在一个处理器的寄存器,指令集,系统总线等各个层次中。   联合体union的存放顺序是所有成员都从低地址开始存放的。以上是网上的原文。让我们看看在ARM处理器上union是如何存储的呢?   地址A ---------------- |A     |A+1   |A+2   |A+3    |int a; |      |         |         |          -------------------- |A     |char b; |      | ---------                                                                            如果是小端如何存储c.a的呢?  

                                         地址A ----------- 

------------------- |A    |A+1   |A+2    |A+3 | int a; 

|0x01 |0x00   |0x00   |0x00 | ------------------------------------- |A    |char b; |     | ---------                                  

                                如果是大端如何存储c.a的呢?   

  地址A --------------------- 

--------- |A      |A+1    |A+2     |A+3     |int a; |0x00   |0x00   |0x00    |0x01    | ------------------------------------------ |A      |char b; |       | ---------                                                                                                                                                        现在知道为什么c.b==0的话是大端,c.b==1的话就是小端了吧。

58

堆和栈上的指针 

指针所指向的这块内存是在哪里分配的,在堆上称为堆上的指针,在栈上为栈上的指针. 

在堆上的指针,可以保存在全局数据结构中,供不同函数使用访问同一块内存. 

在栈上的指针,在函数退出后,该内存即不可访问. 

59什么是指针的释放? 

具体来说包括两个概念. 

1 释放该指针指向的内存,只有堆上的内存才需要我们手工释放,栈上不需要. 

2 将该指针重定向为NULL. 

60数据结构中的指针? 

其实就是指向一块内存的地址,通过指针传递,可实现复杂的内存访问. 

7 函数指针? 

指向一块函数的入口地址. 

 

8 指针作为函数的参数? 

比如指向一个复杂数据结构的指针作为函数变量 

这种方法避免整个复杂数据类型内存的压栈出栈操作,提高效率. 

注意:指针本身不可变,但指针指向的数据结构可以改变. 

 

9 指向指针的指针? 

指针指向的变量是一个指针,即具体内容为一个指针的值,是一个地址. 

此时指针指向的变量长度也是4位. 

61指针与地址的区别? 

区别: 

1指针意味着已经有一个指针变量存在,他的值是一个地址,指针变量本身也存放在一个长度为四个字节的地址当中,而地址概念本身并不代表有任何变量存在. 

2 指针的值,如果没有限制,通常是可以变化的,也可以指向另外一个地址. 

   地址表示内存空间的一个位置点,他是用来赋给指针的,地址本身是没有大小概念,指针指向变量的大小,取决于地址后面存放的变量类型. 

62指针与数组名的关系? 





































































































  其值都是一个地址,但前者是可以移动的,后者是不可变的. 

收起阅读 »

iOS HTTP协议详解

HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中。  http(超文本传输协议)是一个基于请求与响应模式的、无状态...
继续阅读 »


HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中。

 http(超文本传输协议)是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式,HTTP1.1版本中给出一种持续连接的机制,绝大多数的Web开发,都是构建在HTTP协议之上的Web应用。
HTTP协议的主要特点可概括如下:
1.支持客户/服务器模式。
2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
3.灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
5.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

48URL

HTTP URL (URL是一种特殊类型的URI是他的子类,包含了用于查找某个资源的足够的信息)的格式如下:
http://host[":"port][abs_path ]
http表示要通过HTTP协议来定位网络资源;host表示合法的Internet主机域名或者IP地址;port指定一个端口号,为空则使用缺省端口80;abs_path指定请求资源的URI;如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。

49TCP/UDP区别联系

TCP---传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。 

UDP---用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快 

TCP(Transmission Control Protocol,传输控制协议)是基于连接的协议,也就是说,在正式收发数据前,必须和对方建立可靠的连接。一个TCP连接必须要经过三次“对话”才能建立起来,我们来看看这三次对话的简单过程:1.主机A向主机B发出连接请求数据包;2.主机B向主机A发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包;3.主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”,这是第三次对话。三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数据。 

UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去!  UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境。 

tcp协议和udp协议的差别 

是否连接面向连接面向非连接 

传输可靠性可靠不可靠 

应用场合传输大量数据少量数据 

速度慢快

50 socket 连接和 http 连接的区别

简单说,你浏览的网页(网址以http://开头)都是http协议传输到你的浏览器的, 而http是基于socket之上的。socket是一套完成tcp,udp协议的接口。

HTTP协议:简单对象访问协议,对应于应用层  ,HTTP协议是基于TCP连接的

tcp协议:    对应于传输层

ip协议:     对应于网络层 
TCP/IP是传输层协议,主要解决数据如何在网络中传输;而HTTP是应用层协议,主要解决如何包装数据。

Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。

http连接:http连接就是所谓的短连接,即客户端向服务器端发送一次请求,服务器端响应后连接即会断掉;

socket连接:socket连接就是所谓的长连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉;但是由于各种环境因素可能会是连接断开,比如说:服务器端或客户端主机down了,网络故障,或者两者之间长时间没有数据传输,网络防火墙可能会断开该连接以释放网络资源。所以当一个socket连接中没有数据的传输,那么为了维持连接需要发送心跳消息~~具体心跳消息格式是开发者自己定义的

我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP文本信息,然后使用TCP/IP做传输层协议将它发到网络上。
1)Socket是一个针对TCP和UDP编程的接口,你可以借助它建立TCP连接等等。而TCP和UDP协议属于传输层 。
  而http是个应用层的协议,它实际上也建立在TCP协议之上。 

 (HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。)

 2)Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。Socket的出现只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口。

51 什么是 TCP 连接的三次握手

第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)

52 利用 Socket 建立网络连接的步骤

建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket 。

套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。

1。服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。

2。客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。

3。连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

53进程与线程

进程(process)是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元。

进程中所包含的一个或多个执行单元称为线程(thread)。进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。

通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。

在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。

由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。

简而言之 , 一个程序至少有一个进程 , 一个进程至少有一个线程 .一个程序就是一个进程,而一个程序中的多个任务则被称为线程。

 线程只能归属于一个进程并且它只能访问该进程所拥有的资源。当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程。应用程序(application)是由一个或多个相互协作的进程组成的。

另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

收起阅读 »

Swift 协议

协议规定了用来实现某一特定功能所必需的方法和属性。任意能够满足协议要求的类型被称为遵循(conform)这个协议。类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。语法协议的语法格式如下:protocol SomeProtocol ...
继续阅读 »

协议规定了用来实现某一特定功能所必需的方法和属性。

任意能够满足协议要求的类型被称为遵循(conform)这个协议。

类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。

语法

协议的语法格式如下:

protocol SomeProtocol {
// 协议内容
}

要使类遵循某个协议,需要在类型名称后加上协议名称,中间以冒号:分隔,作为类型定义的一部分。遵循多个协议时,各协议之间用逗号,分隔。

struct SomeStructure: FirstProtocol, AnotherProtocol {
// 结构体内容
}

如果类在遵循协议的同时拥有父类,应该将父类名放在协议名之前,以逗号分隔。

class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 类的内容
}

对属性的规定

协议用于指定特定的实例属性或类属性,而不用指定是存储型属性或计算型属性。此外还必须指明是只读的还是可读可写的。

协议中的通常用var来声明变量属性,在类型声明后加上{ set get }来表示属性是可读可写的,只读属性则用{ get }来表示。

protocol classa {

var marks: Int { get set }
var result: Bool { get }

func attendance() -> String
func markssecured() -> String

}

protocol classb: classa {

var present: Bool { get set }
var subject: String { get set }
var stname: String { get set }

}

class classc: classb {
var marks = 96
let result = true
var present = false
var subject = "Swift 协议"
var stname = "Protocols"

func attendance() -> String {
return "The \(stname) has secured 99% attendance"
}

func markssecured() -> String {
return "\(stname) has scored \(marks)"
}
}

let studdet = classc()
studdet.stname = "Swift"
studdet.marks = 98
studdet.markssecured()

print(studdet.marks)
print(studdet.result)
print(studdet.present)
print(studdet.subject)
print(studdet.stname)

以上程序执行输出结果为:

98
true
false
Swift 协议
Swift

对 Mutating 方法的规定

有时需要在方法中改变它的实例。

例如,值类型(结构体,枚举)的实例方法中,将mutating关键字作为函数的前缀,写在func之前,表示可以在该方法中修改它所属的实例及其实例属性的值。

protocol daysofaweek {
mutating func show()
}

enum days: daysofaweek {
case sun, mon, tue, wed, thurs, fri, sat
mutating func show() {
switch self {
case .sun:
self = .sun
print("Sunday")
case .mon:
self = .mon
print("Monday")
case .tue:
self = .tue
print("Tuesday")
case .wed:
self = .wed
print("Wednesday")
case .thurs:
self = .thurs
print("Wednesday")
case .fri:
self = .fri
print("Firday")
case .sat:
self = .sat
print("Saturday")
default:
print("NO Such Day")
}
}
}

var res = days.wed
res.show()

以上程序执行输出结果为:

Wednesday

对构造器的规定

协议可以要求它的遵循者实现指定的构造器。

你可以像书写普通的构造器那样,在协议的定义里写下构造器的声明,但不需要写花括号和构造器的实体,语法如下:

protocol SomeProtocol {
init(someParameter: Int)
}

实例

protocol tcpprotocol {
init(aprot: Int)
}

协议构造器规定在类中的实现

你可以在遵循该协议的类中实现构造器,并指定其为类的指定构造器或者便利构造器。在这两种情况下,你都必须给构造器实现标上"required"修饰符:

class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// 构造器实现
}
}

protocol tcpprotocol {
init(aprot: Int)
}

class tcpClass: tcpprotocol {
required init(aprot: Int) {
}
}

使用required修饰符可以保证:所有的遵循该协议的子类,同样能为构造器规定提供一个显式的实现或继承实现。

如果一个子类重写了父类的指定构造器,并且该构造器遵循了某个协议的规定,那么该构造器的实现需要被同时标示required和override修饰符:

protocol tcpprotocol {
init(no1: Int)
}

class mainClass {
var no1: Int // 局部变量
init(no1: Int) {
self.no1 = no1 // 初始化
}
}

class subClass: mainClass, tcpprotocol {
var no2: Int
init(no1: Int, no2 : Int) {
self.no2 = no2
super.init(no1:no1)
}
// 因为遵循协议,需要加上"required"; 因为继承自父类,需要加上"override"
required override convenience init(no1: Int) {
self.init(no1:no1, no2:0)
}
}
let res = mainClass(no1: 20)
let show = subClass(no1: 30, no2: 50)

print("res is: \(res.no1)")
print("res is: \(show.no1)")
print("res is: \(show.no2)")

以上程序执行输出结果为:

res is: 20
res is: 30
res is: 50

协议类型

尽管协议本身并不实现任何功能,但是协议可以被当做类型来使用。

协议可以像其他普通类型一样使用,使用场景:

  • 作为函数、方法或构造器中的参数类型或返回值类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中的元素类型

实例

protocol Generator {
associatedtype members
func next() -> members?
}

var items = [10,20,30].makeIterator()
while let x = items.next() {
print(x)
}

for lists in [1,2,3].map( {i in i*5}) {
print(lists)
}

print([100,200,300])
print([1,2,3].map({i in i*10}))

以上程序执行输出结果为:

10
20
30
5
10
15
[100, 200, 300]
[10, 20, 30]

在扩展中添加协议成员

我们可以可以通过扩展来扩充已存在类型( 类,结构体,枚举等)。

扩展可以为已存在的类型添加属性,方法,下标脚本,协议等成员。

protocol AgeClasificationProtocol {
var age: Int { get }
func agetype() -> String
}

class Person {
let firstname: String
let lastname: String
var age: Int
init(firstname: String, lastname: String) {
self.firstname = firstname
self.lastname = lastname
self.age = 10
}
}

extension Person : AgeClasificationProtocol {
func fullname() -> String {
var c: String
c = firstname + " " + lastname
return c
}

func agetype() -> String {
switch age {
case 0...2:
return "Baby"
case 2...12:
return "Child"
case 13...19:
return "Teenager"
case let x where x > 65:
return "Elderly"
default:
return "Normal"
}
}
}

协议的继承

协议能够继承一个或多个其他协议,可以在继承的协议基础上增加新的内容要求。

协议的继承语法与类的继承相似,多个被继承的协议间用逗号分隔:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// 协议定义
}

实例

protocol Classa {
var no1: Int { get set }
func calc(sum: Int)
}

protocol Result {
func print(target: Classa)
}

class Student2: Result {
func print(target: Classa) {
target.calc(1)
}
}

class Classb: Result {
func print(target: Classa) {
target.calc(5)
}
}

class Student: Classa {
var no1: Int = 10

func calc(sum: Int) {
no1 -= sum
print("学生尝试 \(sum) 次通过")

if no1 <= 0 {
print("学生缺席考试")
}
}
}

class Player {
var stmark: Result!

init(stmark: Result) {
self.stmark = stmark
}

func print(target: Classa) {
stmark.print(target)
}
}

var marks = Player(stmark: Student2())
var marksec = Student()

marks.print(marksec)
marks.print(marksec)
marks.print(marksec)
marks.stmark = Classb()
marks.print(marksec)
marks.print(marksec)
marks.print(marksec)

以上程序执行输出结果为:

学生尝试 1 次通过
学生尝试 1 次通过
学生尝试 1 次通过
学生尝试 5 次通过
学生尝试 5 次通过
学生缺席考试
学生尝试 5 次通过
学生缺席考试

类专属协议

你可以在协议的继承列表中,通过添加class关键字,限制协议只能适配到类(class)类型。

该class关键字必须是第一个出现在协议的继承列表中,其后,才是其他继承协议。格式如下:

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
// 协议定义
}

实例

protocol TcpProtocol {
init(no1: Int)
}

class MainClass {
var no1: Int // 局部变量
init(no1: Int) {
self.no1 = no1 // 初始化
}
}

class SubClass: MainClass, TcpProtocol {
var no2: Int
init(no1: Int, no2 : Int) {
self.no2 = no2
super.init(no1:no1)
}
// 因为遵循协议,需要加上"required"; 因为继承自父类,需要加上"override"
required override convenience init(no1: Int) {
self.init(no1:no1, no2:0)
}
}

let res = MainClass(no1: 20)
let show = SubClass(no1: 30, no2: 50)

print("res is: \(res.no1)")
print("res is: \(show.no1)")
print("res is: \(show.no2)")

以上程序执行输出结果为:

res is: 20
res is: 30
res is: 50

协议合成

Swift 支持合成多个协议,这在我们需要同时遵循多个协议时非常有用。

语法格式如下:

protocol Stname {
var name: String { get }
}

protocol Stage {
var age: Int { get }
}

struct Person: Stname, Stage {
var name: String
var age: Int
}

func show(celebrator: Stname & Stage) {
print("\(celebrator.name) is \(celebrator.age) years old")
}

let studname = Person(name: "Priya", age: 21)
print(studname)

let stud = Person(name: "Rehan", age: 29)
print(stud)

let student = Person(name: "Roshan", age: 19)
print(student)

以上程序执行输出结果为:

Person(name: "Priya", age: 21)
Person(name: "Rehan", age: 29)
Person(name: "Roshan", age: 19)

检验协议的一致性

你可以使用is和as操作符来检查是否遵循某一协议或强制转化为某一类型。

  • is操作符用来检查实例是否遵循了某个协议
  • as?返回一个可选值,当实例遵循协议时,返回该协议类型;否则返回nil
  • as用以强制向下转型,如果强转失败,会引起运行时错误。

实例

下面的例子定义了一个 HasArea 的协议,要求有一个Double类型可读的 area:

protocol HasArea {
var area: Double { get }
}

// 定义了Circle类,都遵循了HasArea协议
class Circle: HasArea {
let pi = 3.1415927
var radius: Double
var area: Double { return pi * radius * radius }
init(radius: Double) { self.radius = radius }
}

// 定义了Country类,都遵循了HasArea协议
class Country: HasArea {
var area: Double
init(area: Double) { self.area = area }
}

// Animal是一个没有实现HasArea协议的类
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}

let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]

for object in objects {
// 对迭代出的每一个元素进行检查,看它是否遵循了HasArea协议
if let objectWithArea = object as? HasArea {
print("面积为 \(objectWithArea.area)")
} else {
print("没有面积")
}
}

以上程序执行输出结果为:

面积为 12.5663708
面积为 243610.0
没有面积
收起阅读 »

Swift 扩展

扩展就是向一个已有的类、结构体或枚举类型添加新功能。扩展可以对一个类型添加新的功能,但是不能重写已有的功能。Swift 中的扩展可以:添加计算型属性和计算型静态属性定义实例方法和类型方法提供新的构造器定义下标定义和使用新的嵌套类型使一个已有类型符合某个协议语法...
继续阅读 »

扩展就是向一个已有的类、结构体或枚举类型添加新功能。

扩展可以对一个类型添加新的功能,但是不能重写已有的功能。

Swift 中的扩展可以:

  • 添加计算型属性和计算型静态属性
  • 定义实例方法和类型方法
  • 提供新的构造器
  • 定义下标
  • 定义和使用新的嵌套类型
  • 使一个已有类型符合某个协议

语法

扩展声明使用关键字 extension

extension SomeType {
// 加到SomeType的新功能写到这里
}

一个扩展可以扩展一个已有类型,使其能够适配一个或多个协议,语法格式如下:

extension SomeType: SomeProtocol, AnotherProctocol {
// 协议实现写到这里
}

计算型属性

扩展可以向已有类型添加计算型实例属性和计算型类型属性。

实例

下面的例子向 Int 类型添加了 5 个计算型实例属性并扩展其功能:

extension Int {
var add: Int {return self + 100 }
var sub: Int { return self - 10 }
var mul: Int { return self * 10 }
var div: Int { return self / 5 }
}

let addition = 3.add
print("加法运算后的值:\(addition)")

let subtraction = 120.sub
print("减法运算后的值:\(subtraction)")

let multiplication = 39.mul
print("乘法运算后的值:\(multiplication)")

let division = 55.div
print("除法运算后的值: \(division)")

let mix = 30.add + 34.sub
print("混合运算结果:\(mix)")

以上程序执行输出结果为:

加法运算后的值:103
减法运算后的值:110
乘法运算后的值:390
除法运算后的值: 11
混合运算结果:154

构造器

扩展可以向已有类型添加新的构造器。

这可以让你扩展其它类型,将你自己的定制类型作为构造器参数,或者提供该类型的原始实现中没有包含的额外初始化选项。

扩展可以向类中添加新的便利构造器 init(),但是它们不能向类中添加新的指定构造器或析构函数 deinit() 。

struct sum {
var num1 = 100, num2 = 200
}

struct diff {
var no1 = 200, no2 = 100
}

struct mult {
var a = sum()
var b = diff()
}


extension mult
{
init
(x: sum, y: diff) {
_
= x.num1 + x.num2
_
= y.no1 + y.no2
}
}


let a = sum(num1: 100, num2: 200)
let b = diff(no1: 200, no2: 100)

let getMult = mult(x: a, y: b)
print("getMult sum\(getMult.a.num1, getMult.a.num2)")
print("getMult diff\(getMult.b.no1, getMult.b.no2)")

以上程序执行输出结果为:

getMult sum(100, 200)
getMult diff
(200, 100)

方法

扩展可以向已有类型添加新的实例方法和类型方法。

下面的例子向Int类型添加一个名为 topics 的新实例方法:

extension Int {
func topics
(summation: () -> ()) {
for _ in 0..<self {
summation
()
}
}
}

4.topics({
print("扩展模块内")
})

3.topics({
print("内型转换模块内")
})

以上程序执行输出结果为:

扩展模块内
扩展模块内
扩展模块内
扩展模块内
内型转换模块内
内型转换模块内
内型转换模块内

这个topics方法使用了一个() -> ()类型的单参数,表明函数没有参数而且没有返回值。

定义该扩展之后,你就可以对任意整数调用 topics 方法,实现的功能则是多次执行某任务:


可变实例方法

通过扩展添加的实例方法也可以修改该实例本身。

结构体和枚举类型中修改self或其属性的方法必须将该实例方法标注为mutating,正如来自原始实现的修改方法一样。

实例

下面的例子向 Swift 的 Double 类型添加了一个新的名为 square 的修改方法,来实现一个原始值的平方计算:

extension Double {
mutating func square
() {
let pi = 3.1415
self = pi * self * self
}
}

var Trial1 = 3.3
Trial1.square()
print("圆的面积为: \(Trial1)")


var Trial2 = 5.8
Trial2.square()
print("圆的面积为: \(Trial2)")


var Trial3 = 120.3
Trial3.square()
print("圆的面积为: \(Trial3)")

以上程序执行输出结果为:

圆的面积为: 34.210935
圆的面积为: 105.68006
圆的面积为: 45464.070735

下标

扩展可以向一个已有类型添加新下标。

实例

以下例子向 Swift 内建类型Int添加了一个整型下标。该下标[n]返回十进制数字

extension Int {
subscript
(var multtable: Int) -> Int {
var no1 = 1
while multtable > 0 {
no1
*= 10
--multtable
}
return (self / no1) % 10
}
}

print(12[0])
print(7869[1])
print(786543[2])

以上程序执行输出结果为:

2
6
5

嵌套类型

扩展可以向已有的类、结构体和枚举添加新的嵌套类型:

extension Int {
enum calc
{
case add
case sub
case mult
case div
case anything
}

var print: calc {
switch self
{
case 0:
return .add
case 1:
return .sub
case 2:
return .mult
case 3:
return .div
default:
return .anything
}
}
}

func result
(numb: [Int]) {
for i in numb {
switch i.print {
case .add:
print(" 10 ")
case .sub:
print(" 20 ")
case .mult:
print(" 30 ")
case .div:
print(" 40 ")
default:
print(" 50 ")

}
}
}

result
([0, 1, 2, 3, 4, 7])

以上程序执行输出结果为:

 10 
20
30
40
50
50

1 篇笔记 写笔记

  1.    沉迷打码小凳子

      100***8089@qq.com

       参考地址

    1

    扩展下标文中的代码对于较高版本的swift可能会报错:

    'var' in this position is interpreted as an argument label
    Left side of mutating operator isn't mutable: 'multtable' is immutable

    验证了写法,这样写可以避免问题:

    extension Int{
    subscript
    (digitIndex:Int)->Int{
    var decimalBase = 1
    var digit = digitIndex
    // 不能直接使用digitIndex,会报错
    while digit > 0 {
    decimalBase
    *= 10
    digit
    = digit - 1
    }
    return (self/decimalBase) % 10
    }
    }

    print(12[0])
    print(7869[1])
    print(786543[2])

    参考了网上的写法,还可以这样写:

    extension Int{
    subscript
    (digitIndex:Int)->Int{

    var decimalBase = 1
    for _ in 0 ..< digitIndex{
    decimalBase
    *= 10
    }
    return (self/decimalBase) % 10
    }
    }
    print(12[0])
    print(7869[1])
    print(786543[2])
收起阅读 »

Swift 类型转换

Swift 语言类型转换可以判断实例的类型。也可以用于检测实例类型是否属于其父类或者子类的实例。Swift 中类型转换使用 is 和 as 操作符实现,is 用于检测值的类型,as 用于转换类型。类型转换也可以用来检查一个类是否实现了某个协议。定义一个类层次以...
继续阅读 »

Swift 语言类型转换可以判断实例的类型。也可以用于检测实例类型是否属于其父类或者子类的实例。

Swift 中类型转换使用 is 和 as 操作符实现,is 用于检测值的类型,as 用于转换类型。

类型转换也可以用来检查一个类是否实现了某个协议。


定义一个类层次

以下定义了三个类:Subjects、Chemistry、Maths,Chemistry 和 Maths 继承了 Subjects。

代码如下:

class Subjects {
var physics: String
init(physics: String) {
self.physics = physics
}
}

class Chemistry: Subjects {
var equations: String
init(physics: String, equations: String) {
self.equations = equations
super.init(physics: physics)
}
}

class Maths: Subjects {
var formulae: String
init(physics: String, formulae: String) {
self.formulae = formulae
super.init(physics: physics)
}
}

let sa = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫")]


let samplechem = Chemistry(physics: "固体物理", equations: "赫兹")
print("实例物理学是: \(samplechem.physics)")
print("实例方程式: \(samplechem.equations)")


let samplemaths = Maths(physics: "流体动力学", formulae: "千兆赫")
print("实例物理学是: \(samplemaths.physics)")
print("实例公式是: \(samplemaths.formulae)")

以上程序执行输出结果为:

实例物理学是: 固体物理
实例方程式: 赫兹
实例物理学是: 流体动力学
实例公式是: 千兆赫

检查类型

类型转换用于检测实例类型是否属于特定的实例类型。

你可以将它用在类和子类的层次结构上,检查特定类实例的类型并且转换这个类实例的类型成为这个层次结构中的其他类型。

类型检查使用 is 关键字。

操作符 is 来检查一个实例是否属于特定子类型。若实例属于那个子类型,类型检查操作符返回 true,否则返回 false。

class Subjects {
var physics: String
init(physics: String) {
self.physics = physics
}
}

class Chemistry: Subjects {
var equations: String
init(physics: String, equations: String) {
self.equations = equations
super.init(physics: physics)
}
}

class Maths: Subjects {
var formulae: String
init(physics: String, formulae: String) {
self.formulae = formulae
super.init(physics: physics)
}
}

let sa = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫"),
Chemistry(physics: "热物理学", equations: "分贝"),
Maths(physics: "天体物理学", formulae: "兆赫"),
Maths(physics: "微分方程", formulae: "余弦级数")]


let samplechem = Chemistry(physics: "固体物理", equations: "赫兹")
print("实例物理学是: \(samplechem.physics)")
print("实例方程式: \(samplechem.equations)")


let samplemaths = Maths(physics: "流体动力学", formulae: "千兆赫")
print("实例物理学是: \(samplemaths.physics)")
print("实例公式是: \(samplemaths.formulae)")

var chemCount = 0
var mathsCount = 0
for item in sa {
// 如果是一个 Chemistry 类型的实例,返回 true,相反返回 false。
if item is Chemistry {
++chemCount
} else if item is Maths {
++mathsCount
}
}

print("化学科目包含 \(chemCount) 个主题,数学包含 \(mathsCount) 个主题")

以上程序执行输出结果为:

实例物理学是: 固体物理
实例方程式: 赫兹
实例物理学是: 流体动力学
实例公式是: 千兆赫
化学科目包含 2 个主题,数学包含 3 个主题

向下转型

向下转型,用类型转换操作符(as? 或 as!)

当你不确定向下转型可以成功时,用类型转换的条件形式(as?)。条件形式的类型转换总是返回一个可选值(optional value),并且若下转是不可能的,可选值将是 nil。

只有你可以确定向下转型一定会成功时,才使用强制形式(as!)。当你试图向下转型为一个不正确的类型时,强制形式的类型转换会触发一个运行时错误。

class Subjects {
var physics: String
init(physics: String) {
self.physics = physics
}
}

class Chemistry: Subjects {
var equations: String
init(physics: String, equations: String) {
self.equations = equations
super.init(physics: physics)
}
}

class Maths: Subjects {
var formulae: String
init(physics: String, formulae: String) {
self.formulae = formulae
super.init(physics: physics)
}
}

let sa = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫"),
Chemistry(physics: "热物理学", equations: "分贝"),
Maths(physics: "天体物理学", formulae: "兆赫"),
Maths(physics: "微分方程", formulae: "余弦级数")]


let samplechem = Chemistry(physics: "固体物理", equations: "赫兹")
print("实例物理学是: \(samplechem.physics)")
print("实例方程式: \(samplechem.equations)")


let samplemaths = Maths(physics: "流体动力学", formulae: "千兆赫")
print("实例物理学是: \(samplemaths.physics)")
print("实例公式是: \(samplemaths.formulae)")

var chemCount = 0
var mathsCount = 0

for item in sa {
// 类型转换的条件形式
if let show = item as? Chemistry {
print("化学主题是: '\(show.physics)', \(show.equations)")
// 强制形式
} else if let example = item as? Maths {
print("数学主题是: '\(example.physics)', \(example.formulae)")
}
}

以上程序执行输出结果为:

实例物理学是: 固体物理
实例方程式: 赫兹
实例物理学是: 流体动力学
实例公式是: 千兆赫
化学主题是: '固体物理', 赫兹
数学主题是: '流体动力学', 千兆赫
化学主题是: '热物理学', 分贝
数学主题是: '天体物理学', 兆赫
数学主题是: '微分方程', 余弦级数

Any和AnyObject的类型转换

Swift为不确定类型提供了两种特殊类型别名:

  • AnyObject可以代表任何class类型的实例。
  • Any可以表示任何类型,包括方法类型(function types)。

注意:
只有当你明确的需要它的行为和功能时才使用AnyAnyObject。在你的代码里使用你期望的明确的类型总是更好的。

Any 实例

class Subjects {
var physics: String
init(physics: String) {
self.physics = physics
}
}

class Chemistry: Subjects {
var equations: String
init(physics: String, equations: String) {
self.equations = equations
super.init(physics: physics)
}
}

class Maths: Subjects {
var formulae: String
init(physics: String, formulae: String) {
self.formulae = formulae
super.init(physics: physics)
}
}

let sa = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫"),
Chemistry(physics: "热物理学", equations: "分贝"),
Maths(physics: "天体物理学", formulae: "兆赫"),
Maths(physics: "微分方程", formulae: "余弦级数")]


let samplechem = Chemistry(physics: "固体物理", equations: "赫兹")
print("实例物理学是: \(samplechem.physics)")
print("实例方程式: \(samplechem.equations)")


let samplemaths = Maths(physics: "流体动力学", formulae: "千兆赫")
print("实例物理学是: \(samplemaths.physics)")
print("实例公式是: \(samplemaths.formulae)")

var chemCount = 0
var mathsCount = 0

for item in sa {
// 类型转换的条件形式
if let show = item as? Chemistry {
print("化学主题是: '\(show.physics)', \(show.equations)")
// 强制形式
} else if let example = item as? Maths {
print("数学主题是: '\(example.physics)', \(example.formulae)")
}
}

// 可以存储Any类型的数组 exampleany
var exampleany = [Any]()

exampleany.append(12)
exampleany.append(3.14159)
exampleany.append("Any 实例")
exampleany.append(Chemistry(physics: "固体物理", equations: "兆赫"))

for item2 in exampleany {
switch item2 {
case let someInt as Int:
print("整型值为 \(someInt)")
case let someDouble as Double where someDouble > 0:
print("Pi 值为 \(someDouble)")
case let someString as String:
print("\(someString)")
case let phy as Chemistry:
print("主题 '\(phy.physics)', \(phy.equations)")
default:
print("None")
}
}

以上程序执行输出结果为:

实例物理学是: 固体物理
实例方程式: 赫兹
实例物理学是: 流体动力学
实例公式是: 千兆赫
化学主题是: '固体物理', 赫兹
数学主题是: '流体动力学', 千兆赫
化学主题是: '热物理学', 分贝
数学主题是: '天体物理学', 兆赫
数学主题是: '微分方程', 余弦级数
整型值为 12
Pi 值为 3.14159
Any 实例
主题 '固体物理', 兆赫

AnyObject 实例

class Subjects {
var physics: String
init(physics: String) {
self.physics = physics
}
}

class Chemistry: Subjects {
var equations: String
init(physics: String, equations: String) {
self.equations = equations
super.init(physics: physics)
}
}

class Maths: Subjects {
var formulae: String
init(physics: String, formulae: String) {
self.formulae = formulae
super.init(physics: physics)
}
}

// [AnyObject] 类型的数组
let saprint: [AnyObject] = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫"),
Chemistry(physics: "热物理学", equations: "分贝"),
Maths(physics: "天体物理学", formulae: "兆赫"),
Maths(physics: "微分方程", formulae: "余弦级数")]


let samplechem = Chemistry(physics: "固体物理", equations: "赫兹")
print("实例物理学是: \(samplechem.physics)")
print("实例方程式: \(samplechem.equations)")


let samplemaths = Maths(physics: "流体动力学", formulae: "千兆赫")
print("实例物理学是: \(samplemaths.physics)")
print("实例公式是: \(samplemaths.formulae)")

var chemCount = 0
var mathsCount = 0

for item in saprint {
// 类型转换的条件形式
if let show = item as? Chemistry {
print("化学主题是: '\(show.physics)', \(show.equations)")
// 强制形式
} else if let example = item as? Maths {
print("数学主题是: '\(example.physics)', \(example.formulae)")
}
}

var exampleany = [Any]()
exampleany.append(12)
exampleany.append(3.14159)
exampleany.append("Any 实例")
exampleany.append(Chemistry(physics: "固体物理", equations: "兆赫"))

for item2 in exampleany {
switch item2 {
case let someInt as Int:
print("整型值为 \(someInt)")
case let someDouble as Double where someDouble > 0:
print("Pi 值为 \(someDouble)")
case let someString as String:
print("\(someString)")
case let phy as Chemistry:
print("主题 '\(phy.physics)', \(phy.equations)")
default:
print("None")
}
}

以上程序执行输出结果为:

实例物理学是: 固体物理
实例方程式: 赫兹
实例物理学是: 流体动力学
实例公式是: 千兆赫
化学主题是: '固体物理', 赫兹
数学主题是: '流体动力学', 千兆赫
化学主题是: '热物理学', 分贝
数学主题是: '天体物理学', 兆赫
数学主题是: '微分方程', 余弦级数
整型值为 12
Pi 值为 3.14159
Any 实例
主题 '固体物理', 兆赫

在一个switch语句的case中使用强制形式的类型转换操作符(as, 而不是 as?)来检查和转换到一个明确的类型。

收起阅读 »

Swift 自动引用计数(ARC)

Swift 使用自动引用计数(ARC)这一机制来跟踪和管理应用程序的内存通常情况下我们不需要去手动释放内存,因为 ARC 会在类的实例不再被使用时,自动释放其占用的内存。但在有些时候我们还是需要在代码中实现内存管理。ARC 功能当每次使用 init() 方法创...
继续阅读 »

Swift 使用自动引用计数(ARC)这一机制来跟踪和管理应用程序的内存

通常情况下我们不需要去手动释放内存,因为 ARC 会在类的实例不再被使用时,自动释放其占用的内存。

但在有些时候我们还是需要在代码中实现内存管理。

ARC 功能

  • 当每次使用 init() 方法创建一个类的新的实例的时候,ARC 会分配一大块内存用来储存实例的信息。

  • 内存中会包含实例的类型信息,以及这个实例所有相关属性的值。

  • 当实例不再被使用时,ARC 释放实例所占用的内存,并让释放的内存能挪作他用。

  • 为了确保使用中的实例不会被销毁,ARC 会跟踪和计算每一个实例正在被多少属性,常量和变量所引用。

  • 实例赋值给属性、常量或变量,它们都会创建此实例的强引用,只要强引用还在,实例是不允许被销毁的。

ARC 实例

class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) 开始初始化")
}
deinit {
print("\(name) 被析构")
}
}

// 值会被自动初始化为nil,目前还不会引用到Person类的实例
var reference1: Person?
var reference2: Person?
var reference3: Person?

// 创建Person类的新实例
reference1 = Person(name: "Runoob")


//赋值给其他两个变量,该实例又会多出两个强引用
reference2 = reference1
reference3 = reference1

//断开第一个强引用
reference1 = nil
//断开第二个强引用
reference2 = nil
//断开第三个强引用,并调用析构函数
reference3 = nil

以上程序执行输出结果为:

Runoob 开始初始化
Runoob 被析构

类实例之间的循环强引用

在上面的例子中,ARC 会跟踪你所新创建的 Person 实例的引用数量,并且会在 Person 实例不再被需要时销毁它。

然而,我们可能会写出这样的代码,一个类永远不会有0个强引用。这种情况发生在两个类实例互相保持对方的强引用,并让对方不被销毁。这就是所谓的循环强引用。

实例

下面展示了一个不经意产生循环强引用的例子。例子定义了两个类:Person和Apartment,用来建模公寓和它其中的居民:

class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) 被析构") }
}

class Apartment {
let number: Int
init(number: Int) { self.number = number }
var tenant: Person?
deinit { print("Apartment #\(number) 被析构") }
}

// 两个变量都被初始化为nil
var runoob: Person?
var number73: Apartment?

// 赋值
runoob = Person(name: "Runoob")
number73 = Apartment(number: 73)

// 意感叹号是用来展开和访问可选变量 runoob 和 number73 中的实例
// 循环强引用被创建
runoob!.apartment = number73
number73!.tenant = runoob

// 断开 runoob 和 number73 变量所持有的强引用时,引用计数并不会降为 0,实例也不会被 ARC 销毁
// 注意,当你把这两个变量设为nil时,没有任何一个析构函数被调用。
// 强引用循环阻止了Person和Apartment类实例的销毁,并在你的应用程序中造成了内存泄漏
runoob = nil
number73 = nil

解决实例之间的循环强引用

Swift 提供了两种办法用来解决你在使用类的属性时所遇到的循环强引用问题:

  • 弱引用
  • 无主引用

弱引用和无主引用允许循环引用中的一个实例引用另外一个实例而不保持强引用。这样实例能够互相引用而不产生循环强引用。

对于生命周期中会变为nil的实例使用弱引用。相反的,对于初始化赋值后再也不会被赋值为nil的实例,使用无主引用。

弱引用实例

class Module {
let name: String
init(name: String) { self.name = name }
var sub: SubModule?
deinit { print("\(name) 主模块") }
}

class SubModule {
let number: Int

init(number: Int) { self.number = number }

weak var topic: Module?

deinit { print("子模块 topic 数为 \(number)") }
}

var toc: Module?
var list: SubModule?
toc = Module(name: "ARC")
list = SubModule(number: 4)
toc!.sub = list
list!.topic = toc

toc = nil
list = nil

以上程序执行输出结果为:

ARC 主模块
子模块 topic 数为 4

无主引用实例

class Student {
let name: String
var section: Marks?

init(name: String) {
self.name = name
}

deinit { print("\(name)") }
}
class Marks {
let marks: Int
unowned let stname: Student

init(marks: Int, stname: Student) {
self.marks = marks
self.stname = stname
}

deinit { print("学生的分数为 \(marks)") }
}

var module: Student?
module = Student(name: "ARC")
module!.section = Marks(marks: 98, stname: module!)
module = nil

以上程序执行输出结果为:

ARC
学生的分数为 98

闭包引起的循环强引用

循环强引用还会发生在当你将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了实例。这个闭包体中可能访问了实例的某个属性,例如self.someProperty,或者闭包中调用了实例的某个方法,例如self.someMethod。这两种情况都导致了闭包 "捕获" self,从而产生了循环强引用。

实例

下面的例子为你展示了当一个闭包引用了self后是如何产生一个循环强引用的。例子中定义了一个叫HTMLElement的类,用一种简单的模型表示 HTML 中的一个单独的元素:

class HTMLElement {

let name: String
let text: String?

lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}

init(name: String, text: String? = nil) {
self.name = name
self.text = text
}

deinit {
print("\(name) is being deinitialized")
}

}

// 创建实例并打印信息
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

HTMLElement 类产生了类实例和 asHTML 默认值的闭包之间的循环强引用。

实例的 asHTML 属性持有闭包的强引用。但是,闭包在其闭包体内使用了self(引用了self.name和self.text),因此闭包捕获了self,这意味着闭包又反过来持有了HTMLElement实例的强引用。这样两个对象就产生了循环强引用。

解决闭包引起的循环强引用:在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。


弱引用和无主引用

当闭包和捕获的实例总是互相引用时并且总是同时销毁时,将闭包内的捕获定义为无主引用。

相反的,当捕获引用有时可能会是nil时,将闭包内的捕获定义为弱引用。

如果捕获的引用绝对不会置为nil,应该用无主引用,而不是弱引用。

实例

前面的HTMLElement例子中,无主引用是正确的解决循环强引用的方法。这样编写HTMLElement类来避免循环强引用:

class HTMLElement {

let name: String
let text: String?

lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}

init(name: String, text: String? = nil) {
self.name = name
self.text = text
}

deinit {
print("\(name) 被析构")
}

}

//创建并打印HTMLElement实例
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

// HTMLElement实例将会被销毁,并能看到它的析构函数打印出的消息
paragraph = nil

以上程序执行输出结果为:

<p>hello, world</p>
p 被析构
收起阅读 »

Swift 可选链

可选链(Optional Chaining)是一种可以请求和调用属性、方法和子脚本的过程,用于请求或调用的目标可能为nil。可选链返回两个值:如果目标有值,调用就会成功,返回该值如果目标为nil,调用将返回nil多次请求或调用可以被链接成一个链,如果任意一个节...
继续阅读 »

可选链(Optional Chaining)是一种可以请求和调用属性、方法和子脚本的过程,用于请求或调用的目标可能为nil。

可选链返回两个值:

  • 如果目标有值,调用就会成功,返回该值

  • 如果目标为nil,调用将返回nil

多次请求或调用可以被链接成一个链,如果任意一个节点为nil将导致整条链失效。


可选链可替代强制解析

通过在属性、方法、或下标脚本的可选值后面放一个问号(?),即可定义一个可选链。

可选链 '?'感叹号(!)强制展开方法,属性,下标脚本可选链
? 放置于可选值后来调用方法,属性,下标脚本! 放置于可选值后来调用方法,属性,下标脚本来强制展开值
当可选为 nil 输出比较友好的错误信息当可选为 nil 时强制展开执行错误

使用感叹号(!)可选链实例

class Person {
var residence: Residence?
}

class Residence {
var numberOfRooms = 1
}

let john = Person()

//将导致运行时错误
let roomCount = john.residence!.numberOfRooms

以上程序执行输出结果为:

fatal error: unexpectedly found nil while unwrapping an Optional value

想使用感叹号(!)强制解析获得这个人residence属性numberOfRooms属性值,将会引发运行时错误,因为这时没有可以供解析的residence值。

使用问号(?)可选链实例

class Person {
var residence: Residence?
}

class Residence {
var numberOfRooms = 1
}

let john = Person()

// 链接可选residence?属性,如果residence存在则取回numberOfRooms的值
if let roomCount = john.residence?.numberOfRooms {
print("John 的房间号为 \(roomCount)。")
} else {
print("不能查看房间号")
}

以上程序执行输出结果为:

不能查看房间号

因为这种尝试获得numberOfRooms的操作有可能失败,可选链会返回Int?类型值,或者称作"可选Int"。当residence是空的时候(上例),选择Int将会为空,因此会出现无法访问numberOfRooms的情况。

要注意的是,即使numberOfRooms是非可选Int(Int?)时这一点也成立。只要是通过可选链的请求就意味着最后numberOfRooms总是返回一个Int?而不是Int。


为可选链定义模型类

你可以使用可选链来多层调用属性,方法,和下标脚本。这让你可以利用它们之间的复杂模型来获取更底层的属性,并检查是否可以成功获取此类底层属性。

实例

定义了四个模型类,其中包括多层可选链:

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

通过可选链调用方法

你可以使用可选链的来调用可选值的方法并检查方法调用是否成功。即使这个方法没有返回值,你依然可以使用可选链来达成这一目的。

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()


if ((john.residence?.printNumberOfRooms()) != nil) {
print("输出房间号")
} else {
print("无法输出房间号")
}

以上程序执行输出结果为:

无法输出房间号

使用if语句来检查是否能成功调用printNumberOfRooms方法:如果方法通过可选链调用成功,printNumberOfRooms的隐式返回值将会是Void,如果没有成功,将返回nil。


使用可选链调用下标脚本

你可以使用可选链来尝试从下标脚本获取值并检查下标脚本的调用是否成功,然而,你不能通过可选链来设置下标脚本。

实例1

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()
if let firstRoomName = john.residence?[0].name {
print("第一个房间名 \(firstRoomName).")
} else {
print("无法检索到房间")
}

以上程序执行输出结果为:

无法检索到房间

在下标脚本调用中可选链的问号直接跟在 john.residence 的后面,在下标脚本括号的前面,因为 john.residence 是可选链试图获得的可选值。

实例2

实例中创建一个 Residence 实例给 john.residence,且在他的 rooms 数组中有一个或多个 Room 实例,那么你可以使用可选链通过 Residence 下标脚本来获取在 rooms 数组中的实例了:

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()
let johnsHouse = Residence()
johnsHouse
.rooms.append(Room(name: "客厅"))
johnsHouse
.rooms.append(Room(name: "厨房"))
john
.residence = johnsHouse

let johnsAddress = Address()
johnsAddress
.buildingName = "The Larches"
johnsAddress
.street = "Laurel Street"
john
.residence!.address = johnsAddress

if let johnsStreet = john.residence?.address?.street {
print("John 所在的街道是 \(johnsStreet)。")
} else {
print("无法检索到地址。 ")
}

以上程序执行输出结果为:

John 所在的街道是 Laurel Street

通过可选链接调用来访问下标

通过可选链接调用,我们可以用下标来对可选值进行读取或写入,并且判断下标调用是否成功。

实例

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()

let johnsHouse = Residence()
johnsHouse
.rooms.append(Room(name: "客厅"))
johnsHouse
.rooms.append(Room(name: "厨房"))
john
.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
print("第一个房间名为\(firstRoomName)")
} else {
print("无法检索到房间")
}

以上程序执行输出结果为:

第一个房间名为客厅

访问可选类型的下标

如果下标返回可空类型值,比如Swift中Dictionary的key下标。可以在下标的闭合括号后面放一个问号来链接下标的可空返回值:

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores
["Dave"]?[0] = 91
testScores
["Bev"]?[0]++
testScores
["Brian"]?[0] = 72
// the "Dave" array is now [91, 82, 84] and the "Bev" array is now [80, 94, 81]

上面的例子中定义了一个testScores数组,包含了两个键值对, 把String类型的key映射到一个整形数组。

这个例子用可选链接调用把"Dave"数组中第一个元素设为91,把"Bev"数组的第一个元素+1,然后尝试把"Brian"数组中的第一个元素设为72。

前两个调用是成功的,因为这两个key存在。但是key"Brian"在字典中不存在,所以第三个调用失败。


连接多层链接

你可以将多层可选链连接在一起,可以掘取模型内更下层的属性方法和下标脚本。然而多层可选链不能再添加比已经返回的可选值更多的层。

如果你试图通过可选链获得Int值,不论使用了多少层链接返回的总是Int?。 相似的,如果你试图通过可选链获得Int?值,不论使用了多少层链接返回的总是Int?。

实例1

下面的例子试图获取john的residence属性里的address的street属性。这里使用了两层可选链来联系residence和address属性,它们两者都是可选类型:

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()

if let johnsStreet = john.residence?.address?.street {
print("John 的地址为 \(johnsStreet).")
} else {
print("不能检索地址")
}

以上程序执行输出结果为:

不能检索地址

实例2

如果你为Address设定一个实例来作为john.residence.address的值,并为address的street属性设定一个实际值,你可以通过多层可选链来得到这个属性值。

class Person {
var residence: Residence?
}

class Residence {

var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
get{
return rooms[i]
}
set {
rooms
[i] = newValue
}
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

class Room {
let name: String
init
(name: String) { self.name = name }
}

class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}
let john = Person()
john
.residence?[0] = Room(name: "浴室")

let johnsHouse = Residence()
johnsHouse
.rooms.append(Room(name: "客厅"))
johnsHouse
.rooms.append(Room(name: "厨房"))
john
.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
print("第一个房间是\(firstRoomName)")
} else {
print("无法检索房间")
}

以上实例输出结果为:

第一个房间是客厅

对返回可选值的函数进行链接

我们还可以通过可选链接来调用返回可空值的方法,并且可以继续对可选值进行链接。

实例

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()

if john.residence?.printNumberOfRooms() != nil {
print("指定了房间号)")
} else {
print("未指定房间号")
}

以上程序执行输出结果为:

未指定房间号
收起阅读 »

Swift 析构过程

在一个类的实例被释放之前,析构函数被立即调用。用关键字deinit来标示析构函数,类似于初始化函数用init来标示。析构函数只适用于类类型。析构过程原理Swift 会自动释放不再需要的实例以释放资源。Swift 通过自动引用计数(ARC)处理实例的内存管理。通...
继续阅读 »

在一个类的实例被释放之前,析构函数被立即调用。用关键字deinit来标示析构函数,类似于初始化函数用init来标示。析构函数只适用于类类型。


析构过程原理

Swift 会自动释放不再需要的实例以释放资源。

Swift 通过自动引用计数(ARC)处理实例的内存管理。

通常当你的实例被释放时不需要手动地去清理。但是,当使用自己的资源时,你可能需要进行一些额外的清理。

例如,如果创建了一个自定义的类来打开一个文件,并写入一些数据,你可能需要在类实例被释放之前关闭该文件。

语法

在类的定义中,每个类最多只能有一个析构函数。析构函数不带任何参数,在写法上不带括号:

deinit {
// 执行析构过程
}

实例

var counter = 0;  // 引用计数器
class BaseClass {
init
() {
counter
+= 1;
}
deinit
{
counter
-= 1;
}
}

var show: BaseClass? = BaseClass()
print(counter)
show
= nil
print(counter)

以上程序执行输出结果为:

1
0

当 show = nil 语句执行后,计算器减去 1,show 占用的内存就会释放。

var counter = 0;  // 引用计数器

class BaseClass {
init
() {
counter
+= 1;
}

deinit
{
counter
-= 1;
}
}

var show: BaseClass? = BaseClass()

print(counter)
print(counter)

以上程序执行输出结果为:

1
1
收起阅读 »

Swift 构造过程

构造过程是为了使用某个类、结构体或枚举类型的实例而进行的准备过程。这个过程包含了为实例中的每个属性设置初始值和为其执行必要的准备和初始化任务。Swift 构造函数使用 init() 方法。与 Objective-C 中的构造器不同,Swift 的构造器无需返回...
继续阅读 »

构造过程是为了使用某个类、结构体或枚举类型的实例而进行的准备过程。这个过程包含了为实例中的每个属性设置初始值和为其执行必要的准备和初始化任务。

Swift 构造函数使用 init() 方法。

与 Objective-C 中的构造器不同,Swift 的构造器无需返回值,它们的主要任务是保证新实例在第一次使用前完成正确的初始化。

类实例也可以通过定义析构器(deinitializer)在类实例释放之前执行清理内存的工作。


存储型属性的初始赋值

类和结构体在实例创建时,必须为所有存储型属性设置合适的初始值。

存储属性在构造器中赋值时,它们的值是被直接设置的,不会触发任何属性观测器。

存储属性在构造器中赋值流程:

  • 创建初始值。

  • 在属性定义中指定默认属性值。

  • 初始化实例,并调用 init() 方法。


构造器

构造器在创建某特定类型的新实例时调用。它的最简形式类似于一个不带任何参数的实例方法,以关键字init命名。

语法

init()
{
// 实例化后执行的代码
}

实例

以下结构体定义了一个不带参数的构造器 init,并在里面将存储型属性 length 和 breadth 的值初始化为 6 和 12:

struct rectangle {
var length: Double
var breadth: Double
init
() {
length
= 6
breadth
= 12
}
}
var area = rectangle()
print("矩形面积为 \(area.length*area.breadth)")

以上程序执行输出结果为:

矩形面积为 72.0

默认属性值

我们可以在构造器中为存储型属性设置初始值;同样,也可以在属性声明时为其设置默认值。

使用默认值能让你的构造器更简洁、更清晰,且能通过默认值自动推导出属性的类型。

以下实例我们在属性声明时为其设置默认值:

struct rectangle {
    
// 设置默认值
var length = 6
var breadth = 12
}
var area = rectangle()
print("矩形的面积为 \(area.length*area.breadth)")

以上程序执行输出结果为:

矩形面积为 72

构造参数

你可以在定义构造器 init() 时提供构造参数,如下所示:

struct Rectangle {
var length: Double
var breadth: Double
var area: Double

init
(fromLength length: Double, fromBreadth breadth: Double) {
self.length = length
self.breadth = breadth
area
= length * breadth
}

init
(fromLeng leng: Double, fromBread bread: Double) {
self.length = leng
self.breadth = bread
area
= leng * bread
}
}

let ar = Rectangle(fromLength: 6, fromBreadth: 12)
print("面积为: \(ar.area)")

let are = Rectangle(fromLeng: 36, fromBread: 12)
print("面积为: \(are.area)")

以上程序执行输出结果为:

面积为: 72.0
面积为: 432.0

内部和外部参数名

跟函数和方法参数相同,构造参数也存在一个在构造器内部使用的参数名字和一个在调用构造器时使用的外部参数名字。

然而,构造器并不像函数和方法那样在括号前有一个可辨别的名字。所以在调用构造器时,主要通过构造器中的参数名和类型来确定需要调用的构造器。

如果你在定义构造器时没有提供参数的外部名字,Swift 会为每个构造器的参数自动生成一个跟内部名字相同的外部名。

struct Color {
let red, green, blue: Double
init
(red: Double, green: Double, blue: Double) {
self.red = red
self.green = green
self.blue = blue
}
init
(white: Double) {
red
= white
green
= white
blue
= white
}
}

// 创建一个新的Color实例,通过三种颜色的外部参数名来传值,并调用构造器
let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)

print("red 值为: \(magenta.red)")
print("green 值为: \(magenta.green)")
print("blue 值为: \(magenta.blue)")

// 创建一个新的Color实例,通过三种颜色的外部参数名来传值,并调用构造器
let halfGray = Color(white: 0.5)
print("red 值为: \(halfGray.red)")
print("green 值为: \(halfGray.green)")
print("blue 值为: \(halfGray.blue)")

以上程序执行输出结果为:

red 值为: 1.0
green
值为: 0.0
blue
值为: 1.0
red
值为: 0.5
green
值为: 0.5
blue
值为: 0.5

没有外部名称参数

如果你不希望为构造器的某个参数提供外部名字,你可以使用下划线_来显示描述它的外部名。

struct Rectangle {
var length: Double

init
(frombreadth breadth: Double) {
length
= breadth * 10
}

init
(frombre bre: Double) {
length
= bre * 30
}
//不提供外部名字
init
(_ area: Double) {
length
= area
}
}

// 调用不提供外部名字
let rectarea = Rectangle(180.0)
print("面积为: \(rectarea.length)")

// 调用不提供外部名字
let rearea = Rectangle(370.0)
print("面积为: \(rearea.length)")

// 调用不提供外部名字
let recarea = Rectangle(110.0)
print("面积为: \(recarea.length)")

以上程序执行输出结果为:

面积为: 180.0
面积为: 370.0
面积为: 110.0

可选属性类型

如果你定制的类型包含一个逻辑上允许取值为空的存储型属性,你都需要将它定义为可选类型optional type(可选属性类型)。

当存储属性声明为可选时,将自动初始化为空 nil。

struct Rectangle {
var length: Double?

init
(frombreadth breadth: Double) {
length
= breadth * 10
}

init
(frombre bre: Double) {
length
= bre * 30
}

init
(_ area: Double) {
length
= area
}
}

let rectarea = Rectangle(180.0)
print("面积为:\(rectarea.length)")

let rearea = Rectangle(370.0)
print("面积为:\(rearea.length)")

let recarea = Rectangle(110.0)
print("面积为:\(recarea.length)")

以上程序执行输出结果为:

面积为:Optional(180.0)
面积为:Optional(370.0)
面积为:Optional(110.0)

构造过程中修改常量属性

只要在构造过程结束前常量的值能确定,你可以在构造过程中的任意时间点修改常量属性的值。

对某个类实例来说,它的常量属性只能在定义它的类的构造过程中修改;不能在子类中修改。

尽管 length 属性现在是常量,我们仍然可以在其类的构造器中设置它的值:

struct Rectangle {
let length: Double?

init
(frombreadth breadth: Double) {
length
= breadth * 10
}

init
(frombre bre: Double) {
length
= bre * 30
}

init
(_ area: Double) {
length
= area
}
}

let rectarea = Rectangle(180.0)
print("面积为:\(rectarea.length)")

let rearea = Rectangle(370.0)
print("面积为:\(rearea.length)")

let recarea = Rectangle(110.0)
print("面积为:\(recarea.length)")

以上程序执行输出结果为:

面积为:Optional(180.0)
面积为:Optional(370.0)
面积为:Optional(110.0)

默认构造器

默认构造器将简单的创建一个所有属性值都设置为默认值的实例:

以下实例中,ShoppingListItem类中的所有属性都有默认值,且它是没有父类的基类,它将自动获得一个可以为所有属性设置默认值的默认构造器

class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()


print("名字为: \(item.name)")
print("数理为: \(item.quantity)")
print("是否付款: \(item.purchased)")

以上程序执行输出结果为:

名字为: nil
数理为: 1
是否付款: false

结构体的逐一成员构造器

如果结构体对所有存储型属性提供了默认值且自身没有提供定制的构造器,它们能自动获得一个逐一成员构造器。

我们在调用逐一成员构造器时,通过与成员属性名相同的参数名进行传值来完成对成员属性的初始赋值。

下面例子中定义了一个结构体 Rectangle,它包含两个属性 length 和 breadth。Swift 可以根据这两个属性的初始赋值100.0 、200.0自动推导出它们的类型Double。

struct Rectangle {
var length = 100.0, breadth = 200.0
}
let area = Rectangle(length: 24.0, breadth: 32.0)

print("矩形的面积: \(area.length)")
print("矩形的面积: \(area.breadth)")

由于这两个存储型属性都有默认值,结构体 Rectangle 自动获得了一个逐一成员构造器 init(width:height:)。 你可以用它来为 Rectangle 创建新的实例。

以上程序执行输出结果为:

矩形的面积: 24.0
矩形的面积: 32.0

值类型的构造器代理

构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能减少多个构造器间的代码重复。

以下实例中,Rect 结构体调用了 Size 和 Point 的构造过程:

struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}

struct Rect {
var origin = Point()
var size = Size()
init
() {}
init
(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
init
(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}


// origin和size属性都使用定义时的默认值Point(x: 0.0, y: 0.0)和Size(width: 0.0, height: 0.0):
let basicRect = Rect()
print("Size 结构体初始值: \(basicRect.size.width, basicRect.size.height) ")
print("Rect 结构体初始值: \(basicRect.origin.x, basicRect.origin.y) ")

// 将origin和size的参数值赋给对应的存储型属性
let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
size
: Size(width: 5.0, height: 5.0))

print("Size 结构体初始值: \(originRect.size.width, originRect.size.height) ")
print("Rect 结构体初始值: \(originRect.origin.x, originRect.origin.y) ")


//先通过center和size的值计算出origin的坐标。
//然后再调用(或代理给)init(origin:size:)构造器来将新的origin和size值赋值到对应的属性中
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size
: Size(width: 3.0, height: 3.0))

print("Size 结构体初始值: \(centerRect.size.width, centerRect.size.height) ")
print("Rect 结构体初始值: \(centerRect.origin.x, centerRect.origin.y) ")

以上程序执行输出结果为:

Size 结构体初始值: (0.0, 0.0) 
Rect 结构体初始值: (0.0, 0.0)
Size 结构体初始值: (5.0, 5.0)
Rect 结构体初始值: (2.0, 2.0)
Size 结构体初始值: (3.0, 3.0)
Rect 结构体初始值: (2.5, 2.5)

构造器代理规则

值类型类类型
不支持继承,所以构造器代理的过程相对简单,因为它们只能代理给本身提供的其它构造器。 你可以使用self.init在自定义的构造器中引用其它的属于相同值类型的构造器。它可以继承自其它类,这意味着类有责任保证其所有继承的存储型属性在构造时也能正确的初始化。

类的继承和构造过程

Swift 提供了两种类型的类构造器来确保所有类实例中存储型属性都能获得初始值,它们分别是指定构造器和便利构造器。

指定构造器便利构造器
类中最主要的构造器类中比较次要的、辅助型的构造器
初始化类中提供的所有属性,并根据父类链往上调用父类的构造器来实现父类的初始化。可以定义便利构造器来调用同一个类中的指定构造器,并为其参数提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入的实例。
每一个类都必须拥有至少一个指定构造器只在必要的时候为类提供便利构造器
Init(parameters) {
statements
}
convenience init(parameters) {
statements
}

指定构造器实例

class mainClass {
var no1 : Int // 局部存储变量
init
(no1 : Int) {
self.no1 = no1 // 初始化
}
}
class subClass : mainClass {
var no2 : Int // 新的子类存储变量
init
(no1 : Int, no2 : Int) {
self.no2 = no2 // 初始化
super.init(no1:no1) // 初始化超类
}
}

let res = mainClass(no1: 10)
let res2 = subClass(no1: 10, no2: 20)

print("res 为: \(res.no1)")
print("res2 为: \(res2.no1)")
print("res2 为: \(res2.no2)")

以上程序执行输出结果为:

res 为: 10
res
为: 10
res
为: 20

便利构造器实例

class mainClass {
var no1 : Int // 局部存储变量
init
(no1 : Int) {
self.no1 = no1 // 初始化
}
}

class subClass : mainClass {
var no2 : Int
init
(no1 : Int, no2 : Int) {
self.no2 = no2
super.init(no1:no1)
}
// 便利方法只需要一个参数
override convenience init(no1: Int) {
self.init(no1:no1, no2:0)
}
}
let res = mainClass(no1: 20)
let res2 = subClass(no1: 30, no2: 50)

print("res 为: \(res.no1)")
print("res2 为: \(res2.no1)")
print("res2 为: \(res2.no2)")

以上程序执行输出结果为:

res 为: 20
res2
为: 30
res2
为: 50

构造器的继承和重载

Swift 中的子类不会默认继承父类的构造器。

父类的构造器仅在确定和安全的情况下被继承。

当你重写一个父类指定构造器时,你需要写override修饰符。

class SuperClass {
var corners = 4
var description: String {
return "\(corners) 边"
}
}
let rectangle = SuperClass()
print("矩形: \(rectangle.description)")

class SubClass: SuperClass {
override init() { //重载构造器
super.init()
corners
= 5
}
}

let subClass = SubClass()
print("五角型: \(subClass.description)")

以上程序执行输出结果为:

矩形: 4 
五角型: 5

指定构造器和便利构造器实例

接下来的例子将在操作中展示指定构造器、便利构造器和自动构造器的继承。

它定义了包含两个个类MainClass、SubClass的类层次结构,并将演示它们的构造器是如何相互作用的。

class MainClass {
var name: String

init
(name: String) {
self.name = name
}

convenience init
() {
self.init(name: "[匿名]")
}
}
let main = MainClass(name: "Runoob")
print("MainClass 名字为: \(main.name)")

let main2 = MainClass()
print("没有对应名字: \(main2.name)")

class SubClass: MainClass {
var count: Int
init
(name: String, count: Int) {
self.count = count
super.init(name: name)
}

override convenience init(name: String) {
self.init(name: name, count: 1)
}
}

let sub = SubClass(name: "Runoob")
print("MainClass 名字为: \(sub.name)")

let sub2 = SubClass(name: "Runoob", count: 3)
print("count 变量: \(sub2.count)")

以上程序执行输出结果为:

MainClass 名字为: Runoob
没有对应名字: [匿名]
MainClass 名字为: Runoob
count
变量: 3

类的可失败构造器

如果一个类,结构体或枚举类型的对象,在构造自身的过程中有可能失败,则为其定义一个可失败构造器。

变量初始化失败可能的原因有:

  • 传入无效的参数值。

  • 缺少某种所需的外部资源。

  • 没有满足特定条件。

为了妥善处理这种构造过程中可能会失败的情况。

你可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器。其语法为在init关键字后面加添问号(init?)。

实例

下例中,定义了一个名为Animal的结构体,其中有一个名为species的,String类型的常量属性。

同时该结构体还定义了一个,带一个String类型参数species的,可失败构造器。这个可失败构造器,被用来检查传入的参数是否为一个空字符串,如果为空字符串,则该可失败构造器,构建对象失败,否则成功。

struct Animal {
let species: String
init
?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}

//通过该可失败构造器来构建一个Animal的对象,并检查其构建过程是否成功
// someCreature 的类型是 Animal? 而不是 Animal
let someCreature = Animal(species: "长颈鹿")

// 打印 "动物初始化为长颈鹿"
if let giraffe = someCreature {
print("动物初始化为\(giraffe.species)")
}

以上程序执行输出结果为:

动物初始化为长颈鹿

枚举类型的可失败构造器

你可以通过构造一个带一个或多个参数的可失败构造器来获取枚举类型中特定的枚举成员。

实例

下例中,定义了一个名为TemperatureUnit的枚举类型。其中包含了三个可能的枚举成员(Kelvin,Celsius,和 Fahrenheit)和一个被用来找到Character值所对应的枚举成员的可失败构造器:

enum TemperatureUnit {
    
// 开尔文,摄氏,华氏
case Kelvin, Celsius, Fahrenheit
init
?(symbol: Character) {
switch symbol {
case "K":
self = .Kelvin
case "C":
self = .Celsius
case "F":
self = .Fahrenheit
default:
return nil
}
}
}


let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
print("这是一个已定义的温度单位,所以初始化成功。")
}

let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
print("这不是一个已定义的温度单位,所以初始化失败。")
}

以上程序执行输出结果为:

这是一个已定义的温度单位,所以初始化成功。
这不是一个已定义的温度单位,所以初始化失败。

类的可失败构造器

值类型(如结构体或枚举类型)的可失败构造器,对何时何地触发构造失败这个行为没有任何的限制。

但是,类的可失败构造器只能在所有的类属性被初始化后和所有类之间的构造器之间的代理调用发生完后触发失败行为。

实例

下例子中,定义了一个名为 StudRecord 的类,因为 studname 属性是一个常量,所以一旦 StudRecord 类构造成功,studname 属性肯定有一个非nil的值。

class StudRecord {
let studname: String!
init
?(studname: String) {
self.studname = studname
if studname.isEmpty { return nil }
}
}
if let stname = StudRecord(studname: "失败构造器") {
print("模块为 \(stname.studname)")
}

以上程序执行输出结果为:

模块为 失败构造器

覆盖一个可失败构造器

就如同其它构造器一样,你也可以用子类的可失败构造器覆盖基类的可失败构造器。

者你也可以用子类的非可失败构造器覆盖一个基类的可失败构造器。

你可以用一个非可失败构造器覆盖一个可失败构造器,但反过来却行不通。

一个非可失败的构造器永远也不能代理调用一个可失败构造器。

实例

以下实例描述了可失败与非可失败构造器:

class Planet {
var name: String

init
(name: String) {
self.name = name
}

convenience init
() {
self.init(name: "[No Planets]")
}
}
let plName = Planet(name: "Mercury")
print("行星的名字是: \(plName.name)")

let noplName = Planet()
print("没有这个名字的行星: \(noplName.name)")

class planets: Planet {
var count: Int

init
(name: String, count: Int) {
self.count = count
super.init(name: name)
}

override convenience init(name: String) {
self.init(name: name, count: 1)
}
}

以上程序执行输出结果为:

行星的名字是: Mercury
没有这个名字的行星: [No Planets]

可失败构造器 init!

通常来说我们通过在init关键字后添加问号的方式(init?)来定义一个可失败构造器,但你也可以使用通过在init后面添加惊叹号的方式来定义一个可失败构造器(init!)。实例如下:

struct StudRecord {
let stname: String

init
!(stname: String) {
if stname.isEmpty {return nil }
self.stname = stname
}
}

let stmark = StudRecord(stname: "Runoob")
if let name = stmark {
print("指定了学生名")
}

let blankname = StudRecord(stname: "")
if blankname == nil {
print("学生名为空")
}

以上程序执行输出结果为:

指定了学生名
学生名为空
收起阅读 »

Swift 继承

继承我们可以理解为一个类获取了另外一个类的方法和属性。当一个类继承其它类时,继承类叫子类,被继承类叫超类(或父类)在 Swift 中,类可以调用和访问超类的方法,属性和下标脚本,并且可以重写它们。我们也可以为类中继承来的属性添加属性观察器。基类没有继承其它类的...
继续阅读 »

继承我们可以理解为一个类获取了另外一个类的方法和属性。

当一个类继承其它类时,继承类叫子类,被继承类叫超类(或父类)

在 Swift 中,类可以调用和访问超类的方法,属性和下标脚本,并且可以重写它们。

我们也可以为类中继承来的属性添加属性观察器。


基类

没有继承其它类的类,称之为基类(Base Class)。

以下实例中我们定义了基类 StudDetails ,描述了学生(stname)及其各科成绩的分数(mark1、mark2、mark3):

class StudDetails {
var stname: String!
var mark1: Int!
var mark2: Int!
var mark3: Int!
init
(stname: String, mark1: Int, mark2: Int, mark3: Int) {
self.stname = stname
self.mark1 = mark1
self.mark2 = mark2
self.mark3 = mark3
}
}
let stname = "swift"
let mark1 = 98
let mark2 = 89
let mark3 = 76

let sds = StudDetails(stname:stname, mark1:mark1, mark2:mark2, mark3:mark3);

print(sds.stname)
print(sds.mark1)
print(sds.mark2)
print(sds.mark3)

以上程序执行输出结果为:

swift
98
89
76

子类

子类指的是在一个已有类的基础上创建一个新的类。

为了指明某个类的超类,将超类名写在子类名的后面,用冒号(:)分隔,语法格式如下

class SomeClass: SomeSuperclass {
// 类的定义
}

实例

以下实例中我们定义了超类 StudDetails,然后使用子类 Tom 继承它:

class StudDetails
{
var mark1: Int;
var mark2: Int;

init
(stm1:Int, results stm2:Int)
{
mark1
= stm1;
mark2
= stm2;
}

func show
()
{
print("Mark1:\(self.mark1), Mark2:\(self.mark2)")
}
}

class Tom : StudDetails
{
init
()
{
super.init(stm1: 93, results: 89)
}
}

let tom = Tom()
tom
.show()

以上程序执行输出结果为:

Mark1:93, Mark2:89

重写(Overriding)

子类可以通过继承来的实例方法,类方法,实例属性,或下标脚本来实现自己的定制功能,我们把这种行为叫重写(overriding)。

我们可以使用 override 关键字来实现重写。

访问超类的方法、属性及下标脚本

你可以通过使用super前缀来访问超类的方法,属性或下标脚本。

重写访问方法,属性,下标脚本
方法super.somemethod()
属性super.someProperty()
下标脚本super[someIndex]

重写方法和属性

重写方法

在我们的子类中我们可以使用 override 关键字来重写超类的方法。

以下实例中我们重写了 show() 方法:

class SuperClass {
func show
() {
print("这是超类 SuperClass")
}
}

class SubClass: SuperClass {
override func show() {
print("这是子类 SubClass")
}
}

let superClass = SuperClass()
superClass
.show()

let subClass = SubClass()
subClass
.show()

以上程序执行输出结果为:

这是超类 SuperClass
这是子类 SubClass

重写属性

你可以提供定制的 getter(或 setter)来重写任意继承来的属性,无论继承来的属性是存储型的还是计算型的属性。

子类并不知道继承来的属性是存储型的还是计算型的,它只知道继承来的属性会有一个名字和类型。所以你在重写一个属性时,必需将它的名字和类型都写出来。

注意点:

  • 如果你在重写属性中提供了 setter,那么你也一定要提供 getter。

  • 如果你不想在重写版本中的 getter 里修改继承来的属性值,你可以直接通过super.someProperty来返回继承来的值,其中someProperty是你要重写的属性的名字。

以下实例我们定义了超类 Circle 及子类 Rectangle, 在 Rectangle 类中我们重写属性 area:

class Circle {
var radius = 12.5
var area: String {
return "矩形半径 \(radius) "
}
}

// 继承超类 Circle
class Rectangle: Circle {
var print = 7
override var area: String {
return super.area + " ,但现在被重写为 \(print)"
}
}

let rect = Rectangle()
rect
.radius = 25.0
rect
.print = 3
print("Radius \(rect.area)")

以上程序执行输出结果为:

Radius 矩形半径 25.0  ,但现在被重写为 3

重写属性观察器

你可以在属性重写中为一个继承来的属性添加属性观察器。这样一来,当继承来的属性值发生改变时,你就会监测到。

注意:你不可以为继承来的常量存储型属性或继承来的只读计算型属性添加属性观察器。

class Circle {
var radius = 12.5
var area: String {
return "矩形半径为 \(radius) "
}
}

class Rectangle: Circle {
var print = 7
override var area: String {
return super.area + " ,但现在被重写为 \(print)"
}
}


let rect = Rectangle()
rect
.radius = 25.0
rect
.print = 3
print("半径: \(rect.area)")

class Square: Rectangle {
override var radius: Double {
didSet
{
print = Int(radius/5.0)+1
}
}
}


let sq = Square()
sq
.radius = 100.0
print("半径: \(sq.area)")
半径: 矩形半径为 25.0  ,但现在被重写为 3
半径: 矩形半径为 100.0 ,但现在被重写为 21

防止重写

我们可以使用 final 关键字防止它们被重写。

如果你重写了final方法,属性或下标脚本,在编译时会报错。

你可以通过在关键字class前添加final特性(final class)来将整个类标记为 final 的,这样的类是不可被继承的,否则会报编译错误。

final class Circle {
final var radius = 12.5
var area: String {
return "矩形半径为 \(radius) "
}
}
class Rectangle: Circle {
var print = 7
override var area: String {
return super.area + " ,但现在被重写为 \(print)"
}
}

let rect = Rectangle()
rect
.radius = 25.0
rect
.print = 3
print("半径: \(rect.area)")

class Square: Rectangle {
override var radius: Double {
didSet
{
print = Int(radius/5.0)+1
}
}
}

let sq = Square()
sq
.radius = 100.0
print("半径: \(sq.area)")

由于以上实例使用了 final 关键字不允许重写,所以执行会报错:

error: var overrides a 'final' var
override var area: String {
^
note
: overridden declaration is here
var area: String {
^
error
: var overrides a 'final' var
override var radius: Double {
^
note
: overridden declaration is here
final var radius = 12.5
^
error
: inheritance from a final class 'Circle'
class Rectangle: Circle {
^
收起阅读 »

Swift 下标脚本

下标脚本 可以定义在类(Class)、结构体(structure)和枚举(enumeration)这些目标中,可以认为是访问对象、集合或序列的快捷方式,不需要再调用实例的特定的赋值和访问方法。举例来说,用下标脚本访问一个数组(Array)实例中的元素可以这样写...
继续阅读 »

下标脚本 可以定义在类(Class)、结构体(structure)和枚举(enumeration)这些目标中,可以认为是访问对象、集合或序列的快捷方式,不需要再调用实例的特定的赋值和访问方法。

举例来说,用下标脚本访问一个数组(Array)实例中的元素可以这样写 someArray[index] ,访问字典(Dictionary)实例中的元素可以这样写 someDictionary[key]。

对于同一个目标可以定义多个下标脚本,通过索引值类型的不同来进行重载,而且索引值的个数可以是多个。


下标脚本语法及应用

语法

下标脚本允许你通过在实例后面的方括号中传入一个或者多个的索引值来对实例进行访问和赋值。

语法类似于实例方法和计算型属性的混合。

与定义实例方法类似,定义下标脚本使用subscript关键字,显式声明入参(一个或多个)和返回类型。

与实例方法不同的是下标脚本可以设定为读写或只读。这种方式又有点像计算型属性的getter和setter:

subscript(index: Int) -> Int {
get {
// 用于下标脚本值的声明
}
set(newValue) {
// 执行赋值操作
}
}

实例 1

import Cocoa

struct subexample {
let decrementer: Int
subscript
(index: Int) -> Int {
return decrementer / index
}
}
let division = subexample(decrementer: 100)

print("100 除以 9 等于 \(division[9])")
print("100 除以 2 等于 \(division[2])")
print("100 除以 3 等于 \(division[3])")
print("100 除以 5 等于 \(division[5])")
print("100 除以 7 等于 \(division[7])")

以上程序执行输出结果为:

100 除以 9 等于 11
100 除以 2 等于 50
100 除以 3 等于 33
100 除以 5 等于 20
100 除以 7 等于 14

在上例中,通过 subexample 结构体创建了一个除法运算的实例。数值 100 作为结构体构造函数传入参数初始化实例成员 decrementer。

你可以通过下标脚本来得到结果,比如 division[2] 即为 100 除以 2。

实例 2

import Cocoa

class daysofaweek {
private var days = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "saturday"]
subscript
(index: Int) -> String {
get {
return days[index] // 声明下标脚本的值
}
set(newValue) {
self.days[index] = newValue // 执行赋值操作
}
}
}
var p = daysofaweek()

print(p[0])
print(p[1])
print(p[2])
print(p[3])

以上程序执行输出结果为:

Sunday
Monday
Tuesday
Wednesday

用法

根据使用场景不同下标脚本也具有不同的含义。

通常下标脚本是用来访问集合(collection),列表(list)或序列(sequence)中元素的快捷方式。

你可以在你自己特定的类或结构体中自由的实现下标脚本来提供合适的功能。

例如,Swift 的字典(Dictionary)实现了通过下标脚本对其实例中存放的值进行存取操作。在下标脚本中使用和字典索引相同类型的值,并且把一个字典值类型的值赋值给这个下标脚来为字典设值:

import Cocoa

var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
numberOfLegs
["bird"] = 2

print(numberOfLegs)

以上程序执行输出结果为:

["ant": 6, "bird": 2, "cat": 4, "spider": 8]

上例定义一个名为numberOfLegs的变量并用一个字典字面量初始化出了包含三对键值的字典实例。numberOfLegs的字典存放值类型推断为Dictionary。字典实例创建完成之后通过下标脚本的方式将整型值2赋值到字典实例的索引为bird的位置中。


下标脚本选项

下标脚本允许任意数量的入参索引,并且每个入参类型也没有限制。

下标脚本的返回值也可以是任何类型。

下标脚本可以使用变量参数和可变参数。

一个类或结构体可以根据自身需要提供多个下标脚本实现,在定义下标脚本时通过传入参数的类型进行区分,使用下标脚本时会自动匹配合适的下标脚本实现运行,这就是下标脚本的重载

import Cocoa

struct Matrix {
let rows: Int, columns: Int
var print: [Double]
init
(rows: Int, columns: Int) {
self.rows = rows
self.columns = columns
print = Array(repeating: 0.0, count: rows * columns)
}
subscript
(row: Int, column: Int) -> Double {
get {
return print[(row * columns) + column]
}
set {
print[(row * columns) + column] = newValue
}
}
}
// 创建了一个新的 3 行 3 列的Matrix实例
var mat = Matrix(rows: 3, columns: 3)

// 通过下标脚本设置值
mat
[0,0] = 1.0
mat
[0,1] = 2.0
mat
[1,0] = 3.0
mat
[1,1] = 5.0

// 通过下标脚本获取值
print("\(mat[0,0])")
print("\(mat[0,1])")
print("\(mat[1,0])")
print("\(mat[1,1])")

以上程序执行输出结果为:

1.0
2.0
3.0
5.0

Matrix 结构体提供了一个两个传入参数的构造方法,两个参数分别是rows和columns,创建了一个足够容纳rows * columns个数的Double类型数组。为了存储,将数组的大小和数组每个元素初始值0.0。

你可以通过传入合适的row和column的数量来构造一个新的Matrix实例。

收起阅读 »