关于iOS开发:如何关闭ios模拟器
抉择文件菜单->抉择敞开窗口或者按住[Command + W ]。它就会敞开在顶层的模拟器。
抉择文件菜单->抉择敞开窗口或者按住[Command + W ]。它就会敞开在顶层的模拟器。
iOS 打包流程配置证书App证书申请地址 旧版本的 App 创立和证书配置流程能够参考 iOS证书(.p12)和形容文件(.mobileprovision)申请 如果你的 Xcode 版本大于11须要留神在证书申请时抉择 Apple Distribution 和 Apple Development 这两种证书类型,不然可能会因为 Xcode 构建模式不一样导致各种问题 Xcode11 曾经简化了证书申请流程,所有的证书申请都能够在 Xcode 内实现,根本步骤如下 在 Xcode 内登录开发者账号在账户的右下角找到 Manage Certificates,点击左下角向下箭头抉择你想要配置的证书。开发证书抉择 Apple Development,生产抉择 Apple Distribution, Xcode 会主动同步证书到 App 治理后台上,而后你只须要登录 https://developer.apple.com/a... 来依据不同的证书创立形容文件即可回到我的项目中找到 TARGETS -> Build Settings -> Signing -> Code Signing Identity -> 配置相干证书接着配置前面的 Development Team、Provisioning Profile 字段打包时须要更新 Build 版本号。目前我采纳的规定是日期字符串格局,每次上传到 Apple Connect 时须要先更改这个字段
介绍KVO全称KeyValueObserving,是苹果提供的一套事件告诉机制。容许对象监听另一个对象特定属性的扭转,并在扭转时接管到事件。因为KVO的实现机制,所以对属性才会产生作用,个别继承自NSObject的对象都默认反对KVO。 KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,绝对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。KVO对被监听对象无侵入性,不须要手动批改其外部代码即可实现监听。 KVO能够监听单个属性的变动,也能够监听汇合对象的变动。通过KVC的mutableArrayValueForKey:等办法取得代理对象,当代理对象的外部对象产生扭转时,会回调KVO监听的办法。汇合对象蕴含NSArray和NSSet。 应用应用KVO分为三个步骤 通过addObserver:forKeyPath:options:context:办法注册观察者,观察者能够接管keyPath属性的变动事件回调。在观察者中实现observeValueForKeyPath:ofObject:change:context:办法,当keyPath属性产生扭转后,KVO会回调这个办法来告诉观察者。当观察者不须要监听时,能够调用removeObserver:forKeyPath:办法将KVO移除。须要留神的是,调用removeObserver须要在观察者隐没之前,否则会导致Crash。注册在注册观察者时,能够传入options参数,参数是一个枚举类型。如果传入NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld示意接管新值和旧值,默认为只接管新值。如果想在注册观察者后,立刻接管一次回调,则能够退出NSKeyValueObservingOptionInitial枚举。 还能够通过办法context传入任意类型的对象,在接管音讯回调的代码中能够接管到这个对象,是KVO中的一种传值形式。 在调用addObserver办法后,KVO并不会对观察者进行强援用。所以须要留神观察者的生命周期,否则会导致观察者被开释带来的Crash。 监听观察者须要实现observeValueForKeyPath:ofObject:change:context:办法,当KVO事件到来时会调用这个办法,如果没有实现会导致Crash。change字典中寄存KVO属性相干的值,依据options时传入的枚举来返回。枚举会对应相应key来从字典中取出值,例如有NSKeyValueChangeOldKey字段,存储扭转之前的旧值。 change中还有NSKeyValueChangeKindKey字段,和NSKeyValueChangeOldKey是平级的关系,来提供本次更改的信息,对应NSKeyValueChange枚举类型的value。例如被察看属性产生扭转时,字段为NSKeyValueChangeSetting。 如果被察看对象是汇合对象,在NSKeyValueChangeKindKey字段中会蕴含NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement的信息,示意汇合对象的操作形式。 其余触发办法调用KVO属性对象时,不仅能够通过点语法和set语法进行调用,KVO兼容很多种调用形式。 // 间接调用set办法,或者通过属性的点语法间接调用[account setName:@"Savings"];// 应用KVC的setValue:forKey:办法[account setValue:@"Savings" forKey:@"name"];// 应用KVC的setValue:forKeyPath:办法[document setValue:@"Savings" forKeyPath:@"account.name"];// 通过mutableArrayValueForKey:办法获取到代理对象,并应用代理对象进行操作Transaction *newTransaction = <#Create a new transaction for the account#>;NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];[transactions addObject:newTransaction];理论利用KVO次要用来做键值察看操作,想要一个值产生扭转后告诉另一个对象,则用KVO实现最为适合。斯坦福大学的iOS教程中有一个很经典的案例,通过KVO在Model和Controller之间进行通信。 触发被动触发KVO在属性产生扭转时的调用是主动的,如果想要手动管制这个调用机会,或想本人实现KVO属性的调用,则能够通过KVO提供的办法进行调用。 - (void)setBalance:(double)theBalance { if (theBalance != _balance) { [self willChangeValueForKey:@"balance"]; _balance = theBalance; [self didChangeValueForKey:@"balance"]; }}能够看到调用KVO次要依附两个办法,在属性产生扭转之前调用willChangeValueForKey:办法,在产生扭转之后调用didChangeValueForKey:办法。然而,如果不调用willChangeValueForKey,间接调用didChangeValueForKey是不失效的,二者有先后顺序并且须要成对呈现。 禁用KVO如果想禁止某个属性的KVO,例如要害信息不想被三方SDK通过KVO的形式获取,能够通过automaticallyNotifiesObserversForKey办法返回NO来禁止其余中央对这个属性进行KVO。办法返回YES则示意能够调用,如果返回NO则示意不能够调用。此办法是一个类办法,能够在办法外部判断keyPath,来抉择这个属性是否容许被KVO。 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { BOOL automatic = NO; if ([theKey isEqualToString:@"balance"]) { automatic = NO; } else { automatic = [super automaticallyNotifiesObserversForKey:theKey]; } return automatic;}KVC触发KVC对KVO有非凡兼容,当通过KVC调用非属性的实例变量时,KVC外部也会触发KVO的回调,并通过NSKeyValueDidChange和NSKeyValueWillChange向上回调。 ...
前言在iOS开发过程的时候,在遇到报错的时候抛出的报错异样,须要疾速定位到具体报错地位,有时候通过惯例的代码排查办法能够解决,然而有些时候通过惯例的形式不能很好的定位到报错地位,尤其是遇到一些隐性的报错信息,不能疾速的定位到报错起因信息,这样就要借助第三方的错误信息剖析来解决了。 那么本文就来分享一下在iOS开发过程中借助第三方工具来剖析解决报错异样的解决办法,本文案例应用的友盟U-APM来做应用案例。通过在我的项目中集成友盟剖析工具,应用友盟U-APM来剖析报错异样信息,就能在友盟给出的错误信息中,很不便的找出客户端异样的信息,比方很多像数组越界却只给出了 * -[__NSArrayM objectAtIndex:]: index 50 beyond bounds [0 .. 39]' 这类错误信息,这种很难定位具体报错地位的信息,如下图所示: 遇到这种问题的话,如果通过 objectAtIndex 去检索谬误的中央,那将会是一个微小的工作量,那么怎么办能力加重工作量呢,那就是上面要介绍的状况了。 一、dSYM 文件Xcode编译我的项目后,咱们会看到一个同名的 dSYM 文件,dSYM 是保留 16 进制函数地址映射信息的直达文件,咱们调试的 symbols 都会蕴含在这个文件中,并且每次编译我的项目的时候都会生成一个新的 dSYM 文件,位于 /Users/<电脑用户名>/Library/Developer/Xcode/Archives 目录下,对于每一个公布版本咱们都很有必要保留对应的 Archives 文件 。 二、dSYM 文件的作用当咱们应用程序release 模式打包或上线后,不会像在Xcode中那样直观的看到用解体的谬误,这个时候就须要剖析 crash report 文件了,iOS 设施中会有日志文件保留咱们每个利用出错的函数内存地址,通过 Xcode 的 Organizer 能够将 iOS 设施中的 DeviceLog 导出成 crash 文件,这样就能够通过出错的函数地址去查问 dSYM 文件中程序对应的函数名和文件名。然而前提是咱们须要有软件版本对应的 dSYM 文件,这也是为什么很有必要保留每个公布版本的 Archives 文件了。 三、如何把文件一一对应每一个 xxx.app 和 xxx.app.dSYM 文件都有对应的 UUID,crash文件也有本人的UUID,只有这三个文件的UUID统一,咱们就能够通过它们解析出正确的谬误函数信息了。 1.查看 xxx.app 文件的 UUID,terminal 中输出命令 :dwarfdump --uuid xxx.app/xxx (xxx是你的项目名称) ...
需要日志对于线上排查问题是十分重要的,很多问题其实是很偶现的,同样的零碎版本,同样的设施,可能就是用户的复现,而开发通过雷同的操作和设施就是不复现。然而这个问题也不能始终不解决,所以能够通过日志的形式排查问题。可能是后盾导致的问题,也可能是客户端逻辑问题,在关键点记录日志能够疾速定位问题。 假如咱们的用户量是一百万日活,其中有1%的用户应用呈现问题,即便这个问题并不是解体,就是业务上或播放呈现问题。那这部分用户就是一万的用户,一万的用户数量是很宏大的。而且大多数用户在遇到问题后,并不会被动去分割客服,而是转到其余平台上。 尽管咱们当初有Kibana网络监控,然而只能排查网络申请是否有问题,用户是否在某个工夫申请了服务器,服务器下发的数据是否正确,然而如果定位业务逻辑的问题,还是要客户端记录日志。 现状咱们我的项目中之前有日志零碎,然而从业务和技术的角度来说,存在两个问题。现有的日志零碎从业务层角度,须要用户手动导出并发给客服,对用户有不必要的打搅。而且大多数用户并不会许可客服的申请,不会导出日志给客服。从技术的角度,现有的日志零碎代码很乱,而且性能很差,导致线上不敢继续记录日志,会导致播放器卡顿。 而且现有的日志零碎仅限于debug环境开启被动记录,线上是不开启的,线上出问题后须要用户手动关上,并且记录时长只有三分钟。正是因为当初存在的诸多问题,所以大家对日志的应用并不是很踊跃,线上排查问题就比拟艰难。 方案设计思路正是针对当初存在的问题,我筹备做一套新的日志零碎,来代替现有的日志零碎。新的日志零碎定位很简略,就是纯正的记录业务日志。Crash、埋点这些,咱们都不记录在外面,这些能够当做当前的扩大。日志零碎就记录三种日志,业务日志、网络日志、播放器日志。 日志收集咱们采纳的被动回捞策略,在日志平台上填写用户的uid,通过uid对指定设施下发回捞指令,回捞指令通过长连贯的形式下发。客户端收到回捞指令后,依据筛选条件对日志进行筛选,随后以天为单位写入到不同的文件中,压缩后上传到后端。 在日志平台能够依据指定的条件进行搜寻,并下载文件查看日志。为了便于开发者查看日志,从数据库取出的日志都会写成.txt模式,并上传此文件。 API设计对于调用的API设计,应该足够简略,业务层应用时就像调用NSLog一样。所以对于API的设计方案,我采纳的是宏定义的形式,调用办法和NSLog一样,调用很简略。 #if DEBUG#define SVLogDebug(frmt, ...) [[SVLogManager sharedInstance] mobileLogContent:(frmt), ##__VA_ARGS__]#else#define SVLogDebug(frmt, ...) NSLog(frmt, ...)#endif日志总共分为三种类型,业务日志、播放器日志、网络日志,对于三种日志别离对应着不同的宏定义。不同的宏定义,写入数据库的类型也不一样,能够用户日志筛选。 业务日志:SVLogDebug。播放器日志:SVLogDebugPlayer。网络日子:SVLogDebugQUIC。淘汰策略不光是要往数据库里写,还须要思考淘汰策略。淘汰策略须要均衡记录的日志数量,以及时效性的问题,日志数量尽量够排查问题,并且还不会占用过多的磁盘空间。所以,在日志上传之后会将已上传日志删除掉,除此之外日志淘汰策略有以下两种。 日志最多只保留三天,三天以前的日志都会被删掉。在利用启动后进行查看,并后盾线程执行这个过程。日志减少一个最大阈值,超过阈值的日志局部,以工夫为序,从前往后删除。咱们定义的阈值大小为200MB,个别不会超过这个大小。记录根底信息在排查问题时一些要害信息也很重要,例如用户过后的网络环境,以及一些配置项,这些因素对代码的执行都会有一些影响。对于这个问题,咱们也会记录一些用户的配置信息及网络环境,不便排查问题,但不会波及用户经纬度等隐衷信息。 数据库旧计划之前的日志计划是通过DDLog实现的,这种计划有很重大的性能问题。其写入日志的形式,是通过NSData来实现的,在沙盒创立一个txt文件,通过一个句柄来向本地写文件,每次写完之后把句柄seek到文件开端,下次间接在文件开端持续写入日志。日志是以NSData的形式进行解决的,相当于始终在频繁的进行本地文件写入操作,还要在内存中维持一个或者多个句柄对象。 这种形式还有个问题在于,因为是间接进行二进制写入,在本地存储的是txt文件。这种形式是没有方法做筛选之类的操作的,扩展性很差,所以新的日志计划咱们打算采纳数据库来实现。 计划抉择我比照了一下iOS平台支流的数据库,发现WCDB是综合性能最好的,某些方面比FMDB都要好,而且因为是C++实现的代码,所以从代码执行的层面来讲,也不会有OC的音讯发送和转发的额定耗费。 依据WCDB官网的统计数据,WCDB和FMDB进行比照,FMDB是对SQLite进行简略封装的框架,和间接用SQLite差异不是很大。而WCDB则在sqlcipher的根底上进行的深度优化,综合性能比FMDB要高,以下是性能比照,数据来自WCDB官网文档。 单次读操作WCDB要比FMDB慢5%左右,在for循环内始终读。 单次写操作WCDB要比FMDB快28%,一个for循环始终写。 批量写操作比拟显著,WCDB要比FMDB快180%,一个批量工作写入一批数据。 从数据能够看出,WCDB在写操作这块性能要比FMDB要快很多,而本地日志最频繁的就是写操作,所以这正好合乎咱们的需要,所以抉择WCDB作为新的数据库计划是最合适的。而且我的项目中曝光模块曾经用过WCDB,证实这个计划是可行并且性能很好的。 表设计咱们数据库的表设计很简略,就上面四个字段,不同类型的日志用type做辨别。如果想减少新的日志类型,也能够在我的项目中扩大。因为应用的是数据库,所以扩展性很好。 index:主键,用来做索引。content:日志内容,记录日志内容。createTime:创立工夫,日志入库的工夫。type:日志类型,用来辨别三种类型。数据库优化咱们是视频类利用,会波及播放、下载、上传等次要性能,这些性能都会大量记录日志,来不便排查线上问题。所以,防止数据库太大就成了我在设计日志零碎时,比拟看重的一点。 依据日志规模,我对播放、下载、上传三个模块进行了大量测试,播放一天两夜、下载40集电视剧、上传多个高清视频,累计记录的日志数量大略五万多条。我发现数据库文件夹曾经到200MB+的大小,这个大小曾经是比拟大的,所以须要对数据库进行优化。 我察看了一下数据库文件夹,有三个文件,db、shm、wal,次要是数据库的日志文件太大,db文件反而并不大。所以须要调用sqlite3_wal_checkpoint将wal内容写入到数据库中,这样能够缩小wal和shm文件的大小。但WCDB并没有提供间接checkpoint的办法,所以通过调研发现,执行database的敞开操作时,能够触发checkpoint。 我在应用程序退出时,监听了terminal告诉,并且把解决理论尽量靠后。这样能够保障日志不被脱漏,而且还能够在程序退出时敞开数据库。通过验证,优化后的数据库磁盘占用很小。143,987条数据库,数据库文件大小为34.8MB,压缩后的日志大小为1.4MB,解压后的日志大小为13.6MB。 wal模式这里顺带讲一下wal模式,以不便对数据库有更深刻的理解。SQLite在3.7版本退出了wal模式,但默认是不开启的,iOS版的WCDB将wal模式主动开启,并且做了一些优化。 wal文件负责优化多线程下的并发操作,如果没有wal文件,在传统的delete模式下,数据库的读写操作是互斥的,为了避免写到一半的数据被读到,会等到写操作执行实现后,再执行读操作。而wal文件就是为了解决并发读写的状况,shm文件是对wal文件进行索引的。 SQLite比拟罕用的delete和wal两种模式,这两种模式各有劣势。delete是间接读写db-page,读写操作的都是同一份文件,所以读写是互斥的,不反对并发操作。而wal是append新的db-page,这样写入速度比拟快,而且能够反对并发操作,在写入的同时不读取正在操作的db-page即可。 因为delete模式操作的db-page是离散的,所以在执行批量写操作时,delete模式的性能会差很多,这也就是为什么WCDB的批量写入性能比拟好的起因。而wal模式读操作会读取db和wal两个文件,这样会肯定水平影响读数据的性能,所以wal的查问性能绝对delete模式要差。 应用wal模式须要管制wal文件的db-page数量,如果page数量太大,会导致文件大小不受管制。wal文件并不是始终减少的,依据SQLite的设计,通过checkpoint操作能够将wal文件合并到db文件中。但同步的时机会导致查问操作被阻塞,所以不能频繁执行checkpoint。在WCDB中设置了一个1000的阈值,当page达到1000后才会执行一次checkpoint。 这个1000是微信团队的一个经验值,太大会影响读写性能,而且占用过多的磁盘空间。太小会频繁执行checkpoint,导致读写碰壁。 # define SQLITE_DEFAULT_WAL_AUTOCHECKPOINT 1000sqlite3_wal_autocheckpoint(db, SQLITE_DEFAULT_WAL_AUTOCHECKPOINT);int sqlite3_wal_autocheckpoint(sqlite3 *db, int nFrame){#ifdef SQLITE_OMIT_WAL UNUSED_PARAMETER(db); UNUSED_PARAMETER(nFrame);#else#ifdef SQLITE_ENABLE_API_ARMOR if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT;#endif if( nFrame>0 ){ sqlite3_wal_hook(db, sqlite3WalDefaultHook, SQLITE_INT_TO_PTR(nFrame)); }else{ sqlite3_wal_hook(db, 0, 0); }#endif return SQLITE_OK;}也能够设置日志文件的大小限度,默认是-1,也就是没限度,journalSizeLimit的意思是,超出的局部会被覆写。尽量不要批改这个文件,可能会导致wal文件损坏。 ...
iOS开发常见问题模拟器零碎软键盘不弹出快捷键:command + k,菜单栏上找到I/O->keyboard->toggle software keyboard
本文转载自:星星月亮0MVC(Model-View-Controller)iOS应用程序都遵循MVC架构,iOS13推出的SwiftUI除外,SwiftUI的设计遵循的MVVM.MVC是构建iOS App的规范模式,是苹果举荐的一个用来组织代码的权威范式,市面上大部分App都是这样构建的Model(模型):数据(业务)模型,负责解决和存取数据model里可能基本不是oc对象,可能是网络api. 数据库跟新了,模型得告诉控制器View(视图):界面,负责显示数据和与用户交互Controller(控制器):业务逻辑,相当于一个解决核心,负责管理Model和View, Model和View都是控制器里的对象,次要负责将数据模型展现在视图上,同时也负责界面交互的解决。Model和View是不相通的,所有的事件,都只能通过控制器替换控制器的工作就是展现模型,对模型了如执掌。控制器为视图翻译和格式化它们须要的模型数据管制UI如何显示就是它天天干的事件 作为一个开发者,有一个学习的气氛跟一个交换圈子特地重要,这是一个我的iOS交换群: 711315161,不论你是小白还是大牛欢送入驻 ,分享BAT,阿里面试题、面试教训,探讨技术, 大家一起交流学习成长!控制器要和模型或是视图沟通是很容易的,因为控制器自身就持有模型和视图,模型和视图对象就是在控制器中创立的,所有控制器很容易就能够获取模型和视图对象的属性和办法 视图要和控制器沟通能够通过button增加点击(target-action)事件,delegate, datasource delegate 模型要和控制器沟通要通过kvo和告诉(Notification) 会弄很多个MVC,层层蕴含,互相帮助iOS里有API,容许一个控制器把其余的MVC当作本人的视图,如下图. 像下图,这样可不行,会把本人都看晕,所以打一个红圈圈,淘汰掉 MVVM(Model-View-ViewModel) MVC尽管是storyboard我的项目中iOS app的规范模式,然而它也有一个别称叫Massive View Controller(重量级控制器),也就是当业务逻辑比较复杂的时候ViewController打代码会很多。为了给控制器瘦身,就引入ViewModel层用来解决网络申请,数据转换等业务逻辑(这些在MVC里也是由控制器来做)。而控制器更多的是负责和界面展现,用户交互相干的性能。并且把controller和view合并成View.这样就由MVC衍生出MVVM架构。 Model:数据View: 视图展现(UIView + UIViewController)ViewModel: 连贯View和Model swiftUI(MVVM)iOS13推出的SwiftUI苹果的规范架构就是MVVM。不须要咱们像在用storyboard的时候一样,原本苹果MVC是苹果的规范模式,咱们硬是要拆一个MVVM进去
一、概述KVO<NSKeyValueObserving>,是一个非正式协定,提供了一个路径,使对象(观察者)可能察看其余对象(被观察者)的属性,当被观察者的属性发生变化时,观察者就会被告知该变动。指定一个被察看对象(例如 A 类),当对象某个属性(例如 A 中的字符串 name)产生更改时,对象会取得告诉,并作出相应解决;【且不须要给被察看的对象增加任何额定代码,就能应用 KVO 机制】 二、应用办法零碎框架曾经反对KVO,所以程序员在应用的时候非常简单。 1、增加观察者: - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;2、实现察看响应办法: - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary*)change context:(nullable void *)context;3、移除观察者: - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;假如一个场景,股票的价格显示在以后屏幕上,当股票价格更改的时候,实时显示更新其价格。 1.定义Model, @interface StockData : NSObject { NSString * stockName; Nsstring * price; } @end @implementation StockData @end#import "KVOViewController.h" @interface KVOViewController (){ StockData *stockForKVO; UILabel *myLabel;}@end @implementation KVOViewController - (void)viewDidLoad { [super viewDidLoad]; //2.定义此model为Controller的属性,实例化它,监听它的属性,并显示在以后的View里边 stockForKVO = [[StockData alloc] init]; [stockForKVO setValue:@"searph" forKey:@"stockName"]; [stockForKVO setValue:@"10.1" forKey:@"price"]; [stockForKVO addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL]; myLabel = [[UILabel alloc]initWithFrame:CGRectMake(100, 100, 100, 30 )]; myLabel.backgroundColor = [UIColor redColor]; myLabel.text = [stockForKVO valueForKey:@"price"]; [self.view addSubview:myLabel]; UIButton * b = [UIButton buttonWithType:UIButtonTypeCustom]; b.frame = CGRectMake(10, 100, 100, 30); [b addTarget:self action:@selector(buttonAction) forControlEvents:UIControlEventTouchUpInside]; [b setTitle:@"测试" forState:(UIControlStateNormal)]; [self.view addSubview:b];// self.view.backgroundColor = [UIColor greenColor]; }-(void) buttonAction{ [stockForKVO setValue:@"20.0" forKey:@"price"];} -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ if([keyPath isEqualToString:@"price"]) { myLabel.text = [stockForKVO valueForKey:@"price"]; }}- (void)dealloc{ [stockForKVO removeObserver:self forKeyPath:@"price"];} @end/** options参数阐明: NSKeyValueObservingOptionNew 拿到新值 NSKeyValueObservingOptionOld 拿到旧值 NSKeyValueObservingOptionInitial 注册就会发一下告诉,扭转后还会发 NSKeyValueObservingOptionPrior 扭转之前发一次,扭转后发一次 */ ...
本文转载自IT之家 IT之家 5 月 4 日音讯 在 4 月下旬公布 iOS 14.5 正式版后,苹果公司明天进行了对 iOS 14.4.2 的验证。这意味着更新到 iOS 14.5 的用户无奈再降级到 iOS 14.4.2 版本。 iOS 14.5 带来了大量更新内容,包含佩戴口罩时通过 Apple Watch 解锁 iPhone 的选项,新增对 AirTag 的反对,并且 App 跟踪透明度可让用户管制哪些 App 可在其余公司的 App 和网站中跟踪其流动。 苹果通常会在推送正式版更新之后的几周内进行对旧版 iOS 的验证。对于一些更新至最新 iOS 零碎的用户,当其遇到重大 Bug 时,降级的形式有时会非常无效。这也是苹果在敞开旧版 iOS 系统验证通道之前会期待一段时间的起因。 IT之家理解到,iOS 14.4.2 则提供了重要的安全更新,苹果此前倡议所有用户装置。
看完这些,大厂不是梦!!!明天刚刚开始面试,接下来这几天把遇到的面试题总结如下:1.内存治理在理论开发中的利用(必考,曾经被问两次了)答:1.UITableView的数据条数太多时会耗费内存,能够给UITableViewCell、UICollectionViewCell、UITableViewHeaderFooterView设置正确的复用ID,充沛复用。 2.有透明度的View(alpha值在0到1之间),opaque的值应该设置为YES,能够优化渲染零碎、进步性能。(当alpha值为0或1时,opaque的值对性能没有影响) 3.防止过于宏大的XIB/StoryBord文件,当加载XIB文件的时候,所有的内容都会被加到内存里,如果有一个不会立即用到的View,就是在节约内存资源。 4.不要让主线程承当过多的工作,否则会阻塞主线程,使app失去反馈。 5.加载本地图片时,应该保障UIImageView的大小和图片的大小雷同,因为缩放图片是很耗费资源的,特地是在UIImageView嵌套在UIScrollView中的状况下。如果是从网络加载的图片,能够等图片加载实现当前,开启一个子线程缩放图片,而后再放到UIImageView中。 6.在适合的场景下抉择适合的数据类型,对于数组:应用索引查问很快,应用值查问很慢,插入删除很慢,对于字典:用键来查找很快,对于汇合:用值来查找很快,插入删除很快。 7.网络下载文件时压缩(目前AFNetworking曾经默认压缩) 8.当UIScrollView嵌套大量UIView时会耗费内存,能够模拟UITableView的重用形式解决,当网络申请的时候能够应用提早加载来显示申请谬误的页面,因为网络申请谬误的页面不会马上用到,如果每次都先加载进去会耗费内存。 9.不大可能扭转但常常应用的货色能够应用缓存,比方cell的行高能够缓存起来,这样reloaddata的时候效率会很高。还有一些网络数据,不须要每次都申请的,应该缓存起来,能够写入数据库,也能够写入plist文件。 10.在appDelegate和UIViewController中都有解决内存正告的办法,注册并承受内存正告的告诉,一旦收到告诉就移除缓存,开释不须要的内存空间。 11.一些对象的初始化很慢,比方NSDateFormatter和NSCalendar,但你又必须要应用它,这时能够重用它们,有两种形式,第一种是增加属性到你的类,第二种是创立动态变量(相似于单例) 12.服务器端和客户端应用雷同的数据结构,防止重复解决数据,UIWebView中尽可能少的应用框架,用原声js最好,因为UIView的加载比较慢。 13.在循环创立变量解决数据的时候,应用主动开释池能够及时的开释内存。 14.加载本地图片的时候,如果只应用一次应用imageWithContentOfFile办法,因为imageNamed办法会缓存图片,耗费内存。 总结:面试时我只答了内存治理的法令和MRC、ARC、主动开释池的区别,并没有说出内存治理在理论开发中是怎么使用的,齐全没有答到点子上。 2.多线程的理论利用场景,回到主线程的办法 (必考,曾经被问两次)在理论开发中可能会有一些耗时的操作,这时能够开拓一个子线程把耗时的操作放到子线程中,当耗时操作执行结束当前再回到主线程刷新UI。必须要在主线程刷新UI,因为多线程是不平安的,如果在子线程中刷新UI可能会导致未知谬误。 回到主线程的办法是performSelectorOnMainTread 延时执行的代码:performSelector:onThread:withObject:waitUntillDone: 应用GCD回到主线程的办法:dispatch_get_main_queue() 应用GCD开启线程:dispatch_async([sŋk] ) 二者的区别:dispatch_async()不受运行循环模式的影响 总结:我只答复出了多线程的概念、长处,而面试官次要想听的关键词是:耗时、开启子线程、回到主线程刷新UI,所答非所问,而且回到主线程的办法名字我也没记住。这段话很重要,简直所有的iOS技术面试都会问到,肯定要熟记于心。 3.对于GCD的了解答:GCD中有两个外围概念,队列和工作。队列寄存工作,工作的取出遵循FIFO准则。队列其实就是线程池,在OC中以dispatch_queue_t示意,队列分串行队列和并发队列。工作其实就是线程执行的代码,在OC中以Block示意。在队列中执行工作有两种形式:同步执行和异步执行。 串行队列:工作一个一个执行。 并发队列:同一时间有多个工作被执行。 区别:会不会有工作放在别的线程(因为并发队列是取出一个工作放到别的线程,再取出一个工作放到另一个线程,因为动作很快,能够忽略不计,所以看起来所有工作都是一起执行的) 同步执行:不会开启新的线程,工作按程序执行。 异步执行:会开启新的线程,工作能够并发执行。 区别:会不会开启新的线程。 组合: 同步串行队列:one by one异步串行队列:one by one (因为前一个工作不执行结束,队列不会调度)同步并行队列:one by one (因为同步执行不会开启新的线程)异步并发队列:能够实现工作的并发,常常用到 主队列:主队列是串行队列,只有一个线程,那就是主线程,增加到主队列中的工作会在主线执行。通过dispatch_get_main_queue获取主队列。 全局队列:全局队列是并发队列。能够通过dispatch_get_global_queue获取不同级别的全局队列。 同步主队列:死锁卡住不执行。主队列异步:one by one (因为没有开启新线程) 总结:我只答复出了延时操作应用gcd,应用加载动画时用到了dispatch_after办法,都太外表了,面试官想要听的是更深层次的了解,于是问了我对于工作和队列,我没有想到这方面,当前提到gcd首先要想到的就是工作和队列,因为工作和队列是gcd的外围概念。 4.TCP、HTTP、WebSokect的区别答:IP协定(网络层协定)TCP:传输控制协议,次要解决数据如何在网络中传输,面向连贯,牢靠。(传输层协定)UDP:用户数据报协定,面向数据报,不牢靠。HTTP:次要解决如何包装数据。(应用层协定)Socket:是对TCP/IP协定的封装,Socket自身并不是协定,而是一个调用接口(API),通过Socket,咱们能力应用TCP/IP协定。(传输层协定) HTTP连贯:短连贯,客户端向服务器发送一次申请,服务器端响应连贯后会立刻端掉。Socket连贯:长连贯,实践上客户端和服务器端一旦建设连贯将不会被动端掉。 建设Socket连贯至多须要一对套接字,其中一个运行于客户端,称为ClientSocket,另一个运行于服务器端,称为ServerSocket。套接字之间的连贯过程分为三个步骤:服务器监听,客户端申请,连贯确认。 WebSocket是双向通信协定,模仿Socket协定,能够双向发送或承受信息。HTTP是单向的。Socket是传输管制层协定,WebSocket是应用层协定。 总结:我本人简历上写的货色我还不了解,很难堪,即便说不出来深层次的货色也应该把他们的次要区别说进去。还有网络七层模型肯定要记住,几年前的面试官就问过我我就没记住。 从底层到顶层别离是物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。 5.iOS应用程序的生命周期,最次要的是什么application:willFinishLaunchingWithOptions: 程序启动application:didFinishLaunchingWithOptions: 入口,只执行一次,启动实现筹备开始运行applicationWillResignActive: 切换到非活动状态,如按下home键、切换程序applicationDidBecomeActive: 切换到激活状态applicationDidEnterBackground: 应用程序进入后盾applicationWillEnterForeground: 应用程序将要被激活applicationWillTerminate: 应用程序将要退出 App的启动过程:关上程序——执行main函数——UIAPPlicationMain函数——初始化UIAPPlicationMain函数(设置代理,开启runloop)——监听系统事件,告诉AppDelegate——程序完结 总结:面试官问的是应用程序的生命周期,而我答的是Viewcontroller的生命周期,面试官次要想听到的关键词应该是:main函数、UIApplicationMain、AppDelegate、runloop、监听 另外总结一下对于runloop的知识点: runloop:运行循环,在程序运行过程中循环做一些事runloop的作用:放弃程序继续运行、解决App中的各种事件、进步资源利用率runloop在理论我的项目中的利用:控制线程的生命周期、解决NSTimer在滑动时进行工作的问题、监控利用的卡顿、性能优化 6.iOS的外围动画 (必考,曾经被问两次)答:动画有两种根本类型:隐式动画(始终存在,须要手动敞开)和显式动画(不存在,须要手动创立)UIView的动画: UIViewAnimationOptionCurveEaseInOut //工夫曲线函数,由慢到快 ...
各位最近应该忙于跳槽与面试吧,毕竟金三银四,珍惜好机会,预祝大家面试顺利通过,迎接大厂offer。有须要材料能够私聊我理解 从输出url到页面展现到底产生了什么1、输出地址2、浏览器查找域名的 IP 地址3、浏览器向 web 服务器发送一个 HTTP 申请4、服务器的永恒重定向响应5、浏览器跟踪重定向地址6、服务器解决申请7、服务器返回一个 HTTP 响应8、浏览器显示 HTML9、浏览器发送申请获取嵌入在 HTML 中的资源(如图片、音频、视频、CSS、JS等等)iOS高频(根底+底层)面试题你在开发过程中罕用到哪些定时器,定时器工夫会有误差吗,如果有,为什么会有误差? iOS中常NSTimer、CADisplayLink、GCD定时器,其中NSTimer、CADisplayLink基于NSRunLoop实现,故存在误差,GCD定时器只依赖零碎内核,绝对一前两者是比拟准时的。 误差起因是:与NSRunLoop机制无关, 因为RunLoop每跑完一次圈再去查看以后累计工夫是否曾经达到定时设置的间隔时间,如果未达到,RunLoop将进入下一轮工作,待工作完结之后再去查看以后累计工夫,而此时的累计工夫可能曾经超过了定时器的间隔时间,故会存在误差。 block是类吗,有哪些类型?block也算是个类,因为它有isa指针,block.isa的类型包含 _NSConcreteGlobalBlock 跟全局变量一样,设置在程序的数据区域(.data)中_NSConcreteStackBlock栈上(后面讲的都是栈上的 block)_NSConcreteMallocBlock 堆上这个isa能够按位运算block的分类,__block的作用,block循环援用产生的起因及解决办法blcok分为全局blcok,堆block,栈block。在 MRC下:只有没有拜访内部变量,就是全局block。拜访了内部变量,就是栈block。显示地调用[block copy]就是堆block。在 ARC下:只有没有拜访内部变量,就是全局block。如果拜访了内部变量,那么在拜访内部变量之前存储在栈区,拜访内部变量之后存储在堆区。__block的作用:将内部变量的传递模式由值传递变为指针传递,从而能够获取并且批改内部变量的值。同样,内部变量的批改,也会影响block函数的输入。block循环援用问题:当一个类的对象持有block,block外面又援用了这个对象,那么就是一个循环援用的关系。能够用strong-weak-dance的办法解除循环援用。 浅复制和深复制的区别?答案:浅层复制:只复制指向对象的指针,而不复制援用对象自身。深层复制:复制援用对象自身。意思就是说我有个A对象,复制一份后失去A_copy对象后,对于浅复制来说,A和A_copy指向的是同一个内存资源,复制的只不过是是一个指针,对象自身资源还是只有一份,那如果咱们对A_copy执行了批改操作,那么发现A援用的对象同样被批改,这其实违反了咱们复制拷贝的一个思维。深复制就好了解了,内存中存在了两份独立对象自身。用网上一哥们艰深的话将就是:浅复制好比你和你的影子,你完蛋,你的影子也完蛋深复制好比你和你的克隆人,你完蛋,你的克隆人还活着。 有想要支付收费材料的能够进裙或者加好友支付,这里有一个iOS交换圈:710 558 675 能够来理解,分享BAT,阿里面试题、面试教训,探讨技术,裙里材料间接下载就行, 大家一起交流学习!类别的作用?继承和类别在实现中有何区别?答案:category 能够在不获悉,不扭转原来代码的状况下往里面增加新的办法,只能增加,不能删除批改。并且如果类别和原来类中的办法产生名称抵触,则类别将笼罩原来的办法,因为类别具备更高的优先级。类别次要有3个作用: (1)将类的实现扩散到多个不同文件或多个不同框架中。(2)创立对公有办法的前向援用。(3)向对象增加非正式协定。继承能够减少,批改或者删除办法,并且能够减少属性。oc中的协定和java中的接口概念有何不同?答案:OC中的代理有2层含意,官网定义为 formal和informal protocol。前者和Java接口一样。informal protocol中的办法属于设计模式思考领域,不是必须实现的,然而如果有实现,就会扭转类的属性。其实对于正式协定,类别和非正式协定我很早前学习的时候大抵看过,也写在了学习教程里“非正式协定概念其实就是类别的另一种表达方式“这里有一些你可能心愿实现的办法,你能够应用他们更好的实现工作”。这个意思是,这些是可选的。比方我门要一个更好的办法,咱们就会申明一个这样的类别去实现。而后你在前期能够间接应用这些更好的办法。这么看,总感觉类别这玩意儿有点像协定的可选协定。"当初来看,其实protocal曾经开始对两者都对立和标准起来操作,因为材料中说“非正式协定应用interface润饰“,当初咱们看到协定中两个修饰词:“必须实现(@requied)”和“可选实现(@optional)”。 oc中可批改和不能够批改类型。答案:可批改不可批改的汇合类。这个我集体简略了解就是可动静增加批改和不可动静增加批改一样。比方NSArray和NSMutableArray。前者在初始化后的内存控件就是固定不可变的,后者能够增加等,能够动静申请新的内存空间。 对于多态性答案:多态,子类指针能够赋值给父类。这个题目其实能够出到所有面向对象语言中,因而对于多态,继承和封装根本最好都有个自我意识的了解,也并非肯定要把书上材料上写的能背进去。最重要的是转化成自我了解。 告诉和协定的不同之处?答案:协定有管制链(has-a)的关系,告诉没有。首先我一开始也不太明确,什么叫管制链(专业术语了~)。然而简略剖析下告诉和代理的行为模式,咱们大抵能够有本人的了解简略来说,告诉的话,它能够一对多,一条音讯能够发送给多个音讯接受者。代理按咱们的了解,到不是间接说不能一对多,比方咱们晓得的明星经济代理人,很多时候一个经济人负责好几个明星的事务。只是对于不同明星间,代理的事物对象都是不一样的,一一对应,不可能说今天要解决A明星要一个发布会,代理人收回解决发布会的音讯后,别称B的发布会了。然而告诉就不一样,他只关怀发出通知,而不关怀多少接管到感兴趣要解决。因而管制链(has-a从英语单词大抵能够看出,繁多领有和可管制的对应关系。 线程和过程的区别?1 一个应用程序对应一个过程,一个过程帮忙程序占据一块存储空间 2 要想在过程中执行工作,就必须开启线程,一条线程就代表一个工作 3 一个过程中容许开启多条线程,也就是同时执行多个工作 Objective-C如何对内存治理的,说说你的认识和解决办法?1 每个对象都有一个援用计数器,每个新对象的计数器是1,当对象的计数器减为0时,就会被销毁 2通过retain能够让对象的计数器+1、release能够让对象的计数器-1 3 还能够通过autorelease pool治理内存 如果用ARC,编译器会主动生成治理内存的代码 堆和栈的区别?1 堆空间的内存是动态分配的,个别寄存对象,并且须要手动开释内存 2 栈空间的内存由零碎主动调配,个别寄存局部变量等,不须要手动治理内存 为什么很多内置的类,如TableView的delegate的属性是assign不是retain? 1 tableView的代理个别都是它所属的控制器,控制器会对它外部的view做一次retain操作2 假如tableView也对代理(控制器)做一次retain操作,那么就呈现循环retain问题定义属性时,什么状况应用copy、assign、retain? 1 copy:NSString、Block等类型2 assign:根本数据类型3> retain:OC对象类型 App冷启动优化?App冷启动优化计划博客十分之多,概括总结大抵如下: pre-main优化:缩小动静动态库,合并动静库,移除废除第三方库及所依赖的零碎库,二进制重排(抖音优化计划) runtime对类的注册,类对象的初始化,load办法加载阶段:精简类,合并分类,移除废除分类等等 main函数之后,推延对三方库注册及延时调用耗时操作函数。能够通过Instruments–>Time Profiler: 性能剖析,定位耗时函数 ...
分类、扩大、代理、告诉、KVC、KVO、属性关键字 一、分类(Category) *1、分类的作用? 申明公有办法,合成体积大的类文件 *2、分类的特点? 能够为零碎类增加分类。在运行时期间,将 Category 中的实例办法列表、协定列表、属性列表增加到主类中后(所有Category中的办法在办法列表中的地位是在主类的同名办法之前的),而后会递归调用所有类的 load 办法,这一切都是在main函数之前执行的。 *3、分类能够增加哪些内容? 实例办法,类办法,属性(增加getter和setter办法,并没有实例变量,增加实例变量须要用关联对象) *4、如果工程里有两个分类A和B,两个分类中有一个同名的办法,哪个办法最终失效? 取决于分类的编译程序,最初编译的那个分类的同名办法最终失效,而之前的都会被笼罩掉(这里并不是真正的笼罩,因为其余办法依然存在,只是拜访不到,因为在动静增加类的办法的时候是倒序遍历办法列表的,而最初编译的分类的办法会放在办法列表后面,拜访的时候就会先被拜访到,同理如果申明了一个和原类办法同名的办法,也会笼罩掉原类的办法)。 5、如果申明了两个同名的分类会怎么? 会报错,所以第三方的分类,个别都带有命名前缀 6、分类能增加成员变量吗? 不能。只能通过关联对象(objc_setAssociatedObject)来模仿实现成员变量,但其实质是关联内容,所有对象的关联内容都放在同一个全局容器哈希表中:AssociationsHashMap,由AssociationsManager对立治理。 二、扩大(Extension) 1、扩大的作用? 申明公有属性,申明公有成员变量 2、扩大的特点? 编译时决定,只能以申明的模式存在,少数状况下放在在宿主类的.m中,不能为零碎类增加扩大 三、代理(Delegate) 代理是一种设计模式,委托方申明协定,定义须要实现的接口,代理方依照协定实现办法 个别用weak来防止循环援用 四、告诉(NSNotification) 应用观察者模式用于实现跨层传递信息的机制。传递形式是一对多。 五、KVO(key-value-observing) KVO是观察者的另一实现 应用了isa混写(isa-swizzling)来实现KVO 应用setter办法扭转值KVO会失效,应用KVC扭转值KVO也会失效,因为KVC会调用setter办法 - (void)setValue:(id)value { [self willChangeValueForKey:@"key"]; [super setValue:value]; [self didChangeValueForKey:@"key"];}间接赋值成员变量不会触发KVO,因为不会调用setter办法,须要加上willChangeValueForKey和didChangeValueForKey 六、KVC(key-value-coding) KVC能够通过key间接拜访对象的属性,或者给对象的属性赋值,这样能够在运行时动静的拜访或批改对象的属性 当调用setValue:属性值 forKey:@”name“的代码时,,底层的执行机制如下: 1、程序优先调用set<Key>:属性值办法,代码通过setter办法实现设置。留神,这里的<key>是指成员变量名,首字母大小写要合乎KVC的命名规定,下同 2、如果没有找到setName:办法,KVC机制会查看+ (BOOL)accessInstanceVariablesDirectly办法有没有返回YES,默认该办法会返回YES,如果你重写了该办法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:办法,不过个别开发者不会这么做。所以KVC机制会搜寻该类外面有没有名为<key>的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的拜访修饰符,只在存在以<key>命名的变量,KVC都能够对该成员变量赋值。 3、如果该类即没有set<key>:办法,也没有_<key>成员变量,KVC机制会搜寻_is<Key>的成员变量。 4、和下面一样,如果该类即没有set<Key>:办法,也没有_<key>和_is<Key>成员变量,KVC机制再会持续搜寻<key>和is<Key>的成员变量。再给它们赋值。 5、如果下面列出的办法或者成员变量都不存在,零碎将会执行该对象的setValue:forUndefinedKey:办法,默认是抛出异样。 如果想禁用KVC,重写+ (BOOL)accessInstanceVariablesDirectly办法让其返回NO即可,这样的话如果KVC没有找到set<Key>:属性名时,会间接用setValue:forUndefinedKey:办法。 当调用valueForKey:@”name“的代码时,KVC对key的搜寻形式不同于setValue:属性值 forKey:@”name“,其搜寻形式如下: 1、首先按get<Key>,<key>,is<Key>的程序办法查找getter办法,找到的话会间接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象 2、如果下面的getter没有找到,KVC则会查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格局的办法。如果countOf<Key>办法和另外两个办法中的一个被找到,那么就会返回一个能够响应NSArray所有办法的代理汇合(它是NSKeyValueArray,是NSArray的子类),调用这个代理汇合的办法,或者说给这个代理汇合发送属于NSArray的办法,就会以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes这几个办法组合的模式调用。还有一个可选的get<Key>:range:办法。所以你想从新定义KVC的一些性能,你能够增加这些办法,须要留神的是你的办法名要合乎KVC的规范命名办法,包含办法签名。 3、如果下面的办法没有找到,那么会同时查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格局的办法。如果这三个办法都找到,那么就返回一个能够响应NSSet所的办法的代理汇合,和下面一样,给这个代理汇合发NSSet的音讯,就会以countOf<Key>,enumeratorOf<Key>,memberOf<Key>组合的模式调用。 4、如果还没有找到,再查看类办法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员变量名,这里不举荐这么做,因为这样间接拜访实例变量毁坏了封装性,使代码更软弱。如果重写了类办法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会间接调用valueForUndefinedKey:办法,默认是抛出异样 七、属性关键字 1、读写权限:readonly,readwrite(默认) 2、原子性:(atomic),nonatimic。atomic读写平安,但效率低,不是相对的平安,比方操作数组,减少或移除,这种状况能够应用互斥锁来保障线程平安 3、援用计数 retain/strong assign润饰根本数据类型 ...
仿悬浮提词器,实现以下性能: 增加任意 UIView 到画中画窗口;按需暗藏零碎的快进快退按钮、播放按钮、进度条;动静批改画中画窗口的形态,横向、竖向 or 方形;旋转窗口;基于常驻线程的线程保活措施,让你的画中画永不停歇;拍摄视频画中画的 timer 也不会进行;审核被拒的胜利应答措施。演示:
我须要制作一个疾速更新 UIImage 的性能,一开始应用 UIImageView 来显示图片,所以须要频繁的调用 UIImageView.image = newImage 办法来更新图片。代码看起来像这样。 func onSomethingChanged() { myUIImageView.image = newImage}这样更新图片十分的卡顿,操作和画面响应有肯定的滞后,体验不好,因而思考本人做一个 UIView 来绘制图片,看看是否能够解决。 于是本人自定义了一个 View,继承 UIView 来做,在 draw(rect:) 办法外面做文章。 首先当然是简略粗犷间接 UIImage.draw(in: bounds) 先看看成果。 var image: UIImage? { didSet { setNeedsDisplay() }} override func draw(_ rect: CGRect) { guard let ctx = UIGraphicsGetCurrentContext() else { return } ctx.addRect(bounds) ctx.setFillColor(UIColor.black.cgColor) ctx.fillPath() if var im = image { im.draw(in: bounds) }}很好,更新速度很快,是现实的成果,不过目前画面是拉伸的,同时还有锯齿。 下图中右边是现实的显示成果,左边则是理论的显示成果,能够看到显著的锯齿。 通过我一番搜寻尝试了以下配置,均无任何帮忙: ctx.setShouldAntialias(true)ctx.setAllowsAntialiasing(true)ctx.interpolationQuality = .highlayer.allowsEdgeAntialiasing = truelayer.minificationFilter = .trilinear我回忆起之前用 CGImageContext 缩放图片的时候也没有这个问题啊,难道是因为 UIView 自带的这个 CGContext 无奈很好的缩放图片? ...
简介:源自支付宝的扫码组件,全网收费接入About Scan随着支付宝的线下场景不断扩大,收钱码、口碑、共享单车、充电宝、停车缴费等产品让咱们的生存越来越便当。 二维码因为成本低、兼容性好成为了线上线上最次要的连贯工具,也因而面临更多新的挑战。 因为二维码是一种点阵式信息编码方式,任何视觉上的缺损、蜿蜒以及光线作用都会极大的影响辨认成功率,如果辨认艰难也就意味着用户可能抉择放弃,影响领取体验也影响用户心智。 源自支付宝的扫码组件,全网收费凋谢!欢送下载接入~ 下载地址关注「mPaaS」CSDN 账号即可收费下载 https://download.csdn.net/download/m0\_47737908/15684443 插件介绍本插件是支付宝 mPaaS 的扫码组件,让您的 app 能够领有像支付宝一样的扫码体验,辨认速度、识别率远超开源扫码。扫码组件完全免费提供应用,接入时须要您在阿里云上进行注册开明并将 mPaaS 扫码增加到您的工程即可。 接入过程中,您遇到任何问题,都能够在钉钉上搜寻「32843812」进群进行解答。 欢送大家应用不同带有扫码性能的 App,对以下三种二位码进行扫码辨认,体验 mPaaS 扫码弱小的辨认能力和辨认速度 弱光二维码 反光二维码 含糊二维码 插件应用筹备1.购买插件,抉择该插件绑定的我的项目。 2.在 HBuilderX 里找到我的项目,在 manifest 的 app 原生插件配置中勾选模块,如须要填写参数则参考本文增加。 3.依据本文的提供的文档开发代码,在代码中援用插件,调用插件性能。 4.打包自定义基座,抉择插件,失去自定义基座,而后运行时抉择自定义基座,进行 log 输入测试。 5.开发结束后正式云打包 付费原生插件目前不反对离线打包。 Android 离线打包原生插件另见文档 https://nativesupport.dcloud.net.cn/NativePlugin/offline\_package/android iOS 离线打包原生插件另见文档 https://nativesupport.dcloud.net.cn/NativePlugin/offline\_package/ios 注意事项:应用 HBuilderX2.7.14 以下版本,如果同一插件且同一appid下购买并绑定了多个包名,提交云打包界面提醒包名绑定不统一时,须要在HBuilderX我的项目中manifest.json->“App原生插件配置”->”云端插件“列表中删除该插件从新抉择 插件应用流程1. 开明阿里云 mPaaS登陆阿里云账号拜访mPaaS 产品页,点击「立刻开明」,即可开沟通 mPaaS 产品。 2. 创立 mPaaS 利用开明后您须要创立一个 mPaaS 利用 3. 配置 Config 并下载3.1 Android3.1.1 填写配置信息,并上传签名 APK。点击 代码治理 > 代码配置 > Android,输出 Package Name(利用包名)(此处以 com.mpaas.demo 为例),上传编译并增加签名后的 APK 安装包。对于疾速生成签名后的 APK 相干信息,请参见 生成控制台用签名 APK。 ...
在IOS上录音,须要先取得权限,不然录音只能返回空数据。 取得权限有两步: 1.Info设置在Xcode->Info->Custom iOS target properties里,增加Privacy-Microphone Usage Description设置,并填写一段形容。 2.代码中动静申请//recordPermission获取以后的权限状况//requestRecordPermission 申请权限-(void)checkRecordPermission{ AVAudioSession* session = [AVAudioSession sharedInstance]; AVAudioSessionRecordPermission recordPermission = [session recordPermission]; if( recordPermission == AVAudioSessionRecordPermissionUndetermined ) { NSLog(@"record permission:%d", recordPermission ); if( [session respondsToSelector:@selector( requestRecordPermission:)]) { [session requestRecordPermission:^(BOOL granted) { if( granted ) { NSLog(@"get record permission"); } else{ NSLog(@"not permit record"); } }]; } } else{ if( recordPermission == AVAudioSessionRecordPermissionGranted ) { NSLog(@"already get record permission"); } else{ NSLog(@"already denied record permission ,please set in settings"); } }}
TableView 是iOS app 中最罕用的控件,许多代码间接或者间接的关联到table view工作中,包含提供数据、更新tableView、管制tableView行为等等。上面会提供放弃tableView代码整洁和构造清晰的办法。 iOS开发交换技术群:563513413,不论你是大牛还是小白都欢送入驻 ,分享BAT,阿里面试题、面试教训,探讨技术, 大家一起交流学习成长! UITableViewController vs. UIViewController TableViewController的个性 table view controllers能够读取table view的数据、设置tabvleView的编辑模式、反馈键盘告诉等等。同时Table view controller可能通过应用UIRefreshControl来反对“下拉刷新”。 Child View Controllers tableViewController也能够作为child view controller增加到其余的viewController中,而后tableViewController会持续治理tableView,而parentViewController能治理其余咱们关怀的货色。 -(void)addDetailTableView{ DetailViewController *detail = [DetailViewController new]; [detail setup]; detail.delegate = self; [self addChildViewController:detail]; [detail setupView]; [self.view addSubview:detail.view]; [detail didMoveToParentViewController:self]; } 如果在应用以上代码时,须要建设child View controller 和 parent view controller之间的分割。比方,如果用户抉择了一个tableView里的cell,parentViewController须要晓得这件事以便可能响应点击工夫。所以最好的办法是table view controller定义一个协定,同时parent view controller实现这个协定。 @protocol DetailViewControllerDelegate-(void)didSelectCell; @end @interface ParentViewController () <DetailViewControllerDelegate> @end @implementation ParentViewController //.... ...
导语view controller通常是一个我的项目中最宏大的文件,因为它外面常常蕴含了不属于它的代码,同时这也使它成为代码中最难以重用的局部。所以为view controller瘦身,让其中的代码复用性更强,把相干代码放到正确的中央显得尤其重要。iOS开发交换技术群:563513413,不论你是大牛还是小白都欢送入驻 ,分享BAT,阿里面试题、面试教训,探讨技术, 大家一起交流学习成长! 将Data Source和其余协定拆散为view controller瘦身最无效的办法就是把UITableViewDataSource中的代码移动到相干的类中,具体的办法能够参阅《iOS利用开发 扼要TableView》中的相干实现。 而更进一步,不只是TableView,这个办法能够扩大到其余的协定上,比方UICollectionViewDataSource。如果在开发中抉择应用UICollectionView代替UITableView时,这个办法能够让你简直不必批改viewController中的任何货色,甚至能够让Data Source同时反对两个协定,给予了极大的便利性。 将弱业务逻辑移到Model中注:markdown对代码块的语法是开始和完结行都要增加:```,其中 ` 为windows键盘左上角 首先是代码,以下的代码是帮忙用户查找优先事项的列表: -(void)loadPriorities { NSDate *now = [NSDate date]; NSString *formatString = @"startDate <= %@ AND endDate >= %@"; NSPredicate *predicate = [NSPredicate predicateWithFormat:formatString, now, now]; NSSet *priorities = [self.user.priorities filteredSetUsingPredicate:predicate]; self.priorities = [priorities allObjects]; } 然而,如果把这些代码移动到User类中会让它变得更加清晰,这时ViewController.m中会是: -(void)loadPriorities { self.priorities = [self.user currentPriorities]; } 而User + Extensions.m中则是: -(NSArray *)currentPriorities { NSDate *now = [NSDate date]; NSString *formatString = @"startDate <= %@ AND endDate >= %@"; NSPredicate *predicate = [NSPredicate predicateWithFormat:formatString, now, now]; return [[self.priorities filteredSetUsingPredicate:predicate] allObjects]; } 将这些代码移动的根本原因是因为ViewController.m是大部分业务逻辑的载体,自身代码的复杂度曾经很高,所以这类跟业务关联不大的代码比方日期转换、图像裁剪、设定过滤器等的操作能够拆散到各自的类中实现,一方面为viewController减负,另一方面也能增进代码的复用。 对于这个题目的翻译我斟酌了比拟久的工夫,因为在原文中是“Move Domain Logic into the Model”,意为“把畛域逻辑移到Model中”。对于“畛域逻辑”一词我进行过讲究,大抵意思为“稳固的、不会扭转的逻辑关系”,同时在原文中也是应用了NSPredicate作为例子援用,而我认为其例子中的代码也是与业务相干的,只不过关联性不大,而且不会轻易改变,所以应用了“弱业务逻辑”一词代替了“畛域逻辑”一词。把数据处理的逻辑移到服务层一些代码可能没方法很无效的挪动到model中,然而这些代码却和model中的代码有清晰的关联,对于这种问题,能够应用Store。比方在上面的代码中,viewController须要实现从一个文件中获取一些数据,并对其进行操作: -(void)readArchive { NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSURL *archiveURL = [bundle URLForResource:@"photodata" withExtension:@"bin"]; NSDate *data = [NSData dataWithContentsOfURL:archiveURL options:0 error:NULL]; NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; _users = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"users"]; _photos = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"photos"]; [unarchiver finishDecoding]; } 事实上,view controller不须要分明怎么实现这些货色,而应该将这些解决交给一个store object来实现。 通过对代码进行拆散,可能增进代码复用、对代码进行单元测试、放弃view controller整洁等。同时可能让view controller更多关注于业务自身的内容,把数据的读取 、缓存、新建等操作交给服务层来解决。 把网络服务的逻辑移到Model层这与下面提到的十分相似:不要把网络服务的逻辑放到view controller中,而应该把它们寄存到不同的类中。 对于view controller,应该只是应用一个completion block来调用这些办法,而把网络申请、错误处理、缓存解决交给这些类来实现 ...
Objective-C内存治理的对象iOS开发中,内存中的对象次要有两类,一类是值类型,比方int、float、struct等根本数据类型,另一类是援用类型,也就是继承自 NSObject类的所有的OC对象。前一种值类型不须要咱们治理,后一种援用类型是须要咱们治理内存的,一旦治理不好,就会产生十分蹩脚的结果。 为什么值类型不须要治理,而援用类型须要治理呢?那是因为他们分配内存形式不一样。 值类型会被放入栈中,他们顺次严密排列,在内存中占有一块间断的内存空间,遵循先进后出的准则。援用类型会被放到堆中,当给对象分配内存空间时,会随机的从内存当中开拓空间,对象与对象之间可能会留有不确定大小的空白空间,因而会产生很多内存碎片,须要咱们治理。 栈内存与堆内存从性能上比拟,栈内存要优于堆内存,这是因为栈遵循先进后出的准则,因而当数据量过大时,存入栈会显著的升高性能。因而,咱们会把大量的数据存入堆中,而后栈中寄存堆的地址,当须要调用数据时,就能够疾速的通过栈内的地址找到堆中的数据。 值类型和援用类型之间是能够互相转化的,把值类型转化为援用类型的过程叫做装箱,比方把int包装为NSNumber,这个过程会减少程序的运行时 间,升高性能。而把援用类型转为值类型的过程叫做拆箱,比方把NSNumer转为float,在拆箱的过程中,咱们肯定要留神数据原有的类型,如果类型错 误,可能导致拆箱失败,因而会存在安全性的问题。手动的拆箱和装箱,都会减少程序的运行工夫,升高代码可读性,影响性能。 在IOS开发过程中,栈内存中的值类型零碎会主动治理,堆内存中的援用类型是须要咱们治理的。每个OC对象外部都专门有四个字节来存储援用计数器, 它是一个整数,示意对象被援用的次数,通过它能够判断对象是否被回收,如果援用计数为0,对象回收,不为0不回收。当对象执行alloc、new或者 retain时,援用计数加1,release时,援用计数减1。
导读6月23日凌晨 1 点,苹果 WWDC20 开发者大会在线上以主题演讲的形式,在 Apple Park 进行直播。 23-26日,苹果公开了 100 多个面向开发者的视频,内容涵盖Swift / SwiftUI 、App Clips、Widgets、Privacy & Security 等等方面。 对于开发者和程序员来说,咱们有哪些新发现和新思考? 淘系技术客户端团队,将给大家带来一系列对于新零碎背地的启发,欢送交换探讨。 本篇内容来自于阿里巴巴淘系技术部,淘宝客户端iOS架构组,高级开发工程师倾寒。 前言SwiftUI 是苹果公司于 2019 年推出的 Apple Platform 的新一代申明式布局引擎,笔者于去年第一工夫降级 Beta 尝鲜全家庭,并在短时间内迅速落地了基于 SwiftUI 的外部 APP, 也分享了几篇对于 SwiftUI 的文章, 但 SwiftUI 1.0 根本没有任何公司敢用在正式上线的主 APP 上,API 在 Beta 版本之间各种废除,UI 款式常常不兼容,大列表性能差,彼时都标识着 SwiftUI 还称为一个 Toy Framewrok. 随着 WWDC 20 相干新个性和介绍视频的释出,都明确的宣告着 SwiftUI 元年曾经到了,SwiftUI 曾经成长为新时代的布局引擎。 以下从几个方面分享对于 SwiftUI 的重大扭转及外围劣势。 PS: 须要读者对 Swift 及 SwiftUI 1.0 有肯定相熟。 ...
APICloud在线打包流程介绍 1、先在APICloud开发工具同步代码到线上,然后进入开发控制台,选择要打包的应用,选择证书,在ios证书那里,上传申请的iOS证书文件 特别注意: 正式证书:这项要上传发布证书(上架用的,打包后用于上架不能直接安装), 测试证书:这项上传开发证书(打包后可以安装到手机测试) 如果你只暂时只申请了开发证书,先上传测试证书那项,正式证书后面申请了再添加也行! APICloud平台可以在Windows直接开发iOS应用! 如果还没申请iOS证书文件,可以看下面教程申请,可以直接在Windows电脑申请iOS证书! Windows系统直接申请iOS证书文档 输入APP ids(应用id、包名,跟证书使用的保持一致) 、 输入申请的.p12文件及配置文件.mobileprovision,输入申请p12设置的密码保存即可! 2、选择云编译,选择iOS平台,设置要对应信息打包! 3、编译完成了,下载IPA。 如果是正式证书打包的下载后用AU软件上传审核 测试证书打包的下载ipa后可以通过爱思助手安装!
现在很多Windows开发平台,可以直接开发安卓及iOS应用。 但一般如果需要上架App Store需要用苹果电脑,如果没有比较麻烦。 Appuploader苹果上架辅助助,可以辅助开发者在Windows申请iOS证书及上传ipa,方便快捷! 工具官网 http://www.applicationloader....
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/c0e12a897971 `这段时间公司一直比较忙,和组里小伙伴一起把公司项目按照之前逻辑重写了一下。由于项目比较大,还要兼顾之前项目的迭代和其他项目,目前为止只写完第一阶段。之前项目本地持久化方案主要用的是SQLite,这次重写项目打算换一种持久化方案,于是我们经过讨论选择了苹果的“亲儿子”CoreData。在使用CoreData的过程中,我也是一边学习一边实践。在学习的过程中,一些写的质量比较高的博客对我的帮助也很大,例如objc.io等博客,在这里就不一一列举出来了,非常感谢这些作者。``先不说项目中用不用得到,其实很多人都是不了解CoreData的,但是经过我的学习发现CoreData还是挺不错的。所以正如这系列文章的名字一样-认识CoreData,打算写这系列文章来认识一下CoreData。这系列博客将从简单到复杂的来讲一下CoreData,其中除了基础使用还会包括多线程、批量数据处理等内容,这些很多都是我公司项目开发过程中接触到的,我们也设想了一些极端的情况,解决方案都会体现在这系列博客中。` `本人接触CoreData时间并不长,只是专门花了一段时间学习CoreData。本系列文章偏重于通过图形化界面使用CoreData,不会全部采取纯代码进行CoreData的所有操作,而且那样操作起来也确实比较麻烦,反而就失去了CoreData的优势和本质。` 文章中如有疏漏或错误,还请各位及时提出,谢谢!???? 写在前面在CoreData中有一些常用的类,称呼可能各不相同。所以这里先约定一些关键字,以便理解后面的一些内容,这些约定很多都是出现在苹果的官方文档中的。NSPersistentStoreCoordinator(Persistent Store Coordinator),缩写为PSC。NSManagedObjectContext(Managed Object Context),缩写为MOC。NSManagedObjectModel(Managed Object Model),缩写为MOM。NSManagedObject及其子类,根据英文翻译和其作用,称之为托管对象。后缀名为.xcdatamodeld的文件,因为存储着所有实体的数据结构和表示,所以称之为模型文件。 什么是CoreData?简单介绍一下CoreData出现在iOS3中,是苹果推出的一个数据存储框架。CoreData提供了一种对象关系映射(ORM)的存储关系,类似于Java的hibernate框架。CoreData可以将OC对象存储到数据库中,也可以将数据库中的数据转化为OC对象,在这个过程中不需要手动编写任何SQL语句,这是系统帮我们完成。 CoreData最大的优势就是使用过程中不需要编写任何SQL语句,CoreData封装了数据库的操作过程,以及数据库中数据和OC对象的转换过程。所以在使用CoreData的过程中,很多操作就像是对数据库进行操作一样,也有过滤条件、排序等操作。 这就相当于CoreData完成了Model层的大量工作,例如Model层的表示和持久化,有效的减少了开发的工作量,使Model层的设计更加面向对象。 CoreData好用吗?之前听人说过,CoreData比较容易入手,但是很难学精。这也是很多人说CoreData不好用的原因之一,只是因为使用方式有问题,或者说并没有真正掌握CoreData。 如果从性能上来说,CoreData比SQLite确实略差一些。但是对于移动端来说,并不需要大型网站的高并发,所以这点性能差别几乎是没有影响的,所以这点可以忽略不计。在后面的文章中,将会给出CoreData的优点和缺点对比,以及详细的性能测评。 CoreData主要的几个类NSManagedObjectContext托管对象上下文,进行数据操作时大多都是和这个类打交道。 NSManagedObjectModel托管对象模型,一个托管对象模型关联一个模型文件(.xcdatamodeld),存储着数据库的数据结构。 NSPersistentStoreCoordinator持久化存储协调器,负责协调存储区和上下文之间的关系。 NSManagedObject托管对象类,所有CoreData中的托管对象都必须继承自当前类,根据实体创建托管对象类文件。 CoreData简单创建流程模型文件操作1.1 创建模型文件,后缀名为.xcdatamodeld。创建模型文件之后,可以在其内部进行添加实体等操作(用于表示数据库文件的数据结构)1.2 添加实体(表示数据库文件中的表结构),添加实体后需要通过实体,来创建托管对象类文件。1.3 添加属性并设置类型,可以在属性的右侧面板中设置默认值等选项。(每种数据类型设置选项是不同的)1.4 创建获取请求模板、设置配置模板等。1.5 根据指定实体,创建托管对象类文件(基于NSManagedObject的类文件) 实例化上下文对象2.1 创建托管对象上下文(NSManagedObjectContext)2.2 创建托管对象模型(NSManagedObjectModel)2.3 根据托管对象模型,创建持久化存储协调器(NSPersistentStoreCoordinator)2.4 关联并创建本地数据库文件,并返回持久化存储对象(NSPersistentStore)2.5 将持久化存储协调器赋值给托管对象上下文,完成基本创建。 CoreData结构CoreData的结构构成之前看到过几张介绍CoreData结构的图片,感觉其表示的结构比较清晰。可以通过这几张图片初步认识一下CoreData,在后面的文章中还会对这几个类进行详细解释。 上图中是初始化MOC所涉及到的一些类,由这些类实例化并最终构成可以使用的MOC。图中编号是实例化一个具备数据处理能力的MOC过程,这个过程和上面介绍过的实例化上下文对象相同。 在PSC创建并关联本地数据库,并设置为MOC的persistentStoreCoordinator属性后,MOC就具备对当前存储区所有托管对象操作的能力。但是需要注意的是,MOC对托管对象是懒加载的,在使用时才会被加载到MOC的缓存中。 MOM对象加载模型文件后,获取到模型文件中所有实体的构成结构。由于MOM中存储着模型文件的结构,PSC需要通过MOM对象实例化本地数据库。 所有属性都存在Entity中,以及有关联关系的属性和请求模板,这都会在后面的章节中讲到。 可以通过Entity创建继承自NSManagedObject类的文件,这个文件就是开发中使用的托管对象,具备模型对象的表示功能,CoreData的本地持久化都是通过这个类及其子类完成的。 持久化存储调度器在CoreData的整体结构中,主要分为两部分。一个是NSManagedObjectContext管理的模型部分,管理着所有CoreData的托管对象。一个是SQLite实现的本地持久化部分,负责和SQL数据库进行数据交互,主要由NSPersistentStore类操作。这就构成了CoreData的大体结构。 从图中可以看出,这两部分都是比较独立的,两部分的交互由一个持久化存储调度器(NSPersistentStoreCoordinator)来控制。上层NSManagedObjectContext存储的数据都是交给持久化调度器,由调度器调用具体的持久化存储对象(NSPersistentStore)来操作对应的数据库文件,NSPersistentStore负责存储的实现细节。这样就很好的将两部分实现了分离。 个人随想对于CoreData的整体结构,因为CoreData底层存储本来就是用SQLite实现的,所以我用CoreData的结构和SQLite对比了一下,发现还是很多相似之处的。 .xcdatamodeld文件代表着数据库文件结构,通过.xcdatamodeld编译后的.momd文件生成数据库。每个实体代表一张数据表,实体之间的关联关系就是SQLite的外键。 下图就是CoreData底层存储的结构,用红圈圈住的部分指向关联表的主键下标。例如1就指向关联表的主键下标为1的行。 CoreData杂谈CoreData数据存储安全CoreData本质还是使用SQLite进行存储,并没有另外提供加密功能,具体的数据加解密还需要自己完成。 CoreData在硬盘上的数据存储结构: 通过PSC指定创建SQLite目录后,会在指定的目录下生成一个数据库文件,同时还会生成两个同名但后缀不同的文件,其中只有后缀.sqlite的文件是存储数据的文件。 这个数据库文件中会默认生成三个表,Z_METADATA、Z_PRIMARYKEY、Z_MODELCACHE,其他我们自己的表也都是大写Z开头的。 在每个表中,系统还会默认生成三个字段,Z_PK、Z_ENT、Z_OPT三个字段,也都是大写Z开头并且带下划线的。其他字段就是我们自己的字段了,大写Z开头但不带下划线。 CoreData执行效率现在市面上的大多数项目,都是使用SQLite作为持久化的方案,而CoreData的使用并不是很普遍。对于这个问题,我认为首先是很多项目开始的比较早,那时候好多iOS程序员都是从其他语言转过来的,更加熟悉SQLite,所以用SQLite比较多一些。后面如果不进行大的项目重构,就很难换其他的持久化方案了。 还有就是不熟悉CoreData,也不想去了解和深入学习CoreData,我认为这是很大的原因。所以项目中用CoreData的人并不多,而真正掌握CoreData技术的人更少。 之前听其他人说CoreData的执行效率不如SQLite高,这个如果深究的话,确实CoreData要比SQLite效率差一些,只不过并没有太大区别。CoreData本质也是在底层执行SQL语句,只是CoreData的SQL语句执行逻辑比较耗时,没有手动编写SQL语句更加直接。我们可以将CoreData的调试功能打开,具体看一下SQL语句的执行。 这里要说一点,客户端毕竟不是服务端,不需要像服务器那样大量的数据查询,所以CoreData是完全可以应对客户端的查询量的。如果从灵活性来说,CoreData确实没有SQLite的灵活性高,一些SQLite的复杂功能可能也不能实现,但是就目前大多数项目来说,CoreData已经能够满足项目持久化需求了。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/0ddfa35c7898 第一篇文章中并没有讲CoreData的具体用法,只是对CoreData做了一个详细的介绍,算是一个开始和总结吧。这篇文章中会主要讲CoreData的基础使用,以及在使用中需要注意的一些细节。因为文章中会插入代码和图片,内容可能会比较多,比较考验各位耐心。 文章中如有疏漏或错误,还请各位及时提出,谢谢!????` 创建自带CoreData的工程在新建一个项目时,可以勾选Use Core Data选项,这样创建出来的工程系统会默认生成一些CoreData的代码以及一个.xcdatamodeld后缀的模型文件,模型文件默认以工程名开头。这些代码在AppDelegate类中,也就是代表可以在全局使用AppDelegate.h文件中声明的CoreData方法和属性。 系统默认生成的代码是非常简单的,只是生成了基础的托管对象模型、托管对象上下文、持久化存储调度器,以及MOC的save方法。但是这些代码已经可以完成基础的CoreData操作了。 这部分代码不应该放在AppDelegate中,尤其对于大型项目来说,更应该把这部分代码单独抽离出去,放在专门的类或模块来管理CoreData相关的逻辑。所以我一般不会通过这种方式创建CoreData,我一般都是新建一个“干净”的项目,然后自己往里面添加,这样对于CoreData的完整使用流程掌握的也比较牢固。 CoreData模型文件的创建构建模型文件使用CoreData的第一步是创建后缀为.xcdatamodeld的模型文件,使用快捷键Command + N,选择Core Data -> Data Model -> Next,完成模型文件的创建。 创建完成后可以看到模型文件左侧列表,有三个选项Entities、Fetch Requests、Configurations,分别对应着实体、请求模板、配置信息。 添加实体现在可以通过长按左侧列表下方的Add Entity按钮,会弹出Add Entity、Add Fetch Request、Add Configuration选项,可以添加实体、请求模板、配置信息。这里先选择Add Entity来添加一个实体,命名为Person。 添加Person实体后,会发现一个实体对应着三部分内容,Attributes、Relationships、Fetched Properties,分别对应着属性、关联关系、获取操作。 现在对Person实体添加两个属性,添加age属性并设置type为Integer 16,添加name属性并设置type为String。 实体属性类型在模型文件的实体中,参数类型和平时创建继承自NSObject的模型类大体类似,但是还是有一些关于类型的说明,下面简单的列举了一下。 Undefined: 默认值,参与编译会报错Integer 16: 整数,表示范围 -32768 ~ 32767Integer 32: 整数,表示范围 -2147483648 ~ 2147483647Integer 64: 整数,表示范围 –9223372036854775808 ~ 9223372036854775807Float: 小数,通过MAXFLOAT宏定义来看,最大值用科学计数法表示是 0x1.fffffep+127fDouble: 小数,小数位比Float更精确,表示范围更大String: 字符串,用NSString表示Boolean: 布尔值,用NSNumber表示Date: 时间,用NSDate表示Binary Data: 二进制,用NSData表示Transformable: OC对象,用id表示。可以在创建托管对象类文件后,手动改为对应的OC类名。使用的前提是,这个OC对象必须遵守并实现NSCoding协议添加实体关联关系创建两个实体Department和Employee,并且在这两个实体中分别添加一些属性,下面将会根据这两个实体来添加关联关系。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/a4710356244d 之前两篇文章都比较偏理论,文字表达比较多一些,但都是干货!学习时先理解理论知识,才能更好的帮助后面的理解。在这篇文章中,将会涉及关于CoreData的一些复杂操作,这些操作会涉及分页查询、模糊查询、批处理等高级操作。 通过这些操作可以更好的使用CoreData,提升CoreData性能。文章中将会出现大量示例代码,通过代码的方式更有助于理解。文章内容还会比较多,希望各位耐心看完。 文章中如有疏漏或错误,还请各位及时提出,谢谢!???? NSPredicate概述在iOS开发过程中,很多需求都需要用到过滤条件。例如过滤一个集合对象中存储的对象,可以通过Foundation框架下的NSPredicate类来执行这个操作。 CoreData中可以通过设置NSFetchRequest类的predicate属性,来设置一个NSPredicate类型的谓词对象当做过滤条件。通过设置这个过滤条件,可以只获取符合过滤条件的托管对象,不会将所有托管对象都加载到内存中。这样是非常节省内存和加快查找速度的,设计一个好的NSPredicate可以优化CoreData搜索性能。 语法NSPredicate更加偏向于自然语言,不像SQLite一样有很多固定的语法,看起来也更加清晰易懂。例如下面需要查找条件为年龄30岁以上,并且包括30岁的条件。 [NSPredicate predicateWithFormat:@"age >= 30"]过滤集合对象可以通过NSPredicate对iOS中的集合对象执行过滤操作,可以是NSArray、NSSet及其子类。 对不可变数组NSArray执行的过滤,过滤后会返回一个NSArray类型的结果数组,其中存储着符合过滤条件的对象。 NSArray *results = [array filteredArrayUsingPredicate:predicate]对可变数组NSMutableArray执行的过滤条件,过滤后会直接改变原集合对象内部存储的对象,删除不符合条件的对象。 [arrayM filterUsingPredicate:predicate]复合过滤条件谓词不只可以过滤简单条件,还可以过滤复杂条件,设置复合过滤条件。 [NSPredicate predicateWithFormat:@"(age < 25) AND (firstName = XiaoZhuang)"]当然也可以通过NSCompoundPredicate对象来设置复合过滤条件,返回结果是一个NSPredicate的子类NSCompoundPredicate对象。 [[NSCompoundPredicate alloc] initWithType:NSAndPredicateType subpredicates:@[predicate1, predicate2]]枚举值NSCompoundPredicateType参数,可以设置三种复合条件,枚举值非常直观很容易看懂。 NSNotPredicateTypeNSAndPredicateTypeNSOrPredicateType基础语法下面是列举的一些NSPredicate的基础语法,这些语法看起来非常容易理解,更复杂的用法可以去看苹果的官方API。 语法作用==判断是否相等>=大于或等于<=小于或等于>大于<小于!=不等于AND 或 &&和OR 或 II或NOT 或 !非正则表达式NSPredicate中还可以使用正则表达式,可以通过正则表达式完成一些复杂需求,这使得谓词的功能更加强大,例如下面是一个手机号验证的正则表达式。 NSString *mobile = @"^1(3[0-9]|5[0-35-9]|8[025-9])\\d{8}$";NSPredicate *regexmobile = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", mobile];模糊查询NSPredicate支持对数据的模糊查询,例如下面使用通配符来匹配包含lxz的结果,具体CoreData中的使用在下面会讲到。 [NSPredicate predicateWithFormat:@"name LIKE %@", @"*lxz*"]keyPathNSPredicate在创建查询条件时,还支持设置被匹配目标的keyPath,也就是设置更深层被匹配的目标。例如下面设置employee的name属性为查找条件,就是用点语法设置的keyPath。 [NSPredicate predicateWithFormat:@"employee.name = %@", @"lxz"]设置查询条件在之前的文章中,执行下面MOC的fetchRequest方法,一般都需要传入一个NSFetchRequest类型的参数。这个request参数可以做一些设置操作,这样就可以以较优的性能获取指定的数据。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/283e67ba12a3 CoreData使用相关的技术点已经讲差不多了,我所掌握的也就这么多了....在本篇文章中主要讲CoreData的多线程,其中会包括并发队列类型、线程安全等技术点。我对多线程的理解可能不是太透彻,文章中出现的问题还请各位指出。在之后公司项目使用CoreData的过程中,我会将其中遇到的多线程相关的问题更新到文章中。 在文章的最后,会根据我对CoreData多线程的学习,以及在工作中的具体使用,给出一些关于多线程结构的设计建议,各位可以当做参考。 文章中如有疏漏或错误,还请各位及时提出,谢谢!???? MOC并发队列类型在CoreData中MOC是支持多线程的,可以在创建MOC对象时,指定其并发队列的类型。当指定队列类型后,系统会将操作都放在指定的队列中执行,如果指定的是私有队列,系统会创建一个新的队列。但这都是系统内部的行为,我们并不能获取这个队列,队列由系统所拥有,并由系统将任务派发到这个队列中执行的。 NSManagedObjectContext并发队列类型:NSConfinementConcurrencyType : 如果使用init方法初始化上下文,默认就是这个并发类型。这个枚举值是不支持多线程的,从名字上也体现出来了。NSPrivateQueueConcurrencyType : 私有并发队列类型,操作都是在子线程中完成的。NSMainQueueConcurrencyType : 主并发队列类型,如果涉及到UI相关的操作,应该考虑使用这个枚举值初始化上下文。其中NSConfinementConcurrencyType类型在iOS9之后已经被苹果废弃,不建议使用这个API。使用此类型创建的MOC,调用某些比较新的CoreData的API可能会导致崩溃。 MOC多线程调用方式在CoreData中MOC不是线程安全的,在多线程情况下使用MOC时,不能简单的将MOC从一个线程中传递到另一个线程中使用,这并不是CoreData的多线程,而且会出问题。对于MOC多线程的使用,苹果给出了自己的解决方案。 在创建的MOC中使用多线程,无论是私有队列还是主队列,都应该采用下面两种多线程的使用方式,而不是自己手动创建线程。调用下面方法后,系统内部会将任务派发到不同的队列中执行。可以在不同的线程中调用MOC的这两个方法,这个是允许的。 - (void)performBlock:(void (^)())block 异步执行的block,调用之后会立刻返回。- (void)performBlockAndWait:(void (^)())block 同步执行的block,调用之后会等待这个任务完成,才会继续向下执行。 下面是多线程调用的示例代码,在多线程的环境下执行MOC的save方法,就是将save方法放在MOC的block体中异步执行,其他方法的调用也是一样的。 [context performBlock:^{ [context save:nil];}];但是需要注意的是,这两个block方法不能在NSConfinementConcurrencyType类型的MOC下调用,这个类型的MOC是不支持多线程的,只支持其他两种并发方式的MOC。 多线程的使用在业务比较复杂的情况下,需要进行大量数据处理,并且还需要涉及到UI的操作。对于这种复杂需求,如果都放在主队列中,对性能和界面流畅度都会有很大的影响,导致用户体验非常差,降低屏幕FPS。对于这种情况,可以采取多个MOC配合的方式。 CoreData多线程的发展中,在iOS5经历了一次比较大的变化,之后可以更方便的使用多线程。从iOS5开始,支持设置MOC的parentContext属性,通过这个属性可以设置MOC的父MOC。下面会针对iOS5之前和之后,分别讲解CoreData的多线程使用。 尽管现在的开发中早就不兼容iOS5之前的系统了,但是作为了解这里还是要讲一下,而且这种同步方式在iOS5之后也是可以正常使用的,也有很多人还在使用这种同步方式,下面其他章节也是同理。 iOS5之前使用多个MOC在iOS5之前实现MOC的多线程,可以创建多个MOC,多个MOC使用同一个PSC,并让多个MOC实现数据同步。通过这种方式不用担心PSC在调用过程中的线程问题,MOC在使用PSC进行save操作时,会对PSC进行加锁,等当前加锁的MOC执行完操作之后,其他MOC才能继续执行操作。 每一个PSC都对应着一个持久化存储区,PSC知道存储区中数据存储的数据结构,而MOC需要使用这个PSC进行save操作的实现。 这样做有一个问题,当一个MOC发生改变并持久化到本地时,系统并不会将其他MOC缓存在内存中的NSManagedObject对象改变。所以这就需要我们在MOC发生改变时,将其他MOC数据更新。 根据上面的解释,在下面例子中创建了一个主队列的mainMOC,主要用于UI操作。一个私有队列的backgroundMOC,用于除UI之外的耗时操作,两个MOC使用的同一个PSC。 // 获取PSC实例对象- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { // 创建托管对象模型,并指明加载Company模型文件 NSURL *modelPath = [[NSBundle mainBundle] URLForResource:@"Company" withExtension:@"momd"]; NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelPath]; // 创建PSC对象,并将托管对象模型当做参数传入,其他MOC都是用这一个PSC。 NSPersistentStoreCoordinator *PSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model]; // 根据指定的路径,创建并关联本地数据库 NSString *dataPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject; dataPath = [dataPath stringByAppendingFormat:@"/%@.sqlite", @"Company"]; [PSC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:nil error:nil]; return PSC;}// 初始化用于本地存储的所有MOC- (void)createManagedObjectContext { // 创建PSC实例对象,其他MOC都用这一个PSC。 NSPersistentStoreCoordinator *PSC = self.persistentStoreCoordinator; // 创建主队列MOC,用于执行UI操作 NSManagedObjectContext *mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; mainMOC.persistentStoreCoordinator = PSC; // 创建私有队列MOC,用于执行其他耗时操作 NSManagedObjectContext *backgroundMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; backgroundMOC.persistentStoreCoordinator = PSC; // 通过监听NSManagedObjectContextDidSaveNotification通知,来获取所有MOC的改变消息 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:nil];}// MOC改变后的通知回调- (void)contextChanged:(NSNotification *)noti { NSManagedObjectContext *MOC = noti.object; // 这里需要做判断操作,判断当前改变的MOC是否我们将要做同步的MOC,如果就是当前MOC自己做的改变,那就不需要再同步自己了。 // 由于项目中可能存在多个PSC,所以下面还需要判断PSC是否当前操作的PSC,如果不是当前PSC则不需要同步,不要去同步其他本地存储的数据。 [MOC performBlock:^{ // 直接调用系统提供的同步API,系统内部会完成同步的实现细节。 [MOC mergeChangesFromContextDidSaveNotification:noti]; }];}在上面的Demo中,创建了一个PSC,并将其他MOC都关联到这个PSC上,这样所有的MOC执行本地持久化相关的操作时,都是通过同一个PSC进行操作的。并在下面添加了一个通知,这个通知是监听所有MOC执行save操作后的通知,并在通知的回调方法中进行数据的合并。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/01f36026da7d 在之前的文章中,已经讲了很多关于CoreData使用相关的知识点。这篇文章中主要讲两个方面,NSFetchedResultsController和版本迁移。文章题目中虽然有“高级”两个字,其实讲的东西并不高级,只是因为上一篇文章中东西太多了,把两个较复杂的知识点挪到这篇文章中。???? 文章中如有疏漏或错误,还请各位及时提出,谢谢!???? NSFetchedResultsController在开发过程中会经常用到UITableView这样的视图类,这些视图类需要自己管理其数据源,包括网络获取、本地存储都需要写代码进行管理。 而在CoreData中提供了NSFetchedResultsController类(fetched results controller,也叫FRC),FRC可以管理UITableView或UICollectionView的数据源。这个数据源主要指本地持久化的数据,也可以用这个数据源配合着网络请求数据一起使用,主要看业务需求了。 本篇文章会使用UITableView作为视图类,配合NSFetchedResultsController进行后面的演示,UICollectionView配合NSFetchedResultsController的使用也是类似,这里就不都讲了。 简单介绍就像上面说到的,NSFetchedResultsController就像是上面两种视图的数据管理者一样。FRC可以监听一个MOC的改变,如果MOC执行了托管对象的增删改操作,就会对本地持久化数据发生改变,FRC就会回调对应的代理方法,回调方法的参数会包括执行操作的类型、操作的值、indexPath等参数。 实际使用时,通过FRC“绑定”一个MOC,将UITableView嵌入在FRC的执行流程中。在任何地方对这个“绑定”的MOC存储区做修改,都会触发FRC的回调方法,在FRC的回调方法中嵌入UITableView代码并做对应修改即可。 由此可以看出FRC最大优势就是,始终和本地持久化的数据保持统一。只要本地持久化的数据发生改变,就会触发FRC的回调方法,从而在回调方法中更新上层数据源和UI。这种方式讲的简单一点,就可以叫做数据带动UI。 但是需要注意一点,在FRC的初始化中传入了一个MOC参数,FRC只能监测传入的MOC发生的改变。假设其他MOC对同一个存储区发生了改变,FRC则不能监测到这个变化,不会做出任何反应。 所以使用FRC时,需要注意FRC只能对一个MOC的变化做出反应,所以在CoreData持久化层设计时,尽量一个存储区只对应一个MOC,或设置一个负责UI的MOC,这在后面多线程部分会详细讲解。 修改模型文件结构在写代码之前,先对之前的模型文件结构做一些修改。 讲FRC的时候,只需要用到Employee这一张表,其他表和设置直接忽略。需要在Employee原有字段的基础上,增加一个String类型的sectionName字段,这个字段就是用来存储section title的,在下面的文章中将会详细讲到。 初始化FRC下面例子是比较常用的FRC初始化方式,初始化时指定的MOC,还用之前讲过的MOC初始化代码,UITableView初始化代码这里也省略了,主要突出FRC的初始化。 // 创建请求对象,并指明操作Employee表NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];// 设置排序规则,指明根据height字段升序排序NSSortDescriptor *heightSort = [NSSortDescriptor sortDescriptorWithKey:@"height" ascending:YES];request.sortDescriptors = @[heightSort];// 创建NSFetchedResultsController控制器实例,并绑定MOCNSError *error = nil;fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:context sectionNameKeyPath:@"sectionName" cacheName:nil];// 设置代理,并遵守协议fetchedResultController.delegate = self;// 执行获取请求,执行后FRC会从持久化存储区加载数据,其他地方可以通过FRC获取数据[fetchedResultController performFetch:&error];// 错误处理if (error) { NSLog(@"NSFetchedResultsController init error : %@", error);}// 刷新UI[tableView reloadData];在上面初始化FRC时,传入的sectionNameKeyPath:参数,是指明当前托管对象的哪个属性当做section的title,在本文中就是Employee表的sectionName字段为section的title。从NSFetchedResultsSectionInfo协议的indexTitle属性获取这个值。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/61b7a615508c 到目前为止,已经将CoreData相关的知识点都讲完了。在这篇文章中,主要讲一个CoreData第三方库-MagicalRecord。目前为止这个第三方在Github上有9500+的Star,是所有CoreData第三方库中使用最多、功能最全的。在文章的后面还会对CoreData做一个总结,以及对本系列所有文章做一个总结。 文章中如有疏漏或错误,还请各位及时提出,谢谢!???? MagicalRecordCoreData是苹果自家推出的一个持久化框架,使用起来更加面向对象。但是在使用过程中会出现大量代码,而且CoreData学习曲线比较陡峭,如果掌握不好,在使用过程中很容易造成其他问题。 国外开发者开源了一个基于CoreData封装的第三方——MagicalRecord,就像是FMDB封装SQLite一样,MagicalRecord封装的CoreData,使得原生的CoreData更加容易使用。并且MagicalRecord降低了CoreData的使用门槛,不用去手动管理之前的PSC、MOC等对象。 根据Github上MagicalRecord的官方文档,MagicalRecord的优点主要有三条: 1. 清理项目中CoreData代码2. 支持清晰、简单、一行式的查询操作3. 当需要优化请求时,可以获取NSFetchRequest进行修改 添加MagicalRecord到项目中将MagicalRecord添加到项目中,和使用其他第三方一样,可以通过下载源码和CocoaPods两种方式添加。 1. 从Github下载MagicalRecord源码,将源码直接拖到项目中,后续需要手动更新源码。 2. 也可以通过CocoaPods安装MagicalRecord,需要在Podfile中加入下面命令,后续只需要通过命令来更新。 pod "MagicalRecord"在之前创建新项目时,通过勾选"Use Core Data"的方式添加CoreData到项目中,会在AppDelegate文件中生成大量CoreData相关代码。如果是大型项目,被占用的位置是很重要的。而对于MagicalRecord来说,只需要两行代码即可。 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 初始化CoreData堆栈,也可以指定初始化某个CoreData堆栈 [MagicalRecord setupCoreDataStack]; return YES; } - (void)applicationWillTerminate:(UIApplication *)application { // 在应用退出时,应该调用cleanUp方法 [MagicalRecord cleanUp]; }MagicalRecord是支持CoreData的.xcdatamodeld文件的,使得CoreData这一优点可以继续使用。建立数据结构时还是像之前使用CoreData一样,通过.xcdatamodeld文件的方式建立。 支持iCloudCoreData是支持iCloud的,MagicalRecord对iCloud相关的操作也做了封装,只需要使用MagicalRecord+iCloud.h类中提供的方法,就可以进行iCloud相关的操作。 例如下面是MagicalRecord+iCloud.h中的一个方法,需要将相关参数传入即可。 + (void)setupCoreDataStackWithiCloudContainer:(NSString *)containerID localStoreNamed:(NSString *)localStore;创建上下文MagicalRecord对上下文的管理和创建也比较全面,下面是MagicalRecord提供的部分创建和获取上下文的代码。因为是给NSManagedObjectContext添加的Category,可以直接用NSManagedObjectContext类调用,使用非常方便。 但是需要注意,虽然系统帮我们管理了上下文对象,对于耗时操作仍然要放在后台线程中处理,并且在主线程中进行UI操作。 + [NSManagedObjectContext MR_context] 设置默认的上下文为它的父级上下文,并发类型为NSPrivateQueueConcurrencyType+ [NSManagedObjectContext MR_newMainQueueContext] 创建一个新的上下文,并发类型为NSMainQueueConcurrencyType+ [NSManagedObjectContext MR_newPrivateQueueContext] 创建一个新的上下文,并发类型为NSPrivateQueueConcurrencyType+ [NSManagedObjectContext MR_contextWithParent:] 创建一个新的上下文,允许自定义父级上下文,并发类型为NSPrivateQueueConcurrencyType+ [NSManagedObjectContext MR_contextWithStoreCoordinator:] 创建一个新的上下文,并允许自定义持久化存储协调器,并发类型为NSPrivateQueueConcurrencyType+ [NSManagedObjectContext MR_defaultContext] 获取默认上下文对象,项目中最基础的上下文对象,并发类型是NSMainQueueConcurrencyType增删改查MagicalRecord对NSManagedObject添加了一个Category,将增删改查等操作放在这个Category中,使得这些操作可以直接被NSManagedObject类及其子类调用。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/4fb2d7014e9e 程序加载过程在iOS程序中会用到很多系统的动态库,这些动态库都是动态加载的。所有iOS程序共用一套系统动态库,在程序开始运行时才会开始链接动态库。 除了在项目设置里显式出现的动态库外,还会有一些隐式存在的动态库。例如objc和Runtime所属的libobjc.dyld和libSystem.dyld,在libSystem中包含常用的libdispatch(GCD)、libsystem_c(C语言基础库)、libsystem_blocks(Block)等。 使用动态库的优点: 防止重复。iOS系统中所有App公用一套系统动态库,防止重复的内存占用。减少包体积。因为系统动态库被内置到iOS系统中,所以打包时不需要把这部分代码打进去,可以减小包体积。动态性。因为系统动态库是动态加载的,所以可以在更新系统后,将动态库换成新的动态库。加载过程在应用程序启动后,由dyld(the dynamic link editor)进行程序的初始化操作。大概流程就像下面列出的步骤,其中第3、4、5步会执行多次,在ImageLoader加载新的image进内存后就会执行一次。 在引用程序启动后,由dyld将应用程序加载到二进制中,并完成一些文件的初始化操作。Runtime向dyld中注册回调函数。通过ImageLoader将所有image加载到内存中。dyld在image发生改变时,主动调用回调函数。Runtime接收到dyld的函数回调,开始执行map_images、load_images等操作,并回调+load方法。调用main()函数,开始执行业务代码。ImageLoader是image的加载器,image可以理解为编译后的二进制。 下面是在Runtime的map_images函数打断点,观察回调情况的汇编代码。可以看出,调用是由dyld发起的,由ImageLoader通知dyld进行调用。 关于dyld我并没有深入研究,有兴趣的同学可以到Github上下载源码研究一下。 动态加载一个OC程序可以在运行过程中动态加载和链接新类或Category,新类或Category会加载到程序中,其处理方式和其他类是相同的。动态加载还可以做许多不同的事,动态加载允许应用程序进行自定义处理。 OC提供了objc_loadModules运行时函数,执行Mach-O中模块的动态加载,在上层NSBundle对象提供了更简单的访问API。 map images在Runtime加载时,会调用_objc_init函数,并在内部注册三个函数指针。其中map_images函数是初始化的关键,内部完成了大量Runtime环境的初始化操作。 在map_images函数中,内部也是做了一个调用中转。然后调用到map_images_nolock函数,内部核心就是_read_images函数。 void _objc_init(void){ // .... 各种init _dyld_objc_notify_register(&map_images, load_images, unmap_image);}void map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]){ rwlock_writer_t lock(runtimeLock); return map_images_nolock(count, paths, mhdrs);}void map_images_nolock(unsigned mhCount, const char * const mhPaths[], const struct mach_header * const mhdrs[]){ if (hCount > 0) { _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses); }}在_read_images函数中完成了大量的初始化操作,函数内部代码量比较大,下面是精简版带注释的源代码。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/77660e626874 在iOS开发调试过程中以及上线之后,程序经常会出现崩溃的问题。简单的崩溃还好说,复杂的崩溃就需要我们通过解析Crash文件来分析了,解析Crash文件在iOS开发中是比较常见的。现在网上有很多关于解析崩溃信息的博客,但是大多质量参差不齐,或者有些细节没有注意到。今天写一篇博客总结一下我对崩溃调试的使用和技巧,如果有哪些错误或遗漏,还请指点,谢谢!???? 获取崩溃信息获取方式在iOS中获取崩溃信息的方式有很多,比较常见的是使用友盟、Bugly等第三方分析工具,或者自己收集崩溃信息并上传公司服务器。下面列举一些我们常用的崩溃分析方式: 使用友盟、Bugly等第三方崩溃统计工具。自己实现应用内崩溃收集,并上传服务器。Xcode-Devices中直接查看某个设备的崩溃信息。使用苹果提供的Crash崩溃收集服务。收集崩溃信息苹果给我们提供了异常处理的类-NSException。这个类可以创建一个异常对象,也可以通过这个类获取一个异常对象。 这个类中最常用的是一个获取崩溃信息的C函数,可以通过这个函数在程序发生异常的时候收集这个异常。 // 将系统提供的获取崩溃信息函数写在这个方法中,以保证在程序开始运行就具有获取崩溃信息的功能 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 将下面C函数的函数地址当做参数 NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler); return YES; } // 设置一个C函数,用来接收崩溃信息 void UncaughtExceptionHandler(NSException *exception){ // 可以通过exception对象获取一些崩溃信息,我们就是通过这些崩溃信息来进行解析的,例如下面的symbols数组就是我们的崩溃堆栈。 NSArray *symbols = [exception callStackSymbols]; NSString *reason = [exception reason]; NSString *name = [exception name]; }我们也可以通过下面方法获取崩溃统计的函数指针: NSUncaughtExceptionHandler *handler = NSGetUncaughtExceptionHandler();崩溃分析dSYM 符号集进行崩溃分析,首先要弄懂一个概念,就是符号集。 符号集是我们对ipa文件进行打包之后,和.app文件同级的后缀名为.dSYM的文件,这个文件必须使用Xcode进行打包才有。每一个.dSYM文件都有一个UUID,和.app文件中的UUID对应,代表着是一个应用。而.dSYM文件中每一条崩溃信息也有一个单独的UUID,用来和程序的UUID进行校对。我们如果不使用.dSYM文件解析出的崩溃信息都不能保证准确。符号集中存储着文件名、方法名、行号的信息,是和可执行文件的16进制函数地址对应的,通过分析崩溃的.Crash文件可以准确知道具体的崩溃信息。我们每次Archive一个包之后,都会随之生成一个dSYM文件。每次发布一个版本,我们都需要备份这个文件,以方便以后的调试。进行崩溃信息符号化的时候,必须使用当前应用打包的电脑所生成的dSYM文件,其他电脑生成的文件可能会导致分析不准确的问题。 当程序崩溃的时候,可以获得到崩溃的错误堆栈,错误堆栈都是0x开头的16进制地址,需要使用Xcode自带的symbolicatecrash工具来将.Crash和.dSYM文件进行符号化,就可以得到详细崩溃的信息。 系统符号化文件在崩溃分析时,dSYM文件是解析App堆栈的,如果是系统库则需要对应的符号化文件。很多解析不出来系统堆栈的问题,就是因为没有系统的符号化文件。符号化文件就在Xcode的资源库里,可以从下面的目录找到符号化文件。 /Users/liuxiaozhuang/Library/Developer/Xcode/iOS DeviceSupport符号化文件对版本和Architectures都有要求,例如崩溃的系统是8.4.1系统 arm64的指令集,就需要对应的系统符号化文件8.4.1 (12H321)。否则还是不能解析出系统崩溃信息,或者解析出来也是错的。如果在iOS DeviceSupport文件中没有找到对应的符号化文件,需要找一个对应的才可以解析。 符号化文件的指令集一般都是兼容低版本的,例如8.4.1 (12H321)的指令集会有arm64、armv7s、armv7三个版本,如果苹果没有明确说明某个iOS版本不兼容32位处理器,那么指令集都会兼容的。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/ea74b230c70d 目前iOS开发中大多数页面都已经开始使用Interface Builder的方式进行UI开发了,但是在一些变化比较复杂的页面,还是需要通过代码来进行UI开发的。而且有很多比较老的项目,本身就还在采用纯代码的方式进行开发。而现在iPhone和iPad屏幕尺寸越来越多,虽然开发者只需要根据屏幕点进行开发,而不需要基于像素点进行UI开发。但如果在项目中根据不同屏幕尺寸进行各种判断,写死坐标的话,这样开发起来是很吃力的。 所以一般用纯代码开发UI的话,一般都是配合一些自动化布局的框架进行屏幕适配。苹果为我们提供的适配框架有:VFL、UIViewAutoresizing、Auto Layout、Size Classes等。 其中Auto Layout是使用频率最高的布局框架,但是其也有弊端。就是在使用NSLayoutConstraint 的时候,会发现代码量很多,而且大多都是重复性的代码,以至于好多人都不想用这个框架。 后来Github上的出现了基于NSLayoutConstraint 封装的第三方布局框架Masonry,Masonry使用起来非常方便,本篇文章就详细讲一下Masonry的使用。 Masonry介绍这篇文章只是简单介绍Masonry,以及Masonry的使用,并且会举一些例子出来。但并不会涉及到Masonry的内部实现,以后会专门写篇文章来介绍其内部实现原理,包括顺便讲一下链式语法。 什么是MasonryMasonry是一个对系统NSLayoutConstraint进行封装的第三方自动布局框架,采用链式编程的方式提供给开发者API。系统AutoLayout支持的操作,Masonry都支持,相比系统API功能来说,Masonry是有过之而无不及。 Masonry采取了链式编程的方式,代码理解起来非常清晰易懂,而且写完之后代码量看起来非常少。之前用NSLayoutConstraint写很多代码才能实现的布局,用Masonry最少一行代码就可以搞定。下面看到Masonry的代码就会发现,太简单易懂了。 Masonry是同时支持Mac和iOS两个平台的,在这两个平台上都可以使用Masonry进行自动布局。我们可以从MASUtilities.h文件中,看到下面的定义,这就是Masonry通过宏定义的方式,区分两个平台独有的一些关键字。 #if TARGET_OS_IPHONE #import <UIKit/UIKit.h> #define MAS_VIEW UIView #define MASEdgeInsets UIEdgeInsets#elif TARGET_OS_MAC #import <AppKit/AppKit.h> #define MAS_VIEW NSView #define MASEdgeInsets NSEdgeInsets#endifGithub地址:https://github.com/SnapKit/Masonry 集成方式Masonry支持CocoaPods,可以直接通过podfile文件进行集成,需要在CocoaPods中添加下面代码: pod 'Masonry'Masonry学习建议在UI开发中,纯代码和Interface Builder我都是用过的,在开发过程中也积累了一些经验。对于初学者学习纯代码AutoLayout,我建议还是先学会Interface Builder方式的AutoLayout,领悟苹果对自动布局的规则和思想,然后再把这套思想嵌套在纯代码上。这样学习起来更好入手,也可以避免踩好多坑。 在项目中设置的AutoLayout约束,起到对视图布局的标记作用。设置好约束之后,程序运行过程中创建视图时,会根据设置好的约束计算frame,并渲染到视图上。 所以在纯代码情况下,视图设置的约束是否正确,要以运行之后显示的结果和打印的log为准。 Masonry中的坑在使用Masonry进行约束时,有一些是需要注意的。 在使用Masonry添加约束之前,需要在addSubview之后才能使用,否则会导致崩溃。在添加约束时初学者经常会出现一些错误,约束出现问题的原因一般就是两种:约束冲突和缺少约束。对于这两种问题,可以通过调试和log排查。之前使用Interface Builder添加约束,如果约束有错误直接就可以看出来,并且会以红色或者黄色警告体现出来。而Masonry则不会直观的体现出来,而是以运行过程中崩溃或者打印异常log体现,所以这也是手写代码进行AutoLayout的一个缺点。这个问题只能通过多敲代码,积攒纯代码进行AutoLayout的经验,慢慢就用起来越来越得心应手了。Masonry基础使用Masonry基础APImas_makeConstraints() 添加约束mas_remakeConstraints() 移除之前的约束,重新添加新的约束mas_updateConstraints() 更新约束,写哪条更新哪条,其他约束不变equalTo() 参数是对象类型,一般是视图对象或者mas_width这样的坐标系对象mas_equalTo() 和上面功能相同,参数可以传递基础数据类型对象,可以理解为比上面的API更强大width() 用来表示宽度,例如代表view的宽度mas_width() 用来获取宽度的值。和上面的区别在于,一个代表某个坐标系对象,一个用来获取坐标系对象的值Auto Boxing上面例如equalTo或者width这样的,有时候需要涉及到使用mas_前缀,这在开发中需要注意作区分。如果在当前类引入#import "Masonry.h"之前,用下面两种宏定义声明一下,就不需要区分mas_前缀。 // 定义这个常量,就可以不用在开发过程中使用"mas_"前缀。#define MAS_SHORTHAND// 定义这个常量,就可以让Masonry帮我们自动把基础数据类型的数据,自动装箱为对象类型。#define MAS_SHORTHAND_GLOBALS修饰语句Masonry为了让代码使用和阅读更容易理解,所以直接通过点语法就可以调用,还添加了and和with两个方法。这两个方法内部实际上什么都没干,只是在内部将self直接返回,功能就是为了更加方便阅读,对代码执行没有实际作用。例如下面的例子: make.top.and.bottom.equalTo(self.containerView).with.offset(padding);其内部代码实现,实际上就是直接将self返回。 - (MASConstraint *)with { return self;}更新约束和布局关于更新约束布局相关的API,主要用以下四个API: ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/2113ffe54b30 在项目中我们经常会用到代理的设计模式,这是iOS中一种消息传递的方式,也可以通过这种方式来传递一些参数。这篇文章会涵盖代理的使用技巧和原理,以及代理的内存管理等方面的知识。我会通过这些方面的知识,带大家真正领略代理的奥妙。写的有点多,但都是干货,我能写下去,不知道你有没有耐心看下去。 本人能力有限,如果文章中有什么问题或没有讲到的点,请帮忙指出,十分感谢! iOS中消息传递方式在iOS中有很多种消息传递方式,这里先简单介绍一下各种消息传递方式。 通知:在iOS中由通知中心进行消息接收和消息广播,是一种一对多的消息传递方式。代理:是一种通用的设计模式,iOS中对代理支持的很好,由代理对象、委托者、协议三部分组成。block:iOS4.0中引入的一种回调方法,可以将回调处理代码直接写在block代码块中,看起来逻辑清晰代码整齐。target action:通过将对象传递到另一个类中,在另一个类中将该对象当做target的方式,来调用该对象方法,从内存角度来说和代理类似。KVO:NSObject的Category-NSKeyValueObserving,通过属性监听的方式来监测某个值的变化,当值发生变化时调用KVO的回调方法。.....当然还有其他回调方式,这里只是简单的列举。 代理的基本使用代理是一种通用的设计模式,在iOS中对代理设计模式支持的很好,有特定的语法来实现代理模式,__OC__语言可以通过@Protocol实现协议。 代理主要由三部分组成: 协议:用来指定代理双方可以做什么,必须做什么。代理:根据指定的协议,完成委托方需要实现的功能。委托:根据指定的协议,指定代理去完成什么功能。这里用一张图来阐述一下三方之间的关系: Protocol-协议的概念从上图中我们可以看到三方之间的关系,在实际应用中通过协议来规定代理双方的行为,协议中的内容一般都是方法列表,当然也可以定义属性,我会在后续文章中顺带讲一下协议中定义属性。 协议是公共的定义,如果只是某个类使用,我们常做的就是写在某个类中。如果是多个类都是用同一个协议,建议创建一个Protocol文件,在这个文件中定义协议。遵循的协议可以被继承,例如我们常用的UITableView,由于继承自UIScrollView的缘故,所以也将UIScrollViewDelegate继承了过来,我们可以通过代理方法获取UITableView偏移量等状态参数。 协议只能定义公用的一套接口,类似于一个约束代理双方的作用。但不能提供具体的实现方法,实现方法需要代理对象去实现。协议可以继承其他协议,并且可以继承多个协议,在iOS中对象是不支持多继承的,而协议可以多继承。 // 当前协议继承了三个协议,这样其他三个协议中的方法列表都会被继承过来@protocol LoginProtocol <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password;@end协议有两个修饰符@optional和@required,创建一个协议如果没有声明,默认是@required状态的。这两个修饰符只是约定代理是否强制需要遵守协议,如果@required状态的方法代理没有遵守,会报一个黄色的警告,只是起一个约束的作用,没有其他功能。 无论是@optional还是@required,在委托方调用代理方法时都需要做一个判断,判断代理是否实现当前方法,否则会导致崩溃。 示例: // 判断代理对象是否实现这个方法,没有实现会导致崩溃if ([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) { [self.delegate userLoginWithUsername:self.username.text password:self.password.text];}下面我们将用一个小例子来讲解一下这个问题:示例:假设我在公司正在敲代码,敲的正开心呢,突然口渴了,想喝一瓶红茶。这时我就可以拿起手机去外卖app上定一个红茶,然后外卖app就会下单给店铺并让店铺给我送过来。这个过程中,外卖app就是我的代理,我就是委托方,我买了一瓶红茶并付给外卖app钱,这就是购买协议。我只需要从外卖app上购买就可以,具体的操作都由外卖app去处理,我只需要最后接收这瓶红茶就可以。我付的钱就是参数,最后送过来的红茶就是处理结果。 但是我买红茶的同时,我还想吃一份必胜客披萨,我需要另外向必胜客app去订餐,上面的外卖app并没有这个功能。我又向必胜客购买了一份披萨,必胜客当做我的代理去为我做这份披萨,并最后送到我手里。这就是多个代理对象,我就是委托方。 在iOS中一个代理可以有多个委托方,而一个委托方也可以有多个代理。我指定了外卖app和必胜客两个代理,也可以再指定麦当劳等多个代理,委托方也可以为多个代理服务。 代理对象在很多情况下其实是可以复用的,可以创建多个代理对象为多个委托方服务,在下面将会通过一个小例子介绍一下控制器代理的复用。 下面是一个简单的代理:首先定义一个协议类,来定义公共协议 #import <Foundation/Foundation.h>@protocol LoginProtocol <NSObject>@optional- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password;@end定义委托类,这里简单实现了一个用户登录功能,将用户登录后的账号密码传递出去,有代理来处理具体登录细节。 #import <UIKit/UIKit.h>#import "LoginProtocol.h"/** * 当前类是委托类。用户登录后,让代理对象去实现登录的具体细节,委托类不需要知道其中实现的具体细节。 */@interface LoginViewController : UIViewController// 通过属性来设置代理对象@property (nonatomic, weak) id<LoginProtocol> delegate;@end实现部分:@implementation LoginViewController- (void)loginButtonClick:(UIButton *)button { // 判断代理对象是否实现这个方法,没有实现会导致崩溃 if ([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) { // 调用代理对象的登录方法,代理对象去实现登录方法 [self.delegate userLoginWithUsername:self.username.text password:self.password.text]; }}代理方,实现具体的登录流程,委托方不需要知道实现细节。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/ff19c04b34d0 公司年底要在新年前发一个版本,最近一直很忙,好久没有更新博客了。正好现在新版本开发的差不多了,抽空总结一下。由于最近开发新版本,就避免不了在开发和调试过程中引起崩溃,以及诱发一些之前的__bug__导致的崩溃。而且项目比较大也很不好排查,正好想起之前研究过的Method Swizzling,考虑是否能用这个苹果的“黑魔法”解决问题,当然用好这个黑魔法并不局限于解决这些问题.... 需求就拿我们公司项目来说吧,我们公司是做导航的,而且项目规模比较大,各个控制器功能都已经实现。突然有一天老大过来,说我们要在所有页面添加统计功能,也就是用户进入这个页面就统计一次。我们会想到下面的一些方法: 手动添加直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴...上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。 继承我们可以使用OOP的特性之一,继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。 然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。 Category我们可以为UIViewController建一个Category,然后在所有控制器中引入这个Category。当然我们也可以添加一个PCH文件,然后将这个Category添加到PCH文件中。 我们创建一个Category来覆盖系统方法,系统会优先调用Category中的代码,然后在调用原类中的代码。 我们可以通过下面的这段伪代码来看一下: #import "UIViewController+EventGather.h"@implementation UIViewController (EventGather)- (void)viewDidLoad { NSLog(@"页面统计:%@", self);}@endMethod Swizzling我们可以使用苹果的“黑魔法”Method Swizzling,Method Swizzling本质上就是对IMP和SEL进行交换。 Method Swizzling原理Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。 而且Method Swizzling也是__iOS__中AOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。 原理分析首先,让我们通过两张图片来了解一下Method Swizzling的实现原理 上面图一中selector2原本对应着IMP2,但是为了更方便的实现特定业务需求,我们在图二中添加了selector3和IMP3,并且让selector2指向了IMP3,而selector3则指向了IMP2,这样就实现了“方法互换”。 在OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用。 在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SEL和IMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP。 Method Swizzling使用在实现Method Swizzling时,核心代码主要就是一个runtime的C语言API: OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);代码示例就拿上面我们说的页面统计的需求来说吧,这个需求在很多公司都很常见,我们下面的Demo就通过Method Swizzling简单的实现这个需求。 我们先给UIViewController添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。 定义Method Swizzling中我们自定义的方法时,需要注意尽量加前缀,以防止和其他地方命名冲突,Method Swizzling的替换方法命名一定要是唯一的,至少在被替换的类中必须是唯一的。 #import "UIViewController+swizzling.h"#import <objc/runtime.h>@implementation UIViewController (swizzling)+ (void)load { // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。 Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad)); Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad)); /** 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。 */ if (!class_addMethod([self class], @selector(swizzlingViewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) { method_exchangeImplementations(fromMethod, toMethod); }}// 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。- (void)swizzlingViewDidLoad { NSString *str = [NSString stringWithFormat:@"%@", self.class]; // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉 if(![str containsString:@"UI"]){ NSLog(@"统计打点 : %@", self.class); } [self swizzlingViewDidLoad];}@end看到上面的代码,肯定有人会问:楼主,你太粗心了,你在swizzlingViewDidLoad方法中又调用了[self swizzlingViewDidLoad];,这难道不会产生递归调用吗?答:然而....并不会????。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/f313e8e32946 当一个对象的方法被调用时,首先在对象所属的类中查找方法列表,如果当前类中没有则向父类查找,一直找到根类NSObject。如果始终没有找到方法实现,则进入消息转发步骤中。 动态消息解析当一个方法没有实现时,也就是在cache lsit和其继承关系的method list中,没有找到对应的方法。这时会进入消息转发阶段,但是在进入消息转发阶段前,Runtime会给一次机会动态添加方法实现。 可以通过重写resolveInstanceMethod:和resolveClassMethod:方法,动态添加未实现的方法。其中第一个是添加实例方法,第二个是添加类方法。这两个方法都有一个BOOL返回值,返回NO则进入消息转发机制。 void dynamicMethodIMP(id self, SEL _cmd) { // implementation ....}+ (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(resolveThisMethodDynamically)) { class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:sel];}在通过class_addMethod函数动态添加实现时,后面有一个"v@:"来描述SEL对应的函数实现,具体的描述可以参考官方文档。 Message Forwarding 在进行消息转发之前,还可以在forwardingTargetForSelector:方法中将未实现的消息,转发给其他对象。可以在下面方法中,返回响应未实现方法的其他对象。 - (id)forwardingTargetForSelector:(SEL)aSelector { NSString *selectorName = NSStringFromSelector(aSelector); if ([selectorName isEqualToString:@"selector"]) { return object; } return [super forwardingTargetForSelector:aSelector];}当forwardingTargetForSelector:方法未做出任何响应的话,会来到消息转发流程。消息转发时会首先调用methodSignatureForSelector:方法,在方法内部生成NSMethodSignature类型的方法签名对象。在生成签名对象时,可以指定target和SEL,可以将这两个参数换成其他参数,将消息转发给其他对象。 [otherObject methodSignatureForSelector:otherSelector];生成NSMethodSignature签名对象后,就会调用forwardInvocation:方法,这是消息转发中最后一步了,如果在这步还没有对消息进行处理,则会导致崩溃。 这个方法中会传入一个NSInvocation对象,这个对象就是通过刚才生成的签名对象创建的,可以通过invocation调用其他对象的方法,调用其invokeWithTarget:即可。 - (void)forwardInvocation:(NSInvocation *)anInvocation { if ([object respondsToSelector:[anInvocation selector]]) { [anInvocation invokeWithTarget:object]; } else { [super forwardInvocation:anInvocation]; }}消息转发将一条消息发送给一个不能处理的对象会引起崩溃,但是在崩溃之前,系统给响应对象一次处理异常的机会。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/0dc2513e117b Category有了之前Runtime的基础,一些内部实现就很好理解了。在OC中可以通过Category添加属性、方法、协议,在Runtime中Class和Category都是通过结构体实现的。 和Category语法很相似的还有Extension,二者的区别在于,Extension在编译期就直接和原类编译在一起,而Category是在运行时动态添加到原类中的。 基于之前的源码分析,我们来分析一下Category的实现原理。在_read_images函数中会执行一个循环嵌套,外部循环遍历所有类,并取出当前类对应Category数组。内部循环会遍历取出的Category数组,将每个category_t对象取出,最终执行addUnattachedCategoryForClass函数添加到Category哈希表中。 // 将category_t添加到list中,并通过NXMapInsert函数,更新所属类的Category列表static void addUnattachedCategoryForClass(category_t *cat, Class cls, header_info *catHeader){ // 获取到未添加的Category哈希表 NXMapTable *cats = unattachedCategories(); category_list *list; // 获取到buckets中的value,并向value对应的数组中添加category_t list = (category_list *)NXMapGet(cats, cls); if (!list) { list = (category_list *) calloc(sizeof(*list) + sizeof(list->list[0]), 1); } else { list = (category_list *) realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1)); } // 替换之前的list字段 list->list[list->count++] = (locstamped_category_t){cat, catHeader}; NXMapInsert(cats, cls, list);}Category维护了一个名为category_map的哈希表,哈希表存储所有category_t对象。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/67a6004f6930 前段时间公司项目打算重构,准确来说应该是按之前的产品逻辑重写一个项目????。在重构项目之前涉及到架构选型的问题,我和组里小伙伴一起研究了一下组件化架构,打算将项目重构为组件化架构。当然不是直接拿来照搬,还是要根据公司具体的业务需求设计架构。在学习组件化架构的过程中,从很多高质量的博客中学到不少东西,例如蘑菇街李忠、casatwy、bang的博客。在学习过程中也遇到一些问题,在微博和QQ上和一些做iOS的朋友进行了交流,非常感谢这些朋友的帮助。 本篇文章主要针对于之前蘑菇街提出的组件化方案,以及casatwy提出的组件化方案进行分析,后面还会简单提到滴滴、淘宝、微信的组件化架构,最后会简单说一下我公司设计的组件化架构。 组件化架构的由来随着移动互联网的不断发展,很多程序代码量和业务越来越多,现有架构已经不适合公司业务的发展速度了,很多都面临着重构的问题。 在公司项目开发中,如果项目比较小,普通的单工程+MVC架构就可以满足大多数需求了。但是像淘宝、蘑菇街、微信这样的大型项目,原有的单工程架构就不足以满足架构需求了。 就拿淘宝来说,淘宝在13年开启的“All in 无线”战略中,就将阿里系大多数业务都加入到手机淘宝中,使客户端出现了业务的爆发。在这种情况下,单工程架构则已经远远不能满足现有业务需求了。所以在这种情况下,淘宝在13年开启了插件化架构的重构,后来在14年迎来了手机淘宝有史以来最大规模的重构,将项目重构为组件化架构。 蘑菇街的组件化架构原因在一个项目越来越大,开发人员越来越多的情况下,项目会遇到很多问题。 业务模块间划分不清晰,模块之间耦合度很大,非常难维护。所有模块代码都编写在一个项目中,测试某个模块或功能,需要编译运行整个项目。 为了解决上面的问题,可以考虑加一个中间层来协调各个模块间的调用,所有的模块间的调用都会经过中间层中转。 但是发现增加这个中间层后,耦合还是存在的。中间层对被调用模块存在耦合,其他模块也需要耦合中间层才能发起调用。这样还是存在之前的相互耦合的问题,而且本质上比之前更麻烦了。 架构改进所以应该做的是,只让其他模块对中间层产生耦合关系,中间层不对其他模块发生耦合。对于这个问题,可以采用组件化的架构,将每个模块作为一个组件。并且建立一个主项目,这个主项目负责集成所有组件。这样带来的好处是很多的: 业务划分更佳清晰,新人接手更佳容易,可以按组件分配开发任务。项目可维护性更强,提高开发效率。更好排查问题,某个组件出现问题,直接对组件进行处理。开发测试过程中,可以只编译自己那部分代码,不需要编译整个项目代码。方便集成,项目需要哪个模块直接通过CocoaPods集成即可。 进行组件化开发后,可以把每个组件当做一个独立的app,每个组件甚至可以采取不同的架构,例如分别使用MVVM、MVC、MVCS等架构,根据自己的编程习惯做选择。 MGJRouter方案蘑菇街通过MGJRouter实现中间层,由MGJRouter进行组件间的消息转发,从名字上来说更像是“路由器”。实现方式大致是,在提供服务的组件中提前注册block,然后在调用方组件中通过URL调用block,下面是调用方式。 架构设计 MGJRouter是一个单例对象,在其内部维护着一个“URL -> block”格式的注册表,通过这个注册表来保存服务方注册的block,以及使调用方可以通过URL映射出block,并通过MGJRouter对服务方发起调用。 MGJRouter是所有组件的调度中心,负责所有组件的调用、切换、特殊处理等操作,可以用来处理一切组件间发生的关系。除了原生页面的解析外,还可以根据URL跳转H5页面。 在服务方组件中都对外提供一个PublicHeader,在PublicHeader中声明当前组件所提供的所有功能,这样其他组件想知道当前组件有什么功能,直接看PublicHeader即可。每一个block都对应着一个URL,调用方可以通过URL对block发起调用。 #ifndef UserCenterPublicHeader_h#define UserCenterPublicHeader_h/** 跳转用户登录界面 */static const NSString * CTBUCUserLogin = @"CTB://UserCenter/UserLogin";/** 跳转用户注册界面 */static const NSString * CTBUCUserRegister = @"CTB://UserCenter/UserRegister";/** 获取用户状态 */static const NSString * CTBUCUserStatus = @"CTB://UserCenter/UserStatus";#endif在组件内部实现block的注册工作,以及block对外提供服务的代码实现。在注册的时候需要注意注册时机,应该保证调用时URL对应的block已经注册。 蘑菇街项目使用git作为版本控制工具,将每个组件都当做一个独立工程,并建立主项目来集成所有组件。集成方式是在主项目中通过CocoaPods来集成,将所有组件当做二方库集成到项目中。详细的集成技术点在下面“标准组件化架构设计”章节中会讲到。 MGJRouter调用下面代码模拟对详情页的注册、调用,在调用过程中传递id参数。参数传递可以有两种方式,类似于Get请求在URL后面拼接参数,以及通过字典传递参数。下面是注册的示例代码: [MGJRouter registerURLPattern:@"mgj://detail" toHandler:^(NSDictionary *routerParameters) { // 下面可以在拿到参数后,为其他组件提供对应的服务 NSString uid = routerParameters[@"id"];}];通过openURL:方法传入的URL参数,对详情页已经注册的block方法发起调用。调用方式类似于GET请求,URL地址后面拼接参数。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/4a22a39b69c5 attribute__attribute__是一套编译器指令,被GNU和LLVM编译器所支持,允许对于__attribute__增加一些参数,做一些高级检查和优化。 __attribute__的语法是,在后面加两个括号,然后写属性列表,属性列表以逗号分隔。在iOS中,很多例如NS_CLASS_AVAILABLE_IOS的宏定义,内部也是通过__attribute__实现的。 __attribute__((attribute1, attribute2));下面是一些__attribute__的常用属性,更完整的属性列表可以到llvm的官网查看。 objc_subclassing_restrictedobjc_subclassing_restricted属性表示被修饰的类不能被其他类继承,否则会报下面的错误。 __attribute__((objc_subclassing_restricted))@interface TestObject : NSObject@property (nonatomic, strong) NSObject *object;@property (nonatomic, assign) NSInteger age;@end@interface Child : TestObject@end错误信息:Cannot subclass a class that was declared with the 'objc_subclassing_restricted' attributeobjc_requires_superobjc_requires_super属性表示子类必须调用被修饰的方法super,否则报黄色警告。 @interface TestObject : NSObject- (void)testMethod __attribute__((objc_requires_super));@end@interface Child : TestObject@end警告信息:(不报错)Method possibly missing a [super testMethod] callconstructor / destructorconstructor属性表示在main函数执行之前,可以执行一些操作。destructor属性表示在main函数执行之后做一些操作。constructor的执行时机是在所有load方法都执行完之后,才会执行所有constructor属性修饰的函数。 __attribute__((constructor)) static void beforeMain() { NSLog(@"before main");}__attribute__((destructor)) static void afterMain() { NSLog(@"after main");}int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"execute main"); } return 0;}执行结果:debug-objc[23391:1143291] before maindebug-objc[23391:1143291] execute maindebug-objc[23391:1143291] after main在有多个constructor或destructor属性修饰的函数时,可以通过设置优先级来指定执行顺序。格式是__attribute__((constructor(101)))的方式,在属性后面直接跟优先级。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/014af0de67cd 方法调用在OC中方法调用是通过Runtime实现的,Runtime进行方法调用本质上是发送消息,通过objc_msgSend()函数进行消息发送。 例如下面的OC代码会被转换为Runtime代码。 原方法:[object testMethod]转换后的调用:objc_msgSend(object, @selector(testMethod));发送消息的第二个参数是一个SEL类型的参数,在项目里经常会出现,不同的类定义了相同的方法,这样就会有相同的SEL。那么问题就来了,也是很多人博客里都问过的一个问题,不同类的SEL是同一个吗? 然而,事实是通过我们的验证,创建两个不同的类,并定义两个相同的方法,通过@selector()获取SEL并打印。我们发现SEL都是同一个对象,地址都是相同的。由此证明,不同类的相同SEL是同一个对象。 @interface TestObject : NSObject- (void)testMethod;@end@interface TestObject2 : NSObject- (void)testMethod;@end// TestObject2实现文件也一样@implementation TestObject- (void)testMethod { NSLog(@"TestObject testMethod %p", @selector(testMethod));}@end// 结果:TestObject testMethod 0x100000f81TestObject2 testMethod 0x100000f81在Runtime中维护了一个SEL的表,这个表存储SEL不按照类来存储,只要相同的SEL就会被看做一个,并存储到表中。在项目加载时,会将所有方法都加载到这个表中,而动态生成的方法也会被加载到表中。 隐藏参数我们在方法内部可以通过self获取到当前对象,但是self又是从哪来的呢?方法实现的本质也是C函数,C函数除了方法传入的参数外,还会有两个默认参数,这两个参数在通过objc_msgSend()调用时也会传入。这两个参数在Runtime中并没有声明,而是在编译时自动生成的。 从objc_msgSend的声明中可以看出这两个隐藏参数的存在。 objc_msgSend(void /* id self, SEL op, ... */ )self,调用当前方法的对象。_cmd,当前被调用方法的SEL。虽然这两个参数在调用和实现方法中都没有明确声明,但是我们仍然可以使用它。响应对象就是self,被调用方法的selector是_cmd。 - (void)method { id target = getTheReceiver(); SEL method = getTheMethod(); if ( target == self || method == _cmd ) return nil; return [target performSelector:method];}函数调用一个对象被创建后,自身的类及其父类一直到NSObject类的部分,都会包含在对象的内存中,例如其父类的实例变量。当通过[super class]的方式调用其父类的方法时,会创建一个结构体。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/3019605a4fc9 本文基于objc-723版本,在Apple Github和Apple OpenSource上有源码,但是需要自己编译。 重点来了~,可以到我的Github上下载编译好的源码,源码中已经写了大量的注释,方便读者研究。(如果觉得还不错,各位大佬麻烦点个Star????)Runtime Analyze 对象的初始化流程在对象初始化的时候,一般都会调用alloc+init方法实例化,或者通过new方法进行实例化。下面将会分析通过alloc+init的方式实例化的过程,以下代码都是关键代码。 前面两步很简单,都是直接进行函数调用。 + (id)alloc { return _objc_rootAlloc(self);}id _objc_rootAlloc(Class cls){ return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);}在创建对象的地方有两种方式,一种是通过calloc开辟内存,然后通过initInstanceIsa函数初始化这块内存。第二种是直接调用class_createInstance函数,由内部实现初始化逻辑。 static ALWAYS_INLINE idcallAlloc(Class cls, bool checkNil, bool allocWithZone=false){ if (fastpath(cls->canAllocFast())) { bool dtor = cls->hasCxxDtor(); id obj = (id)calloc(1, cls->bits.fastInstanceSize()); if (slowpath(!obj)) return callBadAllocHandler(cls); obj->initInstanceIsa(cls, dtor); return obj; } else { id obj = class_createInstance(cls, 0); if (slowpath(!obj)) return callBadAllocHandler(cls); return obj; }}但是在最新版的objc-723中,调用canAllocFast函数直接返回false,所以只会执行上面第二个else代码块。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/5b7e7c8075ef NSObject之前的定义在OC1.0中,Runtime很多定义都写在NSObject.h文件中,如果之前研究过Runtime的同学可以应该见过下面的定义,定义了一些基础的信息。 // 声明Class和idtypedef struct objc_class *Class;typedef struct objc_object *id;// 声明常用变量typedef struct objc_method *Method;typedef struct objc_ivar *Ivar;typedef struct objc_category *Category;typedef struct objc_property *objc_property_t;// objc_object和objc_classstruct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY;};struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;#endif } OBJC2_UNAVAILABLE;之前的Runtime结构也比较简单,都是一些很直接的结构体定义,现在新版的Runtime在操作的时候,各种地址偏移操作和位运算。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/ce97c66027cd Runtime是iOS系统中重要的组成部分,面试也是必问的问题,所以Runtime是一个iOS工程师必须掌握的知识点。现在市面上有很多关于Runtime的学习资料,也有不少高质量的,但是大多数质量都不是很高,而且都只介绍某个点,并不全面。 这段时间正好公司内部组织技术分享,我分享的主题就是Runtime,我把分享的资料发到博客,大家一起学习交流。 文章都是我的一些笔记,和平时的技术积累。个人水平有限,文章有什么问题还请各位大神指导,谢谢!???? 描述OC语言是一门动态语言,会将程序的一些决定工作从编译期推迟到运行期。由于OC语言运行时的特性,所以其不只需要依赖编译器,还需要依赖运行时环境。 OC语言在编译期都会被编译为C语言的Runtime代码,二进制执行过程中执行的都是C语言代码。而OC的类本质上都是结构体,在编译时都会以结构体的形式被编译到二进制中。Runtime是一套由C、C++、汇编实现的API,所有的方法调用都叫做发送消息。 根据Apple官方文档的描述,目前OC运行时分为两个版本,Modern和Legacy。二者的区别在于Legacy在实例变量发生改变后,需要重新编译其子类。Modern在实例变量发生改变后,不需要重新编译其子类。 Runtime不只是一些C语言的API,其由Class、Meta Class、Instance、Class Instance组成,是一套完整的面向对象的数据结构。所以研究Runtime整体的对象模型,比研究API是怎么实现的更有意义。 使用RuntimeRuntime是一个共享动态库,其目录位于/usr/include/objc,由一系列的C函数和结构体构成。和Runtime系统发生交互的方式有三种,一般都是用前两种: 使用OC源码直接使用上层OC源码,底层会通过Runtime为其提供运行支持,上层不需要关心Runtime运行。NSObject在OC代码中绝大多数的类都是继承自NSObject的,NSProxy类例外。Runtime在NSObject中定义了一些基础操作,NSObject的子类也具备这些特性。Runtime动态库上层的OC源码都是通过Runtime实现的,我们一般不直接使用Runtime,直接和OC代码打交道就可以。使用Runtime需要引入下面两个头文件,一些基础方法都定义在这两个文件中。 #import <objc/runtime.h>#import <objc/message.h>对象模型下面图中表示了对象间isa的关系,以及类的继承关系。 从Runtime源码可以看出,每个对象都是一个objc_object的结构体,在结构体中有一个isa指针,该指针指向自己所属的类,由Runtime负责创建对象。 类被定义为objc_class结构体,objc_class结构体继承自objc_object,所以类也是对象。在应用程序中,类对象只会被创建一份。在objc_class结构体中定义了对象的method list、protocol、ivar list等,表示对象的行为。 既然类是对象,那类对象也是其他类的实例。所以Runtime中设计出了meta class,通过meta class来创建类对象,所以类对象的isa指向对应的meta class。而meta class也是一个对象,所有元类的isa都指向其根元类,根原类的isa指针指向自己。通过这种设计,isa的整体结构形成了一个闭环。 // 精简版定义typedef struct objc_class *Class;struct objc_class : objc_object { // Class ISA; Class superclass;}struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY;};在对象的继承体系中,类和元类都有各自的继承体系,但它们都有共同的根父类NSObject,而NSObject的父类指向nil。需要注意的是,上图中Root Class(Class)是NSObject类对象,而Root Class(Meta)是NSObject的元类对象。 基础定义在objc-private.h文件中,有一些项目中常用的基础定义,这是最新的objc-723中的定义,可以来看一下。 typedef struct objc_class *Class;typedef struct objc_object *id;typedef struct method_t *Method;typedef struct ivar_t *Ivar;typedef struct category_t *Category;typedef struct property_t *objc_property_t;IMP在Runtime中IMP本质上就是一个函数指针,其定义如下。在IMP中有两个默认的参数id和SEL,id也就是方法中的self,这和objc_msgSend()函数传递的参数一样。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/d27c1f5ee3ff iOS接入Flutter在进行iOS和Flutter的混编时,iOS比Android的接入方式略复杂,但也还好。现在市面上有不少接入Flutter的方案,但大多数都是千篇一律相互抄的,没什么意义。 进行Flutter混编之前,有一些必要的文件。 xcode_backend.sh文件,在配置flutter环境的时候由Flutter工具包提供。xcconfig环境变量文件,在Flutter工程中自动生成,每个工程都不一样。xcconfig文件xcconfig是Xcode的配置文件,Flutter在里面配置了一些基本信息和路径,接入Flutter前需要先将xcconfig接入进来,否则一些路径等信息将会出错或找不到。 Flutter的xcconfig包含三个文件,Debug.xcconfig、Release.xcconfig、Generated.xcconfig,需要将这些文件配置在下面的位置,并且按照不同环境配置不同的文件。 Project -> Info -> Development Target -> Configurations 有些比较大的工程中已经在Configurations中设置了xcconfig文件,由于每个Target的一种环境只能配置一个xcconfig文件,所以可以在已有的xcconfig文件中import引入Generated.xcconfig文件,并且不需要区分环境。 脚本文件xcode_backend.sh脚本文件用来构建和导出Flutter产物,这是Flutter开发包为我们默认提供的。需要在工程Target的Build Phases加入一个Run Script文件,并将下面的脚本代码粘贴进去。需要注意的是,不要忘记前面的/bin/sh操作,否则会导致权限错误。 /bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed在xcode_backend.sh中有三个参数类型,build、thin、embed,thin没有太大意义,其他两个则负责构建和导出。 混合开发随后可以对Xcode工程进行编译,这时候肯定会报错的。但是不要慌张,报错后我们在工程主目录下会发现一个名为Flutter的文件夹,其中会包含两个framework,这个文件夹就是Flutter的编译产物,我们将这个文件夹整体拖入项目中即可。 这时候就可以在iOS工程中添加Flutter代码了,下面是详细步骤。 将AppDelegate的集成改为FlutterAppDelegate,并且需要遵循FlutterAppLifeCycleProvider代理。#import <Flutter/Flutter.h>#import <UIKit/UIKit.h>@interface AppDelegate : FlutterAppDelegate <FlutterAppLifeCycleProvider>@end创建一个FlutterPluginAppLifeCycleDelegate的实例对象,这个对象负责管理Flutter的生命周期,并从Platform侧接收AppDelegate的事件。我直接将其声明为一个属性,在AppDelegate中的各个方法中,调用其方法进行中转操作。- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self.lifeCycleDelegate application:application willFinishLaunchingWithOptions:launchOptions]; return YES;}- (void)applicationWillResignActive:(UIApplication *)application { [self.lifeCycleDelegate applicationWillResignActive:application];} - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { [self.lifeCycleDelegate application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; return YES;}随后即可加入Flutter代码,加入的方式也很简单,直接实例化一个FlutterViewController控制器即可,也不需要传其他参数进去(这里先不考虑多实例的问题)。FlutterViewController *flutterViewController = [[FlutterViewController alloc] init];Flutter将其看做是一个画布,实例化一个画布上去之后,任何操作其实都是在当前页面完成的。 ...
au-signer-win工具可以实现在Windows电脑直接重签名ipa,无需苹果电脑! 对现用的ipa文件进行重签,实现达到可以安装自己苹果手机的目的。 扩展功能可以设置签名时间控制,可以去除第三方签名锁,安装量、启动次数统计,很方便管理! 详细文档 http://www.applicationloader....
现在很多iOS的APP没有做任何的安全防范措施,导致存在很多安全隐患和事故,今天我们来聊聊iOS开发人员平时怎么做才更安全。 一、网络方面用抓包工具可以抓取手机通信接口的数据。以Charles为例,用Charles可以获取http的所有明文数据,配置好它的证书后就可以模拟中间人攻击,获取https加密前的明文数据。 1.1 中间人攻击先简要地说下什么是中间人攻击: ①客户端:“我是客户端,给我你的公钥” -> 服务端(被中间人截获)。 所以现在是: 客户端->中间人 ②然后中间人把消息转给服务端,也就是: 中间人->服务端 ③服务端把带有公钥的信息发送给客户端,但是被中间截获。所以是: 服务端-[服务端的公钥] ->中间人 ④中间人把服务端的公钥替换成自己的公钥,发送给客户端,声称是服务端的公钥: 中间人-[中间人的公钥] ->客户端 ⑤客户端用得到的公钥加密,实际是用中间人的公钥进行加密,所以中间人可以用自己的私钥解密,获取原始数据,然后再用服务端的公钥对原始数据(或者修改原始数据内容)加密后发送给服务端。 这样中间人就可以获取到双方的通信数据,并可以制造虚假数据。 1.2 如何防范中间人攻击?下面开始说如何防范: 1.2.1 SSL PinningSSL Pinning的原理就是把服务端的公钥存到客户端中,客户端会校验服务端返回的证书是否和客户端保存的一致,这样就避免了中间人替换证书进行的攻击。 SSL Pinning的实现比较简单,只需要把CA证书放入项目中,通过Security framework实现NSURLSession上的SSL Pinning。如果用的是AFNetworking,代码更简单一点: 这样通过Charles抓包就会报错。 证书验证有可以只验证公钥(AFSSLPinningModePublicKey),也可以完全验证证书(AFSSLPinningModeCertificate)。 但是用SSL Pinning有个很严重的问题,就是如果证书有问题,只有发布新版本才能解决。如果新版本一直审核不通过,app的网络通信就全部挂掉了。 比如赛门铁克(Symantec)证书被google和iOS12不信任的问题。如果app内置了证书,就必须要重新发版。 1.2.2 接口内容进行加密很多的app接口只对请求的参数进行加密和各种验证,而接口返回过来的数据就是明文。如果不用SSL Pinning来防止中间人攻击,也可以把接口返回的数据也进行加密,这样抓包工具抓到包后也依然不能破解。 比如微信,微信中的接口用的是http协议,但是内容全部进行了加密。 现在常用的是对称加密,加密效率比较快。如果app里有的数据特别重要,还是要用非对称加密,非对称加密更安全,但是效率会比较慢。 二、日志2.1 Swift日志Swift中打印日志的语法可以用print,也可以用NSLog。但是尽量别用NSLog,因为Swift中用NSLog,系统日志中是能查到的。可以通过pp助手、iTools或者Xcode的Devices and Simulators 来查看系统日志。 用print打印日志就不会出现在系统日志中。 2.2 OC日志在release环境下不要输出NSLog日志。一般大家都会用宏定义解决,如下: 三、信息的存储3.1 密钥大部分的程序员喜欢直接把密钥放到宏或者常量里。 如:#define AES_KEY @“aaa123" 这样做很容易就可以被反编译出来。安全性比较差。可以用以下方法加强安全,增加破解的难度。 对密钥(A)进行加密后定义为宏(B),使用的时候进行解密得到密钥(A)。其中对密钥A加密的密钥为C。 因为在宏定义的时候我们如果定义成字符串,会直接存在data段,这样破解者很容易获取到。比较安全的做法是把C和B定义成uint8_t[]数组,这样每个字符就会放到text段的每个单独指令中。指令执行后生成字符串。这样就会很安全。 用一段长文本,按规则提取出里面的密钥,密钥是随机的。 在服务端和客户端定义一段长文本,app端随机生成起始位置和长度,把起始位置和长度进行移位等操作,生成相应的数字,对数字进行Base64编码,生成的字符串 传给服务端,服务端根据这个字符串 就能 解析出相关的密钥。 代码如下: ...
Core Data 是苹果原生自带的数据库管理框架,功能强大但使用起来也很复杂。在配置 Core Data 的 relationship 时有一个属性叫 Delete Rule。Delete Rule 表明了数据对象在被删除时,和他有 relationship 的其他数据对象的处理规则。 Deny除非有 relationship 的其他数据对象全部被删除,否则该数据对象将无法被删除。 你在撤销一个事业部门前,需要先将部门员工全部 fire。Cascade当你删除一个数据对象时,有 relationship 的其他数据对象将一并被自动删除。 你撤销了一个事业部门,该部门的员工在同一时间一并被 fire 。Nullify当你删除一个数据对象时,有 relationship 的其他数据对象的 relationship 指针将被设置为 null。 只有当 relationship 是 Optional 时,该设置才是有意义的。否则,你必须在删除前,为有 relationship 的数据对象手动设置新的 relationship。 No Action正如其名。在删除时,不做对 relationship 任何操作。 注意⚠️ 当你使用这种删除规则时,所有的 relationship 都需要你手动管理。
2016年是直播行业被资本疯狂追逐的一年,可至今却经历着“浪潮”褪去,洗刷的不止是中小型直播平台,就连熊猫TV等有资本加持的大平台都纷纷遭遇倒闭危机。然而,直播行业作为泛娱乐的模式之一,其实本身“未死”,在行业洗牌和整合的围困下,秀场直播抱团取暖,跨界直播异军突起,直播出海也成为各平台生存和扩张的出路之一。由于东南亚、中东等国家和地区的互联网生态比中国晚 2 到 3 年,直播模式尚处于萌芽阶段,各平台将国内直播模式照搬,以寻求在相对空白的市场加快商业变现。当前国内有近 50 家直播企业出海,覆盖亚洲、欧洲、非洲、美洲和大洋洲等 45 个国家和地区。直播扎堆出海首先面对的就是技术考验,众多直播公司就遭遇过自研的直播底层 IM 技术无法支持平台流量的困境。“平台没有经历过高并发考验,导致我们刚在东南亚推广上线一天,就彻底崩了。”业内技术人士说道。相对于技术环境较稳定的国内市场,直播出海企业不仅面临 IM 丢包丢消息的风险,其链路能否支撑企业的海外布局也被严格考验着。克服“水土不服”,小象直播杀破重围近期,一款主打海外直播的 App 小象直播,吸引了人们关注的目光,这款致力打造“最懂华人”品牌的直播平台于 2018 年 5 月正式上线,并成功入围 2019 年 1 月海外收入 Top 20 视频/直播榜单,并在同年 3 月登上 Google Play 马来西亚应用类 App 总榜第 5 名。( 2019 年 1 月海外收入 Top 20 视频/直播榜单)小象直播上线以来,不断更新迭代功能,先后推出直播弹幕、送礼、无限制视频/语音连麦,以及在线抓娃娃等丰富的线上玩法,实现主播与用户连接“无卡顿”、互动“零距离”,为海外用户呈现出超强娱乐性,以及身临其境的直播盛宴。技术驱动平台发展。在小象直播互动功能持续升级的背后,选择的是全球互联网通信云服务商融云强有力的支持,通过其 SDK 的接入,小象直播快速集成直播互动能力,让海外用户轻松获得极致的互动体验。融云直播聊天室构建多种类互动场景,亿级并发稳定可靠融云直播聊天室帮助小象直播构建了稳固的“平台地基”,真正将主播与用户连接起来,稳定、高并发、多种类的直播场景,大大满足了平台用户全面的互动需求。据了解,融云直播聊天室消息库通用性极高,支持弹幕、礼物、点赞、禁言、踢出聊天室、系统通知等多种消息类型,同时支持 iOS 、 Android 、 Web 、桌面端多平台接入使用;小象直播无需进行繁杂的业务梳理、消息的定义及实现,可灵活使用消息库中的消息类型,满足各类直播场景聊天互动需求、节省研发成本,符合其低成本的运营模式。融云经过 2218 亿消息峰值和海量用户的锤炼,帮助小象直播平台实现亿级消息并发即时到达,保证实时互动稳定流畅无卡顿。今年 1 月,小象直播在马来西亚举办了一场颁奖典礼,马来西亚当红明星作为嘉宾自带了超高流量,即便如此,融云依然全面保障了小象直播这场年度派对的直播质量。全球化通信能力支撑小象直播海外用户沟通无障碍小象直播是一款主打“最懂华人的直播平台”,主要用户群体集中在东南亚,多样的海外业务对通信网络的连通率及稳定性要求极其严格。融云直播聊天室帮助小象直播打消了通信云技术应用的后顾之忧。(小象直播官宣图)融云在全球设立了多数据中心,具备 3000 多个加速点,通信网络覆盖全球所有国家及地区,有效保证了海外用户对于链路通畅的超高要求,不仅为小象直播百万用户提供稳定流畅的沟通环境,也对吸引主播加入、提高主播的活跃度及留存率起到很大帮助。融云作为全球互联网通信云的领导者,致力为全球 27 万 App 提供安全可靠的全球通信云服务,开发者只需简单集成 SDK ,就能获得良好的直播聊天互动体验,完美将互动融入自己的直播业务。目前,融云与荔枝 FM、羚萌 Show、得到、汽车之家、吱呀、蜜芽等平台进行深度合作,助其直播业务的顺利开展。未来,融云将更专注于技术的研发与服务的升级,为更多开发者及互联网企业带来方便快捷、个性化、多样化的用户体验,领跑“新直播时代”。
目的模仿Masonry连续运用点语法的操作[self.view mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(@10).offset(1); }];写出一个连加的操作 make.add(10).add(10);想看结果的请直接跳到“最终结果”分析一.定义SQMath类SQMath.h#import <Foundation/Foundation.h>@interface SQMath : NSObject- (NSInteger)result;- (void)add:(int)number;@endSQMath.m#import “SQMath.h”@interface SQMath ()@property (nonatomic, assign) NSInteger number;@end@implementation SQMath- (NSInteger)result { return self.number;}- (void)add:(int)number { self.number += number;}@end使用这个SQMath的add方法 SQMath *math = [[SQMath alloc] init]; [math add:10]; [math add:20]; NSLog(@"%ld", [math result]);二.将函数调用改为点语法如果要用点语法,需要让-add从一个方法变成一个add的属性。但是这样就没有办法传参了- (NSInteger)add;但是如果返回值是一个 NSInteger (^)(NSInteger) 类型的block就可以了。math.add返回的是这个block,这个block是需要一个NSInteger为参数(加数),返回值是NSInteger(结果)。SQMath.m- (NSInteger (^)(NSInteger count))add { __weak typeof(self) weakSelf=self; NSInteger (^addBlock)(NSInteger) = ^(NSInteger addCount){ __strong typeof(weakSelf) strongSelf = weakSelf; strongSelf.number += addCount; return strongSelf.number; }; return addBlock;}或者- (NSInteger (^)(NSInteger count))add { __weak typeof(self) weakSelf=self; return ^(NSInteger addCount) { __strong typeof(weakSelf) strongSelf = weakSelf; strongSelf.number += addCount; return strongSelf.number; };}使用这个SQMath的add方法 SQMath math = [[SQMath alloc] init]; NSLog(@"%ld", math.add(10)); NSLog(@"%ld", math.add(20)); NSLog(@"%ld", math.add(30));三.连续使用点语法只要将Block的返回值更改为self。这样每次add返回的则变成了SQMath的实例对象,这样就可以实现连续点语法的效果了。- (SQMath (^)(NSInteger count))add { __weak typeof(self) weakSelf=self; return ^(NSInteger addCount) { __strong typeof(weakSelf) strongSelf = weakSelf; strongSelf.number += addCount; return self; };}使用这个SQMath的add方法SQMath *math = [[SQMath alloc] init];NSLog(@"%ld", math.add(10).add(20).add(30).result) ;四.将这个改为NSNumber的CategoryNSNumber+SQMath.h#import <Foundation/Foundation.h>#import “SQMath.h”@interface NSNumber (Math)- (NSInteger)sq_add:(void(^)(SQMath *make))block;@endNSNumber+SQMath.m#import “NSNumber+SQMath.h”@implementation NSNumber (SQMath)- (NSInteger)sq_add:(void(^)(SQMath *))block { SQMath *math = [[SQMath alloc] init]; block(math); return math.result;}@endNSNumber+SQMath 使用 NSInteger result = [@10 sq_add:^(SQMath * make) { make.add(10).add(20); }]; NSLog(@"%ld", result);最终结果SQChainProgrammingps:链式编程什么时候用我还真不太清楚,但我知道面试的时候肯定有用 哈哈。 ...
该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/1a90adc09e99介绍Flutter是Google开发的新一代跨平台方案,Flutter可以实现写一份代码同时运行在iOS和Android设备上,并且提供很好的性能体验。Flutter使用Dart作为开发语言,这是一门简洁、强类型的编程语言。Flutter对于iOS和Android设备,提供了两套视觉库,可以针对不同的平台有不同的展示效果。Flutter原本是为了解决Web开发中的一些问题,而开发的一套精简版Web框架,拥有独立的渲染引擎和开发语言,但后来逐渐演变为移动端开发框架。正是由于Dart当初的定位是为了替代JS成为Web框架,所以Dart的语法更接近于JS语法。例如定义对象构建方法,以及实例化对象的方式等。在Google刚推出Flutter时,其发展很缓慢,终于在18年发布第一个Bate版之后迎来了爆发性增长,发布第一个Release版时增长速度更快。可以从Github上Star数据看出来这个增长的过程。在19年最新的Flutter 1.2版本中,已经开放Web支持的Beta版。目前已经有不少大型项目接入Flutter,阿里的咸鱼、头条的抖音、腾讯的NOW直播,都将Flutter当做应用程序的开发语言。除此之外,还有一些其他中小型公司也在做。整体架构Flutter可以理解为开发SDK或者工具包,其通过Dart作为开发语言,并且提供Material和Cupertino两套视觉控件,视图或其他和视图相关的类,都以Widget的形式表现。Flutter有自己的渲染引擎,并不依赖原生平台的渲染。Flutter还包含一个用C++实现的Engine,渲染也是包含在其中的。EngineFlutter是一套全新的跨平台方案,Flutter并不像React Native那样,依赖原生应用的渲染,而是自己有自己的渲染引擎,并使用Dart当做Flutter的开发语言。Flutter整体框架分为两层,底层是通过C++实现的引擎部分,Skia是Flutter的渲染引擎,负责跨平台的图形渲染。Dart作为Flutter的开发语言,在C++引擎上层是Dart的Framework。Flutter不仅仅提供了一套视觉库,在Flutter整体框架中包含各个层级阶段的库。例如实现一个游戏功能,上面一些游戏控件可以用上层视觉库,底层游戏可以直接基于Flutter的底层库进行开发,而不需要调用原生应用的底层库。Flutter的底层库是基于Open GL实现的,所以Open GL可以做的Flutter都可以。视觉库在上层Framework中包含两套视觉库,符合Android风格的Material,和符合iOS风格的Cupertino。也可以在此基础上,封装自己风格的系统组件。Cupertino是一套iOS风格的视觉库,包含iOS的导航栏、button、alertView等。Flutter对不同硬件平台有不同的兼容,例如同样的Material代码运行在iOS和Android不同平台上,有一些平台特有的显示和交互,Flutter依然对其进行了区分适配。例如滑动ScrollView时,iOS平台是有回弹效果的,而Android平台则是阻尼效果。例如iOS的导航栏标题是居中的,Android导航栏标题是向左的,等等。这些Flutter都做了区分兼容。除了Flutter为我们做的一些适配外,有一些控件是需要我们自己做适配的,例如AlertView,在Android和iOS两个平台下的表现就是不同的。这些iOS特性的控件都定义在Cupertino中,所以建议在进行App开发时,对一些控件进行上层封装。例如AlertView则对其进行一个二次封装,控件内部进行设备判断并选择不同的视觉库,这样可以保证各个平台的效果。虽然Flutter对于iOS和Android两个平台,开发有cupertino和material两个视觉库,但实际开发过程中的选择,应该使用material当做视觉库。因为Flutter对iOS的支持并不是很好,主要对Android平台支持比较好,material中的UI控件要比cupertino多好几倍。DartDart是Google在2011年推出的一款应用于Web开发的编程语言,Dart刚推出的时候,定位是替代JS做前端开发,后来逐步扩展到移动端和服务端。Dart是Flutter的开发语言,Flutter必须遵循Dart的语言特性。在此基础上,也会有自己的东西,例如Flutter的上层Framework,自己的渲染引擎等。可以说,Dart只是Flutter的一部分。Dart是强类型的,对定义的变量不需要声明其类型,Flutter会对其进行类型推导。如果不想使用类型推导,也可以自己声明指定的类型。Hot ReloadFlutter支持亚秒级热重载,Android Studio和VSCode都支持Hot Reload的特性。但需要区分的是,热重载和热更新是不同的两个概念,热重载是在运行调试状态下,将新代码直接更新到执行中的二进制。而热更新是在上线后,通过Runtime或其他方式,改变现有执行逻辑。AOT、JITFlutter支持AOT(Ahead of time)和JIT(Just in time)两种编译模式,JIT模式支持在运行过程中进行Hot Reload。刷新过程是一个增量的过程,由系统对本次和上次的代码做一次snapshot,将新的代码注入到DartVM中进行刷新。但有时会不能进行Hot Reload,此时进行一次全量的Hot Reload即可。而AOT模式则是在运行前预先编译好,这样在每次运行过程中就不需要进行分析、编译,此模式的运行速度是最快的。Flutter同时采用了两种方案,在开发阶段采用JIT模式进行开发,在release阶段采用AOT模式,将代码打包为二进制进行发布。在开发原生应用时,每次修改代码后都需要重新编译,并且运行到硬件设备上。由于Flutter支持Hot Reload,可以进行热重载,对项目的开发效率有很大的提升。由于Flutter实现机制支持JIT的原因,理论上来说是支持热更新以及服务器下发代码的。可以从服务器。但是由于这样会使性能变差,而且还有审核的问题,所以Flutter并没有采用这种方案。实现原理Flutter的热重载是基于State的,也就是我们在代码中经常出现的setState方法,通过这个来修改后,会执行相应的build方法,这就是热重载的基本过程。Flutter的hot reload的实现源码在下面路径中,在此路径中包含run_cold.dart和run_hot.dart两个文件,前者负责冷启动,后者负责热重载。~/flutter/packages/flutter_tools/lib/src/run_hot.dart热重载的代码实现在run_hot.dart文件中,有HotRunner来负责具体代码执行。当Flutter进行热重载时,会调用restart函数,函数内部会传入一个fullRestart的bool类型变量。热重载分为全量和非全量,fullRestart参数就是表示是否全量。以非全量热重载为例,函数的fullRestart会传入false,根据传入false参数,下面是部分核心代码。Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) async { if (fullRestart) { // ….. } else { final bool reloadOnTopOfSnapshot = _runningFromSnapshot; final String progressPrefix = reloadOnTopOfSnapshot ? ‘Initializing’ : ‘Performing’; final Status status = logger.startProgress( ‘$progressPrefix hot reload…’, progressId: ‘hot.reload’ ); OperationResult result; try { result = await _reloadSources(pause: pauseAfterRestart, reason: reason); } finally { status.cancel(); } }}调用restart函数后,内部会调用_reloadSources函数,去执行内部逻辑。下面是大概逻辑执行流程。在_reloadSources函数内部,会调用_updateDevFS函数,函数内部会扫描修改的文件,并将文件修改前后进行对比,随后会将被改动的代码生成一个kernel files文件。随后会通过HTTP Server将生成的kernel files文件发送给Dart VM虚拟机,虚拟机拿到kernel文件后会调用_reloadSources函数进行资源重载,将kernel文件注入正在运行的Dart VM中。当资源重载完成后,会调用RPC接口触发Widgets的重绘。跨平台方案对比现在市面上RN、Weex的技术方案基本一样,所以这里就以RN来代表类似的跨平台方案。Flutter是基于GPU进行渲染的,而RN则将渲染交给原生平台,而自己只是负责通过JSCore将视图组织起来,并处理业务逻辑。所以在渲染效果和性能这块,Flutter的性能比RN要强很多。跨平台方案一般都需要对各个平台进行平台适配,也就是创建各自平台的适配层,RN的平台适配层要比Flutter要大很多。因为从技术实现来说,RN是通过JSCore引擎进行原生代码调用的,和原生代码交互很多,所以需要更多的适配。而Flutter则只需要对各自平台独有的特性进行适配即可,例如调用系统相册、粘贴板等。Flutter技术实现是基于更底层实现的,对平台依赖度不是很高,相对来说,RN对平台的依赖度是很高的。所以RN未来的技术升级,包括扩展之类的,都会受到很大的限制。而Flutter未来的潜力将会很大,可以做很多技术改进。Widget在Flutter中将显示以及和显示相关的部分,都统一定义为widget,下面列举一些widget包含的类型:用于显示的视图,例如ListView、Text、Container等。用来操作视图,例如Transform等动画相关。视图布局相关,例如Center、Expanded、Column等。在Flutter中,所有的视图都是由Widget组成,Label、AppBar、ViewController等。在Flutter的设计中,组合的优先级要大于继承,整体视图类结构继承层级很浅但单层很多类。如果想定制或封装一些控件,也应该以组合为主,而不是继承。在iOS开发中,我也经常采用这种设计方案,组合大于继承。因为如果继承层级过多的话,一个是不便于阅读代码,还有就是不好维护代码。例如底层需要改一个通用的样式,但这个类的继承层级比较复杂,这样改动的话影响范围就比较大,会将一些不需要改的也改掉,这时候就会发现继承很鸡肋。但在iOS中有Category的概念,这也是一种组合的方式,可以通过将一些公共的东西放在Category中,使继承的方便性和组合的灵活性达到一个平衡。Flutter中并没有单独的布局文件,例如iOS的XIB这种,代码都在Widget中定义。和UIView的区别在于,Widget只是负责描述视图,并不参与视图的渲染。UIView也是负责描述视图,而UIView的layer则负责渲染操作,这是二者的区别。了解Widget在应用程序启动时,main方法接收一个Widget当做主页面,所以任何一个Widget都可以当做根视图。一般都是传一个MaterialApp,也可以传一个Container当做根视图,这都是被允许的。在Flutter应用中,和界面显示及用户交互的对象都是由Widget构成的,例如视图、动画、手势等。Widget分为StatelessWidget和StatefulWidget两种,分别是无状态和有状态的Widget。StatefulWidget本质上也是无状态的,其通过State来处理Widget的状态,以达到有状态,State出现在整个StatefulWidget的生命周期中。当构建一个Widget时,可以通过其build获得构建流程,在构建流程中可以加入自己的定制操作,例如对其设置title或视图等。return Scaffold( appBar: AppBar( title: Text(‘ListView Demo’), ), body: ListView.builder( itemCount: dataList.length, itemBuilder: (BuildContext context, int index) { return Text(dataList[index]); }, ),);有些Widget在构建时,也提供一些参数来帮助构建,例如构建一个ListView时,会将index返回给build方法,来区别构建的Cell,以及构建的上下文context。itemBuilder: (BuildContext context, int index) { return Text(dataList[index]);}StatelessWidgetStatelessWidget是一种静态Widget,即创建后自身就不能再进行改变。在创建一个StatelessWidget后,需要重写build函数。每个静态Widget都会有一个build函数,在创建视图对象时会调用此方法。同样的,此函数也接收一个Widget类型的返回值。class RectangleWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Center ( // UI Code ); }}StatefulWidgetWidget本质上是不可被改变的,但StatefulWidget将状态拆分到State中去管理,当数据发生改变时由State去处理视图的改变。下面是创建一个动态Widget,当创建一个动态Widget需要配合一个State,并且需要重写createState方法。重写此函数后,指定一个Widget对应的State并初始化。下面例子中,在StatefulWidget的父类中包含一个Key类型的key变量,这是无论静态Widget还是动态Widget都具备的参数。在动态Widget中定义了自己的成员变量title,并在自定义的初始化方法中传入,通过下面DynamicWidget类的构造方法,并不需要在内部手动进行title的赋值,title即为传入的值,是由系统完成的。class DynamicWidget extends StatefulWidget { DynamicWidget({Key key, this.title}) : super (key : key); final String title; @override DynamicWidgetState createState() => new DynamicWidgetState();}由于上面动态Widget定义了初始化方法,在调用动态Widget时可以直接用自定义初始化方法即可。DynamicWidget(key: ‘key’, title: ’title’);StateStatefulWidget的改变是由State来完成的,State中需要重写build方法,在build中进行视图组织。StatefulWidget是一种响应式视图改变的方式,数据源和视图产生绑定关系,由数据源驱动视图的改变。改变StatefulWidget的数据源时,需要调用setState方法,并将数据源改变的操作写在里面。使用动态Widget后,是不需要我们手动去刷新视图的。系统在setState方法调用后,会重新调用对应Widget的build方法,重新绘制某个Widget。下面的代码示例中添加了一个float按钮,并给按钮设置了一个回调函数_onPressAction,这样在每次触发按钮事件时都会调用此函数。counter是一个整型变量并和Text相关联,当counter的值在setState方法中改变时,Text Widget也会跟着变化。class DynamicWidgetState extends State<DynamicWidget> { int counter = 0; void _onPressAction() { setState(() { counter++; }); } @override Widget build(BuildContext context) { return new Scaffold( body: Center( child: Text(‘Button tapped $_counter.’) ), floatingActionButton: FloatingActionButton( onPressed: _onPressAction, tooltip: ‘Increment’, child: Icon(Icons.add) ) ); } }主要Widget在iOS中有UINavigationController的概念,其并不负责显示,而是负责控制各个页面的跳转操作。在Flutter中可以将MaterialApp理解为iOS的导航控制器,其包含一个navigationBar以及导航栈,这和iOS是一样的。在iOS中除了用来显示的视图外,视图还有对应的UIViewController。在Flutter中并没有专门用来管理视图并且和View一对一的类,但从显示的角度来说,有类似的类Scaffold,其包含控制器的appBar,也可以通过body设置一个widget当做其视图。themetheme是Flutter提供的界面风格API,MaterialApp提供有theme属性,可以在MaterialApp中设置全局样式,这样可以统一整个应用的风格。new MaterialApp( title: title, theme: new ThemeData( brightness: Brightness.dark, primaryColor: Colors.lightBlue[800], accentColor: Colors.cyan[600], ));如果不想使用系统默认主题,可以将对应的控件或试图用Theme包起来,并将Theme当做Widget赋值给其他Widget。new Theme( data: new ThemeData( accentColor: Colors.yellow, ), child: new FloatingActionButton( onPressed: () {}, child: new Icon(Icons.add), ),);有时MaterialApp设定的统一风格,并不能满足某个Widget的要求,可能还需要有其他的外观变化,可以通过Theme.of传入当前的BuildContext,来对theme进行扩展。Flutter会根据传入的context,顺着Widget树查找最近的Theme,并对Theme复制一份防止影响原有的Theme,并对其进行扩展。new Theme( data: Theme.of(context).copyWith(accentColor: Colors.yellow), child: new FloatingActionButton( onPressed: null, child: new Icon(Icons.add), ),);网络请求Flutter中可以通过async、await组合使用,进行网络请求。Flutter中的网络请求大体有三种:系统自带的HttpClient网络请求,缺点是代码量相对而言比较多,而且对post请求支持不是很好。三方库http.dart,请求简单。三方库dio,请求简单。http网络库http网络库定义在http.dart中,内部代码定义很全,包括HttpStatus、HttpHeaders、Cookie等很多基础信息,有助于我们了解http请求协议。因为是三方库,所以需要在pubspec.yaml中加入下面的引用。http: ‘>=0.11.3+12’下面是http.dart的请求示例代码,可以看到请求很简单,真正的请求代码其实就两行。生成一个Client请求对象,调用client实例的get方法(如果是post则调用post方法),并用Response对象去接收请求结果即可。通过async修饰发起请求的方法,表示这是一个异步操作,并在请求代码的前面加入await,修饰这里的代码需要等待数据返回,需要过一段时间后再处理。请求回来的数据默认是json字符串,需要对其进行decode并解析为数据对象才可以使用,这里使用系统自带的convert库进行解析,并解析为数组。import ‘package:http/http.dart’ as http;class RequestDemoState extends State<MyHomePage> { List dataList = []; @override void initState() { super.initState(); loadData(); } // 发起网络请求 loadData() async{ String requestURL = ‘https://jsonplaceholder.typicode.com/posts'; Client client = Client(); Response response = await client.get(requestURL); String jsonString = response.body; setState(() { // 数据解析 dataList = json.decode(jsonString); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title) ), body: ListView.builder( itemCount: dataList.length, itemBuilder: (BuildContext context, int index) { return Text(dataList[index][’title’]); }, ), ); }}在调用Client进行post数据请求时,需要传入一个字典进去,Client会通过将字典当做post的from表单。 void requestData() async { var params = Map<String, String>(); params[“username”] = “lxz”; params[“password”] = “123456”; var client = http.Client(); var response = await client.post(url_post, body: params); _content = response.body;}dio网络库dio库的调用方式和http库类似,这里不过多介绍。dio库相对于http库强大的在于,dio库提供了更好的Cookie管理、文件的上传下载、fromData表单等处理。所以,如果对网络库需求比较复杂的话,还是建议使用dio。// 引入外部依赖dio: ^1.0.9数据解析convert系统自带有convert解析库,在使用时直接import即可。convert类似于iOS自带的JSON解析类NSJSONSerialization,可以直接将json字符串解析为字典或数组。import ‘dart:convert’;// 解析代码dataList = json.decode(jsonString);但是,我们在项目中使用时,一般都不会直接使用字典取值,这是一种很不好的做法。一般都会将字典或数组转换为模型对象,在项目中使用模型对象。可以定义类似Model.dart这样的模型类,并在模型类中进行数据解析,对外直接暴露公共变量来让外界获取值。自动序列化但如果定义模型类的话,一个是要在代码内部写取值和赋值代码,这些都需要手动完成。另外如果当服务端字段发生改变后,客户端也需要跟着进行改变,所以这种方式并不是很灵活。可以采用json序列化的三方库json_serializable,此库可以将一个类标示为自动JSON序列化的类,并对类提供JSON和对象相互转换的能力。也可以通过命令行开启一个watch,当类中的变量定义发生改变时,相关代码自动发生改变。首先引入下面的三个库,其中包括依赖库一个,以及调试库两个。dependencies: json_annotation: ^2.0.0dev_dependencies: build_runner: ^1.0.0 json_serializable: ^2.0.0定义一个模型文件,例如这里叫做User.dart文件,并在内部定义一个User的模型类。随后引入json_annotation的依赖,通过@JsonSerializable()标示此类需要被json_serializable进行合成。定义的User类包含两部分,实例变量和两个转换函数。在下面定义json转换函数时,需要注意函数命名一定要按照下面格式命名,否则不能正常生成user.g.dart文件。import ‘package:json_annotation/json_annotation.dart’;// 定义合成后的新文件为user.g.dartpart ‘user.g.dart’;@JsonSerializable()class User { String name; int age; String email; factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); Map<String, dynamic> toJson() => _$UserToJson(this);}下面就是user.dart指定生成的user.g.dart文件,其中包含JSON和对象相互转换的代码。part of ‘user.dart’;User _$UserFromJson(Map<String, dynamic> json) { return User( json[’name’] as String, json[‘age’] as int, json[’email’] as String);}Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{ ’name’: instance.name, ‘age’: instance.age, ’email’: instance.email };有的时候服务端返回的参数名和本地的关键字冲突,或者命名不规范,导致本地定义和服务器字段不同的情况。这种情况可以通过@JsonKey关键字,来修饰json字段匹配新的本地变量。除此之外,也可以做其他修饰,例如变量不能为空等。@JsonKey(name: ‘id’)final int user_id;现在项目中依然是报错的,随后我们在flutter工程的根目录文件夹下,运行下面命令。flutter packages pub run build_runner watch此命令的好处在于,其会在后台监听模型类的定义,当模型类定义发生改变后,会自动修改本地源码以适配新的定义。以文中User类为例,当User.dart文件发生改变后,使用Cmd+s保存文件,随后VSCode会将自定改变user.g.dart文件的定义,以适配新的变量定义。系统文件主要文件iOS文件:iOS工程文件Android:Android工程文件lib:Flutter的dart代码assets:资源文件夹,例如font、image等都可以放在里面.gitignore:git忽略文件packages这是一个系统文件,Flutter通过.packages文件来管理一些系统依赖库,例如material、cupertino、widgets、animation、gesture等系统库就在里面,这些主要的系统库由.packages下的flutter统一管理,源码都在flutter/lib/scr目录下。除此之外,还有一些其他的系统库或系统资源都在.packages中。yaml文件在Flutter中通过pubspec.yaml文件来管理外部引用,包含本地资源文件、字体文件、依赖库等依赖,以及应用的一些配置信息。这些配置在项目中时,需要注意代码对其的问题,否则会导致加载失败。当修改yaml文件的依赖信息后,需要执行flutter get packages命令更新本地文件。但并不需要这么麻烦,可以直接Cmd+s保存文件,VSCode编译器会自动更新依赖。// 项目配置信息name: WeChatdescription: Tencent WeChat App.version: 1.0.0+1// 常规依赖dependencies: flutter:125864 sdk: flutter cupertino_icons: ^0.1.2 english_words: ^3.1.0// 开发依赖dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true // 图片依赖 assets: - assets/images/ic_file_transfer.png - assets/images/ic_fengchao.png // 字体依赖 fonts: - family: appIconFont fonts: - asset: assets/fonts/iconfont.ttfFlutter开发启动函数和大多数编程语言一样,dart也包含一个main方法,是Flutter程序执行的主入口,在main方法中写的代码就是在程序启动时执行的代码。main方法中会执行runApp方法,runApp方法类似于iOS的UIApplicationMain方法,runApp函数接收一个Widget用来做应用程序的显示。void main() { runApp() // code}生命周期在iOS中通过AppDelegate可以获取应用程序的生命周期回调,在Flutter中也可以获取到。可以通过向Binding添加一个Observer,并实现didChangeAppLifecycleState方法,来监听指定事件的到来。但是由于Flutter提供的状态有限,在iOS平台只能监听三种状态,下面是示例代码。class LifeCycleDemoState extends State<MyHomePage> with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); switch (state) { case AppLifecycleState.inactive: print(‘Application Lifecycle inactive’); break; case AppLifecycleState.paused: print(‘Application Lifecycle paused’); break; case AppLifecycleState.resumed: print(‘Application Lifecycle resumed’); break; default: print(‘Application Lifecycle other’); } }}矩阵变换在Flutter中是支持矩阵变化的,例如rotate、scale等方式。Flutter的矩阵变换由Widget完成,需要进行矩阵变换的视图,在外面包一层Transform Widget即可,内部可以设置其变换方式。child: Container( child: Transform( child: Container( child: Text( “Lorem ipsum”, style: TextStyle(color: Colors.orange[300], fontSize: 12.0), textAlign: TextAlign.center, ), decoration: BoxDecoration( color: Colors.red[400], ), padding: EdgeInsets.all(16.0), ), alignment: Alignment.center, transform: Matrix4.identity() ..rotateZ(15 * 3.1415927 / 180), ), width: 320.0, height: 240.0, color: Colors.grey[300],)在Transform中可以通过transform指定其矩阵变换方式,通过alignment指定变换的锚点。页面导航在iOS中可以通过UINavigationController对页面进行管理,控制页面间的push、pop跳转。Flutter中使用Navigator和Routers来实现类似UINavigationController的功能,Navigator负责管理导航栈,包含push、pop的操作,可以把UIViewController看做一个Routers,Routers被Navigator管理着。Navigator的跳转方式分为两种,一种是直接跳转到某个Widget页面,另一种是为MaterialApp构建一个map,通过key来跳转对应的Widget页面。map的格式是key : context的形式。void main() { runApp(MaterialApp( home: MyAppHome(), // becomes the route named ‘/’ routes: <String, WidgetBuilder> { ‘/a’: (BuildContext context) => MyPage(title: ‘page A’), ‘/b’: (BuildContext context) => MyPage(title: ‘page B’), ‘/c’: (BuildContext context) => MyPage(title: ‘page C’), }, ));}跳转时通过pushNamed指定map中的key,即可跳转到对应的Widget。如果需要从push出来的页面获取参数,可以通过await修饰push操作,这样即可在新页面pop的时候将参数返回到当前页面。Navigator.of(context).pushNamed(’/b’);Map coordinates = await Navigator.of(context).pushNamed(’/location’);Navigator.of(context).pop({“lat”:43.821757,“long”:-79.226392});编码规范VSCode有很好的语法检查,如果有命名不规范等问题,都会以警告的形式表现出来。驼峰命名法,方法名、变量名等,都以首字母小写的驼峰命名法。类名也是驼峰命名法,但类名首字母大写。文件名,文件命名以下划线进行区分,不使用驼峰命名法。Flutter中创建Widget对象,可以用new修饰,也可以不用。child: new Container( child: Text( ‘Hello World’, style: TextStyle(color: Colors.orange, fontSize: 15.0) ))函数中可以定义可选参数,以及必要参数。下面是一个函数定义,这里定义了一个必要参数url,以及一个Map类型的可选参数headers。Future<Response> get(url, {Map<String, String> headers});Dart中在函数定义前加下划线,则表示是私有方法或变量。Dart通过import引入外部引用,除此之外也可以通过下面的语法单独引入文件中的某部分。import “dart:collection” show HashMap, IterableBase;=>调用在Dart中经常可以看到=>的调用方式,这种调用方式类似于一种语法糖,下面是一些常用的调用方式。当进行函数调用时,可以将普通函数调用转换为=>的调用方式,例如下面第一个示例。在此基础上,如果调用函数只有一个参数,可以将其改为第二个示例的方式,也就是可以省略调用的括号,直接写参数名。(单一参数) => {函数声明}elements.map((element) => { return element.length;});单一参数 => {函数声明}elements.map(element => { return element.length;});当只有一个返回值,并且没有逻辑处理时,可以直接省略return,返回数值。(参数1, 参数2, …, 参数N) => 表达式elements.map(element => element.length);当调用的函数中没有参数时,可以直接省略参数,写一对空括号即可。() => {函数实现}小技巧代码重构VSCode支持对Dart语言进行重构,一般作用范围都是在函数内小范围的。例如在创建Widget对象的地方,将鼠标焦点放在这里,当前行的最前面会有提示。点击提示后会有下面两个选项:Extract Local Variable 将当前Widget及其子Widget创建的代码,剥离到一个变量中,并在当前位置使用这个变量。Extract Method 将当前Widget及其子Widget创建的代码,封装到一个函数中,并在当前位置调用此函数。除此之外,将鼠标焦点放在方法的一行,点击最前面的提示,会出现下面两个选项:Convert to expression body 将当前函数转换为一个表达式。Convert to async function body 将当前函数转换为一个异步线程中执行的代码。附加效果在Dart中添加任何附加效果,例如动画效果或矩阵转换,除了直接给Widget子类的属性赋值外,就是在被当前Widget外面包一层,就可以使当前Widget拥有对应的效果。// 动画效果floatingActionButton: FloatingActionButton( tooltip: ‘Fade’, child: Icon(Icons.brush), onPressed: () { controller.forward(); },),// 矩阵转换Transform( child: Container( child: Text( “Lorem ipsum”, style: TextStyle(color: Colors.orange[300], fontSize: 12.0), textAlign: TextAlign.center, ) ), alignment: Alignment.center, transform: Matrix4.identity() ..rotateZ(15 * 3.1415927 / 180),),快捷键(VSCode)Cmd + Shift + p:可以进行快速搜索。需要注意的是,默认是带有一个>的,这样搜索结果主要是dart代码。如果想搜索其他配置文件,或者安装插件等操作,需要把>去掉。Cmd + Shift + o:可以在某个文件中搜索某个类,但前提是需要提前进入这个文件。例如进入framework.dart,搜索StatefulWidget类。注意点使用Flutter要注意代码缩进,如果缩进有问题可能会影响最后的结果,尤其是在.yaml中写配置文件的时候。因为Flutter是开源的,所以遇到问题后可以进入源码中,找解决方案。在代码中要注意标点符号的使用,例如第二个创建Stack的代码,如果上面是以逗号结尾,则后面的创建会失败,如果上面是以分号结尾则没问题。Widget unreadMsgText = Container( width: Constants.UnreadMsgNotifyDotSize, height: Constants.UnreadMsgNotifyDotSize, child: Text( conversation.unreadMsgCount.toString(), style: TextStyle( color: Color(AppColors.UnreadMsgNotifyTextColor), fontSize: 12.0 ), ), ); avatarContainer = Stack( overflow: Overflow.visible, children: <Widget>[ avatar ], ); ...
无痕埋点的设计与实现在移动互联网时代,对于每个公司、企业来说,用户的数据非常重要。重要到什么程度,用户在这个页面停留多久、点击了什么按钮、浏览了什么内容、什么手机、什么网络环境、App什么版本等都需要清清楚楚。甚至一些大厂的蛮多业务成果都是靠基于用户操作行为和记录的推荐转换二次。那么有了上述的诉求,那么技术人员如何满足这些需求?引出来了一个技术点-“埋点”埋点手段业界中对于代码埋点主要有3种主流的方案:代码手动埋点、可视化埋点、无痕埋点。简单说说这几种埋点方案。代码手动埋点:根据业务需求(运营、产品、开发多个角度出发)在需要埋点地方手动调用埋点接口,上传埋点数据。可视化埋点:通过可视化配置工具完成采集节点,在前端自动解析配置并上报埋点数据,从而实现可视化“无痕埋点”无痕埋点:通过技术手段,完成对用户行为数据无差别的统计上传的工作。后期数据分析处理的时候通过技术手段筛选出合适的数据进行统计分析。技术选型代码手动埋点该方案情况下,如果需要埋点,则需要在工程代码中,写埋点相关代码。因为侵入了业务代码,对业务代码产生了污染,显而易见的缺点是埋点的成本较高、且违背了单一原则。例1:假如你需要知道用户在点击“购买按钮”时的相关信息(手机型号、App版本、页面路径、停留时间、动作等等),那么就需要在按钮的点击事件里面去写埋点统计的代码。这样明显的弊端就是在之前业务逻辑的代码上面又多出了埋点的代码。由于埋点代码分散、埋点的工作量很大、代码维护成本较高、后期重构很头痛。例2:假如 App 采用了 Hybrid 架构,当 App 的第一版本发布的时候 H5 的关键业务逻辑统计是由 Native 定义好关键逻辑(比如H5调起了Native的分享功能,那么存在一个分享的埋点事件)的桥接。假如某天增加了一个扫一扫功能,未定义扫一扫的埋点桥接,那么 H5 页面变动的时候,Native 埋点代码不去更新的话,变动的 H5 的业务就未被精确统计。优点:产品、运营工作量少,对照业务映射表就可以还原出相关业务场景、数据精细无须大量的加工和处理缺点:开发工作量大、前期需要和运营、产品指定的好业务标识,以便产品和运营进行数据统计分析可视化埋点可视化埋点的出现,是为解决代码埋点流程复杂、成本高、新开发的页面(H5、或者服务端下发的 json 去生成相应页面)不能及时拥有埋点能力前端在「埋点编辑模式」下,以“可视化”的方式去配置、绑定关键业务模块的路径到前端可以唯一确定到view的xpath过程。用户每次操作的控件,都生成一个 xpath 字符串,然后通过接口将 xpath 字符串(view在前端系统中的唯一定位。以 iOS 为例,App名称、控制器名称、一层层view、同类型view的序号:“GoodCell.21.RetailTableView.GoodsViewController.*baoApp”)到真正的业务模块(“宝App-商城控制器-分销商品列表-第21个商品被点击了”)的映射关系上传到服务端。xpath 具体是什么在下文会有介绍。之后操作 App 就生成对应的 xpath 和埋点数据(开发者通过技术手段将从服务端获取的关键数据塞到前端的 UI 控件上。 iOS 端为例, UIView 的 accessibilityIdentifier 属性可以设置我们从服务端获取的埋点数据)上传到服务端。优点:数据量相对准确、后期数据分析成本低缺点:前期控件的唯一识别、定位都需要额外开发;可视化平台的开发成本较高;对于额外需求的分析可能会比较困难无痕埋点通过技术手段无差别地记录用户在前端页面上的行为。可以正确的获取 PV、UV、IP、Action、Time 等信息。缺点:前期开发统计基础信息的技术产品成本较高、后期数据分析数据量很大、分析成本较高(大量数据传统的关系型数据库压力大)优点:开发人员工作量小、数据全面、无遗漏、产品和运营按需分析、支持动态页面的统计分析如何选择结合上述优缺点,我们选择了无痕埋点+可视化埋点结合的技术方案。怎么说呢?对于关键的业务开发结束上线后、通过可视化方案(类似于一个界面,想想看 Dreamwaver,你在界面上拖拖控件,简单编辑下就可以生成对应的 HTML 代码)点击一下绑定对应关系到服务端。那么这个对应关系是什么?我们需要唯一定位一个前端元素,那么想到的办法就是不管 Native 和 Web 前端,控件或者元素来说就是一个树形层级,DOM tree 或者 UI tree,所以我们通过技术手段定位到这个元素,以 Native iOS 为例子假如我点击商品详情页的加入购物车按钮会根据 UI 层级结构生成一个唯一标识 “addCartButton.GoodsViewController.GoodsView.*BaoApp” 。但是用户在使用 App 的时候,上传的是这串东西的 MD5到服务端。这么做有2个原因:服务端数据库存储这串很长的东西不是很好;埋点数据被劫持的话直接看到明文不太好。所以 MD5 再上传。操刀就干数据的收集实现方案由以下几个关键指标:现有代码改动少、尽量不要侵入业务代码去实现拦截系统事件全量收集如何唯一标识一个控件元素不侵入业务代码拦截系统事件以 iOS 为例。我们会想到 AOP(Aspect Oriented Programming)面向切面编程思想。动态地在函数调用前后插入相应的代码,在 Objective-C 中我们可以利用 Runtime 特性,用 Method Swizzling 来 hook 相应的函数为了给所有类方便地 hook,我们可以给 NSObject 添加个 Category,名字叫做 NSObject+MethodSwizzling+ (void)swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{ Class class = [self class]; //原有方法 Method originalMethod = class_getInstanceMethod(class, originalSelector); //替换原有方法的新方法 Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); //先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况 BOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) {//添加成功:表明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else {//添加失败:表明源SEL已经有IMP,直接将两个SEL的IMP交换即可 method_exchangeImplementations(originalMethod, swizzledMethod); }}+ (void)swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{ Class class = [self class]; //原有方法 Method originalMethod = class_getClassMethod(class, originalSelector); //替换原有方法的新方法 Method swizzledMethod = class_getClassMethod(class, swizzledSelector); //先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况 BOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) {//添加成功:表明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP method_exchangeImplementations(originalMethod, swizzledMethod); } else {//添加失败:表明SEL已经有IMP,直接将两个SEL的IMP交换即可 method_exchangeImplementations(originalMethod, swizzledMethod); }}全量收集我们会想到 hook AppDelegate 代理方法、UIViewController 生命周期方法、按钮点击事件、手势事件、各种系统控件的点击回调方法、应用状态切换等等。动作事件App 状态的切换给 Appdelegate 添加分类,hook 生命周期UIViewController 生命周期函数给 UIViewController 添加分类,hook 生命周期UIButton 等的点击UIButton 添加分类,hook 点击事件UICollectionView、UITableView 等的在对应的 Cell 添加分类,hook 点击事件手势事件 UITapGestureRecognizer、UIControl、UIResponder相应系统事件以统计页面的打开时间和统计页面的打开、关闭的需求为例,我们对 UIViewController 进行 hookstatic char *viewController_open_time = “viewController_open_time”;static char *viewController_close_time = “viewController_close_time”;// load 方法里面添加 dispatch_once 是为了防止手动调用 load 方法。+ (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @autoreleasepool { [[self class] swizzleMethod:@selector(viewWillAppear:) swizzledSelector:@selector(viewWillAppear:)]; [[self class] swizzleMethod:@selector(viewWillDisappear:) swizzledSelector:@selector(viewWillDisappear:)]; } });}#pragma mark - add prop- (void)setOpenTime:(NSDate *)openTime{ objc_setAssociatedObject(self,&viewController_open_time, openTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (NSDate *)getOpenTime{ return objc_getAssociatedObject(self, &viewController_open_time);}- (void)setCloseTime:(NSDate *)closeTime{ objc_setAssociatedObject(self,&viewController_close_time, closeTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (NSDate *)getCloseTime{ return objc_getAssociatedObject(self, &viewController_close_time);}- (void)viewWillAppear:(BOOL)animated{ NSString *className = NSStringFromClass([self class]); NSString *refer = [NSString string]; if ([self getPageUrl:className]) { //设置打开时间 [self setOpenTime:[NSDate dateWithTimeIntervalSinceNow:0]]; if (self.navigationController) { if (self.navigationController.viewControllers.count >=2) { //获取当前vc 栈中 上一个VC UIViewController referVC = self.navigationController.viewControllers[self.navigationController.viewControllers.count-2]; refer = [self getPageUrl:NSStringFromClass([referVC class])]; } } if (!refer || refer.length == 0) { refer = @“unknown”; } [SDGDataCenter openPage:[self getPageUrl:className] fromPage:refer]; } [self viewWillAppear:animated];}- (void)viewWillDisappear:(BOOL)animated{ NSString className = NSStringFromClass([self class]); if ([self getPageUrl:className]) { [self setCloseTime:[NSDate dateWithTimeIntervalSinceNow:0]]; [SDGDataCenter leavePage:[self getPageUrl:className] spendTime:[self p_calculationTimeSpend]]; } [self viewWillDisappear:animated];}#pragma mark - private method- (NSString )p_calculationTimeSpend{ if (![self getOpenTime] || ![self getCloseTime]) { return @“unknown”; } NSTimeInterval aTimer = [[self getCloseTime] timeIntervalSinceDate:[self getOpenTime]]; int hour = (int)(aTimer/3600); int minute = (int)(aTimer - hour3600)/60; int second = aTimer - hour3600 - minute60; return [NSString stringWithFormat:@"%d",second];}@end如何唯一标识一个控件元素xpath 是移动端定义可操作区域的唯一标识。既然想通过一个字符串标识前端系统中可操作的控件,那么 xpath 需要2个指标:唯一性:在同一系统中不存在不同控件有着相同的 xpath稳定性:不同版本的系统中,在页面结构没有变动的情况下,不同版本的相同页面,相同的控件的 xpath 需要保持一致。我们想到 Naive、H5 页面等系统渲染的时候都是以树形结构去绘制和渲染,所以我们以当前的 View 到系统的根元素之间的所有关键点(UIViewController、UIView、UIView容器(UITableView、UICollectionView等)、UIButton…)串联起来这样就唯一定位了控件元素。为了精确定位元素节点,参看下图假设一个 UIView 中有三个子 view,先后顺序是:label、button1、button2,那么深度依次为: 0、1、2。假如用户做了某些操作将 label1 从父 view 中被移除了。此时 UIView 只有 2 个子view:button1、button2,而且深度变为了:0、1。可以看出仅仅由于其中某个子 view 的改变,却导致其它子 view 的深度都发生了变化。因此,在设计的时候需要注意,在新增/移除某一 view 时,尽量减少对已有 view 的深度的影响,调整了对节点的深度的计算方式:采用当前 view 位于其父 view 中的所有 与当前 view 同类型 子view 中的索引值。我们再看一下上面的这个例子,最初 label、button1、button2 的深度依次是:0、0、1。在 label 被移除后,button1、button2 的深度依次为:0、1。可以看出,在这个例子中,label 的移除并未对 button1、button2 的深度造成影响,这种调整后的计算方式在一定程度上增强了 xpath 的抗干扰性。另外,调整后的深度的计算方式是依赖于各节点的类型的,因此,此时必须要将各节点的名称放到viewPath中,而不再是仅仅为了增加可读性。在标识控件元素的层级时,需要知道「当前 view 位于其父 view 中的所有 与当前 view 同类型 子view 中的索引值」。参看上图,如果不是同类型的话,则唯一性得不到保证。有个问题,比如我们点击的元素是 UITableViewCell,那么它虽然可以定位到类似于这个标示 xxApp.GoodsViewController.GoodsTableView.GoodsCell,同类型的 Cell 有多个,所以单凭借这个字符串是没有办法定位具体的那个 Cell 被点击了。有2个解决方案利用系统提供的 accessibilityIdentifier 官方给出的解释是标识用户界面元素的字符串找出当前元素在父层同类型元素中的索引。根据当前的元素遍历当前元素的父级元素的子元素,如果出现相同的元素,则需要判断当前元素是所在层级的第几个元素/A string that identifies the user interface element.default == nil/@property(nullable, nonatomic, copy) NSString *accessibilityIdentifier NS_AVAILABLE_IOS(5_0);服务端下发唯一标识接口获取的数据,里面有当前元素的唯一标识。比如在 UITableView 的界面去请求接口拿到数据,那么在在获取到的数据源里面会有一个字段,专门用来存储动态化的经常变动的数据。cell.accessibilityIdentifier = [[[SDGGoodsCategoryServices sharedInstance].categories[indexPath.section] children][indexPath.row].spmContent yy_modelToJSONString];判断在同层级、同类型的控件元素里面的序号对当前的控件元素的父视图的全部子视图进行遍历,如果存在和当前的控件元素同类型的控件,那么需要判断当前控件元素在同类型控件元素中的所处的位置,那么则可以唯一定位。举例:GoodsCell-3.GoodsTableView.GoodsViewController.xxApp//UIResponder分类{ // if (self.xq_identifier_ka == nil) { if ([self isKindOfClass:[UIView class]]) { UIView *view = (id)self; NSString *sameViewTreeNode = [view obtainSameSuperViewSameClassViewTreeIndexPath]; NSMutableString *str = [NSMutableString string]; //特殊的 加减购 因为带有spm但是要区分加减 需要带TreeNode NSString *className = [NSString stringWithUTF8String:object_getClassName(view)]; if (!view.accessibilityIdentifier || [className isEqualToString:@“XQButton”]) { [str appendString:sameViewTreeNode]; [str appendString:@","]; } while (view.nextResponder) { [str appendFormat:@"%@,", NSStringFromClass(view.class)]; if ([view.class isSubclassOfClass:[UIViewController class]]) { break; } view = (id)view.nextResponder; } self.xq_identifier_ka = [self md5String:[NSString stringWithFormat:@"%@",str]]; // self.xq_identifier_ka = [NSString stringWithFormat:@"%@",str]; }// } return self.xq_identifier_ka;}// UIView 分类(NSString *)obtainSameSuperViewSameClassViewTreeIndexPat{ NSString *classStr = NSStringFromClass([self class]); //cell的子view //UITableView 特殊的superview (UITableViewContentView) //UICollectionViewCell BOOL shouldUseSuperView = ([classStr isEqualToString:@“UITableViewCellContentView”]) || ([[self.superview class] isKindOfClass:[UITableViewCell class]])|| ([[self.superview class] isKindOfClass:[UICollectionViewCell class]]); if (shouldUseSuperView) { return [self obtainIndexPathByView:self.superview]; }else { return [self obtainIndexPathByView:self]; }}(NSString )obtainIndexPathByView:(UIView )view{ NSInteger viewTreeNodeDepth = NSIntegerMin; NSInteger sameViewTreeNodeDepth = NSIntegerMin; NSString *classStr = NSStringFromClass([view class]); NSMutableArray *sameClassArr = [[NSMutableArray alloc]init]; //所处父view的全部subviews根节点深度 for (NSInteger index =0; index < view.superview.subviews.count; index ++) { //同类型 if ([classStr isEqualToString:NSStringFromClass([view.superview.subviews[index] class])]){ [sameClassArr addObject:view.superview.subviews[index]]; } if (view == view.superview.subviews[index]) { viewTreeNodeDepth = index; break; } } //所处父view的同类型subviews根节点深度 for (NSInteger index =0; index < sameClassArr.count; index ++) { if (view == sameClassArr[index]) { sameViewTreeNodeDepth = index; break; } } return [NSString stringWithFormat:@"%ld",sameViewTreeNodeDepth]; }## 数据的上传数据通过上面的办法收集完了,那么如何及时、高效的上传到后端,给运营分析、处理呢?App 运行期间用户会点击非常多的数据,如果实时上传的话对于网络的利用率较低,所以需要考虑一个机制去控制用户产生的埋点数据的上传。思路是这样的。对外部暴露出一个接口,用来将产生的数据往数据中心存储。用户产生的数据会先保存到 AppMonitor 的内存中去,设置一个临界值(memoryEventMax = 50),如果存储的值达到设置的临界值 memoryEventMax,那么将内存中的数据写入文件系统,以 zip 的形式保存下来,然后上传到埋点系统。如果没有达到临界值但是存在一些 App 状态切换的情况,这时候需要及时保存数据到持久化。当下次打开 App 就去从本地持久化的地方读取是否有未上传的数据,如果有就上传日志信息,成功后删除本地的日志压缩包。App 应用状态的切换策略如下:- didFinishLaunchWithOptions:内存日志信息写入硬盘- didBecomeActive:上传- willTerimate:内存日志信息写入硬盘- didEnterBackground:内存日志信息写入硬盘// 将App日志信息写入到内存中。当内存中的数量到达一定规模(超过设置的内存中存储的数量)的时候就将内存中的日志存储到文件信息中(void)joinEvent:(NSDictionary *)dictionary{if (dictionary) { NSDictionary *tmp = [self createDicWithEvent:dictionary]; if (!s_memoryArray) { s_memoryArray = [NSMutableArray array]; } [s_memoryArray addObject:tmp]; if ([s_memoryArray count] >= s_flushNum) { [self writeEventLogsInFilesCompletion:^{ [self startUploadLogFile]; }]; }}}// 外界调用的数据传递入口(App埋点统计)(void)traceEvent:(AMStatisticEvent *)event{// 线程锁,防止多处调用产生并发问题@synchronized (self) { if (event && event.userInfo) { [self joinEvent:event.userInfo]; }}}// 将内存中的数据写入到文件中,持久化存储(void)writeEventLogsInFilesCompletion:(void(^)(void))completionBlock{NSArray *tmp = nil;@synchronized (self) { tmp = s_memoryArray; s_memoryArray = nil;}if (tmp) { __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSString *jsonFilePath = [weakSelf createTraceJsonFile]; if ([weakSelf writeArr:tmp toFilePath:jsonFilePath]) { NSString *zipedFilePath = [weakSelf zipJsonFile:jsonFilePath]; if (zipedFilePath) { [AppMonotior clearCacheFile:jsonFilePath]; if (completionBlock) { completionBlock(); } } } });}}// 从App埋点统计压缩包文件夹中的每个压缩包文件上传服务端,成功后就删除本地的日志压缩包(void)startUploadLogFile{NSArray *fList = [self listFilesAtPath:[self eventJsonPath]];if (!fList || [fList count] == 0) { return;}[fList enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { if (![obj hasSuffix:@".zip"]) { return; } NSString *zipedPath = obj; unsigned long long fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:zipedPath error:nil] fileSize]; if (!fileSize || fileSize < 1) { return; } [self uploadZipFileWithPath:zipedPath completion:^(NSString *completionResult) { if ([completionResult isEqual:@“OK”]) { [AppMonotior clearCacheFile:zipedPath]; } }];}];}总结下来关键步骤:1. hook 系统的各种事件(UIResponder、UITableView、UICollectionView代理事件、UIControl事件、UITapGestureRecognizers)、hook 应用程序、控制器生命周期。在做本来的逻辑之前添加额外的监控代码2. 对于点击的元素按照视图树生成对应的唯一标识(addCartButton.GoodsView.GoodsViewController) 的 md5 值3. 在业务开发完毕,进入埋点的编辑模式,将 md5 和关键的页面的关键事件(运营、产品想统计的关键模块:App层级、业务模块、关键页面、关键操作)给绑定起来。比如 addCartButton.GoodsView.GoodsViewController.tbApp 对应了 tbApp-商城模块-商品详情页-加入购物车功能。4. 将所需要的数据存储下来5. 设计机制等到合适的时机去上传数据 ...
原文链接Key-Value Observing 键值观察 ,是一种设计模式观察者模式的实现官方定义键值观察提供了一种机制,允许对象通知其他对象的特定属性的更改。它对应用程序中模型和控制器层之间的通信特别有用。(在OS X中,控制器层绑定技术严重依赖于键值观察。)控制器对象通常观察模型对象的属性,视图对象通过控制器观察模型对象的属性。然而,另外,模型对象可以观察其他模型对象(通常用于确定从属值何时改变)或甚至自身(再次确定从属值何时改变)。先看下使用KVO的姿势Xcode -> New -> MacOS -> CommandLine 新建工程,创建Person类Person.h#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface Person : NSObject@property (nonatomic ,copy) NSString *name;@property (nonatomic ,assign) NSUInteger age;@property (nonatomic ,copy) NSArray<Person *> *friends;@endNS_ASSUME_NONNULL_ENDPerson.m#import “Person.h”@implementation Person- (instancetype)init { self = [super init]; if (self) { _name = @""; _age = 0; _friends = @[]; } return self;}- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { // 当收到通知的时候打印观察的对象,旧值和新值 NSLog(@"\nReceving ObserveValueChanged \nObject: %@ OldValue: %@, NewValue: %@",object, change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]); change[@“new”]);}// 重写以便打印对象的属性- (NSString *)description { return [NSString stringWithFormat:@"- name: %@, age: %ld, friends: %@",self.name, self.age, self.friends];}@end打开main.m,创建Alice和Bob,设置Bob观察Alice的age属性Person *Alice = Person.new;Alice.name = @“Alice”;Alice.age = 18;Person *Bob = Person.new;Bob.name = @“Bob”;Bob.age = 28;[Alice addObserver:Bob forKeyPath:@“age” options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:PersonChangeContext];Alice.age = 100;[Alice removeObserver:Bob forKeyPath:@“age”];Alice.age = 200;可以看到控制台输出了一次Alice的age属性的前后变化.其中,NSKeyValueObservingOptions 有以下几个选项,可以使用 | 符号组合使用NSKeyValueObservingOptionNew // 收到通知时change字典将包含新值NSKeyValueObservingOptionOld // 收到通知时change字典将包含旧值NSKeyValueObservingOptionInitial // 在addObserver时会发送通知,change字典将包含初始值NSKeyValueObservingOptionPrior // 在所观察keyPath改变之前将收到通知change字典的key值在这里FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey KVO的使用场景有很多,比如Person拥有一个account属性,person需要获取account的变化该如何处理呢,轮训或许是个办法,但是无疑效率低下,而且很不安全,更合理的办法就是使用KVO观察account,在account发生变化时更新。原理现在我们已经知其然了,但是还不知其所以然。先说结论系统通过runtime生成继承与被观察者的新类(NSKVONotifying_Person),对原对象进行isa-swizzing(isa混写)根据KVC(键值编码)对对象的keypath进行hock在将要对属性进行赋值操作时发送通知如何证明上述结论呢,上代码!#import <Foundation/Foundation.h>#import <objc/runtime.h>#import “Person.h"static void *PersonChangeContext = &PersonChangeContext;int main(int argc, const char * argv[]) { @autoreleasepool { Person *Alice = Person.new; Alice.name = @“Alice”; Alice.age = 18; Person *Bob = Person.new; Bob.name = @“Bob”; Bob.age = 28; Class cls0 = object_getClass(Alice); [Alice addObserver:Bob forKeyPath:@“age” options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:PersonChangeContext]; Alice.age = 100; Class cls1 = object_getClass(Alice); NSLog(@”%@ %@",cls0,cls1); } return 0;}首先引入runtime,这样我们可以打印isa所指向的真实的类,在向Alice添加观察者之前,先获取Class在向Alice添加观察者之后,获取ClassPerson NSKVONotifying_Person可以看到输出确实如此这就是KVO的核心思路了,关于KVC请看这里. ...
最近在使用ReactNative技术搭建新项目的过程中,在集成神策分析时,在进行IOS端配置的时候Xcode进行build项目的时候遇到如下报错:library not found for -lRNSensorsAnalyticsModule,忘记截图了,用如下这张代替一下甚是烦恼,不知道是咋回事。。。最后找到了原因,是因为在如下libararies中无意添加了一个无效的libRNSensorsAnalyticsModule.a文件解决方法:选中这个文件,点击图中的减号即可使用Xcode重新clean product后,重新build即可。其他常见问题的处理方法1:library not found for -XXX 的问题有可能是你的某个库的连接引用有问题,解决的办法就是在项目的target里,选中Link Binary With Libraries 里的.a或framework 取消再加入,就可以了。2:warning:directory not found option 的问题可能是framework search paths 还有Library Search Paths 里面并没有这个路径,删除即可。3:duplicate就是重复的意思,那么看代码 要删除重复的openUDID,以及libwoa_two和sbjson冲突了,删除sbjson的点m即可.
首先需要在Mac上打开Charles,并启动代理,完成了一系列配置后,能正常监听流量了以后,再进行下面操作(配置电脑证书等).PC端Charles设置,可以参考我另外一篇文章https://segmentfault.com/a/11…监听iOS设备流量(信任https请求),安卓端与iOS类似,只不过安卓端下载证书后,需要去系统文件中打开证书安装并信任(IOS只有下载立即安装,如果下载完成没有安装,以后如果需要安装证书,只能重新下载)打开Charles帮助, 查看移动端证书安装提示按照提示设置IOS WIFI代理.(IOS设备与电脑需要处于同一网络).上图红圈标红的内容是,我电脑在局域网的内网IP,8888是我电脑设置的代理监听端口.如果有必要,修改步骤2红框中的端口Mac电脑Charles代理设置方式.上图中1是必选项,只有勾选了,才能启动代理.2是代理监听端口,默认8888配置iOS设置代理(需要与电脑处于同一WIFI),服务器和端口就是步骤2红框中的内容下载证书.在iOS上配置好WIFI代理后,使用Safari浏览器打开http://chls.pro/ssl ,浏览器会自动下载一个 证书,安装即可.注意:只能用Safari浏览器打开才有用浏览器打开http://chls.pro/ssl 后的提示如果下载文件成功,点击允许,会跳转到该界面点击安装即可.安装成功会如下图所示如果安装成功,在设置–> 通用 –> 描述文件 中会新增一个Charles Proxy的描述文件.前往 设置–> 通用 –> 关于本机–> 证书信任设置 里,将刚刚新增的Charles证书设置为启用状态,设置为启用后,应该如下图所示(开关选项会显示为绿色)完成上面的步骤,iOS上的Charles代理https证书就设置完成啦.打开浏览器,随便打开个https网页,电脑端的记录就会显示详细内容了,而不会显示问号疑难杂症:问: 如果你移动端配置好以后,还是无法监听https流量答: 请你先确定,你是否能够监听移动端的http流量,如果http流量都无法监听,那么是你电脑Charles设置问题,与移动设备无关问: iOS设备用浏览器打开http://chls.pro/ssl 后,手机没有弹出允许的窗口,而是直接让你保存下载文件,或者打开后无反应.答: 请先确认你手机电脑是否使用到是同一WIFI,并且保证你iOS中WIFI设置的代理是步骤2中的 >IP+端口.然后,再使用移动设备默认浏览器打开,iOS必须要用Safari浏览器打开如果你是使用Mac电脑,那么,你还需要检查你的电脑中是否安装有Charles证书,并且设置为始终信任.下图是我电脑的钥匙串系统证书列表
ipa上传助手Appuploader是一个iOS APP上架辅助助手,帮助开发者可以快速的申请iOS证书打包ipa文件上传到App Store审核。非常方便的iOS上架助手,提升上架效率。ipa上传助手Appuploader官网http://www.applicationloader….ipa上传助手Appuploader介绍一、可以在Windows系统直接申请iOS证书上传ipa(不用苹果电脑也不用装Mac虚拟机)二、帮助不懂上架流程、初次接触上架的开发者,快速掌握上架流程
本文主要记录日常工作中积累的一些iOS小技巧SDWebImage 加载大量高清图片时内存暴增解决方案:关闭SD加载高清大图时的解压缩static BOOL SDImageCacheOldShouldDecompressImages = YES; static BOOL SDImagedownloderOldShouldDecompressImages = YES; - (void)viewDidLoad { [super viewDidLoad]; // 关闭SD加载高清大图时的解压缩 SDImageCache *canche = [SDImageCache sharedImageCache]; SDImageCacheOldShouldDecompressImages = canche.shouldDecompressImages; canche.shouldDecompressImages = NO; SDWebImageDownloader *downloder = [SDWebImageDownloader sharedDownloader]; SDImagedownloderOldShouldDecompressImages = downloder.shouldDecompressImages; downloder.shouldDecompressImages = NO; } -(void)dealloc { SDImageCache *canche = [SDImageCache sharedImageCache]; canche.shouldDecompressImages = SDImageCacheOldShouldDecompressImages; SDWebImageDownloader *downloder = [SDWebImageDownloader sharedDownloader]; downloder.shouldDecompressImages = SDImagedownloderOldShouldDecompressImages; } SDWebImage本地缓存有时候会害人。如果之前缓存过一张图片,即使下次服务器换了这张图片,但是图片url没换,用SDWebimage下载下来的还是以前那张,所以遇到这种问题,不要先去怼服务器,清空下缓存再试就好了。禁止手机睡眠[UIApplication sharedApplication].idleTimerDisabled = YES;隐藏某行cell- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{// 如果是你需要隐藏的那一行,返回高度为0 if(indexPath.row == YouWantToHideRow){ return 0; } return 44;}// 然后再你需要隐藏cell的时候调用[self.tableView beginUpdates];[self.tableView endUpdates];禁用button高亮button.adjustsImageWhenHighlighted = NO;或者在创建的时候UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];动画切换window的根控制器[UIView transitionWithView:[UIApplication sharedApplication].keyWindow duration:.5f options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ BOOL oldState = [UIView areAnimationsEnabled]; [UIView setAnimationsEnabled:NO]; [UIApplication sharedApplication].keyWindow.rootViewController = [[RootViewController alloc]init]; [UIView setAnimationsEnabled:oldState]; } completion:^(BOOL finished) { }];去除数组中重复的对象NSArray *newArr = [oldArr valueForKeyPath:@"@distinctUnionOfObjects.self"];编译的时候遇到 no such file or directory: /users/apple/XXX是因为编译的时候,在此路径下找不到这个文件,解决这个问题,首先是是要检查缺少的文件是不是在工程中,如果不在工程中,需要从本地拖进去,如果发现已经存在工程中了,或者拖进去还是报错,这时候需要去build phases中搜索这个文件,这时候很可能会搜出现两个相同的文件,这时候,有一个路径是正确的,删除另外一个即可。如果删除了还是不行,需要把两个都删掉,然后重新往工程里拖进这个文件即可iOS8系统中,tableView最好实现下-tableView: heightForRowAtIndexPath:这个代理方法,要不然在iOS8中可能就会出现显示不全或者无法响应事件的问题iOS8中实现侧滑功能的时候这个方法必须实现,要不然在iOS8中无法侧滑// 必须写的方法,和editActionsForRowAtIndexPath配对使用,里面什么不写也行- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {}三个比较特殊的通知NSSystemTimeZoneDidChangeNotification监听修改时间界面的两个按钮状态变化UIApplicationSignificantTimeChangeNotification 监听用户改变时间 (只要点击自动设置按钮就会调用)NSSystemClockDidChangeNotification 监听用户修改时间(时间不同才会调用)获取不到UICollectionView指定cell的问题[self.collectionView layoutIfNeeded];//添加这句话就好 QTMResContinueEditeCell *cell = (QTMResContinueEditeCell )[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:_index inSection:0]];跳进app权限设置// 跳进app设置if (UIApplicationOpenSettingsURLString != NULL) { UIApplication application = [UIApplication sharedApplication]; NSURL URL = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; if ([application respondsToSelector:@selector(openURL:options:completionHandler:)]) { [application openURL:URL options:@{} completionHandler:nil]; } else { [application openURL:URL]; }} 给一个view截图- (UIImage )cutImageWithView:(UIView )view{ UIGraphicsBeginImageContextWithOptions(view.frame.size, NO, 0); [view.layer renderInContext:UIGraphicsGetCurrentContext()]; UIImage image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image;}注意对象为nil的时候,调用此对象分类的方法不会执行collectionView的内容小于其宽高的时候是不能滚动的,设置可以滚动:collectionView.alwaysBounceHorizontal = YES;collectionView.alwaysBounceVertical = YES;颜色转图片+ (UIImage )imageWithColor:(UIColor )color { //描述一个矩形 CGRect rect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); //开启图形上下文 UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0); //获得图形上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); //使用color演示填充上下文 CGContextSetFillColorWithColor(ctx, [color CGColor]); //渲染上下文 CGContextFillRect(ctx, rect); UIImage image = UIGraphicsGetImageFromCurrentImageContext(); //关闭图形上下文 UIGraphicsEndImageContext(); return image; }view设置圆角#define ViewBorderRadius(View, Radius, Width, Color)\[View.layer setCornerRadius:(Radius)];[View.layer setMasksToBounds:YES];[View.layer setBorderWidth:(Width)];[View.layer setBorderColor:[Color CGColor]] // view圆角view某个角度设置圆角UIBezierPath maskPath = [UIBezierPath bezierPathWithRoundedRect:self.whiteView.bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(10, 10)]; CAShapeLayer maskLayer = [[CAShapeLayer alloc] init]; maskLayer.frame = self.whiteView.bounds; maskLayer.path = maskPath.CGPath; self.whiteView.layer.mask = maskLayer; ##指定了需要成为圆角的角该参数是UIRectCorner类型的,可选的值有: UIRectCornerTopLeft UIRectCornerTopRight UIRectCornerBottomLeft UIRectCornerBottomRight UIRectCornerAllCorners 由角度转换弧度#define DegreesToRadian(x) (M_PI * (x) / 180.0)由弧度转换角度#define RadianToDegrees(radian) (radian180.0)/(M_PI)获取图片资源//建议使用前两种宏定义,性能高于后者#define LOADIMAGE(file,ext) [UIImage imageWithContentsOfFile:[[NSBundle mainBundle]pathForResource:file ofType:ext]]#define IMAGE(A) [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:A ofType:nil]]#define ImageNamed(_pointer) [UIImage imageNamed:[UIUtil imageName:_pointer]]#define UIImageName(name) [UIImage imageNamed:name]文件路径//获取temp#define PathTemp NSTemporaryDirectory()//获取Document#define PathDocument [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]//获取 Cache#define PathCache [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]GCD代码只执行一次#define kDISPATCH_ONCE_BLOCK(onceBlock) static dispatch_once_t onceToken; dispatch_once(&onceToken, onceBlock);自定义NSLog//DEBUG 模式下打印日志,当前行#ifdef DEBUG# define DLog(fmt, …) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTION, LINE, ##VA_ARGS);#else# define DLog(…)#endif//重写NSLog,Debug模式下打印日志和当前行数#if DEBUG#define NSLog(FORMAT, …) fprintf(stderr,"\nfunction:%s line:%d content:%s\n", FUNCTION, LINE, [[NSString stringWithFormat:FORMAT, ##VA_ARGS] UTF8String]);#else#define NSLog(FORMAT, …) nil#endif//DEBUG 模式下打印日志,当前行 并弹出一个警告#ifdef DEBUG# define ULog(fmt, …) { UIAlertView alert = [[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:@"%s\n [Line %d] “, PRETTY_FUNCTION, LINE] message:[NSString stringWithFormat:fmt, ##VA_ARGS] delegate:nil cancelButtonTitle:@“Ok” otherButtonTitles:nil]; [alert show]; }#else# define ULog(…)#endifFont#define FONTS(size) [UIFont systemFontOfSize:(size)]#define BOLDFONTS(size) [UIFont boldSystemFontOfSize:(size)]GCD//主线程#define kDISPATCH_MAIN_THREAD(mainQueueBlock) dispatch_async(dispatch_get_main_queue(), mainQueueBlock);//异步线程#define kDISPATCH_GLOBAL_QUEUE_DEFAULT(globalQueueBlock) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), globalQueueBlocl);通知#define NOTIF_ADD(n, f) [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(f) name:n object:nil]#define NOTIF_POST(n, o) [[NSNotificationCenter defaultCenter] postNotificationName:n object:o]#define NOTIF_REMV() [[NSNotificationCenter defaultCenter] removeObserver:self]Color// RGB颜色转换(16进制->10进制)#define UIColorFromRGB(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]// 获取RGB颜色#define RGBA(r,g,b,a) [UIColor colorWithRed:r/255.0f green:g/255.0f blue:b/255.0f alpha:a]//清除背景色#define CLEARCOLOR [UIColor clearColor]// 随机色#define RandomCOLOR RGBCOLOR(arc4random_uniform(256),arc4random_uniform(256),arc4random_uniform(256))获取window+(UIWindow)getWindow { UIWindow win = nil; //[UIApplication sharedApplication].keyWindow; for (id item in [UIApplication sharedApplication].windows) { if ([item class] == [UIWindow class]) { if (!((UIWindow)item).hidden) { win = item; break; } } } return win;}修改textField的placeholder的字体颜色、大小[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor”];[textField setValue:[UIFont boldSystemFontOfSize:16] forKeyPath:@"_placeholderLabel.font"];// 或者textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:placeholder attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:13],NSForegroundColorAttributeName : [UIColor grayColor]}];APP缓存/ * 存 数组数据/+(void)setObectOfArray:(NSArray *)array fileName:(NSString )fileName{ //缓存文件的 根路径 NSString path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; // 缓存 文件夹 路径 NSString filePath = [path stringByAppendingPathComponent:@“XXX.dataCache”]; NSFileManager fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:filePath] == NO) { [fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:nil]; } //缓存文件的路径 NSString * cacheFilePath = [filePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.plist",fileName]]; BOOL isSuccess = [array writeToFile:cacheFilePath atomically:NO]; if (isSuccess) { NSLog(@“缓存数据成功:—%@",cacheFilePath); }else { NSLog(@“缓存数据失败:—%@",cacheFilePath); } }/ 取 数组数据/+(NSArray *)cacheArrayForFileName:(NSString *)fileName{ //缓存文件的 根路径 NSString path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; // 缓存 文件夹 路径 NSString filePath = [path stringByAppendingPathComponent:@“XXX.dataCache”]; //缓存文件的路径 NSString * cacheFilePath = [filePath stringByAppendingPathComponent:[NSString stringWithFormat:@”%@.plist”,fileName]]; //取得缓存文件 NSArray array = [NSArray arrayWithContentsOfFile:cacheFilePath]; return array;}/ 清除 这个全部的缓存数据 */+(void)clearCacheListData:(void (^)())completion{ // 异线程 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 清楚缓存 //缓存文件的 根路径 NSString path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; // 缓存 文件夹 路径 NSString filePath = [path stringByAppendingPathComponent:@“FortuneDonkey.dataCache”]; NSFileManager manager = [NSFileManager defaultManager]; //移除文件夹 [manager removeItemAtPath:filePath error:nil]; // 创建一个新的文件夹 [manager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:nil]; if (completion) { //回调主线程 dispatch_async(dispatch_get_main_queue(), ^{ completion(); }); } });}/ 清除SD缓存数据 */-(void)clearSDCacheData{ //先清除内存中的图片缓存 [[SDImageCache sharedImageCache] clearMemory]; //清除磁盘的缓存 [[SDImageCache sharedImageCache] clearDisk];}获取APP缓存大小- (CGFloat)getCachSize { NSUInteger imageCacheSize = [[SDImageCache sharedImageCache] getSize]; //获取自定义缓存大小 //用枚举器遍历 一个文件夹的内容 //1.获取 文件夹枚举器 NSString *myCachePath = [NSHomeDirectory() stringByAppendingPathComponent:@“Library/Caches”]; NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:myCachePath]; __block NSUInteger count = 0; //2.遍历 for (NSString *fileName in enumerator) { NSString *path = [myCachePath stringByAppendingPathComponent:fileName]; NSDictionary fileDict = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil]; count += fileDict.fileSize;//自定义所有缓存大小 } // 得到是字节 转化为M CGFloat totalSize = ((CGFloat)imageCacheSize+count)/1024/1024; return totalSize;}几个常用权限判断 if ([CLLocationManager authorizationStatus] ==kCLAuthorizationStatusDenied) { NSLog(@“没有定位权限”); } AVAuthorizationStatus statusVideo = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; if (statusVideo == AVAuthorizationStatusDenied) { NSLog(@“没有摄像头权限”); } //是否有麦克风权限 AVAuthorizationStatus statusAudio = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; if (statusAudio == AVAuthorizationStatusDenied) { NSLog(@“没有录音权限”); } [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { if (status == PHAuthorizationStatusDenied) { NSLog(@“没有相册权限”); } }];系统相关的一些方法//获取系统版本#define IOS_VERSION [[[UIDevice currentDevice] systemVersion] floatValue]#define IS_IOS10_OR_LATER ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0)//获取当前语言#define CurrentLanguage ([[NSLocale preferredLanguages] objectAtIndex:0])//判断是否 Retina屏、设备是否%fhone 5、是否是iPad#define isRetina ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(640, 960), [[UIScreen mainScreen] currentMode].size) : NO)#define iPhone5 ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(640, 1136), [[UIScreen mainScreen] currentMode].size) : NO)#define iPhone6 ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(750, 1334), [[UIScreen mainScreen] currentMode].size) : NO)#define isPad (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)//判断是真机还是模拟器#if TARGET_OS_IPHONE//iPhone Device#endif#if TARGET_IPHONE_SIMULATOR//iPhone Simulator#endif长按复制功能- (void)viewDidLoad{ [self.view addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(pasteBoard:)]];}- (void)pasteBoard:(UILongPressGestureRecognizer )longPress { if (longPress.state == UIGestureRecognizerStateBegan) { UIPasteboard pasteboard = [UIPasteboard generalPasteboard]; pasteboard.string = @“我是文字”; }}判断图片类型//通过图片 Data 数据第一个字节 来获取图片扩展名- (NSString )contentTypeForImageData:(NSData )data{ uint8_t c; [data getBytes:&c length:1]; switch (c) { case 0xFF: return @“jpeg”; case 0x89: return @“png”; case 0x47: return @“gif”; case 0x49: case 0x4D: return @“tiff”; case 0x52: if ([data length] < 12) { return nil; } NSString testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding]; if ([testString hasPrefix:@“RIFF”] && [testString hasSuffix:@“WEBP”]) { return @“webp”; } return nil; } return nil;}获取手机和 APP 信息 //手机序列号NSString identifierNumber = [[UIDevice currentDevice] uniqueIdentifier];//手机别名: 用户定义的名称NSString userPhoneName = [[UIDevice currentDevice] name];//设备名称NSString deviceName = [[UIDevice currentDevice] systemName];//手机系统版本NSString phoneVersion = [[UIDevice currentDevice] systemVersion];//手机型号NSString phoneModel = [[UIDevice currentDevice] model];//地方型号 (国际化区域名称)NSString localPhoneModel = [[UIDevice currentDevice] localizedModel];NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];// 当前应用名称NSString *appCurName = [infoDictionary objectForKey:@“CFBundleDisplayName”];// 当前应用版本 NSString *appCurVersion = [infoDictionary objectForKey:@“CFBundleShortVersionString”];// 当前应用版本号码 int类型NSString *appCurVersionNum = [infoDictionary objectForKey:@“CFBundleVersion”]; UIImage绘制圆角- (UIImage *)circleImage{ // NO代表透明 UIGraphicsBeginImageContextWithOptions(self.size, NO, 1); // 获得上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 添加一个圆 CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); // 方形变圆形 CGContextAddEllipseInRect(ctx, rect); // 裁剪 CGContextClip(ctx); // 将图片画上去 [self drawInRect:rect]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image;}准确获取图片像素CGFloat fixelW = CGImageGetWidth(image.CGImage);CGFloat fixelH = CGImageGetHeight(image.CGImage);JSON字符串转字典+ (NSDictionary *)parseJSONStringToNSDictionary:(NSString *)JSONString { NSData *JSONData = [JSONString dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *responseJSON = [NSJSONSerialization JSONObjectWithData:JSONData options:NSJSONReadingMutableLeaves error:nil]; return responseJSON;}获取当前控制器//获取当前屏幕显示的viewcontroller- (UIViewController *)getCurrentVC{ UIWindow *window = [[UIApplication sharedApplication].windows firstObject]; if (!window) { return nil; } UIView *tempView; for (UIView *subview in window.subviews) { if ([[subview.classForCoder description] isEqualToString:@“UILayoutContainerView”]) { tempView = subview; break; } } if (!tempView) { tempView = [window.subviews lastObject]; } id nextResponder = [tempView nextResponder]; while (![nextResponder isKindOfClass:[UIViewController class]] || [nextResponder isKindOfClass:[UINavigationController class]] || [nextResponder isKindOfClass:[UITabBarController class]]) { tempView = [tempView.subviews firstObject]; if (!tempView) { return nil; } nextResponder = [tempView nextResponder]; } return (UIViewController *)nextResponder;}KVO监听某个对象的属性// 添加监听者[self addObserver:self forKeyPath:property options:NSKeyValueObservingOptionNew context:nil];// 当监听的属性值变化的时候会来到这个方法- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@“property”]) { [self textViewTextChange]; } else { }}Reachability判断网络状态NetworkStatus status = [[Reachability reachabilityForInternetConnection] currentReachabilityStatus]; if (status == NotReachable) { NSLog(@“当前设备无网络”); } if (status == ReachableViaWiFi) { NSLog(@“当前wifi网络”); } if (status == ReachableViaWWAN) { NSLog(@“当前蜂窝移动网络”); } AFNetworking监听网络状态// 监听网络状况 AFNetworkReachabilityManager mgr = [AFNetworkReachabilityManager sharedManager]; [mgr setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { switch (status) { case AFNetworkReachabilityStatusUnknown: break; case AFNetworkReachabilityStatusNotReachable: { NSLog(@“当前设备无网络”); } break; case AFNetworkReachabilityStatusReachableViaWiFi: NSLog(@“当前wifi网络”); break; case AFNetworkReachabilityStatusReachableViaWWAN: NSLog(@“当前蜂窝移动网络”); break; default: break; } }]; [mgr startMonitoring];父视图透明不影响子视图的做法self.view.backgroundColor = [[UIColor whiteColor]colorWithAlphaComponent:0.7f];取图片某一点的颜色 if (point.x < 0 || point.y < 0) return nil; CGImageRef imageRef = self.CGImage; NSUInteger width = CGImageGetWidth(imageRef); NSUInteger height = CGImageGetHeight(imageRef); if (point.x >= width || point.y >= height) return nil; unsigned char rawData = malloc(height * width * 4); if (!rawData) return nil; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); NSUInteger bytesPerPixel = 4; NSUInteger bytesPerRow = bytesPerPixel * width; NSUInteger bitsPerComponent = 8; CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); if (!context) { free(rawData); return nil; } CGColorSpaceRelease(colorSpace); CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); CGContextRelease(context); int byteIndex = (bytesPerRow * point.y) + point.x * bytesPerPixel; CGFloat red = (rawData[byteIndex] * 1.0) / 255.0; CGFloat green = (rawData[byteIndex + 1] * 1.0) / 255.0; CGFloat blue = (rawData[byteIndex + 2] * 1.0) / 255.0; CGFloat alpha = (rawData[byteIndex + 3] * 1.0) / 255.0; UIColor result = nil; result = [UIColor colorWithRed:red green:green blue:blue alpha:alpha]; free(rawData); return result;判断该图片是否有透明度通道 - (BOOL)hasAlphaChannel{ CGImageAlphaInfo alpha = CGImageGetAlphaInfo(self.CGImage); return (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast);}两张图片合成+ (UIImage)mergeImage:(UIImage)firstImage withImage:(UIImage)secondImage { CGImageRef firstImageRef = firstImage.CGImage; CGFloat firstWidth = CGImageGetWidth(firstImageRef); CGFloat firstHeight = CGImageGetHeight(firstImageRef); CGImageRef secondImageRef = secondImage.CGImage; CGFloat secondWidth = CGImageGetWidth(secondImageRef); CGFloat secondHeight = CGImageGetHeight(secondImageRef); CGSize mergedSize = CGSizeMake(MAX(firstWidth, secondWidth), MAX(firstHeight, secondHeight)); UIGraphicsBeginImageContext(mergedSize); [firstImage drawInRect:CGRectMake(0, 0, firstWidth, firstHeight)]; [secondImage drawInRect:CGRectMake(0, 0, secondWidth, secondHeight)]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image;}制作水印// 画水印- (void) setImage:(UIImage *)image withWaterMark:(UIImage *)mark inRect:(CGRect)rect{ if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 4.0) { UIGraphicsBeginImageContextWithOptions(self.frame.size, NO, 0.0); } //原图 [image drawInRect:self.bounds]; //水印图 [mark drawInRect:rect]; UIImage *newPic = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); self.image = newPic;}让label的文字内容显示在左上/右上/左下/右下/中心顶/中心底部自定义UILabel// 重写label的textRectForBounds方法- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines { CGRect rect = [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines]; switch (self.textAlignmentType) { case WZBTextAlignmentTypeLeftTop: { rect.origin = bounds.origin; } break; case WZBTextAlignmentTypeRightTop: { rect.origin = CGPointMake(CGRectGetMaxX(bounds) - rect.size.width, bounds.origin.y); } break; case WZBTextAlignmentTypeLeftBottom: { rect.origin = CGPointMake(bounds.origin.x, CGRectGetMaxY(bounds) - rect.size.height); } break; case WZBTextAlignmentTypeRightBottom: { rect.origin = CGPointMake(CGRectGetMaxX(bounds) - rect.size.width, CGRectGetMaxY(bounds) - rect.size.height); } break; case WZBTextAlignmentTypeTopCenter: { rect.origin = CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth(rect)) / 2, CGRectGetMaxY(bounds) - rect.origin.y); } break; case WZBTextAlignmentTypeBottomCenter: { rect.origin = CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth(rect)) / 2, CGRectGetMaxY(bounds) - CGRectGetMaxY(bounds) - rect.size.height); } break; case WZBTextAlignmentTypeLeft: { rect.origin = CGPointMake(0, rect.origin.y); } break; case WZBTextAlignmentTypeRight: { rect.origin = CGPointMake(rect.origin.x, 0); } break; case WZBTextAlignmentTypeCenter: { rect.origin = CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth(rect)) / 2, (CGRectGetHeight(bounds) - CGRectGetHeight(rect)) / 2); } break; default: break; } return rect;}- (void)drawTextInRect:(CGRect)rect { CGRect textRect = [self textRectForBounds:rect limitedToNumberOfLines:self.numberOfLines]; [super drawTextInRect:textRect];}移除字符串中的空格和换行+ (NSString *)removeSpaceAndNewline:(NSString *)str { NSString *temp = [str stringByReplacingOccurrencesOfString:@" " withString:@""]; temp = [temp stringByReplacingOccurrencesOfString:@"\r" withString:@""]; temp = [temp stringByReplacingOccurrencesOfString:@"\n" withString:@""]; return temp;}判断字符串中是否有空格+ (BOOL)isBlank:(NSString *)str { NSRange _range = [str rangeOfString:@" “]; if (_range.location != NSNotFound) { //有空格 return YES; } else { //没有空格 return NO; }}获取一个视频的第一帧图片NSURL *url = [NSURL URLWithString:filepath];AVURLAsset *asset1 = [[AVURLAsset alloc] initWithURL:url options:nil];AVAssetImageGenerator *generate1 = [[AVAssetImageGenerator alloc] initWithAsset:asset1];generate1.appliesPreferredTrackTransform = YES;NSError *err = NULL;CMTime time = CMTimeMake(1, 2);CGImageRef oneRef = [generate1 copyCGImageAtTime:time actualTime:NULL error:&err];UIImage *one = [[UIImage alloc] initWithCGImage:oneRef]; return one;获取视频的时长+ (NSInteger)getVideoTimeByUrlString:(NSString *)urlString { NSURL *videoUrl = [NSURL URLWithString:urlString]; AVURLAsset *avUrl = [AVURLAsset assetWithURL:videoUrl]; CMTime time = [avUrl duration]; int seconds = ceil(time.value/time.timescale); return seconds;}当tableView占不满一屏时,去除下边多余的单元格self.tableView.tableHeaderView = [UIView new];self.tableView.tableFooterView = [UIView new];isKindOfClass和isMemberOfClass的区别isKindOfClass可以判断某个对象是否属于某个类,或者这个类的子类。isMemberOfClass更加精准,它只能判断这个对象类型是否为这个类(不能判断子类)禁用系统滑动返回功能- (void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {self.navigationController.interactivePopGestureRecognizer.delegate = self; }}- (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {self.navigationController.interactivePopGestureRecognizer.delegate = nil; }}- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{ return NO;}UILabel设置内边距子类化UILabel,重写drawTextInRect方法- (void)drawTextInRect:(CGRect)rect { // 边距,上左下右 UIEdgeInsets insets = {0, 5, 0, 5}; [super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)];}UILabel设置文字描边子类化UILabel,重写drawTextInRect方法- (void)drawTextInRect:(CGRect)rect{ CGContextRef c = UIGraphicsGetCurrentContext(); // 设置描边宽度 CGContextSetLineWidth(c, 1); CGContextSetLineJoin(c, kCGLineJoinRound); CGContextSetTextDrawingMode(c, kCGTextStroke); // 描边颜色 self.textColor = [UIColor redColor]; [super drawTextInRect:rect]; // 文本颜色 self.textColor = [UIColor yellowColor]; CGContextSetTextDrawingMode(c, kCGTextFill); [super drawTextInRect:rect];}UIView背景颜色渐变+ (CAGradientLayer *)setGradualChangingColor:(UIView *)view fromColor:(NSString *)fromHexColorStr toColor:(NSString *)toHexColorStr{ // CAGradientLayer类对其绘制渐变背景颜色、填充层的形状(包括圆角) CAGradientLayer *gradientLayer = [CAGradientLayer layer]; gradientLayer.frame = view.bounds; // 创建渐变色数组,需要转换为CGColor颜色 gradientLayer.colors = @[(__bridge id)[UIColor colorWithHexString:fromHexColorStr].CGColor,(__bridge id)[UIColor colorWithHexString:toHexColorStr].CGColor]; // 设置渐变颜色方向,左下点为(0,0), 右上点为(1,1) gradientLayer.startPoint = CGPointMake(0, 0.5); gradientLayer.endPoint = CGPointMake(1, 0.5); // 设置颜色变化点,取值范围 0.0~1.0 gradientLayer.locations = @[@0,@1]; return gradientLayer;}UIView某个角添加圆角// 左上角和右下角添加圆角UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:(UIRectCornerTopLeft | UIRectCornerBottomRight) cornerRadii:CGSizeMake(20, 20)];CAShapeLayer *maskLayer = [CAShapeLayer layer];maskLayer.frame = view.bounds;maskLayer.path = maskPath.CGPath;view.layer.mask = maskLayer;UIImage和base64互转// view分类方法- (NSString *)encodeToBase64String:(UIImage *)image { return [UIImagePNGRepresentation(image) base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];}- (UIImage *)decodeBase64ToImage:(NSString *)strEncodeData { NSData *data = [[NSData alloc]initWithBase64EncodedString:strEncodeData options:NSDataBase64DecodingIgnoreUnknownCharacters]; return [UIImage imageWithData:data];}UIWebView设置背景透明[webView setBackgroundColor:[UIColor clearColor]];[webView setOpaque:NO];设置tableView分割线颜色以及顶到头// 分割线颜色[self.tableView setSeparatorColor:[UIColor myColor]];// 顶到头- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { [cell setSeparatorInset:UIEdgeInsetsZero]; [cell setLayoutMargins:UIEdgeInsetsZero]; cell.preservesSuperviewLayoutMargins = NO;}- (void)viewDidLayoutSubviews { [self.tableView setSeparatorInset:UIEdgeInsetsZero]; [self.tableView setLayoutMargins:UIEdgeInsetsZero];}为一个view添加虚线边框CAShapeLayer *border = [CAShapeLayer layer]; border.strokeColor = [UIColor colorWithRed:67/255.0f green:37/255.0f blue:83/255.0f alpha:1].CGColor; border.fillColor = nil; border.lineDashPattern = @[@4, @2]; border.path = [UIBezierPath bezierPathWithRect:view.bounds].CGPath; border.frame = view.bounds; [view.layer addSublayer:border];UITextView中打开或禁用复制,剪切,选择,全选等功能// 继承UITextView重写这个方法- (BOOL)canPerformAction:(SEL)action withSender:(id)sender{// 返回NO为禁用,YES为开启 // 粘贴 if (action == @selector(paste:)) return NO; // 剪切 if (action == @selector(cut:)) return NO; // 复制 if (action == @selector(copy:)) return NO; // 选择 if (action == @selector(select:)) return NO; // 选中全部 if (action == @selector(selectAll:)) return NO; // 删除 if (action == @selector(delete:)) return NO; // 分享 if (action == @selector(share)) return NO; return [super canPerformAction:action withSender:sender];} ...
世人都说阅读源代码对于功力的提升是十分显著的, 但是很多的著名开源框架源代码动辄上万行, 复杂度实在太高, 这里只做基础的分析。简洁的接口首先来介绍一下这个 SDWebImage 这个著名开源框架吧, 这个开源框架的主要作用就是:Asynchronous image downloader with cache support with an UIImageView category.一个异步下载图片并且支持缓存的 UIImageView 分类.就这么直译过来相信各位也能理解, 框架中最最常用的方法其实就是这个:[self.imageView sd_setImageWithURL:[NSURL URLWithString:@“url”] placeholderImage:[UIImage imageNamed:@“placeholder.png”]]; 当然这个框架中还有 UIButton 的分类, 可以给 UIButton 异步加载图片, 不过这个并没有 UIImageView 分类中的这个方法常用.这个框架的设计还是极其的优雅和简洁, 主要的功能就是这么一行代码, 而其中复杂的实现细节全部隐藏在这行代码之后, 正应了那句话:把简洁留给别人, 把复杂留给自己.我们已经看到了这个框架简洁的接口, 接下来我们看一下 SDWebImage 是用什么样的方式优雅地实现异步加载图片和缓存的功能呢?复杂的实现其实复杂只是相对于简洁而言的, 并不是说 SDWebImage 的实现就很糟糕, 相反, 它的实现还是非常 amazing 的, 在这里我们会忽略很多的实现细节, 并不会对每一行源代码逐一解读.首先, 我们从一个很高的层次来看一下这个框架是如何组织的.UIImageView+WebCache 和 UIButton+WebCache 直接为表层的 UIKit 框架提供接口, 而 SDWebImageManger 负责处理和协调 SDWebImageDownloader 和 SDWebImageCache. 并与 UIKit 层进行交互, 而底层的一些类为更高层级的抽象提供支持.UIImageView+WebCache接下来我们就以 UIImageView+WebCache 中的- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;这一方法为入口研究一下 SDWebImage 是怎样工作的. 我们打开上面这段方法的实现代码 UIImageView+WebCache.m当然你也可以 git clone git@github.com:rs/SDWebImage.git 到本地来查看.- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];}这段方法唯一的作用就是调用了另一个方法[self sd_setImageWithURL:placeholderImage:options:progress:completed:]在这个文件中, 你会看到很多的 sd_setImageWithURL…… 方法, 它们最终都会调用上面这个方法, 只是根据需要传入不同的参数, 这在很多的开源项目中乃至我们平时写的项目中都是很常见的. 而这个方法也是 UIImageView+WebCache 中的核心方法.这里就不再复制出这个方法的全部实现了.操作的管理这是这个方法的第一行代码:// UIImageView+WebCache// sd_setImageWithURL:placeholderImage:options:progress:completed: #1[self sd_cancelCurrentImageLoad];这行看似简单的代码最开始是被我忽略的, 我后来才发现蕴藏在这行代码之后的思想, 也就是 SDWebImage 管理操作的办法.框架中的所有操作实际上都是通过一个 operationDictionary 来管理, 而这个字典实际上是动态的添加到 UIView 上的一个属性, 至于为什么添加到 UIView 上, 主要是因为这个 operationDictionary 需要在 UIButton 和 UIImageView 上重用, 所以需要添加到它们的根类上.这行代码是要保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突, 它会调用:// UIImageView+WebCache// sd_cancelCurrentImageLoad #1[self sd_cancelImageLoadOperationWithKey:@“UIImageViewImageLoad”]而这个方法会使当前 UIImageView 中的所有操作都被 cancel. 不会影响之后进行的下载操作.占位图的实现// UIImageView+WebCache// sd_setImageWithURL:placeholderImage:options:progress:completed: #4if (!(options & SDWebImageDelayPlaceholder)) { self.image = placeholder;}如果传入的 options 中没有 SDWebImageDelayPlaceholder(默认情况下 options == 0), 那么就会为 UIImageView 添加一个临时的 image, 也就是占位图.获取图片// UIImageView+WebCache// sd_setImageWithURL:placeholderImage:options:progress:completed: #8if (url)接下来会检测传入的 url 是否非空, 如果非空那么一个全局的 SDWebImageManager 就会调用以下的方法获取图片:[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]下载完成后会调用 (SDWebImageCompletionWithFinishedBlock)completedBlock 为 UIImageView.image 赋值, 添加上最终所需要的图片.// UIImageView+WebCache// sd_setImageWithURL:placeholderImage:options:progress:completed: #10dispatch_main_sync_safe(^{ if (!wself) return; if (image) { wself.image = image; [wself setNeedsLayout]; } else { if ((options & SDWebImageDelayPlaceholder)) { wself.image = placeholder; [wself setNeedsLayout]; } } if (completedBlock && finished) { completedBlock(image, error, cacheType, url); }});dispatch_main_sync_safe 宏定义上述代码中的 dispatch_main_sync_safe 是一个宏定义, 点进去一看发现宏是这样定义的#define dispatch_main_sync_safe(block)\ if ([NSThread isMainThread]) {\ block();\ } else {\ dispatch_sync(dispatch_get_main_queue(), block);\ }相信这个宏的名字已经讲他的作用解释的很清楚了: 因为图像的绘制只能在主线程完成, 所以, dispatch_main_sync_safe 就是为了保证 block 能在主线程中执行.而最后, 在 [SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:] 返回 operation 的同时, 也会向 operationDictionary 中添加一个键值对, 来表示操作的正在进行:// UIImageView+WebCache// sd_setImageWithURL:placeholderImage:options:progress:completed: #28[self sd_setImageLoadOperation:operation forKey:@“UIImageViewImageLoad”];它将 opertion 存储到 operationDictionary 中方便以后的 cancel.到此为止我们已经对 SDWebImage 框架中的这一方法分析完了, 接下来我们将要分析 SDWebImageManager 中的方法[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]SDWebImageManager在 SDWebImageManager.h 中你可以看到关于 SDWebImageManager 的描述:The SDWebImageManager is the class behind the UIImageView+WebCache category and likes. It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). You can use this class directly to benefit from web image downloading with caching in another context than a UIView.这个类就是隐藏在 UIImageView+WebCache 背后, 用于处理异步下载和图片缓存的类, 当然你也可以直接使用 SDWebImageManager 的上述方法 downloadImageWithURL:options:progress:completed: 来直接下载图片.可以看到, 这个类的主要作用就是为 UIImageView+WebCache 和 SDWebImageDownloader, SDImageCache 之间构建一个桥梁, 使它们能够更好的协同工作, 我们在这里分析这个核心方法的源代码, 它是如何协调异步下载和图片缓存的.// SDWebImageManager// downloadImageWithURL:options:progress:completed: #6if ([url isKindOfClass:NSString.class]) { url = [NSURL URLWithString:(NSString *)url];}if (![url isKindOfClass:NSURL.class]) { url = nil;}这块代码的功能是确定 url 是否被正确传入, 如果传入参数的是 NSString 类型就会被转换为 NSURL. 如果转换失败, 那么 url 会被赋值为空, 这个下载的操作就会出错.SDWebImageCombinedOperation当 url 被正确传入之后, 会实例一个非常奇怪的 “operation”, 它其实是一个遵循 SDWebImageOperation 协议的 NSObject 的子类. 而这个协议也非常的简单:@protocol SDWebImageOperation <NSObject>- (void)cancel;@end这里仅仅是将这个 SDWebImageOperation 类包装成一个看着像 NSOperation 其实并不是 NSOperation 的类, 而这个类唯一与 NSOperation 的相同之处就是它们都可以响应 cancel 方法. (不知道这句看似像绕口令的话, 你看懂没有, 如果没看懂..请多读几遍).而调用这个类的存在实际是为了使代码更加的简洁, 因为调用这个类的 cancel 方法, 会使得它持有的两个 operation 都被 cancel.// SDWebImageCombinedOperation// cancel #1- (void)cancel { self.cancelled = YES; if (self.cacheOperation) { [self.cacheOperation cancel]; self.cacheOperation = nil; } if (self.cancelBlock) { self.cancelBlock(); _cancelBlock = nil; }}而这个类, 应该是为了实现更简洁的 cancel 操作而设计出来的.既然我们获取了 url, 再通过 url 获取对应的 keyNSString *key = [self cacheKeyForURL:url];下一步是使用 key 在缓存中查找以前是否下载过相同的图片.operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { … }]; 这里调用 SDImageCache 的实例方法 queryDiskCacheForKey:done: 来尝试在缓存中获取图片的数据. 而这个方法返回的就是货真价实的 NSOperation.如果我们在缓存中查找到了对应的图片, 那么我们直接调用 completedBlock 回调块结束这一次的图片下载操作.// SDWebImageManager// downloadImageWithURL:options:progress:completed: #47dispatch_main_sync_safe(^{ completedBlock(image, nil, cacheType, YES, url);});如果我们没有找到图片, 那么就会调用 SDWebImageDownloader 的实例方法:id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { … }]; 如果这个方法返回了正确的 downloadedImage, 那么我们就会在全局的缓存中存储这个图片的数据:[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; 并调用 completedBlock 对 UIImageView 或者 UIButton 添加图片, 或者进行其它的操作.最后, 我们将这个 subOperation 的 cancel 操作添加到 operation.cancelBlock 中. 方便操作的取消.operation.cancelBlock = ^{ [subOperation cancel]; }SDWebImageCacheSDWebImageCache.h 这个类在源代码中有这样的注释:SDImageCache maintains a memory cache and an optional disk cache.它维护了一个内存缓存和一个可选的磁盘缓存, 我们先来看一下在上一阶段中没有解读的两个方法, 首先是:- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;这个方法的主要功能是异步的查询图片缓存. 因为图片的缓存可能在两个地方, 而该方法首先会在内存中查找是否有图片的缓存.// SDWebImageCache// queryDiskCacheForKey:done: #9UIImage *image = [self imageFromMemoryCacheForKey:key];这个 imageFromMemoryCacheForKey 方法会在 SDWebImageCache 维护的缓存 memCache 中查找是否有对应的数据, 而 memCache 就是一个 NSCache.如果在内存中并没有找到图片的缓存的话, 就需要在磁盘中寻找了, 这个就比较麻烦了..在这里会调用一个方法 diskImageForKey 这个方法的具体实现我在这里就不介绍了, 涉及到很多底层 Core Foundation 框架的知识, 不过这里文件名字的存储使用 MD5 处理过后的文件名.// SDImageCache// cachedFileNameForKey: #6CC_MD5(str, (CC_LONG)strlen(str), r);对于其它的实现细节也就不多说了…如果在磁盘中查找到对应的图片, 我们会将它复制到内存中, 以便下次的使用.// SDImageCache// queryDiskCacheForKey:done: #24UIImage *diskImage = [self diskImageForKey:key];if (diskImage) { CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale; [self.memCache setObject:diskImage forKey:key cost:cost];}这些就是 SDImageCache 的核心内容了, 而接下来将介绍如果缓存没有命中, 图片是如何被下载的.SDWebImageDownloader按照之前的惯例, 我们先来看一下 SDWebImageDownloader.h 中对这个类的描述.Asynchronous downloader dedicated and optimized for image loading.专用的并且优化的图片异步下载器.这个类的核心功能就是下载图片, 而核心方法就是上面提到的:- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;回调这个方法直接调用了另一个关键的方法:- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback它为这个下载的操作添加回调的块, 在下载进行时, 或者在下载结束时执行一些操作, 先来阅读一下这个方法的源代码:// SDWebImageDownloader// addProgressCallback:andCompletedBlock:forURL:createCallback: #10BOOL first = NO;if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES;}// Handle single download of simultaneous download request for the same URLNSMutableArray *callbacksForURL = self.URLCallbacks[url];NSMutableDictionary *callbacks = [NSMutableDictionary new];if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];[callbacksForURL addObject:callbacks];self.URLCallbacks[url] = callbacksForURL;if (first) { createCallback();}方法会先查看这个 url 是否有对应的 callback, 使用的是 downloader 持有的一个字典 URLCallbacks.如果是第一次添加回调的话, 就会执行 first = YES, 这个赋值非常的关键, 因为 first 不为 YES 那么 HTTP 请求就不会被初始化, 图片也无法被获取.然后, 在这个方法中会重新修正在 URLCallbacks 中存储的回调块.NSMutableArray *callbacksForURL = self.URLCallbacks[url];NSMutableDictionary *callbacks = [NSMutableDictionary new];if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];[callbacksForURL addObject:callbacks];self.URLCallbacks[url] = callbacksForURL;如果是第一次添加回调块, 那么就会直接运行这个 createCallback 这个 block, 而这个 block, 就是我们在前一个方法 downloadImageWithURL:options:progress:completed: 中传入的回调块.// SDWebImageDownloader// downloadImageWithURL:options:progress:completed: #4[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{ … }];我们下面来分析这个传入的无参数的代码. 首先这段代码初始化了一个 NSMutableURLRequest:// SDWebImageDownloader// downloadImageWithURL:options:progress:completed: #11NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:… timeoutInterval:timeoutInterval];这个 request 就用于在之后发送 HTTP 请求.在初始化了这个 request 之后, 又初始化了一个 SDWebImageDownloaderOperation 的实例, 这个实例, 就是用于请求网络资源的操作. 它是一个 NSOperation 的子类,// SDWebImageDownloader// downloadImageWithURL:options:progress:completed: #20operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request options:options progress:… completed:… cancelled:…}]; 但是在初始化之后, 这个操作并不会开始(NSOperation 实例,只有在调用 start 方法或者加入 NSOperationQueue 才会执行), 我们需要将这个操作加入到一个 NSOperationQueue 中.// SDWebImageDownloader// downloadImageWithURL:option:progress:completed: #59[wself.downloadQueue addOperation:operation];只有将它加入到这个下载队列中, 这个操作才会执行.SDWebImageDownloaderOperation这个类就是处理 HTTP 请求, URL 连接的类, 当这个类的实例被加入队列之后, start 方法就会被调用, 而 start 方法首先就会产生一个 NSURLConnection.// SDWebImageDownloaderOperation// start #1@synchronized (self) { if (self.isCancelled) { self.finished = YES; [self reset]; return; } self.executing = YES; self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; self.thread = [NSThread currentThread];}而接下来这个 connection 就会开始运行:// SDWebImageDownloaderOperation// start #29[self.connection start];它会发出一个 SDWebImageDownloadStartNotification 通知// SDWebImageDownloaderOperation// start #35[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];代理在 start 方法调用之后, 就是 NSURLConnectionDataDelegate中代理方法的调用.- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;在这三个代理方法中的前两个会不停回调 progressBlock 来提示下载的进度.而最后一个代理方法会在图片下载完成之后调用 completionBlock 来完成最后 UIImageView.image 的更新.而这里调用的 progressBlock completionBlock cancelBlock 都是在之前存储在 URLCallbacks 字典中的.到目前为止, 我们就基本解析了 SDWebImage 中[self.imageView sd_setImageWithURL:[NSURL URLWithString:@“url”] placeholderImage:[UIImage imageNamed:@“placeholder.png”]];这个方法执行的全部过程了.总结SDWebImage 的图片加载过程其实很符合我们的直觉:查看缓存缓存命中 * 返回图片更新 UIImageView缓存未命中 * 异步下载图片加入缓存更新 UIImageView ...
之前在上家公司的时候做过一些爬虫的工作,也帮助爬虫工程师解决过一些问题。然后我写过一些文章发布到网上,之后有一些人就找我做一些爬虫的外包,内容大概是爬取小红书的用户数据和商品数据,但是我没做。我觉得对于国内的大数据公司没几家是有真正的大数据量,而是通过爬虫工程师团队不断的去各地爬取数据,因此不要以为我们的数据没价值,对于内容型的公司来说,数据是可信竞争力。那么我接下来想说的就是网络和数据的安全性问题。对于内容型的公司,数据的安全性很重要。对于内容公司来说,数据的重要性不言而喻。比如你一个做在线教育的平台,题目的数据很重要吧,但是被别人通过爬虫技术全部爬走了?如果核心竞争力都被拿走了,那就是凉凉。再比说有个独立开发者想抄袭你的产品,通过抓包和爬虫手段将你核心的数据拿走,然后短期内做个网站和 App,短期内成为你的劲敌。背景目前通过 App 中的 网页分析后,我们的数据安全性做的较差,有以下几个点存在问题:网站的数据通过最早期的前后端分离来实现。稍微学过 Web 前端的工程师都可以通过神器 Chrome 分析网站,进而爬取需要的数据。打开 「Network」就可以看到网站的所有网络请求了,哎呀,不小心我看到了什么?没错就是网站的接口信息都可以看到了。比如 “detail.json?itemId=141529859”。或者你的网站接口有些特殊的判断处理,将一些信息存储到 sessionStorage、cookie、localStorage 里面,有点前端经验的爬虫工程师心想”嘿嘿嘿,这不是在裸奔数据么“。或者有些参数是通过 JavaScript 临时通过函数生成的。问题不大,工程师也可以对网页元素进行查找,找到关键的 id、或者 css 类名,然后在 “Search“ 可以进行查找,找到对应的代码 JS 代码,点击查看代码,如果是早期前端开发模式那么代码就是裸奔的,跟开发者在自己的 IDE 里面看到的内容一样,有经验的爬虫就可以拿这个做事情,因此安全性问题亟待解决。想知道 Chrome 更多的调试使用技巧,看看这篇文章App 的数据即使采用了 HTTPS,但是对于专业的抓包工具也是可以直接拿到数据的,因此 App 的安全问题也可以做一些提高,具体的策略下文会讲到。想知道 Charles 的更多使用技巧,可以看看这篇文章爬虫手段目前爬虫技术都是从渲染好的 html 页面直接找到感兴趣的节点,然后获取对应的文本有些网站安全性做的好,比如列表页可能好获取,但是详情页就需要从列表页点击对应的 item,将 itemId 通过 form 表单提交,服务端生成对应的参数,然后重定向到详情页(重定向过来的地址后才带有详情页的参数 detailID),这个步骤就可以拦截掉一部分的爬虫开发者解决方案制定出Web 端反爬技术方案本人从这2个角度(网页所见非所得、查接口请求没用)出发,制定了下面的反爬方案。使用HTTPS 协议单位时间内限制掉请求次数过多,则封锁该账号前端技术限制 (接下来是核心技术)# 比如需要正确显示的数据为“19950220”1. 先按照自己需求利用相应的规则(数字乱序映射,比如正常的0对应还是0,但是乱序就是 0 <-> 1,1 <-> 9,3 <-> 8,…)制作自定义字体(ttf)2. 根据上面的乱序映射规律,求得到需要返回的数据 19950220 -> 177302203. 对于第一步得到的字符串,依次遍历每个字符,将每个字符根据按照线性变换(y=kx+b)。线性方程的系数和常数项是根据当前的日期计算得到的。比如当前的日期为“2018-07-24”,那么线性变换的 k 为 7,b 为 24。4. 然后将变换后的每个字符串用“3.1415926”拼接返回给接口调用者。(为什么是3.1415926,因为对数字伪造反爬,所以拼接的文本肯定是数字的话不太会引起研究者的注意,但是数字长度太短会误伤正常的数据,所以用所熟悉的 )1773 -> “1*7+24” + “3.1415926” + “7*7+24” + “3.1415926” + “7*7+24” + “3.1415926” + “3*7+24” -> 313.1415926733.1415926733.14159264502 -> "0*7+24" + "3.1415926" + "2*7+24" -> 243.14159263820 -> "2*7+24" + "3.1415926" + "0*7+24" -> 383.141592624# 前端拿到数据后再解密,解密后根据自定义的字体 Render 页面1. 先将拿到的字符串按照“3.1415926”拆分为数组2. 对数组的每1个数据,按照“线性变换”(y=kx+b,k和b同样按照当前的日期求解得到),逆向求解到原本的值。3. 将步骤2的的到的数据依次拼接,再根据 ttf 文件 Render 页面上。后端需要根据上一步设计的协议将数据进行加密处理下面以 Node.js 为例讲解后端需要做的事情首先后端设置接口路由获取路由后面的参数根据业务需要根据 SQL 语句生成对应的数据。如果是数字部分,则需要按照上面约定的方法加以转换。将生成数据转换成 JSON 返回给调用者// jsonvar JoinOparatorSymbol = “3.1415926”;function encode(rawData, ruleType) { if (!isNotEmptyStr(rawData)) { return “”; } var date = new Date(); var year = date.getFullYear(); var month = date.getMonth() + 1; var day = date.getDate(); var encodeData = “”; for (var index = 0; index < rawData.length; index++) { var datacomponent = rawData[index]; if (!isNaN(datacomponent)) { if (ruleType < 3) { var currentNumber = rawDataMap(String(datacomponent), ruleType); encodeData += (currentNumber * month + day) + JoinOparatorSymbol; } else if (ruleType == 4) { encodeData += rawDataMap(String(datacomponent), ruleType); } else { encodeData += rawDataMap(String(datacomponent), ruleType) + JoinOparatorSymbol; } } else if (ruleType == 4) { encodeData += rawDataMap(String(datacomponent), ruleType); } } if (encodeData.length >= JoinOparatorSymbol.length) { var lastTwoString = encodeData.substring(encodeData.length - JoinOparatorSymbol.length, encodeData.length); if (lastTwoString == JoinOparatorSymbol) { encodeData = encodeData.substring(0, encodeData.length - JoinOparatorSymbol.length); } }//字体映射处理function rawDataMap(rawData, ruleType) { if (!isNotEmptyStr(rawData) || !isNotEmptyStr(ruleType)) { return; } var mapData; var rawNumber = parseInt(rawData); var ruleTypeNumber = parseInt(ruleType); if (!isNaN(rawData)) { lastNumberCategory = ruleTypeNumber; //字体文件1下的数据加密规则 if (ruleTypeNumber == 1) { if (rawNumber == 1) { mapData = 1; } else if (rawNumber == 2) { mapData = 2; } else if (rawNumber == 3) { mapData = 4; } else if (rawNumber == 4) { mapData = 5; } else if (rawNumber == 5) { mapData = 3; } else if (rawNumber == 6) { mapData = 8; } else if (rawNumber == 7) { mapData = 6; } else if (rawNumber == 8) { mapData = 9; } else if (rawNumber == 9) { mapData = 7; } else if (rawNumber == 0) { mapData = 0; } } //字体文件2下的数据加密规则 else if (ruleTypeNumber == 0) { if (rawNumber == 1) { mapData = 4; } else if (rawNumber == 2) { mapData = 2; } else if (rawNumber == 3) { mapData = 3; } else if (rawNumber == 4) { mapData = 1; } else if (rawNumber == 5) { mapData = 8; } else if (rawNumber == 6) { mapData = 5; } else if (rawNumber == 7) { mapData = 6; } else if (rawNumber == 8) { mapData = 7; } else if (rawNumber == 9) { mapData = 9; } else if (rawNumber == 0) { mapData = 0; } } //字体文件3下的数据加密规则 else if (ruleTypeNumber == 2) { if (rawNumber == 1) { mapData = 6; } else if (rawNumber == 2) { mapData = 2; } else if (rawNumber == 3) { mapData = 1; } else if (rawNumber == 4) { mapData = 3; } else if (rawNumber == 5) { mapData = 4; } else if (rawNumber == 6) { mapData = 8; } else if (rawNumber == 7) { mapData = 3; } else if (rawNumber == 8) { mapData = 7; } else if (rawNumber == 9) { mapData = 9; } else if (rawNumber == 0) { mapData = 0; } } else if (ruleTypeNumber == 3) { if (rawNumber == 1) { mapData = “”; } else if (rawNumber == 2) { mapData = “”; } else if (rawNumber == 3) { mapData = “”; } else if (rawNumber == 4) { mapData = “”; } else if (rawNumber == 5) { mapData = “”; } else if (rawNumber == 6) { mapData = “”; } else if (rawNumber == 7) { mapData = “”; } else if (rawNumber == 8) { mapData = “”; } else if (rawNumber == 9) { mapData = “”; } else if (rawNumber == 0) { mapData = “”; } } else{ mapData = rawNumber; } } else if (ruleTypeNumber == 4) { var sources = [“年”, “万”, “业”, “人”, “信”, “元”, “千”, “司”, “州”, “资”, “造”, “钱”]; //判断字符串为汉字 if (/^[\u4e00-\u9fa5]$/.test(rawData)) { if (sources.indexOf(rawData) > -1) { var currentChineseHexcod = rawData.charCodeAt(0).toString(16); var lastCompoent; var mapComponetnt; var numbers = [“0”, “1”, “2”, “3”, “4”, “5”, “6”, “7”, “8”, “9”]; var characters = [“a”, “b”, “c”, “d”, “e”, “f”, “g”, “h”, “h”, “i”, “j”, “k”, “l”, “m”, “n”, “o”, “p”, “q”, “r”, “s”, “t”, “u”, “v”, “w”, “x”, “y”, “z”]; if (currentChineseHexcod.length == 4) { lastCompoent = currentChineseHexcod.substr(3, 1); var locationInComponents = 0; if (/[0-9]/.test(lastCompoent)) { locationInComponents = numbers.indexOf(lastCompoent); mapComponetnt = numbers[(locationInComponents + 1) % 10]; } else if (/[a-z]/.test(lastCompoent)) { locationInComponents = characters.indexOf(lastCompoent); mapComponetnt = characters[(locationInComponents + 1) % 26]; } mapData = “&#x” + currentChineseHexcod.substr(0, 3) + mapComponetnt + “;”; } } else { mapData = rawData; } } else if (/[0-9]/.test(rawData)) { mapData = rawDataMap(rawData, 2); } else { mapData = rawData; } } return mapData;}//apimodule.exports = { “GET /api/products”: async (ctx, next) => { ctx.response.type = “application/json”; ctx.response.body = { products: products }; }, “GET /api/solution1”: async (ctx, next) => { try { var data = fs.readFileSync(pathname, “utf-8”); ruleJson = JSON.parse(data); rule = ruleJson.data.rule; } catch (error) { console.log(“fail: " + error); } var data = { code: 200, message: “success”, data: { name: “@杭城小刘”, year: LBPEncode(“1995”, rule), month: LBPEncode(“02”, rule), day: LBPEncode(“20”, rule), analysis : rule } } ctx.set(“Access-Control-Allow-Origin”, “”); ctx.response.type = “application/json”; ctx.response.body = data; }, “GET /api/solution2”: async (ctx, next) => { try { var data = fs.readFileSync(pathname, “utf-8”); ruleJson = JSON.parse(data); rule = ruleJson.data.rule; } catch (error) { console.log(“fail: " + error); } var data = { code: 200, message: “success”, data: { name: LBPEncode(“建造师”,rule), birthday: LBPEncode(“1995年02月20日”,rule), company: LBPEncode(“中天公司”,rule), address: LBPEncode(“浙江省杭州市拱墅区石祥路”,rule), bidprice: LBPEncode(“2万元”,rule), negative: LBPEncode(“2018年办事效率太高、负面基本没有”,rule), title: LBPEncode(“建造师”,rule), honor: LBPEncode(“最佳奖”,rule), analysis : rule } } ctx.set(“Access-Control-Allow-Origin”, “*”); ctx.response.type = “application/json”; ctx.response.body = data; }, “POST /api/products”: async (ctx, next) => { var p = { name: ctx.request.body.name, price: ctx.request.body.price }; products.push(p); ctx.response.type = “application/json”; ctx.response.body = p; }};//路由const fs = require(“fs”);function addMapping(router, mapping){ for(var url in mapping){ if (url.startsWith(“GET”)) { var path = url.substring(4); router.get(path,mapping[url]); console.log(Register URL mapping: GET: ${path}); }else if (url.startsWith(‘POST ‘)) { var path = url.substring(5); router.post(path, mapping[url]); console.log(Register URL mapping: POST ${path}); } else if (url.startsWith(‘PUT ‘)) { var path = url.substring(4); router.put(path, mapping[url]); console.log(Register URL mapping: PUT ${path}); } else if (url.startsWith(‘DELETE ‘)) { var path = url.substring(7); router.del(path, mapping[url]); console.log(Register URL mapping: DELETE ${path}); } else { console.log(Invalid URL: ${url}); } }}function addControllers(router, dir){ fs.readdirSync(__dirname + “/” + dir).filter( (f) => { return f.endsWith(".js”); }).forEach( (f) => { console.log(Process controllers:${f}...); let mapping = require(__dirname + “/” + dir + “/” + f); addMapping(router,mapping); });}module.exports = function(dir){ let controllers = dir || “controller”; let router = require(“koa-router”)(); addControllers(router,controllers); return router.routes();};前端根据服务端返回的数据逆向解密$("#year”).html(getRawData(data.year,log));// util.jsvar JoinOparatorSymbol = “3.1415926”;function isNotEmptyStr($str) { if (String($str) == "” || $str == undefined || $str == null || $str == “null”) { return false; } return true;}function getRawData($json,analisys) { $json = $json.toString(); if (!isNotEmptyStr($json)) { return; } var date= new Date(); var year = date.getFullYear(); var month = date.getMonth() + 1; var day = date.getDate(); var datacomponents = $json.split(JoinOparatorSymbol); var orginalMessage = “”; for(var index = 0;index < datacomponents.length;index++){ var datacomponent = datacomponents[index]; if (!isNaN(datacomponent) && analisys < 3){ var currentNumber = parseInt(datacomponent); orginalMessage += (currentNumber - day)/month; } else if(analisys == 3){ orginalMessage += datacomponent; } else{ //其他情况待续,本 Demo 根据本人在研究反爬方面的技术并实践后持续更新 } } return orginalMessage;}比如后端返回的是323.14743.14743.1446,根据我们约定的算法,可以的到结果为1773根据 ttf 文件 Render 页面上面计算的到的1773,然后根据ttf文件,页面看到的就是1995然后为了防止爬虫人员查看 JS 研究问题,所以对 JS 的文件进行了加密处理。如果你的技术栈是 Vue 、React 等,webpack 为你提供了 JS 加密的插件,也很方便处理JS混淆工具个人觉得这种方式还不是很安全。于是想到了各种方案的组合拳。比如反爬升级版个人觉得如果一个前端经验丰富的爬虫开发者来说,上面的方案可能还是会存在被破解的可能,所以在之前的基础上做了升级版本组合拳1: 字体文件不要固定,虽然请求的链接是同一个,但是根据当前的时间戳的最后一个数字取模,比如 Demo 中对4取模,有4种值 0、1、2、3。这4种值对应不同的字体文件,所以当爬虫绞尽脑汁爬到1种情况下的字体时,没想到再次请求,字体文件的规则变掉了 ????组合拳2: 前面的规则是字体问题乱序,但是只是数字匹配打乱掉。比如 1 -> 4, 5 -> 8。接下来的套路就是每个数字对应一个 unicode 码 ,然后制作自己需要的字体,可以是 .ttf、.woff 等等。这几种组合拳打下来。对于一般的爬虫就放弃了。反爬手段再升级上面说的方法主要是针对数字做的反爬手段,如果要对汉字进行反爬怎么办?接下来提供几种方案方案1: 对于你站点频率最高的词云,做一个汉字映射,也就是自定义字体文件,步骤跟数字一样。先将常用的汉字生成对应的 ttf 文件;根据下面提供的链接,将 ttf 文件转换为 svg 文件,然后在下面的“字体映射”链接点进去的网站上面选择前面生成的 svg 文件,将svg文件里面的每个汉字做个映射,也就是将汉字专为 unicode 码(注意这里的 unicode 码不要去在线直接生成,因为直接生成的东西也就是有规律的。我给的做法是先用网站生成,然后将得到的结果做个简单的变化,比如将“e342”转换为 “e231”);然后接口返回的数据按照我们的这个字体文件的规则反过去映射出来。方案2: 将网站的重要字体,将 html 部分生成图片,这样子爬虫要识别到需要的内容成本就很高了,需要用到 OCR。效率也很低。所以可以拦截掉一部分的爬虫方案3: 看到携程的技术分享“反爬的最高境界就是 Canvas 的指纹,原理是不同的机器不同的硬件对于 Canvas 画出的图总是存在像素级别的误差,因此我们判断当对于访问来说大量的 canvas 的指纹一致的话,则认为是爬虫,则可以封掉它”。本人将方案1实现到 Demo 中了。关键步骤先根据你们的产品找到常用的关键词,生成词云根据词云,将每个字生成对应的 unicode 码将词云包括的汉字做成一个字体库将字体库 .ttf 做成 svg 格式,然后上传到 icomoon 制作自定义的字体,但是有规则,比如 “年” 对应的 unicode 码是 “u5e74” ,但是我们需要做一个 恺撒加密 ,比如我们设置 偏移量 为1,那么经过恺撒加密 “年”对应的 unicode 码是“u5e75” 。利用这种规则制作我们需要的字体库在每次调用接口的时候服务端做的事情是:服务端封装某个方法,将数据经过方法判断是不是在词云中,如果是词云中的字符,利用规则(找到汉字对应的 unicode 码,再根据凯撒加密,设置对应的偏移量,Demo 中为1,将每个汉字加密处理)加密处理后返回数据客户端做的事情:先引入我们前面制作好的汉字字体库调用接口拿到数据,显示到对应的 Dom 节点上如果是汉字文本,我们将对应节点的 css 类设置成汉字类,该类对应的 font-family 是我们上面引入的汉字字体库//style.css@font-face { font-family: “NumberFont”; src: url(‘http://127.0.0.1:8080/Util/analysis’); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}@font-face { font-family: “CharacterFont”; src: url(‘http://127.0.0.1:8080/Util/map’); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}h2 { font-family: “NumberFont”;}h3,a{ font-family: “CharacterFont”;}传送门字体制作的步骤、ttf转svg、字体映射规则实现的效果页面上看到的数据跟审查元素看到的结果不一致去查看接口数据跟审核元素和界面看到的三者不一致页面每次刷新之前得出的结果更不一致对于数字和汉字的处理手段都不一致这几种组合拳打下来。对于一般的爬虫就放弃了。前面的 ttf 转 svg 网站当 ttf 文件太大会限制转换,让你购买,下面贴出个新的链接。ttf转svgDemo 地址运行步骤//客户端。先查看本机 ip 在 Demo/Spider-develop/Solution/Solution1.js 和 Demo/Spider-develop/Solution/Solution2.js 里面将接口地址修改为本机 ip$ cd Demo$ lsREST Spider-release file-Server.jsSpider-develop Util rule.json$ node file-Server.js Server is runnig at http://127.0.0.1:8080///服务端 先安装依赖$ cd REST/$ npm install$ node app.js App 端安全的解决方案目前 App 的网络通信基本都是用 HTTPS 的服务,但是随便一个抓包工具都是可以看到 HTTPS 接口的详细数据,为了做到防止抓包和无法模拟接口的情况,我们采取以下措施:中间人盗用数据,我们可以采取 HTTPS 证书的双向认证,这样子实现的效果就是中间人在开启抓包软件分析 App 的网络请求的时候,网络会自动断掉,无法查看分析请求的情况对于防止用户模仿我们的请求再次发起请求,我们可以采用 「防重放策略」,用户再也无法模仿我们的请求,再次去获取数据了。对于 App 内的 H5 资源,反爬虫方案可以采用上面的解决方案,H5 内部的网络请求可以通过 Hybrid 层让 Native 的能力去完成网络请求,完成之后将数据回调给 JS。这么做的目的是往往我们的 Native 层有完善的账号体系和网络层以及良好的安全策略、鉴权体系等等。后期会讨论 App 安全性的更深层次玩法,比如从逆向的角度出发如何保护 App 的安全性。提前给出一篇逆向安全方面的文章关于 Hybrid 的更多内容,可以看看这篇文章 Awesome Hybrid比如 JS 需要发起一个网络请求,那么按照上面将网络请求让 Native 去完成,然后回调给 JSJS 端代码var requestObject = { url: arg.Api + “SearchInfo/getLawsInfo”, params: requestparams, Hybrid_Request_Method: 0};requestHybrid({ tagname: ‘NativeRequest’, param: requestObject, encryption: 1, callback: function (data) { renderUI(data); }})Native 代码(iOS为例)[self.bridge registerHandler:@“NativeRequest” handler:^(id data, WVJBResponseCallback responseCallback) { NSAssert([data isKindOfClass:[NSDictionary class]], @“H5 端不按套路”); if ([data isKindOfClass:[NSDictionary class]]) { NSDictionary *dict = (NSDictionary *)data; RequestModel *requestModel = [RequestModel yy_modelWithJSON:dict]; NSAssert( (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Post) || (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Get ), @“H5 端不按套路”); [HybridRequest requestWithNative:requestModel hybridRequestSuccess:^(id responseObject) { NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableLeaves error:nil]; responseCallback([self convertToJsonData:@{@“success”:@“1”,@“data”:json}]); } hybridRequestfail:^{ LBPLog(@“H5 call Native`s request failed”); responseCallback([self convertToJsonData:@{@“success”:@“0”,@“data”:@""}]); }]; }}];以上是第一阶段的安全性总结,后期应该会更新(App逆向、防重放、服务端等)。 ...
苹果APP的审核是一道大难关,遇见被拒是很正常的事情,如果被拒就根据反馈问题,和下面提供的思路去寻求解决方案。苹果审核大体分为三部分,预审、机审和人工审核。ipa包上传后首先进入的是预审,会被扫描API等,预审通过后会在https://itunesconnect.apple.c…,然后才可以提交审核(如果没通过,会有反馈邮件,查看原因,很多上传ipa成功了,但后台没看到ipa构建版本,就是预审都没过。)在 提交审核开始显示是(等待审核)这个阶段一般是机审,机审主要是对代码进行机器审核,排查APP是否重复应用,“2.1苹果狗年大礼包”事件就更多地依赖机器自动审核,减少人工成本;通过后会显示为(审核)这个阶段,即人工审核阶段,这个阶段主要看的是App的元数据,例如APP封面、功能、体验等等,注重用户体验,到了这步很快就会有结果了。目前机审机制越来越完善了,而且也越来越受重视,相比前几个月,近期的苹果审核时间逐渐缩短,平均审核时间为23.9 小时。2.1 App 完成度主要有应用出现崩溃、加载失败等非常明显的Bug、应用不支持 IPv6网络下使用、测试账号、隐藏开关等。解决方法:提前测试产品是否有bug、在IPV6网络下是否能使用等,根据反馈邮件,一个个审查自身产品信息是否符合,适当情况下可以发送截图视频给苹果官方以证明自己的清白。2.3 准确的元数据主要是应用标题、描述、截图等与应用功能严重不符。如用安卓手机截图,浏览器截图!解决方法:重新更换截图,保证整个APP功能、流程看起来是一致的。去除隐藏功能模块代码或将需要隐藏功能的代码及定向跳转链接网址做混淆处理,适当增加逻辑复杂度。3.2.1 可接受的商业模式主要是没有资质。解决方法:最佳方案是拿到资质,如果实在没有资质,建议大家尽可能多的把自己公司合规的证据资料发给苹果,而套壳、换新账号碰运气上架等操作,不得已的话可以尝试。5.1.1 数据收集和存储主要是App 强制用户注册,且基于不需要用户信息的功能之上、暗中采集/共享用户的个人信息。解决方法:先与用户协商,让用户同意后注册,有“强登陆”功能的一定要修改为提示登陆的版本。4.3 重复 App主要针对的是重复App,意思就是你的App跟别人上架的APP功能或者代码很相似,通常就是苹果认为是马甲包。解决办法:可通过修改名字、icon、主色调、代码等解决,并且注意相同的APP包提交至少间隔一天以上。4.2 最低功能要求主要问题在于苹果认为部分开发者上传的App功能不够,或者没有自己的核心功能,比如直接打包一个网页上架的很容易触发这个问题。解决办法:可以添加一些功能丰富产品(导航栏,下拉刷新,推送通知等功能),如果觉得功能已经全了,还没有通过审核,可以向苹果解释产品解决的用户需求,以及具体功能的展现。3.1.1 购买项目主要是接入第三方支付,支付宝、微信等。解决方法:老老实实地走苹果支付的支付方式,用内购。如果隐藏虚拟产品或者通过后更改支付方式,都是有一定风险的。2.5 软件要求主要是产品加入违规代码解决方法:很可能是三方库中含有SDK,可以更新所有三方库,或者反编译提交的ipa,检查文档中是否有违规字符串,有的话删掉。5.1.5 定位服务主要是 App 未得到允许,与第三方共享收集的用户数据,且并未说明使用目的等,例:位置、账号……解决方法:如果要采取用户数据信息,需要给予用户提示,并得到用户的允许,或设置为可选,并且明确告知苹果采集用户数据信息的使用目的。总的来说就是要弹出提示说明使用这个权限做什么用,写清楚。5.2 知识产权主要是未经授权,使用受版权保护的第三方材料、App不得与苹果现有产品类似等。解决方法:确保 app 只包含由您创建或拥有使用许可的内容,提交产品时使用受版权保护的第三方的书面证据或者将产品中包含的未经第三方授权的部分隐藏。审核还会遇到各种各样的问题,根据反馈来进行相应的修改。iOS APP上架被拒重新提交审核教程
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由鹅厂新鲜事儿发表于云+社区专栏作者:oceanlong | 腾讯 移动客户端开发工程师前言UI布局是整个前端体系里不可或缺的一环。代码的布局是设计语言与用户视觉感受沟通的桥梁,不论它看起来多么简单或是琐碎,但不得不承认,绝大部分软件开发的问题,都是界面问题。那么,如何高效的完成UI开发,也是软件行业一直在克服的问题。所以,软件界面开发的核心点即是:如何减少UI设计稿的建模难度和减少建模转化到代码的实现难度最初iOS提供了平面直角坐标系的方式,来解决布局问题,即所谓的手动布局。平面直角坐标系确实是一套完备在理论,这在数学上已经验证过了,只要我们的屏幕还是平面,它就肯定是有效的。但有效不一定高效,我们在日常的生活中,很少会用平面直角坐标系来向人描述位置关系。更多的是依靠相对位置。所幸,iOS为我们提供自动布局的方法,来解决这一困境。自动布局的基本理念其实说到本质,它和手动布局是一样的。对一个控件放在哪里,我们依然只关心它的(x, y, width, height)。但手动布局的方式是,一次性计算出这四个值,然后设置进去,完成布局。但当父控件或屏幕发生变化时,子控件的计算就要重新来过,非常麻烦。因此,在自动布局中,我们不再关心(x, y, width, height)的具体值,我们只关心(x, y, width, height)四个量对应的约束。约束那么何为约束呢?obj1.property1 =(obj2.property2 * multiplier)+ constant value子控件的某一个量一定与另一个控件的某一个量呈线性关系,这就是约束。那么,给(x, y, width, height)四个量,分别给一个约束,就可以确定一个控件的最终位置。 //创建左边约束 NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20]; [self.view addConstraint:leftLc];这一段代码即是:控件(blueView)的 x = rootView的x * 1.0 + 20这里一定要注意,这样的一条约束,涉及了子控件和父控件,所以这条约束一定要添加到父控件中。添加约束的规则:如果两个控件是父子控件,则添加到父控件中。如果两个控件不是父子控件,则添加到层级最近的共同父控件中。示例 //关闭Autoresizing blueView.translatesAutoresizingMaskIntoConstraints = NO; //创建左边约束 NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20]; [self.view addConstraint:leftLc]; //创建右边约束 NSLayoutConstraint *rightLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeRight multiplier:1.0 constant:-20]; [self.view addConstraint:rightLc]; //创建底部约束 NSLayoutConstraint *bottomLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-20]; [self.view addConstraint:bottomLc]; //创建高度约束 NSLayoutConstraint *heightLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:50]; [blueView addConstraint: heightLc];我们注意到,自动布局其实工作分两步:创建视图的约束将约束添加到合适的位置约束关系从上面的描述中,已经非常清晰了。那么如何寻找约束添加的合适位置呢?到这里,我们只是解决了如何减少UI设计稿的建模难度的问题,显然,减少建模转化到代码的实现难度这个效果没能达成。关于如何解决减少建模转化到代码的实现难度的问题,开源库上面的代码,我们可以看到,虽然自动布局已经比手动布局优雅不少了,但它依然行数较多。每条约束大约都需要三行代码,面对复杂的页面,这样开发出来,会很难阅读。Masonry则为我们解决了这个问题。Masonry地址引入Masonry我们选择使用Cocoapods的方式。引入比较简单:我们先在工程目录下,创建Podfile文件:2.编辑Podfile其中,‘IosOcDemo’就是我们工程的名字,根据需要,我们自行替换。3.添加依赖完成后,执行指令pod install。CocoaPods就会为我们自动下载并添加依赖。实践这样的一个代码,用手动布局,我们大致的代码应该是这样:-(void)initBottomView{ self.bottomBarView = [[UIView alloc]initWithFrame:CGRectZero]; self.bottomButtons = [[NSMutableArray alloc]init]; _bottomBarView.backgroundColor = [UIColor yellowColor]; [self addSubview:_bottomBarView]; for(int i = 0 ; i < 3 ; i++) { UIButton *button = [[UIButton alloc]initWithFrame:CGRectZero]; button.backgroundColor = [UIColor redColor]; [_bottomButtons addObject:button]; [self addSubview:button]; }}-(void)layoutBottomView{ _bottomBarView.frame = CGRectMake(20, _viewHeight - 200, _viewWidth - 40, 200); for (int i = 0 ; i < 3; i++) { UIButton button = _bottomButtons[i]; CGFloat x = i * (_viewWidth - 40 - 20 * 4) / 3 + 20(i+1) + 20; CGFloat y = _viewHeight - 200; CGFloat width = (_viewWidth - 40 - 20 * 4) / 3; CGFloat height = 200; button.frame = CGRectMake(x, y, width, height); }}我们来看一下,在Masonry的帮助下,我们可以把刚刚的代码写成什么样的: -(void)initBottomView{ _bottomBarView = [[UIView alloc]initWithFrame:CGRectZero]; _bottomBarView.backgroundColor = [UIColor yellowColor]; _bottomBarView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_bottomBarView]; [_bottomBarView mas_makeConstraints:^(MASConstraintMaker make) { make.left.equalTo(self).with.offset(20); make.right.equalTo(self).with.offset(-20); make.height.mas_equalTo(200); make.bottom.equalTo(self); }]; _bottomButtons = [[NSMutableArray alloc]init]; for(int i = 0 ; i < 3 ; i++) { UIButton button = [[UIButton alloc]initWithFrame: CGRectZero]; button.backgroundColor = [UIColor redColor]; button.translatesAutoresizingMaskIntoConstraints = NO; [_bottomButtons addObject:button]; [_bottomBarView addSubview:button]; [button mas_makeConstraints:^(MASConstraintMaker make) { if (i == 0) { make.left.mas_equalTo(20); }else{ UIButton previousButton = _bottomButtons[i-1]; make.left.equalTo(previousButton.mas_right).with.offset(20); } make.top.mas_equalTo(_bottomBarView.mas_top); make.width.equalTo(_bottomBarView.mas_width).with.multipliedBy(1.0f/3).offset(-204/3); make.height.equalTo(_bottomBarView.mas_height); }]; }}我们可以看到在Masonry的封装下,代码变得非常简练易读,需要行数略有增加,但是计算过程减少了,我们能更加关注于多个UIView间的位置关系,这与当前的UI设计语言是契合的。所以Masonry能否让我们更直观地表达UI。源码解读Masonry的封装很有魅力,那么,我们可以简单地来看一下,它是如何封装的。我们再仔细看一下Masonry的API会发现,我们是直接在UIView上进行调用的。也就是说,Masonry对UIView进行了扩展。在View+MASUtilities.h中:#if TARGET_OS_IPHONE || TARGET_OS_TV #import <UIKit/UIKit.h> #define MAS_VIEW UIView #define MAS_VIEW_CONTROLLER UIViewController #define MASEdgeInsets UIEdgeInsets然后在View+MASAdditions.h中,我们看到了Masonry的扩展:#import “MASUtilities.h”#import “MASConstraintMaker.h”#import “MASViewAttribute.h”/ * Provides constraint maker block * and convience methods for creating MASViewAttribute which are view + NSLayoutAttribute pairs /@interface MAS_VIEW (MASAdditions)/ * following properties return a new MASViewAttribute with current view and appropriate NSLayoutAttribute */@property (nonatomic, strong, readonly) MASViewAttribute *mas_left;@property (nonatomic, strong, readonly) MASViewAttribute *mas_top;@property (nonatomic, strong, readonly) MASViewAttribute *mas_right;@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottom;@property (nonatomic, strong, readonly) MASViewAttribute *mas_leading;@property (nonatomic, strong, readonly) MASViewAttribute *mas_trailing;@property (nonatomic, strong, readonly) MASViewAttribute *mas_width;@property (nonatomic, strong, readonly) MASViewAttribute *mas_height;@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerX;@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerY;@property (nonatomic, strong, readonly) MASViewAttribute mas_baseline;@property (nonatomic, strong, readonly) MASViewAttribute (^mas_attribute)(NSLayoutAttribute attr);…/ * Creates a MASConstraintMaker with the callee view. * Any constraints defined are added to the view or the appropriate superview once the block has finished executing * * @param block scope within which you can build up the constraints which you wish to apply to the view. * * @return Array of created MASConstraints */- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;一些,适配的代码,我省略了,先看核心代码。在刚刚的例子中,我们正是调用的mas_makeConstraints方法。- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install];}mas_makeConstraints方法比较简单,只是封装了MASConstraintMaker初始化,设置约束和安装。这里的block就是我们刚刚在外层设置的约束的函数指针。也就是这一串:^(MASConstraintMaker *make) { make.left.equalTo(self.view).with.offset(10); make.right.equalTo(self.view).with.offset(-10); make.height.mas_equalTo(50); make.bottom.equalTo(self.view).with.offset(-10); }由于约束条件的设置比较复杂,我们先来看看初始化和安装。初始化- (id)initWithView:(MAS_VIEW *)view { self = [super init]; if (!self) return nil; self.view = view; self.constraints = NSMutableArray.new; return self;}初始化的代码比较简单,将传入的view放入MASConstraintMaker成员,然后创建MASConstraintMaker的约束容器(NSMutableArray)。安装- (NSArray *)install { if (self.removeExisting) { NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view]; for (MASConstraint *constraint in installedConstraints) { [constraint uninstall]; } } NSArray *constraints = self.constraints.copy; for (MASConstraint *constraint in constraints) { constraint.updateExisting = self.updateExisting; [constraint install]; } [self.constraints removeAllObjects]; return constraints;}安装的代码分为三块:判断是否需要移除已有的约束。如果需要,会遍历已有约束,然后逐个uninstallcopy已有的约束,遍历,并逐一installremove掉所有约束,并将已添加的constraints返回。install的方法,还是继续封装到了Constraint中,我们继续跟进阅读:我们会发现Constraint只是一个接口,Masonry中对于Constraint接口有两个实现,分别是:MASViewConstraint和MASCompositeConstraint。这两个类,分别是单个约束和约束集合。在上面的例子中,我们只是对单个UIView进行约束,所以我们先看MASViewConstraint的代码。以下代码MASViewConstraint进行了一定程度的简化,省略了一些扩展属性,只展示我们的例子中,会执行的代码:- (void)install { if (self.hasBeenInstalled) { return; } … MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item; NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute; MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item; NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute; // alignment attributes must have a secondViewAttribute // therefore we assume that is refering to superview // eg make.left.equalTo(@10) if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) { secondLayoutItem = self.firstViewAttribute.view.superview; secondLayoutAttribute = firstLayoutAttribute; } MASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:firstLayoutItem attribute:firstLayoutAttribute relatedBy:self.layoutRelation toItem:secondLayoutItem attribute:secondLayoutAttribute multiplier:self.layoutMultiplier constant:self.layoutConstant]; layoutConstraint.priority = self.layoutPriority; layoutConstraint.mas_key = self.mas_key; if (self.secondViewAttribute.view) { MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view]; NSAssert(closestCommonSuperview, @“couldn’t find a common superview for %@ and %@”, self.firstViewAttribute.view, self.secondViewAttribute.view); self.installedView = closestCommonSuperview; } else if (self.firstViewAttribute.isSizeAttribute) { self.installedView = self.firstViewAttribute.view; } else { self.installedView = self.firstViewAttribute.view.superview; } MASLayoutConstraint *existingConstraint = nil; … else { [self.installedView addConstraint:layoutConstraint]; self.layoutConstraint = layoutConstraint; [firstLayoutItem.mas_installedConstraints addObject:self]; }}自动布局是一种相对布局,所以,绝大部分情况下,需要两个UIView(约束方与参照方)。在上面的方法中:firstLayoutItem是约束方,secondLayoutItem是参照方firstLayoutAttribute是约束方的属性,secondLayoutAttribute是参照方的属性。MASLayoutConstraint就是NSLayoutConstraint的子类,只是添加了mas_key属性。到这里,我们就与系统提供的API对应上了。 NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20]; [self.view addConstraint:leftLc];再看看我们之前用系统API完成的例子,是不是格外熟悉?那么接下来,我们就是要阅读 make.left.equalTo(self).with.offset(20); make.right.equalTo(self).with.offset(-20); make.height.mas_equalTo(200); make.bottom.equalTo(self);是如何变成firstLayoutItem, secondLayoutItem, firstLayoutAttribute, secondLayoutAttribute和layoutRelation的。约束条件的设置回到前面的:- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install];}我们接下来,就要看block的实现:block其实是一个函数指针。此处真正调用的方法是: make.left.equalTo(self).with.offset(20); make.right.equalTo(self).with.offset(-20); make.height.mas_equalTo(200); make.bottom.equalTo(self);我们挑选其中一个,来看看源码实现:left- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];}- (MASConstraint *)left { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];}- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute]; MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute]; if ([constraint isKindOfClass:MASViewConstraint.class]) { //replace with composite constraint NSArray *children = @[constraint, newConstraint]; MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children]; compositeConstraint.delegate = self; [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint; } if (!constraint) { newConstraint.delegate = self; [self.constraints addObject:newConstraint]; } return newConstraint;}在对单个view添加约束时,constraint为nil。我们直接生成了一个新约束newConstraint。它的firstViewAttribute就是我们传入的NSLayoutAttributeLeftequalTo- (MASConstraint * (^)(id))equalTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationEqual); };}- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { return ^id(id attribute, NSLayoutRelation relation) { if ([attribute isKindOfClass:NSArray.class]) { NSAssert(!self.hasLayoutRelation, @“Redefinition of constraint relation”); NSMutableArray *children = NSMutableArray.new; for (id attr in attribute) { MASViewConstraint *viewConstraint = [self copy]; viewConstraint.layoutRelation = relation; viewConstraint.secondViewAttribute = attr; [children addObject:viewConstraint]; } MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children]; compositeConstraint.delegate = self.delegate; [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint; } else { NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @“Redefinition of constraint relation”); self.layoutRelation = relation; self.secondViewAttribute = attribute; return self; } };}此处,我们依然先看attribute不是NSArray的情况。这里在单个属性的约束中,就比较简单了,将relation和attribue传入MASConstraint对应的成员。在上面介绍install方法时,我们就曾提到过: MASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:firstLayoutItem attribute:firstLayoutAttribute relatedBy:self.layoutRelation toItem:secondLayoutItem attribute:secondLayoutAttribute multiplier:self.layoutMultiplier constant:self.layoutConstant];firstLayoutItem和secondLayoutItem在install方法中已收集完成,此时,经过left和equalTo我们又收集到了:firstViewAttribute、secondViewAttribute和layoutRelation胜利即在眼前。- (MASConstraint * (^)(CGFloat))offset { return ^id(CGFloat offset){ self.offset = offset; return self; };}- (void)setOffset:(CGFloat)offset { self.layoutConstant = offset;}通过OC的set语法,Masonry将offset传入layoutConstant。至此,layoutConstraint就完成了全部的元素收集,可以使用添加约束的方式,只需要解决最后一个问题,约束添加到哪里呢?我们似乎在调用时,并不需要关心这件事情,那说明框架帮我们完成了这个工作。closestCommonSuperview我们在MASViewConstraint中,可以找到这样一段: if (self.secondViewAttribute.view) { MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view]; NSAssert(closestCommonSuperview, @“couldn’t find a common superview for %@ and %@”, self.firstViewAttribute.view, self.secondViewAttribute.view); self.installedView = closestCommonSuperview; } else if (self.firstViewAttribute.isSizeAttribute) { self.installedView = self.firstViewAttribute.view; } else { self.installedView = self.firstViewAttribute.view.superview; }注意到,closetCommonSuperview就是Masonry为我们找到的最近公共父控件。- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view { MAS_VIEW *closestCommonSuperview = nil; MAS_VIEW *secondViewSuperview = view; while (!closestCommonSuperview && secondViewSuperview) { MAS_VIEW *firstViewSuperview = self; while (!closestCommonSuperview && firstViewSuperview) { if (secondViewSuperview == firstViewSuperview) { closestCommonSuperview = secondViewSuperview; } firstViewSuperview = firstViewSuperview.superview; } secondViewSuperview = secondViewSuperview.superview; } return closestCommonSuperview;}实现也比较简单。至此,我们完成了所有准备,就可以开始愉快的自动布局啦。以上就是Masonry对iOS自动布局封装的解读。如有问题,欢迎指正。问答iOS:如何使用自动布局约束?相关阅读走进 MasonryiOS自动布局框架之MasonryiOS学习——布局利器Masonry框架源码深度剖析 【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...