日期: 2021 年 6 月 23 日

iOS App 稳定性指标及监测

前言

一个iOS App的稳定性,主要决定于整体的系统架构设计,同时也不可忽略编程的细节,正所谓“千里之堤,溃于蚁穴”,一旦考虑不周,看似无关紧要的代码片段可能会带来整体软件系统的崩溃。尤其因为苹果限制了热更新机制,App本身的稳定性及容错性就显的更加重要,之前可以通过发布热补丁的方式解决线上代码问题,现在就需要在提交之前对App开发周期内的各个指标进行实时监测,尽量让问题暴漏在开发阶段,然后及时修复,减少线上出问题的几率。针对一个App的开发周期,它的稳定性指标主要有以下几个环节构成,用一个脑图表示如下:

 

稳定性指标

1 开发过程

开发过程中,主要是通过监控内存使用及泄露,CPU使用率,FPS,启动时间等指标,以及常见的UI的主线程监测,NSAssert断言等,*好能在Debug模式下,实时显示在界面上,针对出现的问题及早解决。

内存问题

内存问题主要包括两个部分,一个是iOS中常见循环引用导致的内存泄露 ,另外就是大量数据加载及使用导致的内存警告。

mmap

虽然苹果并没有明确每个App在运行期间可以使用的内存*大值,但是有开发者进行了实验和统计,一般在占用系统内存超过20%的时候会有内存警告,而超过50%的时候,就很容易Crash了,所以内存使用率还是尽量要少,对于数据量比较大的应用,可以采用分步加载数据的方式,或者采用mmap方式。mmap 是使用逻辑内存对磁盘文件进行映射,中间只是进行映射没有任何拷贝操作,避免了写文件的数据拷贝。 操作内存就相当于在操作文件,避免了内核空间和用户空间的频繁切换。之前在开发输入法的时候 ,词库的加载也是使用mmap方式,可以有效降低App的内存占用率,具体使用可以参考链接*篇文章。

循环引用

循环引用是iOS开发中经常遇到的问题,尤其对于新手来说是个头疼的问题。循环引用对App有潜在的危害,会使内存消耗过高,性能变差和Crash等,iOS常见的内存主要以下三种情况:

Delegate

代理协议是一个*典型的场景,需要你使用弱引用来避免循环引用。ARC时代,需要将代理声明为weak是一个即好又安全的做法:

1
@property (nonatomic, weak) id <mycustomdelegate> delegate;</mycustomdelegate>

NSTimer

NSTimer我们开发中会用到很多,比如下面一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)viewDidLoad {
     [ super  viewDidLoad];
     self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self
                                             selector:@selector(doSomeThing)
                                             userInfo:nil
                                             repeats:YES];
}
- (void)doSomeThing {
}
- (void)dealloc {
      [self.timer invalidate];
      self.timer = nil;
}

这是典型的循环引用,因为timer会强引用self,而self又持有了timer,所有就造成了循环引用。那有人可能会说,我使用一个weak指针,比如

__weak typeof(self) weakSelf = self;

self.mytimer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(doSomeThing) userInfo:nil repeats:YES];

但是其实并没有用,因为不管是weakSelf还是strongSelf,*终在NSTimer内部都会重新生成一个新的指针指向self,这是一个强引用的指针,结果就会导致循环引用。那怎么解决呢?主要有如下三种方式:

  • 使用类方法
  • 使用weakProxy
  • 使用GCD timer

具体如何使用,我就不做具体的介绍,网上有很多可以参考。

Block

Block的循环引用,主要是发生在ViewController中持有了block,比如:

1
@property (nonatomic, copy) LFCallbackBlock callbackBlock;

同时在对callbackBlock进行赋值的时候又调用了ViewController的方法,比如:

1
2
3
self.callbackBlock = ^{
     [self doSomething];
}];

就会发生循环引用,因为:ViewController->强引用了callback->强引用了ViewController,解决方法也很简单:

1
2
3
4
__weak __typeof(self) weakSelf = self;
self.callbackBlock = ^{
   [weakSelf doSomething];
}];

原因是使用MRC管理内存时,Block的内存管理需要区分是Global(全局)、Stack(栈)还是Heap(堆),而在使用了ARC之后,苹果自动会将所有原本应该放在栈中的Block全部放到堆中。全局的Block比较简单,凡是没有引用到Block作用域外面的参数的Block都会放到全局内存块中,在全局内存块的Block不用考虑内存管理问题。(放在全局内存块是为了在之后再次调用该Block时能快速反应,当然没有调用外部参数的Block根本不会出现内存管理问题)。

所以Block的内存管理出现问题的,*大部分都是在堆内存中的Block出现了问题。默认情况下,Block初始化都是在栈上的,但可能随时被收回,通过将Block类型声明为copy类型,这样对Block赋值的时候,会进行copy操作,copy到堆上,如果里面有对self的引用,则会有一个强引用的指针指向self,就会发生循环引用,如果采用weakSelf,内部不会有强类型的指针,所以可以解决循环引用问题。

那是不是所有的block都会发生循环引用呢?其实不然,比如UIView的类方法Block动画,NSArray等的类的遍历方法,也都不会发生循环引用,因为当前控制器一般不会强引用一个类。

其他内存问题

1 NSNotification addObserver之后,记得在dealloc里面添加remove;

2 动画的repeat count无限大,而且也不主动停止动画,基本就等于无限循环了;

3 forwardingTargetForSelector返回了self。

内存解决思路:

1 通过Instruments来查看leaks

2 集成Facebook开源的FBRetainCycleDetector

3 集成MLeaksFinder

具体原理及使用,可以参考链接。

CPU使用率

CPU的使用也可以通过两种方式来查看,一种是在调试的时候Xcode会有展示,具体详细信息可以进入Instruments内查看,通过查看Instruments的time profile来定位并解决问题。另一种常见的方法是通过代码读取CPU使用率,然后显示在App的调试面板上,可以在Debug环境下显示信息,具体代码如下:

1
2
3
4
5
6
7
8
9
int result;
mib[0] = CTL_HW;
mib[1] = HW_CPU_FREQ;
length = sizeof(result);
if  (sysctl(mib, 2, &result, &length, NULL, 0) < 0)
{
      perror( "getting cpu frequency" );
}
printf( "CPU Frequency = %u hz\n" , result);

FPS监控

目前主要使用CADisplayLink来监控FPS,CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的 CADisplayLink 对象,把它添加到一个runloop中,并给它提供一个 target 和selector 在屏幕刷新的时候调用,需要注意的是添加到runloop的common mode里面,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)setupDisplayLink {
     _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTicks:)];
     [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)linkTicks:(CADisplayLink *)link
{
     //执行次数
     _scheduleTimes ++;
     //当前时间戳
     if (_timestamp == 0){
         _timestamp = link.timestamp;
     }
     CFTimeInterval timePassed = link.timestamp - _timestamp;
     if (timePassed >= 1.f)
         //fps
         CGFloat fps = _scheduleTimes/timePassed; 
         printf( "fps:%.1f, timePassed:%f\n" , fps, timePassed);
     }
}

启动时间

点评App里面本身就包含了很多复杂的业务,比如外卖、团购、到综和酒店等,同时还引入了很多第三方SDK比如微信、QQ、微博等,在App初始化的时候,很多SDK及业务也开始初始化,这就会拖慢应用的启动时间。

1
2
3
App的启动时间t(App总启动时间) = t1(main()之前的加载时间) + t2(main()之后的加载时间)。 
t1 = 系统dylib(动态链接库)和自身App可执行文件的加载; 
t2 = main方法执行之后到AppDelegate类中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法执行结束前这段时间,主要是构建*个界面,并完成渲染展示。
  • 针对t1的优化,优化主要有如下:
  • 减少不必要的framework,因为动态链接比较耗时;
  • 检查framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查;
  • 合并或者删减一些OC类,这些我会在后续的静态检查中进行详解;

针对t2的时间优化,可以采用:

  • 异步初始化部分操作,比如网络,数据读取;
  • 采用延迟加载或者懒加载某些视图,图片等的初始化操作;
  • 对与图片展示类的App,可以将解码的图片保存到本地,下次启动时直接加载解码后的图片;
  • 对实现了+load()方法的类进行分析,尽量将load里的代码延后调用。

UI的主线程监测

我们都知道iOS的UI的操作一定是在主线程进行,该监测可以通过hook UIView的如下三个方法

1
2
3
-setNeedsLayout,
-setNeedsDisplay,
-setNeedsDisplayInRect

确保它们都是在主线程执行。子线程操作UI可能会引起什么问题,苹果说得并不清楚,但是在实际开发中,我们经常会遇到整个App的动画丢失,很大原因就是UI操作不是在主线程导致。

2 静态分析过程

静态分析在这里,我主要介绍两方面,一个是正常的code review机制,另外一个就是代码静态检查工具

code review

组内的code review机制,可以参考团队之前的OpenDoc – 前端团队CodeReview制度,iOS客户端开发,会在此基础上进行一些常见手误及Crash情况的重点标记,比如:

1 我们开发中首先都是在测试环境开发,开发时可以将测试环境的url写死到代码中,但是在提交代码的时候一定要将他改为线上环境的url,这个就可以通过gitlab中的重点比较部分字符串,给提交者一个强力的提示;

2 其他常见Crash的重点检查,比如NSMutableString/NSMutableArray/NSMutableDictionary/NSMutableSet 等类下标越界判断保护,或者 append/insert/add nil对象的保护;

3 ARC下的release操作,UITableViewCell返回nil,以及前面介绍的常见的循环引用等。

code review机制,一方面是依赖写代码者的代码习惯及质量,另一名依赖审查者的经验和细心程度,即使让多人revew,也可能会漏过一些错误,所以我们又添加了代码的静态检查。

代码静态检查

代码静态分析(Static Program Analysis)是指在不运行程序的条件下,由代码静态分析工具自动对程序进行分析的方法. iOS常见的静态扫描工具有Clang Static Analyzer、OCLint、Infer,这些主要是用来检查可能存在的问题,还有Deploymate用来检查api的兼容性。

Clang Static Analyzer

Clang Static Analyzer是一款静态代码扫描工具,专门用于针对C,C++和Objective-C的程序进行分析。已经被Xcode集成,可以直接使用Xcode进行静态代码扫描分析,Clang默认的配置主要是空指针检测,类型转换检测,空判断检测,内存泄漏检测这种等问题。如果需要更多的配置,可以使用开源的Clang项目,然后集成到自己的CI上。

OCLint

OCLint是一个强大的静态代码分析工具,可以用来提高代码质量,查找潜在的bug,主要针对 C、C++和Objective-C的静态分析。功能非常强大,而且是出自国人之手。OCLint基于 Clang 输出的抽象语法树对代码进行静态分析,支持与现有的CI集成,部署之后基本不需要维护,简单方便。

OCLint可以发现这些问题

  • 可能的bug – 空的 if / else / try / catch / finally 语句
  • 未使用的代码 – 未使用的局部变量和参数
  • 复杂的代码 – 高圈复杂度, NPath复杂, 高NCSS
  • 冗余代码 – 多余的if语句和无用的括号
  • 坏味道的代码 – 过长的方法和过长的参数列表
  • 不好的使用 – 倒逻辑和入参重新赋值

对于OCLint的与原理和部署方法,可以参考团队成员之前的文章:静态代码分析之OCLint的那些事儿,每次提交代码后,可以在打包的过程中进行代码检查,及早发现有问题的代码。当然也可以在合并代码之前执行对应的检查,如果检查不通过,不能合并代码,这样检查的力度更大。

Infer

Infer facebook开源的静态分析工具,Infer可以分析 Objective-C, Java 或者 C 代码,报告潜在的问题。Infer效率高,规模大,几分钟能扫描数千行代码;

C/OC中捕捉的bug类型主要有:

1
2
3
4
1:Resource leak
2:Memory leak
3:Null dereference
4:Premature nil termination argument

只在 OC中捕捉的bug类型

1
2
3
1:Retain cycle
2:Parameter not  null  checked
3:Ivar not  null  checked

结论

Clang Static Analyzer和Xcode集成度更高、更好用,支持命令行形式,并且能够用于持续集成。OCLint有更多的检查规则和定制,和很多工具集成,也同样可用于持续集成。Infer效率高,规模大,几分钟能扫描数千行代码;支持增量及非增量分析;分解分析,整合输出结果。infer能将代码分解,小范围分析后再将结果整合在一起,兼顾分析的深度和速度,所以根据自己的项目特点,选择合适的检查工具对代码进行检查,减少人力review成本,保证代码质量,*大限度的避免运行错误。

3 测试过程

前面介绍了很多指标的监测,代码静态检查,这些都是性能相关的,真正决定一个App功能稳定是否的是测试环节。测试是发布之前的*后一道卡,如果bug不能在测试中发现,那么*终就会触达用户,所以一个App的稳定性,很大程度决定它的测试过程。iOS App的测试包括以下几个层次:单元测试,UI测试,功能测试,异常测试。

单元测试

XCTest是苹果官方提供的单元测试框架,与Xcode集成在一起,由此苹果提供了很详细的文档XCTest。

Xcode单元测试包含在一个XCTestCase的子类中。依据约束,每一个 XCTestCase 子类封装一个特殊的有关联的集合,例如一个功能、用例或者一个程序流。同时还提供了XCTestExpectation来处理异步任务的测试,以及性能测试measureBlock(),还包括很多第三方测试框架比如:KiWi,Quick,Specta等,以及常用的mock框架OCMock。

单元测试的目的是将程序中所有的源代码,隔离成*小的可测试单元,以确保每个单元的正确性,如果每个单元都能保证正确,就能保证应用程序整体相当程度的正确性。但是在实际的操作过程中,很多公司都很难彻底执行单元测试,主要就是单元测试代码量甚至大于功能开发,比较难于维护。

对于测试用例覆盖度多少合适这个话题,也是仁者见仁智者见智,其实一个软件覆盖度在50%以上就可以称为一个健壮的软件了,要达到70,80这些已经是非常难了,不过我们常见的一些第三方开源框架的测试用例覆盖率还是非常高的,让人咋舌。例如,AFNNetWorking的覆盖率高达87%,SDWebImage的覆盖率高达77%。

UI测试

Xcode7中新增了UI Test测试,UI测试是模拟用户操作,进而从业务处层面测试,常用第三方库有KIF,appium。关于XCTest的UI测试,建议看看WWDC 2015的视频UI Testing in Xcode。 UI测试还有一个核心功能是UI Recording。选中一个UI测试用例,然后点击图中的小红点既可以开始UI Recoding。你会发现:随着点击模拟器,自动合成了测试代码。(通常自动合成代码后,还需要手动的去调整)

32131.png

UI测试

功能测试

功能测试跟上述的UT和UI测试有一些相通的地方,首先针对各个模块设计的功能,测试是否达到产品的目的,通常功能测试主要是测试及产品人员,然后还需要进行专项测试,比如我们公司的云测平台,会对整个App的性能,稳定性,UI等都进行整体评测,看是否达到标准,对于大规模的活动,还需要进行服务端的压力测试,确保整个功能无异常。测试通过后,可以进行estFlight测试,到*后正式发布。

功能测试还包括如下场景:系统兼容性测试,屏幕分辨率兼容性测试,覆盖安装测试,UI是否符合设计,消息推送等,以及前面开发过程中需要监控的内存、cpu、电量、网络流量、冷启动时间、热启动时间、存储、安装包的大小等测试。

异常测试

异常测试主要是针对一些不常规的操作

  • 使用过程中的来电时及结束后,界面显示是否正常;
  • 状态栏为两倍高度时,界面是否显示正常;
  • 意外断电后,数据是否保存,数据是否有损害等;
  • 设备充电时,不同电量时的App响应速度及操作流畅度等;
  • 其他App的相互切换,前后台转换时,是否正常;
  • 网络变化时的提示,弱网环境下的网络请求成功率等;
  • 各种monkey的随机点击,多点触摸测试等是否正常;
  • 更改系统时间,字体大小,语言等显示是否正常;
  • 设备存储不够时,是否能正常操作;

异常测试有很多,App针对自身的特点,可以选择性的进行边界和异常测试,也是保证App稳定行的一个重要方面。

4 发布及监控

因为移动App的特点,即使我们通过了各种测试,产品*终发布后,还是会遇到很多问题,比如Crash,网络失败,数据损坏,账号异常等等。针对已经发布的App,主要有一下方式保证稳定性:

热修复

目前比较流行的热修复方案都是基于JSPatch、React Native、Weex、lua+wax。

JSPatch能做到通过js调用和改写OC方法。*根本的原因是 Objective-C 是动态语言,OC上所有方法的调用/类的生成都通过 objective-c Runtime 在运行时进行,我们可以通过类名和方法名反射得到相应的类和方法,也可以替换某个类的方法为新的实现,还可以新注册一个类,为类添加方法。JSPatch 的原理就是:JS传递字符串给OC,OC通过 Runtime 接口调用和替换OC方法。

React Native 是从 Web 前端开发框架 React 延伸出来的解决方案,主要解决的问题是 Web 页面在移动端性能低的问题,React Native 让开发者可以像开发 Web 页面那样用 React 的方式开发功能,同时框架会通过 JavaScript 与 Objective-C 的通信让界面使用原生组件渲染,让开发出来的功能拥有原生App的性能和体验。

Weex阿里开源的,基于Vue+Native的开发模式,跟RN的主要区别就在React和Vue的区别,同时在RN的基础上进行了部分性能优化,总体开发思路跟RN是比较像的。

但是在今年上半年,苹果以安全为理由,开始拒*有热修复功能的应用,但其实苹果拒的不是热更新,拒的是从网络下载代码并修改应用行为,苹果禁止的是“基于反射的热更新“,而不是 “基于沙盒接口的热更新”。而大部分框架(如 React Native、weex)和游戏引擎(比如 Unity、Cocos2d-x等)都属于后者,所以不在被警告范围内。而JSPatch因为在国内大部分应用来做热更新修复bug的行为,所以才回被苹果禁止。

降级

用户使用App一段时间后,可能会遇到这样的情况:每次打开App时闪退,或者正常操作到某个界面时闪退,无法正常使用App。这样的用户体验十分糟糕,如果没有一个好的解决方案,很容易被用户删除App,导致用户量的流失。因为热更新基本不能使用,那就只能是App自身修复能力。目前常用的修复能力有:

  • 启动Crash的监控及修复

1 在应用起来的时候,记录flag并保存本地,启动一个定时器,比如5秒钟内,如果没有发生Crash,则认为用户操作正常,清空本地flag。

2 下次启动,发现有flag,则表明上次启动Crash,如果flag数组越大,则说明Crash的次数越多,这样就需要对整个App进行降级处理,比如登出账号,清空Documents/Library/Caches目录下的文件。

  • 具体业务下的Crash及修复

针对某些具体业务Crash场景,如果是上线的前端页面引起的,可以先对前端功能进行回滚,或者隐藏入口,等修复完毕后再上线,如果是客户端的某些异常,比如数据库升迁问题,主要是进行业务数据库修复,缓存文件的删除,账号退出等操作,尽量只修复此业务的相关的数据。

  • 网络降级

比如点评App,本身有CIP(公司内部自己研发的)长连接,接入腾讯云的WNS长连接,UDP连接,HTTP短连接,如果CIP服务器发生问题,可以及时切换到WNS连接,或者降级到Http连接,保证网络连接的成功率。

线上监控

Crash监控

Crash是对用户来说是*糟糕的体验,Crash日志能够记录用户闪退的崩溃日志及堆栈,进程线程信息,版本号,系统版本号,系统机型等有用信息,收集的信息越详细,越能够帮助解决崩溃,所以各大App都有自己崩溃日志收集系统,或者也可以使用开源或者付费的第三方Crash收集平台。

端到端成功率监控

端到端监控是从客户端App发出请求时计时,到App收到数据数据的成功率,统计对象是:网络接口请求(包括H5页面加载)的成败和端到端延时情况。端到端监控SDK提供了监控上传接口,调用SDK提供的监控API可以将数据上报到监控服务器中。

整个端到端监控的可以在多个维度上做查询端到端成功率、响应时间、访问量的查询,维度包括:返回码、网络、版本、平台、地区、运营商等。

用户行为日志

用户行为日志,主要记录用户在使用App过程中,点击元素的时间点,浏览时长,跳转流程等,然后基于此进行用户行为分析,大部分应用的推荐算法都是基于用户行为日志来统计的。某些情况下,Crash分析需要查询用户的行为日志,获取用户使用App的流程,帮助解决Crash等其他问题。

代码级日志

代码级别的日志,主要用来记录一个App的性能相关的数据,比如页面打开速度,内存使用率,CPU占用率,页面的帧率,网络流量,请求错误统计等,通过收集相关的上下文信息,优化App性能。

总结

虽然现在市面上第三方平台已经很成熟,但是各大互联公司都会自己开发线上监控系统,这样保证数据安全,同时更加灵活。因为移动用户的特点,在开发测试过程中,很难完全覆盖所有用户的全部场景,有些问题也只会在特定环境下才发生,所以通过线上监控平台,通过日志回捞等机制,及时获取特定场景的上下文环境,结合数据分析,能够及时发现问题,并后续修复,提高App的稳定性。

全文总结

本文主要从开发测试发布等流程来介绍了一个App稳定性指标及监测方法,开发阶段主要针对一些比较具体的指标,静态检查主要是扫描代码潜在问题,然后通过测试保证App功能的稳定性,线上降级主要是在尽量不发版的情况下,进行自修复,配合线上监控,信息收集,用户行为记录,方便后续问题修复及优化。本文观点是作者从事iOS开发的一些经验,希望能对你有所帮助,观点不同欢迎讨论。

iOS 25个性能优化/内存优化常用方法

1. 用ARC管理内存

ARC(Automatic ReferenceCounting, 自动引用计数)和iOS5一起发布,它避免了*常见的也就是经常是由于我们忘记释放内存所造成的内存泄露。它自动为你管理retain和release的过程,所以你就不必去手动干预了。忘掉代码段结尾的release简直像记得吃饭一样简单。而ARC会自动在底层为你做这些工作。除了帮你避免内存泄露,ARC还可以帮你提高性能,它能保证释放掉不再需要的对象的内存。

 

2. 在正确的地方使用 reuseIdentifier

一个开发中常见的错误就是没有给UITableViewCells, UICollectionViewCells,甚至是UITableViewHeaderFooterViews设置正确的reuseIdentifier。

为了性能*优化,table view用`tableView:cellForRowAtIndexPath:`为rows分配cells的时候,它的数据应该重用自UITableViewCell。一个table view维持一个队列的数据可重用的UITableViewCell对象。

不使用reuseIdentifier的话,每显示一行table view就不得不设置全新的cell。这对性能的影响可是相当大的,尤其会使app的滚动体验大打折扣。

自iOS6起,除了UICollectionView的cells和补充views,你也应该在header和footer views中使用reuseIdentifiers。

想要使用reuseIdentifiers的话,在一个table view中添加一个新的cell时在data source object中添加这个方法:

staticNSString *CellIdentifier = @”Cell”;

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

这个方法把那些已经存在的cell从队列中排除,或者在必要时使用先前注册的nib或者class创造新的cell。如果没有可重用的cell,你也没有注册一个class或者nib的话,这个方法返回nil。

 

3.尽量把views设置为透明

如果你有透明的Views你应该设置它们的opaque属性为YES。

原因是这会使系统用一个*优的方式渲染这些views。这个简单的属性在IB或者代码里都可以设定。

Apple的文档对于为图片设置透明属性的描述是:

(opaque)这个属性给渲染系统提供了一个如何处理这个view的提示。如果设为YES,渲染系统就认为这个view是完全不透明的,这使得渲染系统优化一些渲染过程和提高性能。如果设置为NO,渲染系统正常地和其它内容组成这个View。默认值是YES。

在相对比较静止的画面中,设置这个属性不会有太大影响。然而当这个view嵌在scroll view里边,或者是一个复杂动画的一部分,不设置这个属性的话会在很大程度上影响app的性能。

你可以在模拟器中用Debug\Color Blended Layers选项来发现哪些view没有被设置为opaque。目标就是,能设为opaque的就全设为opaque!

 

4.避免过于庞大的XIB

iOS5中加入的Storyboards(分镜)正在快速取代XIB。然而XIB在一些场景中仍然很有用。比如你的app需要适应iOS5之前的设备,或者你有一个自定义的可重用的view,你就不可避免地要用到他们。

如果你不得不XIB的话,使他们尽量简单。尝试为每个Controller配置一个单独的XIB,尽可能把一个View Controller的view层次结构分散到单独的XIB中去。

需要注意的是,当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。Storyboards就是另一码事儿了,storyboard仅在需要时实例化一个view controller.

当家在XIB是,所有图片都被chache,如果你在做OS X开发的话,声音文件也是。Apple在相关文档中的记述是:

当你加载一个引用了图片或者声音资源的nib时,nib加载代码会把图片和声音文件写进内存。在OS X中,图片和声音资源被缓存在named cache中以便将来用到时获取。在iOS中,仅图片资源会被存进named caches。取决于你所在的平台,使用NSImage 或UIImage的`imageNamed:`方法来获取图片资源。

 

5.不要阻塞主线程

永远不要使主线程承担过多。因为UIKit在主线程上做所有工作,渲染,管理触摸反应,回应输入等都需要在它上面完成。

一直使用主线程的风险就是如果你的代码真的block了主线程,你的app会失去反应。

大部分阻碍主进程的情形是你的app在做一些牵涉到读写外部资源的I/O操作,比如存储或者网络。

你可以使用`NSURLConnection`异步地做网络操作:

+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue*)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler

或者使用像AFNetworking这样的框架来异步地做这些操作。

如果你需要做其它类型的需要耗费巨大资源的操作(比如时间敏感的计算或者存储读写)那就用 Grand Central Dispatch,或者NSOperation和 NSOperationQueues.

下面代码是使用GCD的模板

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

// switch to a background thread and perform your expensive operation

dispatch_async(dispatch_get_main_queue(), ^{

// switch back to the main thread to update your UI

});

});

发现代码中有一个嵌套的`dispatch_async`吗?这是因为任何UIKit相关的代码需要在主线程上进行。

 

6. 在Image Views中调整图片大小

如果要在`UIImageView`中显示一个来自bundle的图片,你应保证图片的大小和UIImageView的大小相同。在运行中缩放图片是很耗费资源的,特别是`UIImageView`嵌套在`UIScrollView`中的情况下。

如果图片是从远端服务加载的你不能控制图片大小,比如在下载前调整到合适大小的话,你可以在下载完成后,*好是用background thread,缩放一次,然后在UIImageView中使用缩放后的图片。

 

7. 选择正确的Collection

学会选择对业务场景*合适的类或者对象是写出能效高的代码的基础。当处理collections时这句话尤其正确。

一些常见collection的总结:

· Arrays: 有序的一组值。使用index来lookup很快,使用value lookup很慢,插入/删除很慢。

· Dictionaries: 存储键值对。用键来查找比较快。

· Sets: 无序的一组值。用值来查找很快,插入/删除很快。

 

8. 打开gzip压缩

大量app依赖于远端资源和第三方API,你可能会开发一个需要从远端下载XML, JSON, HTML或者其它格式的app。

问题是我们的目标是移动设备,因此你就不能指望网络状况有多好。一个用户现在还在edge网络,下一分钟可能就切换到了3G。不论什么场景,你肯定不想让你的用户等太长时间。

减小文档的一个方式就是在服务端和你的app中打开gzip。这对于文字这种能有更高压缩率的数据来说会有更显著的效用。

好消息是,iOS已经在NSURLConnection中默认支持了gzip压缩,当然AFNetworking这些基于它的框架亦然。像Google App Engine这些云服务提供者也已经支持了压缩输出。

 

9. 重用和延迟加载(lazy load) Views

更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的app更是如此。

这里我们用到的技巧就是模仿`UITableView`和`UICollectionView`的操作:不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。

这样的话你就只需要在滚动发生时创建你的views,避免了不划算的内存分配。

创建views的能效问题也适用于你app的其它方面。想象一下一个用户点击一个按钮的时候需要呈现一个view的场景。有两种实现方法:

1. 创建并隐藏这个view当这个screen加载的时候,当需要时显示它;

2. 当需要时才创建并展示。

每个方案都有其优缺点。用*种方案的话因为你需要一开始就创建一个view并保持它直到不再使用,这就会更加消耗内存。然而这也会使你的app操作更敏感因为当用户点击按钮的时候它只需要改变一下这个view的可见性。

第二种方案则相反-消耗更少内存,但是会在点击按钮的时候比*种稍显卡顿。

 

10. Cache, Cache, 还是Cache!

一个*好的原则就是,缓存所需要的,也就是那些不大可能改变但是需要经常读取的东西。

我们能缓存些什么呢?一些选项是,远端服务器的响应,图片,甚至计算结果,比如UITableView的行高。

NSURLConnection默认会缓存资源在内存或者存储中根据它所加载的HTTP Headers。你甚至可以手动创建一个NSURLRequest然后使它只加载缓存的值。

下面是一个可用的代码段,你可以可以用它去为一个基本不会改变的图片创建一个NSURLRequest并缓存它:

+ (NSMutableURLRequest *)imageRequestWithURL:(NSURL *)url {

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;// this will make sure the request always returns the cached image

request.HTTPShouldHandleCookies = NO;

request.HTTPShouldUsePipelining = YES;

[request addValue:@”image/*”forHTTPHeaderField:@”Accept”];

return request;

}

注意你可以通过 NSURLConnection 获取一个URL request, AFNetworking也一样的。这样你就不必为采用这条tip而改变所有的networking代码了。

 

如果你需要缓存其它不是HTTP Request的东西,你可以用NSCache。

NSCache和NSDictionary类似,不同的是系统回收内存的时候它会自动删掉它的内容。

 

11.权衡渲染方法

在iOS中可以有很多方法做出漂亮的按钮。你可以用整幅的图片,可调大小的图片,uozhe可以用CALayer, CoreGraphics甚至OpenGL来画它们。

当然每个不同的解决方法都有不同的复杂程度和相应的性能。

简单来说,就是用事先渲染好的图片更快一些,因为如此一来iOS就免去了创建一个图片再画东西上去然后显示在屏幕上的程序。问题是你需要把所有你需要用到的图片放到app的bundle里面,这样就增加了体积–这就是使用可变大小的图片更好的地方了:你可以省去一些不必要的空间,也不需要再为不同的元素(比如按钮)来做不同的图。

然而,使用图片也意味着你失去了使用代码调整图片的机动性,你需要一遍又一遍不断地重做他们,这样就很浪费时间了,而且你如果要做一个动画效果,虽然每幅图只是一些细节的变化你就需要很多的图片造成bundle大小的不断增大。

总得来说,你需要权衡一下利弊,到底是要性能能还是要bundle保持合适的大小。

 

12.处理内存警告

一旦系统内存过低,iOS会通知所有运行中app。在官方文档中是这样记述:

如果你的app收到了内存警告,它就需要尽可能释放更多的内存。*佳方式是移除对缓存,图片object和其他一些可以重创建的objects的strong references.

幸运的是,UIKit提供了几种收集低内存警告的方法:

· 在app delegate中使用`applicationDidReceiveMemoryWarning:`的方法

· 在你的自定义UIViewController的子类(subclass)中覆盖`didReceiveMemoryWarning`

· 注册并接收 UIApplicationDidReceiveMemoryWarningNotification的通知

一旦收到这类通知,你就需要释放任何不必要的内存使用。

例如,UIViewController的默认行为是移除一些不可见的view,它的一些子类则可以补充这个方法,删掉一些额外的数据结构。一个有图片缓存的app可以移除不在屏幕上显示的图片。

这样对内存警报的处理是很必要的,若不重视,你的app就可能被系统杀掉。

然而,当你一定要确认你所选择的object是可以被重现创建的来释放内存。一定要在开发中用模拟器中的内存提醒模拟去测试一下。

 

13.重用大开销对象

一些objects的初始化很慢,比如NSDateFormatter和NSCalendar。然而,你又不可避免地需要使用它们,比如从JSON或者XML中解析数据。

想要避免使用这个对象的瓶颈你就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。

注意如果你要选择第二种方法,对象会在你的app运行时一直存在于内存中,和单例(singleton)很相似。

下面的代码说明了使用一个属性来延迟加载一个date formatter. *次调用时它会创建一个新的实例,以后的调用则将返回已经创建的实例:

// in your .h or inside a class extension

@property (nonatomic, strong) NSDateFormatter *formatter;

// inside the implementation (.m)

// When you need, just use self.formatter

– (NSDateFormatter *)formatter {

if(! _formatter) {

_formatter = [[NSDateFormatter alloc] init];

_formatter.dateFormat = @”EEE MMM dd HH:mm:ss Z yyyy”;// twitter date format

}

return_formatter;

}

还需要注意的是,其实设置一个NSDateFormatter的速度差不多是和创建新的一样慢的!所以如果你的app需要经常进行日期格式处理的话,你会从这个方法中得到不小的性能提升。

 

14. 使用Sprite Sheets

Sprite sheet可以让渲染速度加快,甚至比标准的屏幕渲染方法节省内存。

 

15.避免反复处理数据

许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要。在内存中操作数据使它们满足你的数据结构是开销很大的。

比如你需要数据来展示一个table view,*好直接从服务器取array结构的数据以避免额外的中间数据结构改变。

类似的,如果需要从特定key中取数据,那么就使用键值对的dictionary。

 

16.选择正确的数据格式

 

从app和网络服务间传输数据有很多方案,*常见的就是JSON和XML。你需要选择对你的app来说*合适的一个。

解析JSON会比XML更快一些,JSON也通常更小更便于传输。从iOS5起有了官方内建的JSON deserialization就更加方便使用了。

但是XML也有XML的好处,比如使用SAX来解析XML就像解析本地文件一样,你不需像解析json一样等到整个文档下载完成才开始解析。当你处理很大的数据的时候就会*大地减低内存消耗和增加性能。

 

17.正确设定背景图片

在View里放背景图片就像很多其它iOS编程一样有很多方法:

使用UIColor的 colorWithPatternImage来设置背景色;

在view中添加一个UIImageView作为一个子View。

如果你使用全画幅的背景图,你就必须使用UIImageView因为UIColor的colorWithPatternImage是用来创建小的重复的图片作为背景的。这种情形下使用UIImageView可以节约不少的内存:

// You could also achieve the same result in Interface Builder

UIImageView *backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@”background”]];

[self.view addSubview:backgroundView];

如果你用小图平铺来创建背景,你就需要用UIColor的colorWithPatternImage来做了,它会更快地渲染也不会花费很多内存:

self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@”background”]];

 

18. 减少使用Web特性

UIWebView很有用,用它来展示网页内容或者创建UIKit很难做到的动画效果是很简单的一件事。

但是你可能有注意到UIWebView并不像驱动Safari的那么快。这是由于以JIT compilation为特色的Webkit的Nitro Engine的限制。

所以想要更高的性能你就要调整下你的HTML了。*件要做的事就是尽可能移除不必要的javascript,避免使用过大的框架。能只用原生js就更好了。

另外,尽可能异步加载例如用户行为统计script这种不影响页面表达的javascript。

*后,永远要注意你使用的图片,保证图片的符合你使用的大小。使用Sprite sheet提高加载速度和节约内存。

 

19. 设定Shadow Path

如何在一个View或者一个layer上加一个shadow呢,QuartzCore框架是很多开发者的选择:

#import

// Somewhere later …

UIView *view = [[UIView alloc] init];

// Setup the shadow …

view.layer.shadowOffset = CGSizeMake(-1.0f, 1.0f);

view.layer.shadowRadius = 5.0f;

view.layer.shadowOpacity = 0.6;

看起来很简单,对吧。可是,坏消息是使用这个方法也有它的问题… Core Animation不得不先在后台得出你的图形并加好阴影然后才渲染,这开销是很大的。

使用shadowPath的话就避免了这个问题:

view.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];

使用shadow path的话iOS就不必每次都计算如何渲染,它使用一个预先计算好的路径。但问题是自己计算path的话可能在某些View中比较困难,且每当view的frame变化的时候你都需要去update shadow path.

 

20. 优化Table View

Table view需要有很好的滚动性能,不然用户会在滚动过程中发现动画的瑕疵。

为了保证table view平滑滚动,确保你采取了以下的措施:

· 正确使用`reuseIdentifier`来重用cells

· 尽量使所有的view opaque,包括cell自身

· 避免渐变,图片缩放,后台选人

· 缓存行高

· 如果cell内现实的内容来自web,使用异步加载,缓存请求结果

· 使用`shadowPath`来画阴影

· 减少subviews的数量

· 尽量不适用`cellForRowAtIndexPath:`,如果你需要用到它,只用一次然后缓存结果

· 使用正确的数据结构来存储数据

· 使用`rowHeight`, `sectionFooterHeight`和 `sectionHeaderHeight`来设定固定的高,不要请求delegate

 

21.选择正确的数据存储选项

当存储大块数据时你会怎么做?

你有很多选择,比如:

· 使用`NSUerDefaults`

· 使用XML, JSON, 或者 plist

· 使用NSCoding存档

· 使用类似SQLite的本地SQL数据库

· 使用 Core Data

NSUserDefaults的问题是什么?虽然它很nice也很便捷,但是它只适用于小数据,比如一些简单的布尔型的设置选项,再大点你就要考虑其它方式了

XML这种结构化档案呢?总体来说,你需要读取整个文件到内存里去解析,这样是很不经济的。使用SAX又是一个很麻烦的事情。

NSCoding?不幸的是,它也需要读写文件,所以也有以上问题。

在这种应用场景下,使用SQLite 或者 Core Data比较好。使用这些技术你用特定的查询语句就能只加载你需要的对象。

在性能层面来讲,SQLite和Core Data是很相似的。他们的不同在于具体使用方法。Core Data代表一个对象的graph model,但SQLite就是一个DBMS。Apple在一般情况下建议使用Core Data,但是如果你有理由不使用它,那么就去使用更加底层的SQLite吧。

如果你使用SQLite,你可以用FMDB(https://GitHub.com/ccgus/fmdb)这个库来简化SQLite的操作,这样你就不用花很多经历了解SQLite的C API了。

 

23. 使用Autorelease Pool

`NSAutoreleasePool`负责释放block中的autoreleased objects。一般情况下它会自动被UIKit调用。但是有些状况下你也需要手动去创建它。

假如你创建很多临时对象,你会发现内存一直在减少直到这些对象被release的时候。这是因为只有当UIKit用光了autorelease pool的时候memory才会被释放。好消息是你可以在你自己的@autoreleasepool里创建临时的对象来避免这个行为:

NSArray *urls = <# An array of file URLs #>;

for(NSURL *url in urls) {

@autoreleasepool {

NSError *error;

NSString *fileContents = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error];

 

/* Process the string, creating and autoreleasing more objects. */

 

}

 

}

这段代码在每次遍历后释放所有autorelease对象

 

24. 选择是否缓存图片

常见的从bundle中加载图片的方式有两种,一个是用`imageNamed`,二是用`imageWithContentsOfFile`,*种比较常见一点。

既然有两种类似的方法来实现相同的目的,那么他们之间的差别是什么呢?

`imageNamed`的优点是当加载时会缓存图片。`imageNamed`的文档中这么说:这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象如果它存在的话。如果缓存中没有找到相应的图片,这个方法从指定的文档中加载然后缓存并返回这个对象。

相反的,`imageWithContentsOfFile`仅加载图片。

下面的代码说明了这两种方法的用法:

UIImage *img = [UIImage imageNamed:@”myImage”];// caching

// or

UIImage *img = [UIImage imageWithContentsOfFile:@”myImage”];// no caching

那么我们应该如何选择呢?

如果你要加载一个大图片而且是一次性使用,那么就没必要缓存这个图片,用`imageWithContentsOfFile`足矣,这样不会浪费内存来缓存它。

然而,在图片反复重用的情况下`imageNamed`是一个好得多的选择。

 

25. 避免日期格式转换

如果你要用`NSDateFormatter`来处理很多日期格式,应该小心以待。就像先前提到的,任何时候重用`NSDateFormatters`都是一个好的实践。

然而,如果你需要更多速度,那么直接用C是一个好的方案。Sam Soffes有一个不错的帖子(https://soff.es/how-to-drastically-improve-your-app-with-an-afternoon-and-instruments)里面有一些可以用来解析ISO-8601日期字符串的代码,简单重写一下就可以拿来用了。

嗯,直接用C来搞,看起来不错了,但是你相信吗,我们还有更好的方案!

如果你可以控制你所处理的日期格式,尽量选择Unix时间戳。你可以方便地从时间戳转换到NSDate:

– (NSDate*)dateFromUnixTimestamp:(NSTimeInterval)timestamp {

return[NSDate dateWithTimeIntervalSince1970:timestamp];

 

}

这样会比用C来解析日期字符串还快!需要注意的是,许多web API会以微秒的形式返回时间戳,因为这种格式在javascript中更方便使用。记住用`dateFromUnixTimestamp`之前除以1000就好了。

iOS Block 详解

一、概述

闭包 = 一个函数「或指向函数的指针」+ 该函数执行的外部的上下文变量「也就是自由变量」;Block 是 Objective-C 对于闭包的实现。

其中,Block:

  • 可以嵌套定义,定义 Block 方法和定义函数方法相似
  • Block 可以定义在方法内部或外部
  • 只有调用 Block 时候,才会执行其{}体内的代码
  • 本质是对象,使代码高聚合

 

使用 clang 将 OC 代码转换为 C++ 文件查看 block 的方法:

  • 在命令行输入代码 clang -rewrite-objc 需要编译的OC文件.m
  • 这时查看当前的文件夹里 多了一个相同的名称的 .cpp 文件,在命令行输入 open main.cpp 查看文件

 

二、Block的定义与使用

1、无参数无返回值

  1. //1,无参数,无返回值,声明和定义
  2. void(^MyBlockOne)(void) = ^(void){
  3. NSLog(@”无参数,无返回值”);
  4. };
  5. MyBlockOne();//block的调用

 

2、有参数无返回值

  1. //2,有参数,无返回值,声明和定义
  2. void(^MyblockTwo)(int a) = ^(int a){
  3. NSLog(@”@ = %d我就是block,有参数,无返回值”,a);
  4. };
  5. MyblockTwo(100);

3、有参数有返回值

  1. //3,有参数,有返回值
  2. int(^MyBlockThree)(int,int) = ^(int a,int b){
  3. NSLog(@”%d我就是block,有参数,有返回值”,a + b);returna + b;
  4. };
  5. MyBlockThree(12,56);

 

4、无参数有返回值(很少用到)

  1. //4,无参数,有返回值
  2. int(^MyblockFour)(void) = ^{NSLog(@”无参数,有返回值”);
  3. return45;
  4. };
  5. MyblockFour();

5、实际开发中常用typedef 定义Block

例如,用typedef定义一个block:

typedef int (^MyBlock)(int , int);

这时,MyBlock就成为了一种Block类型
在定义类的属性时可以这样:

@property (nonatomic,copy) MyBlock myBlockOne;

使用时:

  1. self.myBlockOne = ^int (int ,int){
  2. //TODO
  3. }

三、Block与外界变量

1、截获自动变量(局部变量)值

(1)默认情况

对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的。也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大。特别要注意的是默认情况下block只能访问不能修改局部变量的值。

1862021-631904c4e9aba96d

 

  1. int age = 10;
  2. myBlock block = ^{
  3. NSLog(@”age = %d”, age);
  4. };
  5. age = 18;
  6. block();

输出结果:

age = 10

 

(2) __block 修饰的外部变量

对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。block可以修改__block 修饰的外部变量的值。

1862021-9d41b9401608ce22

  1. __block int age = 10;
  2. myBlock block = ^{
  3. NSLog(@”age = %d”, age);
  4. };
  5. age = 18;
  6. block();

输出为:

age = 18

 

为什么使用__block 修饰的外部变量的值就可以被block修改呢?

我们使用 clang 将 OC 代码转换为 C++ 文件:

clang -rewrite-objc 源代码文件名

便可揭开其真正面纱:

  1. __block int val = 10;
  2. 转换成
  3. __Block_byref_val_0 val = {
  4. 0,
  5. &val,
  6. 0,
  7. sizeof(__Block_byref_val_0),
  8. 10
  9. };

会发现一个局部变量加上__block修饰符后竟然跟block一样变成了一个__Block_byref_val_0结构体类型的自动变量实例!!!!

此时我们在block内部访问val变量则需要通过一个叫__forwarding的成员变量来间接访问val变量(下面会对__forwarding进行详解)

 

四、Block的copy操作

1、Block的存储域及copy操作

在开始研究Block的copy操作之前,先来思考一下:Block是存储在栈上还是堆上呢?

我们先来看看一个由C/C++/OBJC编译的程序占用内存分布的结构:

memory_structure

其实,block有三种类型:

  • 全局块(_NSConcreteGlobalBlock)
  • 栈块(_NSConcreteStackBlock)
  • 堆块(_NSConcreteMallocBlock)

这三种block各自的存储域如下图:

2107810-db72f760c3eaa8ab

  • 全局块存在于全局内存中, 相当于单例.
  • 栈块存在于栈内存中, 超出其作用域则马上被销毁
  • 堆块存在于堆内存中, 是一个带引用计数的对象, 需要自行管理其内存

简而言之,存储在栈中的Block就是栈块、存储在堆中的就是堆块、既不在栈中也不在堆中的块就是全局块。

 

遇到一个Block,我们怎么这个Block的存储位置呢?

(1)Block不访问外界变量(包括栈中和堆中的变量)

Block 既不在栈又不在堆中,在代码段中,ARC和MRC下都是如此。此时为全局块。

 

(2)Block访问外界变量

MRC 环境下:访问外界变量的 Block 默认存储栈中。
ARC 环境下:访问外界变量的 Block 默认存储在堆中(实际是放在栈区,然后ARC情况下自动又拷贝到堆区),自动释放。

 

 

ARC下,访问外界变量的 Block为什么要自动从栈区拷贝到堆区呢?

栈上的Block,如果其所属的变量作用域结束,该Block就被废弃,如同一般的自动变量。当然,Block中的__block变量也同时被废弃。如下图:

3629436-49cdbfca00a85bb6

为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把Block复制到堆中,延长其生命周期。开启ARC时,大多数情况下编译器会恰当地进行判断是否有需要将Block从栈复制到堆,如果有,自动生成将Block从栈上复制到堆上的代码。Block的复制操作执行的是copy实例方法。Block只要调用了copy方法,栈块就会变成堆块。

如下图:

例如下面一个返回值为Block类型的函数:

  1. typedef int (^blk_t)(int);
  2. blk_t func(int rate) {
  3. return ^(int count) { return rate * count; };
  4. }

分析可知:上面的函数返回的Block是配置在栈上的,所以返回函数调用方时,Block变量作用域就结束了,Block会被废弃。但在ARC有效,这种情况编译器会自动完成复制。

在非ARC情况下则需要开发者调用copy方法手动复制,由于开发中几乎都是ARC模式,所以手动复制内容不再过多研究。

将Block从栈上复制到堆上相当消耗CPU,所以当Block设置在栈上也能够使用时,就不要复制了,因为此时的复制只是在浪费CPU资源。

 

Block的复制操作执行的是copy实例方法。不同类型的Block使用copy方法的效果如下表:

snip20170108_2

根据表得知,Block在堆中copy会造成引用计数增加,这与其他Objective-C对象是一样的。虽然Block在栈中也是以对象的身份存在,但是栈块没有引用计数,因为不需要,我们都知道栈区的内存由编译器自动分配释放。关于堆区和栈区详细内容可以参考下峰哥之前的文章:《总结:堆、栈、队列

 

不管Block存储域在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可。

在ARC有效时,多次调用copy方法完全没有问题:

  1. blk = [[[[blk copy] copy] copy] copy];
  2. // 经过多次复制,变量blk仍然持有Block的强引用,该Block不会被废弃。

2、__block变量与__forwarding

在copy操作之后,既然__block变量也被copy到堆上去了, 那么访问该变量是访问栈上的还是堆上的呢?__forwarding 终于要闪亮登场了,如下图:

1862021-100fdd59e5b0c03a

通过__forwarding, 无论是在block中还是 block外访问__block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量。

 

五、防止 Block 循环引用

Block 循环引用的情况:
某个类将 block 作为自己的属性变量,然后该类在 block 的方法体里面又使用了该类本身,如下:

  1. self.someBlock = ^(Type var){
  2. [self dosomething];
  3. };

解决办法:

(1)ARC 下:使用 __weak

  1. __weak typeof(self) weakSelf = self;
  2. self.someBlock = ^(Type var){
  3. [weakSelf dosomething];
  4. };

(2)MRC 下:使用 __block

  1. __block typeof(self) blockSelf = self;
  2. self.someBlock = ^(Type var){
  3. [blockSelf dosomething];
  4. };

值得注意的是,在ARC下,使用 __block 也有可能带来的循环引用,如下:

  1. // 循环引用 self -> _attributBlock -> tmp -> self
  2. typedef void (^Block)();
  3. @interface TestObj : NSObject
  4. {
  5. Block _attributBlock;
  6. }
  7. @end
  8. @implementation TestObj
  9. – (id)init {
  10. self = [super init];
  11. __block id tmp = self;
  12. self.attributBlock = ^{
  13. NSLog(@”Self = %@”,tmp);
  14. tmp = nil;
  15. };
  16. }
  17. – (void)execBlock {
  18. self.attributBlock();
  19. }
  20. @end
  21. // 使用类
  22. id obj = [[TestObj alloc] init];
  23. [obj execBlock]; // 如果不调用此方法,tmp 永远不会置 nil,内存泄露会一直在

 

六、Block的使用示例

1、Block作为变量(Xcode快捷键:inlineBlock)

  1. int (^sum) (int, int); // 定义一个 Block 变量 sum
  2. // 给 Block 变量赋值
  3. // 一般 返回值省略:sum = ^(int a,int b)…
  4. sum = ^int (int a,int b){
  5. return a+b;
  6. }; // 赋值语句*后有 分号
  7. int a = sum(10,20); // 调用 Block 变量

2、Block作为属性(Xcode 快捷键:typedefBlock)

  1. // 1. 给 Calculate 类型 sum变量 赋值「下定义」
  2. typedef int (^Calculate)(int, int); // calculate就是类型名
  3. Calculate sum = ^(int a,int b){
  4. return a+b;
  5. };
  6. int a = sum(10,20); // 调用 sum变量
  7. // 2. 作为对象的属性声明,copy 后 block 会转移到堆中和对象一起
  8. @property (nonatomic, copy) Calculate sum; // 使用 typedef
  9. @property (nonatomic, copy) int (^sum)(int, int); // 不使用 typedef
  10. // 声明,类外
  11. self.sum = ^(int a,int b){
  12. return a+b;
  13. };
  14. // 调用,类内
  15. int a = self.sum(10,20);

3、作为 OC 中的方法参数

  1. // —- 无参数传递的 Block —————————
  2. // 实现
  3. – (CGFloat)testTimeConsume:(void(^)())middleBlock {
  4. // 执行前记录下当前的时间
  5. CFTimeInterval startTime = CACurrentMediaTime();
  6. middleBlock();
  7. // 执行后记录下当前的时间
  8. CFTimeInterval endTime = CACurrentMediaTime();
  9. return endTime – startTime;
  10. }
  11. // 调用
  12. [self testTimeConsume:^{
  13. // 放入 block 中的代码
  14. }];
  15. // —- 有参数传递的 Block —————————
  16. // 实现
  17. – (CGFloat)testTimeConsume:(void(^)(NSString * name))middleBlock {
  18. // 执行前记录下当前的时间
  19. CFTimeInterval startTime = CACurrentMediaTime();
  20. NSString *name = @”有参数”;
  21. middleBlock(name);
  22. // 执行后记录下当前的时间
  23. CFTimeInterval endTime = CACurrentMediaTime();
  24. return endTime – startTime;
  25. }
  26. // 调用
  27. [self testTimeConsume:^(NSString *name) {
  28. // 放入 block 中的代码,可以使用参数 name
  29. // 参数 name 是实现代码中传入的,在调用时只能使用,不能传值
  30. }];

4、Block回调

Block回调是关于Block*常用的内容,比如网络下载,我们可以用Block实现下载成功与失败的反馈。开发者在block没发布前,实现回调基本都是通过代理的方式进行的,比如负责网络请求的原生类NSURLConnection类,通过多个协议方法实现请求中的事件处理。而在*新的环境下,使用的NSURLSession已经采用block的方式处理任务请求了。各种第三方网络请求框架也都在使用block进行回调处理。这种转变很大一部分原因在于block使用简单,逻辑清晰,灵活等原因。

如下:

  1. //DownloadManager.h
  2. #import <Foundation/Foundation.h>
  3. @interface DownloadManager : NSObject <NSURLSessionDownloadDelegate>
  4. // block 重命名
  5. typedef void (^DownloadHandler)(NSData * receiveData, NSError * error);
  6. – (void)downloadWithURL:(NSString *)URL parameters:(NSDictionary *)parameters handler:(DownloadHandler)handler ;
  7. @end
  1. //DownloadManager.m
  2. #import “DownloadManager.h”
  3. @implementation DownloadManager
  4. – (void)downloadWithURL:(NSString *)URL parameters:(NSDictionary *)parameters handler:(DownloadHandler)handler
  5. {
  6. NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]];
  7. NSURLSession * session = [NSURLSession sharedSession];
  8. //执行请求任务
  9. NSURLSessionDataTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
  10. if (handler) {
  11. dispatch_async(dispatch_get_main_queue(), ^{
  12. handler(data,error);
  13. });
  14. }
  15. }];
  16. [task resume];
  17. }

上面通过封装NSURLSession的请求,传入一个处理请求结果的block对象,就会自动将请求任务放到工作线程中执行实现,我们在网络请求逻辑的代码中调用如下:

  1. – (IBAction)buttonClicked:(id)sender {
  2. #define DOWNLOADURL @“https://codeload.github.com/AFNetworking/AFNetworking/zip/master”
  3. //下载类
  4. DownloadManager * downloadManager = [[DownloadManager alloc] init];
  5. [downloadManager downloadWithURL: DOWNLOADURL parameters:nil handler:^(NSData *receiveData, NSError *error) {
  6. if (error) {
  7. NSLog(@”下载失败:%@”,error);
  8. }else {
  9. NSLog(@”下载成功,%@”,receiveData);
  10. }
  11. }];
  12. }

为了加深理解,再来一个简单的小例子:

A,B两个界面,A界面中有一个label,一个buttonA。点击buttonA进入B界面,B界面中有一个UITextfield和一个buttonB,点击buttonB退出B界面并将B界面中UITextfield的值传到A界面中的label。

A界面中,也就是ViewController类中:

  1. //关键demo:
  2. – (IBAction)buttonAction {
  3. MyFirstViewController *myVC = [[MyFirstViewController alloc] init];
  4. [self presentViewController:myVC animated:YES completion:^{
  5. }];
  6. __weak typeof(self) weakSelf = self;//防止循环引用
  7. //用属性定义的注意:这里属性是不会自动补全的,方法就会自动补全
  8. [myVC setBlock:^(NSString *string){
  9. weakSelf.labelA.text = string;
  10. }];
  11. }

B界面中,也就是MyFirstViewController类中.m文件:

  1. – (IBAction)buttonBAction {
  2. [self dismissViewControllerAnimated:YES completion:^{
  3. }];
  4. self.block(_myTextfielf.text);
  5. }

.h文件:

  1. #import <UIKit/UIKit.h>
  2. //typedef定义一下block,为了更好用
  3. typedef void(^MyBlock)(NSString *string);
  4. @interface MyFirstViewController : UIViewController
  5. @property (nonatomic, copy) MyBlock block;
  6. @end

看了以上两个Block回调示例,是不是感觉比delegate清爽了不少?

PS:Block是个值得深入学习的东西,这篇文章整整花了峰哥两天时间整理,但是觉得很值得~

iOS- 如何集成支付宝

现在不少app内都集成了支付宝功能
使用支付宝进行一个完整的支付功能,大致有以下步骤:
1>先与支付宝签约,获得商户ID(partner)和账号ID(seller)
(这个主要是公司的负责)
2>下载相应的公钥私钥文件(加密签名用)
3>下载支付宝SDK(登录网站: http://club.alipay.com /
%title插图%num
里面提供了非常详细的文档、如何签约、如何获得公钥私钥、如何调用支付接口。
4>生成订单信息
5>调用支付宝客户端,由支付宝客户端跟支付宝安全服务器打交道
6>支付完毕后返回支付结果给商户客户端和服务器
SDK里有集成支付宝功能的一个Demo>  集成支付功能的具体操作方式,可以参考Demo
%title插图%num
当*次打开Demo时,可能会出现以下问题:
%title插图%num
错误原因很简单,就是项目的部署版本设置太低了,从3.0改为4.3即可
%title插图%num
要想集成支付功能,依赖以下文件夹的库文件(把这3个添加到你的客户端中)
%title插图%num
调用支付接口可以参考AlixPayDemoViewController的下面方法
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

如何创建订单 ( 订单根据自己公司看是什么样的)

如何签名

如何调用支付接口

都在这个方法里面了

复制代码
 1 //
 2 //选中商品调用支付宝快捷支付
 3 //
 4 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
 5 {
 6     /*
 7      *点击获取prodcut实例并初始化订单信息
 8      */
 9     Product *product = [_products objectAtIndex:indexPath.row];
10     
11     /*
12      *商户的唯一的parnter和seller。
13      *本demo将parnter和seller信息存于(AlixPayDemo-Info.plist)中,外部商户可以考虑存于服务端或本地其他地方。
14      *签约后,支付宝会为每个商户分配一个唯一的 parnter 和 seller。
15      */
16     //如果partner和seller数据存于其他位置,请改写下面两行代码
17     NSString *partner = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"Partner"];
18     NSString *seller = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"Seller"];
19     
20     //partner和seller获取失败,提示
21     if ([partner length] == 0 || [seller length] == 0)
22     {
23         UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"提示"
24                                                         message:@"缺少partner或者seller。" 
25                                                        delegate:self 
26                                               cancelButtonTitle:@"确定" 
27                                               otherButtonTitles:nil];
28         [alert show];
29         [alert release];
30         return;
31     }
32     
33     /*
34      *生成订单信息及签名
35      *由于demo的局限性,本demo中的公私钥存放在AlixPayDemo-Info.plist中,外部商户可以存放在服务端或本地其他地方。
36      */
37     //将商品信息赋予AlixPayOrder的成员变量
38     AlixPayOrder *order = [[AlixPayOrder alloc] init];
39     order.partner = partner;
40     order.seller = seller;
41     order.tradeNO = [self generateTradeNO]; //订单ID(由商家自行制定)
42     order.productName = product.subject; //商品标题
43     order.productDescription = product.body; //商品描述
44     order.amount = [NSString stringWithFormat:@"%.2f",product.price]; //商品价格
45     order.notifyURL =  @"http://www.xxx.com"; //回调URL
46     
47     //应用注册scheme,在AlixPayDemo-Info.plist定义URL types,用于快捷支付成功后重新唤起商户应用
48     NSString *appScheme = @"AlixPayDemo"; 
49     
50     //将商品信息拼接成字符串
51     NSString *orderSpec = [order description];
52     NSLog(@"orderSpec = %@",orderSpec);
53     
54     //获取私钥并将商户信息签名,外部商户可以根据情况存放私钥和签名,只需要遵循RSA签名规范,并将签名字符串base64编码和UrlEncode
55     id<DataSigner> signer = CreateRSADataSigner([[NSBundle mainBundle] objectForInfoDictionaryKey:@"RSA private key"]);
56     NSString *signedString = [signer signString:orderSpec];
57     
58     //将签名成功字符串格式化为订单字符串,请严格按照该格式
59     NSString *orderString = nil;
60     if (signedString != nil) {
61         orderString = [NSString stringWithFormat:@"%@&sign=\"%@\"&sign_type=\"%@\"",
62                                  orderSpec, signedString, @"RSA"];
63         
64         //获取快捷支付单例并调用快捷支付接口
65         AlixPay * alixpay = [AlixPay shared];
66         int ret = [alixpay pay:orderString applicationScheme:appScheme];
67         
68         if (ret == kSPErrorAlipayClientNotInstalled) {
69             UIAlertView * alertView = [[UIAlertView alloc] initWithTitle:@"提示" 
70                                                                  message:@"您还没有安装支付宝快捷支付,请先安装。" 
71                                                                 delegate:self 
72                                                        cancelButtonTitle:@"确定" 
73                                                        otherButtonTitles:nil];
74             [alertView setTag:123];
75             [alertView show];
76             [alertView release];
77         }
78         else if (ret == kSPErrorSignError) {
79             NSLog(@"签名错误!");
80         }
81 
82     }
83 
84     [tableView deselectRowAtIndexPath:indexPath animated:YES];
85 }

 

主要集成的关键就是下面几步:

 

//.封装订单模型
AlixPayOrder *order = [[AlixPayOrder alloc] init];
// 生成订单描述
NSString *orderSpec = [order description];

//2.签名
id<DataSigner> signer = CreateRSADataSigner(@“私钥key”);
// 传入订单描述 进行 签名
NSString *signedString = [signer signString:orderSpec];


//3.生成订单字符串
NSString *orderString = [NSString stringWithFormat:@"%@&sign=\"%@\"&sign_type=\"%@\"",
                         orderSpec, signedString, @"RSA"];

//4.调用支付接口
AlixPay * alixpay = [AlixPay shared];
// appScheme:商户自己的协议头
int ret = [alixpay pay:orderString applicationScheme:appScheme];

iOS GPUImage研究总结

Part one: 关于GPUImage
这里直接引用官方描述:
The GPUImage framework is a BSD-licensed iOS library that lets you apply GPU-accelerated filters and other effects to images, live camera video, and movies. In comparison to Core Image (part of iOS 5.0), GPUImage allows you to write your own custom filters, supports deployment to iOS 4.0, and has a simpler interface. However, it currently lacks some of the more advanced features of Core Image, such as facial detection.

内容大意为:GPUImage是使用GPU处理图像的、他可以对图片、实时画面、视频进行处理。他允许你自定义滤镜、支持iOS4.0。然而,目前缺乏核心形象的一些更高级的功能,比如面部检测。

[外链图片转存失败(img-dfiZZwuD-1562823290869)(https://camo.githubusercontent.com/68ce8767f20b6a40f2a695c56396d30234363431/687474703a2f2f73756e7365746c616b65736f6674776172652e636f6d2f73697465732f64656661756c742f66696c65732f475055496d6167654c6f676f2e706e67)]

附一张GPUImage的结构图:

%title插图%num

%title插图%num

Part two:AVFoundation 系列
AVFoundation系列五:关于音视频的导出
AVFoundation系列四:如何配置一个合格的Camera
AVFoundation系列三:音视频编辑
AVFoundation系列二:用AVPlayer播放视频
AVFoundation系列一:AVAsset的使用方法

Part three: 有关GPUImage的研究成果
图像处理之GPUImage图片滤镜
http://blog.csdn.net/xoxo_x/article/details/53507016

图像处理之CPU图片滤镜
http://blog.csdn.net/xoxo_x/article/details/53518322

图像处理之CoreImage图片滤镜
http://blog.csdn.net/xoxo_x/article/details/53518529

iOS GPUImage研究序一:内置滤镜:
http://blog.csdn.net/Xoxo_x/article/details/57082804

iOS GPUImage研究二:捕获图像stillCamera写入相册
http://blog.csdn.net/Xoxo_x/article/details/57086446

iOS GPUImage研究三:视频采集并添加实时滤镜
http://blog.csdn.net/xoxo_x/article/details/58357978

iOS GPUImage研究四:为视频文件添加滤镜
http://blog.csdn.net/Xoxo_x/article/details/58818703

iOS GPUImage研究五:短视频拍摄(滤镜、文件写入)
http://blog.csdn.net/Xoxo_x/article/details/70198469

iOS GPUImage研究六:为视频添加图片水印
http://blog.csdn.net/xoxo_x/article/details/71055867

iOS GPUImage研究七:动态相册初探(水印)
http://blog.csdn.net/xoxo_x/article/details/71076584

将图片保存到本地
http://blog.csdn.net/xoxo_x/article/details/53517878

基于IM实现直播*物效果
http://blog.csdn.net/xoxo_x/article/details/52044388

GPUImage之为视频添加10种原生滤镜
http://blog.csdn.net/xoxo_x/article/details/52749033

GPUImage–流行的美颜滤镜GPUImageBeautifyFilter
http://blog.csdn.net/xoxo_x/article/details/52743107

GPUImage基于OpenGL ES 2.0,比基于CPU的图形和视频处理更快速.

GPUImage把OpenGL ES封装为简洁的Objective-C接口.

Part Three: 有关GPUImage的导入方式
iOS GPUImage研究序二:更简单GPUImage导入方式
http://blog.csdn.net/xoxo_x/article/details/60323297

Part Four: 相关参考资料
https://github.com/Guikunzhi/BeautifyFaceDemo
已经没办法运行,需要cd 文件夹 pod install

http://download.csdn.net/detail/xoxo_x/9642503
我整理过的,去除不需要的东西2016.09.28

2、
http://www.cnblogs.com/salam/p/4980992.html 文章不错

http://blog.csdn.net/jcp312097937/article/details/45849341 文章不错

http://www.ios122.com/2015/08/gpuimage/非常好的文章

3、
http://developer.apple.com/library/ios/samplecode/RosyWriter/RosyWriter.zip

Apple官方对视频流的处理,GPUImage中的VideoCamera部分代码就是根据这个写的

4、http://blog.csdn.net/Xoxo_x/article/details/52523466
我写的不怎么样有时候会蹦,基于coreImage在GPU渲染,图像处理的不好,但能正常运行。

5、http://blog.csdn.net/Xoxo_x/article/details/52523912
coreImage图片处理,看这个就可以,我也是借鉴别人的,但很全

6、
原文链接:http://nshipster.com/gpuimage/ 英文CPU与GPU的比较

https://leafduo.com/articles/2013/05/13/gpuimage/ 中文翻译

7、这个也不错哦https://github.com/loyinglin/GPUImage

GPUImage详细解析(一)
http://url.cn/2FwjqIr
GPUImage详细解析(二)
http://url.cn/2GsW2qg
GPUImage详细解析(三)- 实时美颜滤镜
http://url.cn/2F0SKgR
GPUImage详细解析(四)模糊图片处理
http://url.cn/2GX6JEQ
GPUImage详细解析(五)滤镜视频录制
http://url.cn/2GHCo71
GPUImage详细解析(六)-用视频做视频水印
http://url.cn/2JrhR7V
GPUImage详细解析(七)文字水印和动态图像水印
http://url.cn/2IzRshs
GPUImage详细解析(八)视频合并混音
http://url.cn/2DmUYiA
GPUImage详细解析(九)图像的输入输出和滤镜通道
http://url.cn/2EFjlGp 收起
**
下载GPUImage,地址:https://github.com/BradLarson/GPUImage
共125个滤镜, 分为四类

1、Color adjustments: 31 filters, 颜色处理相关
2、Image processing: 40 filters, 图像处理相关.
3、Blending modes: 29 filters, 混合模式相关.
4、Visual effects: 25 filters, 视觉效果相关.

 

#import <Foundation/Foundation.h>
#import “GPUImageBrightnessFilter.h” //亮度
#import “GPUImageExposureFilter.h” //曝光
#import “GPUImageContrastFilter.h” //对比度
#import “GPUImageSaturationFilter.h” //饱和度
#import “GPUImageGammaFilter.h” //伽马线
#import “GPUImageColorInvertFilter.h” //反色
#import “GPUImageSepiaFilter.h” //褐色(怀旧)
#import “GPUImageLevelsFilter.h” //色阶
#import “GPUImageGrayscaleFilter.h” //灰度
#import “GPUImageHistogramFilter.h” //色彩直方图,显示在图片上
#import “GPUImageHistogramGenerator.h” //色彩直方图
#import “GPUImageRGBFilter.h” //RGB
#import “GPUImageToneCurveFilter.h” //色调曲线
#import “GPUImageMonochromeFilter.h” //单色
#import “GPUImageOpacityFilter.h” //不透明度
#import “GPUImageHighlightShadowFilter.h” //提亮阴影
#import “GPUImageFalseColorFilter.h” //色彩替换(替换亮部和暗部色彩)
#import “GPUImageHueFilter.h” //色度
#import “GPUImageChromaKeyFilter.h” //色度键
#import “GPUImageWhiteBalanceFilter.h” //白平横
#import “GPUImageAverageColor.h” //像素平均色值
#import “GPUImageSolidColorGenerator.h” //纯色
#import “GPUImageLuminosity.h” //亮度平均
#import “GPUImageAverageLuminanceThresholdFilter.h” //像素色值亮度平均,图像黑白(有类似漫画效果)
#import “GPUImageLookupFilter.h” //lookup 色彩调整
#import “GPUImageAmatorkaFilter.h” //Amatorka lookup
#import “GPUImageMissEtikateFilter.h” //MissEtikate lookup
#import “GPUImageSoftEleganceFilter.h” //SoftElegance lookup
#pragma mark – 图像处理 Handle Image
#import “GPUImageCrosshairGenerator.h” //十字
#import “GPUImageLineGenerator.h” //线条
#import “GPUImageTransformFilter.h” //形状变化
#import “GPUImageCropFilter.h” //剪裁
#import “GPUImageSharpenFilter.h” //锐化
#import “GPUImageUnsharpMaskFilter.h” //反遮罩锐化
//#import “GPUImageFastBlurFilter.h” //模糊
#import “GPUImageGaussianBlurFilter.h” //高斯模糊
#import “GPUImageGaussianSelectiveBlurFilter.h” //高斯模糊,选择部分清晰
#import “GPUImageBoxBlurFilter.h” //盒状模糊
#import “GPUImageTiltShiftFilter.h” //条纹模糊,中间清晰,上下两端模糊
#import “GPUImageMedianFilter.h” //中间值,有种稍微模糊边缘的效果
#import “GPUImageBilateralFilter.h” //双边模糊
#import “GPUImageErosionFilter.h” //侵蚀边缘模糊,变黑白
#import “GPUImageRGBErosionFilter.h” //RGB侵蚀边缘模糊,有色彩
#import “GPUImageDilationFilter.h” //扩展边缘模糊,变黑白
#import “GPUImageRGBDilationFilter.h” //RGB扩展边缘模糊,有色彩
#import “GPUImageOpeningFilter.h” //黑白色调模糊
#import “GPUImageRGBOpeningFilter.h” //彩色模糊
#import “GPUImageClosingFilter.h” //黑白色调模糊,暗色会被提亮
#import “GPUImageRGBClosingFilter.h” //彩色模糊,暗色会被提亮
#import “GPUImageLanczosResamplingFilter.h” //Lanczos重取样,模糊效果
#import “GPUImageNonMaximumSuppressionFilter.h” //非*大抑制,只显示亮度*高的像素,其他为黑
#import “GPUImageThresholdedNonMaximumSuppressionFilter.h” //与上相比,像素丢失更多
#import “GPUImageSobelEdgeDetectionFilter.h” //Sobel边缘检测算法(白边,黑内容,有点漫画的反色效果)
#import “GPUImageCannyEdgeDetectionFilter.h” //Canny边缘检测算法(比上更强烈的黑白对比度)
#import “GPUImageThresholdEdgeDetectionFilter.h” //阈值边缘检测(效果与上差别不大)
#import “GPUImagePrewittEdgeDetectionFilter.h” //普瑞维特(Prewitt)边缘检测(效果与Sobel差不多,貌似更平滑)
#import “GPUImageXYDerivativeFilter.h” //XYDerivative边缘检测,画面以蓝色为主,绿色为边缘,带彩色
#import “GPUImageHarrisCornerDetectionFilter.h” //Harris角点检测,会有绿色小十字显示在图片角点处
#import “GPUImageNobleCornerDetectionFilter.h” //Noble角点检测,检测点更多
#import “GPUImageShiTomasiFeatureDetectionFilter.h” //ShiTomasi角点检测,与上差别不大
#import “GPUImageMotionDetector.h” //动作检测
#import “GPUImageHoughTransformLineDetector.h” //线条检测
#import “GPUImageParallelCoordinateLineTransformFilter.h” //平行线检测
#import “GPUImageLocalBinaryPatternFilter.h” //图像黑白化,并有大量噪点
#import “GPUImageLowPassFilter.h” //用于图像加亮
#import “GPUImageHighPassFilter.h” //图像低于某值时显示为黑

#pragma mark – 视觉效果 Visual Effect
#import “GPUImageSketchFilter.h” //素描
#import “GPUImageThresholdSketchFilter.h” //阀值素描,形成有噪点的素描
#import “GPUImageToonFilter.h” //卡通效果(黑色粗线描边)
#import “GPUImageSmoothToonFilter.h” //相比上面的效果更细腻,上面是粗旷的画风
#import “GPUImageKuwaharaFilter.h” //桑原(Kuwahara)滤波,水粉画的模糊效果;处理时间比较长,慎用
#import “GPUImageMosaicFilter.h” //黑白马赛克
#import “GPUImagePixellateFilter.h” //像素化
#import “GPUImagePolarPixellateFilter.h” //同心圆像素化
#import “GPUImageCrosshatchFilter.h” //交叉线阴影,形成黑白网状画面
#import “GPUImageColorPackingFilter.h” //色彩丢失,模糊(类似监控摄像效果)

#import “GPUImageVignetteFilter.h” //晕影,形成黑色圆形边缘,突出中间图像的效果
#import “GPUImageSwirlFilter.h” //漩涡,中间形成卷曲的画面
#import “GPUImageBulgeDistortionFilter.h” //凸起失真,鱼眼效果
#import “GPUImagePinchDistortionFilter.h” //收缩失真,凹面镜
#import “GPUImageStretchDistortionFilter.h” //伸展失真,哈哈镜
#import “GPUImageGlassSphereFilter.h” //水晶球效果
#import “GPUImageSphereRefractionFilter.h” //球形折射,图形倒立

#import “GPUImagePosterizeFilter.h” //色调分离,形成噪点效果
#import “GPUImageCGAColorspaceFilter.h” //CGA色彩滤镜,形成黑、浅蓝、紫色块的画面
#import “GPUImagePerlinNoiseFilter.h” //柏林噪点,花边噪点
#import “GPUImage3x3ConvolutionFilter.h” //3×3卷积,高亮大色块变黑,加亮边缘、线条等
#import “GPUImageEmbossFilter.h” //浮雕效果,带有点3d的感觉
#import “GPUImagePolkaDotFilter.h” //像素圆点花样
#import “GPUImageHalftoneFilter.h” //点染,图像黑白化,由黑点构成原图的大致图形

#pragma mark – 混合模式 Blend

#import “GPUImageMultiplyBlendFilter.h” //通常用于创建阴影和深度效果
#import “GPUImageNormalBlendFilter.h” //正常
#import “GPUImageAlphaBlendFilter.h” //透明混合,通常用于在背景上应用前景的透明度
#import “GPUImageDissolveBlendFilter.h” //溶解
#import “GPUImageOverlayBlendFilter.h” //叠加,通常用于创建阴影效果
#import “GPUImageDarkenBlendFilter.h” //加深混合,通常用于重叠类型
#import “GPUImageLightenBlendFilter.h” //减淡混合,通常用于重叠类型
#import “GPUImageSourceOverBlendFilter.h” //源混合
#import “GPUImageColorBurnBlendFilter.h” //色彩加深混合
#import “GPUImageColorDodgeBlendFilter.h” //色彩减淡混合
#import “GPUImageScreenBlendFilter.h” //屏幕包裹,通常用于创建亮点和镜头眩光
#import “GPUImageExclusionBlendFilter.h” //排除混合
#import “GPUImageDifferenceBlendFilter.h” //差异混合,通常用于创建更多变动的颜色
#import “GPUImageSubtractBlendFilter.h” //差值混合,通常用于创建两个图像之间的动画变暗模糊效果
#import “GPUImageHardLightBlendFilter.h” //强光混合,通常用于创建阴影效果
#import “GPUImageSoftLightBlendFilter.h” //柔光混合
#import “GPUImageChromaKeyBlendFilter.h” //色度键混合
#import “GPUImageMaskFilter.h” //遮罩混合
#import “GPUImageHazeFilter.h” //朦胧加暗
#import “GPUImageLuminanceThresholdFilter.h” //亮度阈
#import “GPUImageAdaptiveThresholdFilter.h” //自适应阈值
#import “GPUImageAddBlendFilter.h” //通常用于创建两个图像之间的动画变亮模糊效果
#import “GPUImageDivideBlendFilter.h” //通常用于创建两个图像之间的动画变暗模糊效果

iOS GPUImage源码解读

前言

GPUImage是iOS上一个基于OpenGL进行图像处理的开源框架,内置大量滤镜,架构灵活,可以在其基础上很轻松地实现各种图像处理功能。本文主要向大家分享一下项目的核心架构、源码解读及使用心得。

GPUImage有哪些特性

  1. 丰富的输入组件 摄像头、图片、视频、OpenGL纹理、二进制数据、UIElement(UIView, CALayer)
  2. 大量现成的内置滤镜(4大类) 1). 颜色类(亮度、色度、饱和度、对比度、曲线、白平衡…) 2). 图像类(仿射变换、裁剪、高斯模糊、毛玻璃效果…) 3). 颜色混合类(差异混合、alpha混合、遮罩混合…) 4). 效果类(像素化、素描效果、压花效果、球形玻璃效果…)
  3. 丰富的输出组件 UIView、视频文件、GPU纹理、二进制数据
  4. 灵活的滤镜链 滤镜效果之间可以相互串联、并联,调用管理相当灵活。
  5. 接口易用 滤镜和OpenGL资源的创建及使用都做了统一的封装,简单易用,并且内置了一个cache模块实现了framebuffer的复用。
  6. 线程管理 OpenGLContext不是多线程安全的,GPUImage创建了专门的contextQueue,所有的滤镜都会扔到统一的线程中处理。
  7. 轻松实现自定义滤镜效果 继承GPUImageFilter自动获得上面全部特性,无需关注上下文的环境搭建,专注于效果的核心算法实现即可。

基本用法

// 获取一张图片
UIImage *inputImage = [UIImage imageNamed:@"sample.jpg"];
// 创建图片输入组件GPUImagePicture *sourcePicture = [[GPUImagePicture alloc] initWithImage:inputImage smoothlyScaleOutput:YES]; 
// 创建素描滤镜
GPUImageSketchFilter *customFilter = [[GPUImageSketchFilter alloc] init]; 
// 把素描滤镜串联在图片输入组件之后
[sourcePicture addTarget:customFilter];
// 创建ImageView输出组件GPUImageView *imageView = [[GPUImageView alloc]initWithFrame:mainScreenFrame];
[self.view addSubView:imageView];
// 把ImageView输出组件串在滤镜链末尾[customFilter addTarget:imageView];
// 调用图片输入组件的process方法,渲染结果就会绘制到imageView上[sourcePicture processImage];

效果如图:

%title插图%num%title插图%num

整个框架的目录结构

%title插图%num%title插图%num

核心架构

%title插图%num%title插图%num

基本上每个滤镜都继承自GPUImageFilter; 而GPUImageFilter作为整套框架的核心; 接收一个GPUImageFrameBuffer输入; 调用GLProgram渲染处理; 输出一个GPUImageFrameBuffer; 把输出的GPUImageFrameBuffer传给通过targets属性关联的下级滤镜; 直到传递至*终的输出组件;

核心架构可以整体划分为三块:输入、滤镜处理、输出 接下来我们就深入源码,看看GPUImage是如何获取数据、传递数据、处理数据和输出数据的

获取数据

GPUImage提供了多种不同的输入组件,但是无论是哪种输入源,获取数据的本质都是把图像数据转换成OpenGL纹理。这里就以视频拍摄组件(GPUImageVideoCamera)为例,来讲讲GPUImage是如何把每帧采样数据传入到GPU的。

GPUImageVideoCamera里大部分代码都是对摄像头的调用管理,不了解的同学可以去学习一下AVFoundation(传送门)。摄像头拍摄过程中每一帧都会有一个数据回调,在GPUImageVideoCamera中对应的处理回调的方法为:

- (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer;

iOS的每一帧摄像头采样数据都会封装成CMSampleBufferRef; CMSampleBufferRef除了包含图像数据、还包含一些格式信息、图像宽高、时间戳等额外属性; 摄像头默认的采样格式为YUV420,关于YUV格式大家可以自行搜索学习一下(传送门):

%title插图%num%title插图%num

YUV420按照数据的存储方式又可以细分成若干种格式,这里主要是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange和kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange两种;

两种格式都是planar类型的存储方式,y数据和uv数据分开放在两个plane中; 这样的数据没法直接传给GPU去用,GPUImageVideoCamera把两个plane的数据分别取出:

- (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer {   
 // 一大坨的代码用于获取采样数据的基本属性(宽、高、格式等等) 
    ......    if ([GPUImageContext supportsFastTextureUpload] && captureAsYUV) {   
      CVOpenGLESTextureRef luminanceTextureRef = NULL;         
      CVOpenGLESTextureRef chrominanceTextureRef = NULL;        
if (CVPixelBufferGetPlaneCount(cameraFrame) > 0) // Check for YUV planar inputs to do RGB conversion                    {
                           ......
// 从cameraFrame的plane-0提取y通道的数据,填充到luminanceTextureRef            
          glActiveTexture(GL_TEXTURE4); 
          err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], cameraFrame, NULL, GL_TEXTURE_2D, GL_LUMINANCE, bufferWidth, bufferHeight, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0, &luminanceTextureRef);   
          ......                  
       // 从cameraFrame的plane-1提取uv通道的数据,填充到chrominanceTextureRef              
       glActiveTexture(GL_TEXTURE5);   
       err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], cameraFrame, NULL, GL_TEXTURE_2D, GL_LUMINANCE_ALPHA, bufferWidth/2, bufferHeight/2, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, 1, &chrominanceTextureRef);          
            ......            
            // 把luminance和chrominance作为2个独立的纹理传入GPU 
            [self convertYUVToRGBOutput];   
             ......       
            }  
         } else {
               ...... 
          } 
  }

注意CVOpenGLESTextureCacheCreateTextureFromImage中对于internalFormat的设置; 通常我们创建一般纹理的时候都会设成GL_RGBA,传入的图像数据也会是rgba格式的; 而这里y数据因为只包含一个通道,所以设成了GL_LUMINANCE(灰度图); uv数据则包含2个通道,所以设成了GL_LUMINANCE_ALPHA(带alpha的灰度图); 另外uv纹理的宽高只设成了图像宽高的一半,这是因为yuv420中,每个相邻的2×2格子共用一份uv数据; 数据传到GPU纹理后,再通过一个颜色转换(yuv->rgb)的shader(shader是OpenGL可编程着色器,可以理解为GPU侧的代码,关于shader需要一些OpenGL编程基础(传送门)),绘制到目标纹理:

 // fullrange
 varying highp vec2 textureCoordinate;
 uniform sampler2D luminanceTexture;
 uniform sampler2D chrominanceTexture;
 uniform mediump mat3 colorConversionMatrix; 
 void main() {
     mediump vec3 yuv;
     lowp vec3 rgb;
     yuv.x = texture2D(luminanceTexture, textureCoordinate).r;
     yuv.yz = texture2D(chrominanceTexture, textureCoordinate).ra - vec2(0.5, 0.5);
     rgb = colorConversionMatrix * yuv;
     gl_FragColor = vec4(rgb, 1);
 }
 // videorange
 varying highp vec2 textureCoordinate;
 uniform sampler2D luminanceTexture;
 uniform sampler2D chrominanceTexture;
 uniform mediump mat3 colorConversionMatrix; void main() {
     mediump vec3 yuv;
     lowp vec3 rgb;
     yuv.x = texture2D(luminanceTexture, textureCoordinate).r - (16.0/255.0);
     yuv.yz = texture2D(chrominanceTexture, textureCoordinate).ra - vec2(0.5, 0.5);
     rgb = colorConversionMatrix * yuv;
     gl_FragColor = vec4(rgb, 1);
 }

注意yuv420fullrange和yuv420videorange的数值范围是不同的,因此转换公式也不同,这里会有2个颜色转换shader,根据实际的采样格式选择正确的shader; 渲染输出到目标纹理后就得到一个转换成rgb格式的GPU纹理,完成了获取输入数据的工作;

传递数据

GPUImage的图像处理过程,被设计成了滤镜链的形式;输入组件、效果滤镜、输出组件串联在一起,每次推动渲染的时候,输入数据就会按顺序传递,经过处理,*终输出。

%title插图%num%title插图%num

GPUImage设计了一个GPUImageInput协议,定义了GPUImageFilter之间传入数据的方法:

- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)textureIndex {
    firstInputFramebuffer = newInputFramebuffer;
    [firstInputFramebuffer lock];
}

firstInputFramebuffer属性用来保存输入纹理; GPUImageFilter作为单输入滤镜基类遵守了GPUImageInput协议,GPUImage还提供了GPUImageTwoInputFilter, GPUImageThreeInputFilter等多输入filter的基类。

这里还有一个很重要的入口方法用于推动数据流转:

- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex {
    ......
    
    [self renderToTextureWithVertices:imageVertices textureCoordinates:[[self class] textureCoordinatesForRotation:inputRotation]];

    [self informTargetsAboutNewFrameAtTime:frameTime];
}

每个滤镜都是由这个入口方法开始启动,这个方法包含2个调用 1). 首先调用render方法进行效果渲染 2). 调用informTargets方法将渲染结果推到下级滤镜

GPUImageFilter继承自GPUImageOutput,定义了输出数据,向后传递的方法:

- (void)notifyTargetsAboutNewOutputTexture;

但是这里比较奇怪的是滤镜链的传递实际并没有用notifyTargets方法,而是用了前面提到的informTargets方法:

- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime {
    ......    
    // Get all targets the framebuffer so they can grab a lock on it
    for (id<GPUImageInput> currentTarget in targets) {        if (currentTarget != self.targetToIgnoreForUpdates) {            NSInteger indexOfObject = [targets indexOfObject:currentTarget];            NSInteger textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue];
            [self setInputFramebufferForTarget:currentTarget atIndex:textureIndex];
            [currentTarget setInputSize:[self outputFrameSize] atIndex:textureIndex];
        }
    }
    
    ......    
    // Trigger processing last, so that our unlock comes first in serial execution, avoiding the need for a callback
    for (id<GPUImageInput> currentTarget in targets) {        if (currentTarget != self.targetToIgnoreForUpdates) {            NSInteger indexOfObject = [targets indexOfObject:currentTarget];            NSInteger textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue];
            [currentTarget newFrameReadyAtTime:frameTime atIndex:textureIndex];
        }
    }
}

GPUImageOutput定义了一个targets属性来保存下一级滤镜,这里可以注意到targets是个数组,因此滤镜链也支持并联结构。可以看到这个方法主要做了2件事情: 1). 对每个target调用setInputFramebuffer方法把自己的渲染结果传给下级滤镜作为输入 2). 对每个target调用newFrameReadyAtTime方法推动下级滤镜启动渲染 滤镜之间通过targets属性相互衔接串在一起,完成了数据传递工作。

%title插图%num

%title插图%num

处理数据

前面提到的renderToTextureWithVertices:方法便是每个滤镜必经的渲染入口。 每个滤镜都可以设置自己的shader,重写该渲染方法,实现自己的效果:

- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates {
    ......

    [GPUImageContext setActiveShaderProgram:filterProgram];

    outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] textureOptions:self.outputTextureOptions onlyTexture:NO];
    [outputFramebuffer activateFramebuffer];
    ......

    [self setUniformsForProgramAtIndex:0];
    
    glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha);
    glClear(GL_COLOR_BUFFER_BIT);

    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
    glUniform1i(filterInputTextureUniform, 2);	

    glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices);
    glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    
    ......
}

上面这个是GPUImageFilter的默认方法,大致做了这么几件事情: 1). 向frameBufferCache申请一个outputFrameBuffer 2). 将申请得到的outputFrameBuffer激活并设为渲染对象 3). glClear清除画布 4). 设置输入纹理 5). 传入顶点 6). 传入纹理坐标 7). 调用绘制方法

再来看看GPUImageFilter使用的默认shader:

 // vertex shader
 attribute vec4 position;
 attribute vec4 inputTextureCoordinate;
 varying vec2 textureCoordinate; void main() {
     gl_Position = position;
     textureCoordinate = inputTextureCoordinate.xy;
 }
 // fragment shader
 varying highp vec2 textureCoordinate;
 uniform sampler2D inputImageTexture; void main() {
     gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
 }

这个shader实际上啥也没做,VertexShader(顶点着色器)就是把传入的顶点坐标和纹理坐标原样传给FragmentShader,FragmentShader(片段着色器)就是从纹理取出原始色值直接输出,*终效果就是把图片原样渲染到画面。

输出数据

比较常用的主要是GPUImageView和GPUImageMovieWriter。

GPUImageView继承自UIView,用于实时预览,用法非常简单 1). 创建GPUImageView 2). 串入滤镜链 3). 插到视图里去 UIView的contentMode、hidden、backgroundColor等属性都可以正常使用 里面比较关键的方法主要有这么2个:

// 申明自己的CALayer为CAEAGLLayer+ (Class)layerClass  {    return [CAEAGLLayer class];
}
- (void)createDisplayFramebuffer {
    [GPUImageContext useImageProcessingContext];
    
    glGenFramebuffers(1, &displayFramebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, displayFramebuffer);
	
    glGenRenderbuffers(1, &displayRenderbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, displayRenderbuffer);
	
    [[[GPUImageContext sharedImageProcessingContext] context] renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer];
	
    GLint backingWidth, backingHeight;

    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight);
    
    ......

    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, displayRenderbuffer);
	
    ......
}

创建frameBuffer和renderBuffer时把renderBuffer和CALayer关联在一起; 这是iOS内建的一种GPU渲染输出的联动方法; 这样newFrameReadyAtTime渲染过后画面就会输出到CALayer。

GPUImageMovieWriter主要用于将视频输出到磁盘; 里面大量的代码都是在设置和使用AVAssetWriter,不了解的同学还是得去看AVFoundation; 这里主要是重写了newFrameReadyAtTime:方法:

- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex {
    ......

    GPUImageFramebuffer *inputFramebufferForBlock = firstInputFramebuffer;
    glFinish();

    runAsynchronouslyOnContextQueue(_movieWriterContext, ^{
        ......        
        // Render the frame with swizzled colors, so that they can be uploaded quickly as BGRA frames
        [_movieWriterContext useAsCurrentContext];
        [self renderAtInternalSizeUsingFramebuffer:inputFramebufferForBlock];
        
        CVPixelBufferRef pixel_buffer = NULL;        
        if ([GPUImageContext supportsFastTextureUpload]) {
            pixel_buffer = renderTarget;
            CVPixelBufferLockBaseAddress(pixel_buffer, 0);
        } else {
            CVReturn status = CVPixelBufferPoolCreatePixelBuffer (NULL, [assetWriterPixelBufferInput pixelBufferPool], &pixel_buffer);            if ((pixel_buffer == NULL) || (status != kCVReturnSuccess)) {
                CVPixelBufferRelease(pixel_buffer);                return;
            } else {
                CVPixelBufferLockBaseAddress(pixel_buffer, 0);
                
                GLubyte *pixelBufferData = (GLubyte *)CVPixelBufferGetBaseAddress(pixel_buffer);
                glReadPixels(0, 0, videoSize.width, videoSize.height, GL_RGBA, GL_UNSIGNED_BYTE, pixelBufferData);
            }
        }
        
        ......        [assetWriterPixelBufferInput appendPixelBuffer:pixel_buffer];        ......
    });
}

这里有几个地方值得注意: 1). 在取数据之前先调了一下glFinish,CPU和GPU之间是类似于client-server的关系,CPU侧调用OpenGL命令后并不是同步等待OpenGL完成渲染再继续执行的,而glFinish命令可以确保OpenGL把队列中的命令都渲染完再继续执行,这样可以保证后面取到的数据是正确的当次渲染结果。 2). 取数据时用了supportsFastTextureUpload判断,这是个从iOS5开始支持的一种CVOpenGLESTextureCacheRef和CVImageBufferRef的映射(映射的创建可以参看获取数据中的CVOpenGLESTextureCacheCreateTextureFromImage),通过这个映射可以直接拿到CVPixelBufferRef而不需要再用glReadPixel来读取数据,这样性能更好。

*后归纳一下本文涉及到的知识点

1. AVFoundation 摄像头调用、输出视频都会用到AVFoundation 2. YUV420 视频采集的数据格式 3. OpenGL shader GPU的可编程着色器 4. CAEAGLLayer iOS内建的GPU到屏幕的联动方法 5. fastTextureUpload iOS5开始支持的一种CVOpenGLESTextureCacheRef和CVImageBufferRef的映射

官网 webrtc_IOS 源码下载和编译

iOS

Development environment

An OS X machine is required for iOS development. While it’s possible to develop purely from the command line and text editors, it’s easiest to use XCode. Both methods will be illustrated here.

Getting the code

  1. Install Prerequisite software
  2. Set the target OS in your environment:
    export GYP_DEFINES="OS=ios"
  3. Create a working directory, enter it, and run:
    fetch --nohooks webrtc_ios
    gclient sync

    This will fetch a regular WebRTC checkout with the iOS-specific parts added. The same checkout can be used for both Mac and iOS development, depending on the OS you set in GYP_DEFINES (see above).

  4. You may want to disable Spotlight indexing for the checkout to speed up file operations.
See  Development  for generic instructions on how to update the code in your checkout.

Compiling the code

GYP is used to generate build instructions for ninja from the relevant .gyp files. Ninja is used to compile the source using the previously generated instructions. In order to configure GYP to generate build files for iOS certain environment variables need to be set. Those variables can be edited for the various build configurations as needed.
Building for iOS device:

export GYP_CROSSCOMPILE=1
export GYP_DEFINES="OS=ios target_arch=arm"
export GYP_GENERATOR_FLAGS="output_dir=out_ios"
export GYP_GENERATORS=ninja

Building for 64-bit iOS device:
As above, except with:

export GYP_DEFINES="OS=ios target_arch=arm64"
export GYP_GENERATOR_FLAGS="output_dir=out_ios64"

Building for simulator:
As above, except with:

export GYP_DEFINES="OS=ios target_arch=ia32"
export GYP_GENERATOR_FLAGS="output_dir=out_sim"

Building for 64-bit simulator:
As above, except with:

export GYP_DEFINES="OS=ios target_arch=x64"
export GYP_GENERATOR_FLAGS="output_dir=out_sim"
 

Building for OSX:
As above, except with:

export GYP_DEFINES="OS=mac target_arch=x64"
export GYP_GENERATOR_FLAGS="output_dir=out_mac"

Note that you can set output_dir to whatever you’d like. It will be created under src/.
Now run the gyp generator script from the source root (<working directory>/src):

webrtc/build/gyp_webrtc

Now to compile, just run ninja on the appropriate target. E.g.

ninja -C out_ios/Debug-iphoneos AppRTCDemo
ninja -C out_ios/Release-iphoneos AppRTCDemo
ninja -C out_sim/Debug-iphonesimulator AppRTCDemo

For interesting targets to build, see the .gyp files in webrtc/webrtc.gyp, webrtc/webrtc_examples.gyp, talk/libjingle.gyp, talk/libjingle_examples.gyp.

Some sample scripts are also available at talk/app/webrtc/objc/README.

Compiling with XCode

Compiling with XCode is not supported! What we do instead is compile using a script that runs ninja from XCode. In order to generate the relevant xcode project, add xcode-ninja to GYP_GENERATORS along with the targets you’re interested in. By using XCode in this manner, we get the build speed of ninja while at the same time getting access to the usual methods of deployment/debugging for iOS.

export GYP_GENERATOR_FLAGS="xcode_project_version=3.2 xcode_ninja_target_pattern=All_iOS xcode_ninja_executable_target_pattern=AppRTCDemo|libjingle_peerconnection_unittest|libjingle_peerconnection_objc_test output_dir=out_ios"

export GYP_GENERATORS="ninja,xcode-ninja"

When running the generator script, you should see an all.ninja.xcworkspace file. You should be able to select the desired target and platform in the XCode usual fashion and build / deploy. Note that you will need to rerun the GYP generator if you want to switch target platforms.

Deploying to device

It’s easiest to deploy to a device using XCode in xcode-ninja mode. Other command line tools exist as well, e.g. ios-deploy.

 在Python中如何实现单例模式。

 在Python中如何实现单例模式。
点评:这个题目在面试中出现的频率*高,因为它考察的不仅仅是单例模式,更是对Python语言到底掌握到何种程度,建议大家用装饰器和元类这两种方式来实现单例模式,因为这两种方式的通用性*强,而且也可以顺便展示自己对装饰器和元类中两个关键知识点的理解。
方法一:使用装饰器实现单例模式。
from functools import wraps
def singleton(cls):
    “””单例类装饰器”””
    instances = {}
    @wraps(cls)
    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper
@singleton
class President:
    pass
扩展:装饰器是Python中非常有特色的语法,用一个函数去装饰另一个函数或类,为其添加额外的能力。通常通过装饰来实现的功能都属横切关注功能,也就是跟正常的业务逻辑没有必然联系,可以动态添加或移除的功能。装饰器可以为代码提供缓存、代理、上下文环境等服务,它是对设计模式中代理模式的践行。在写装饰器的时候,带装饰功能的函数(上面代码中的wrapper函数)通常都会用functools模块中的wraps再加以装饰,这个装饰器*重要的作用是给被装饰的类或函数动态添加一个__wrapped__属性,这个属性会将被装饰之前的类或函数保留下来,这样在我们不需要装饰功能的时候,可以通过它来取消装饰器,例如可以使用President = President.__wrapped__来取消对President类做的单例处理。需要提醒大家的是:上面的单例并不是线程安全的,如果要做到线程安全,需要对创建对象的代码进行加锁的处理。在Python中可以使用threading模块的RLock对象来提供锁,可以使用锁对象的acquire和release方法来实现加锁和解锁的操作。当然,更为简便的做法是使用锁对象的with上下文语法来进行隐式的加锁和解锁操作。
方法二:使用元类实现单例模式。
class SingletonMeta(type):
    “””自定义单例元类”””
    def __init__(cls, *args, **kwargs):
        cls.__instance = None
        super().__init__(*args, **kwargs)
    def __call__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super().__call__(*args, **kwargs)
        return cls.__instance
class President(metaclass=SingletonMeta):
    pass
扩展:Python是面向对象的编程语言,在面向对象的世界中,一切皆为对象。对象是通过类来创建的,而类本身也是对象,类这样的对象是通过元类来创建的。我们在定义类时,如果没有给一个类指定父类,那么默认的父类是object,如果没有给一个类指定元类,那么默认的元类是type。通过自定义的元类,我们可以改变一个类默认的行为,就如同上面的代码中,我们通过元类的__call__魔术方法,改变了President类的构造器那样。
关于单例模式,在面试中还有可能被问到它的应用场景。通常一个对象的状态是被其他对象共享的,就可以将其设计为单例,例如项目中使用的数据库连接池对象和配置对象通常都是单例,这样才能保证所有地方获取到的数据库连接和配置信息是完全一致的;而且由于对象只有唯一的实例,因此从根本上避免了重复创建对象造成的时间和空间上的开销,也避免了对资源的多重占用。再举个例子,项目中的日志操作通常也会使用单例模式,这是因为共享的日志文件一直处于打开状态,只能有一个实例去操作它,否则在写入日志的时候会产生混乱。
题目002:不使用中间变量,交换两个变量a和b的值。
点评:典型的送人头的题目,在其他编程语言中不使用中间变量交换两个变量的值可以使用异或运算,Python中还可以通过内置的字节码指令直接交换两个变量的值。
方法一:
a = a ^ b
b = a ^ b
a = a ^ b
方法二:
a, b = b, a
扩展:需要注意,a, b = b, a这种做法其实并不是元组解包,虽然很多人都这样认为。Python字节码指令中有ROT_TWO指令来支持这个操作,类似的还有ROT_THREE,对于3个以上的元素,如a, b, c, d = b, c, d, a,才会用到创建元组和元组解包。想知道你的代码对应的字节码指令,可以使用Python标准库中dis模块的dis函数来反汇编你的Python代码。
题目003:写一个删除列表中重复元素的函数,要求去重后元素相对位置保持不变。
点评:这个题目在初中级Python岗位面试的时候经常出现,题目源于《Python Cookbook》这本书*章的第10个问题,有很多面试题其实都是这本书上的原题,所以建议大家有时间的话好好研读一下这本书。
def dedup(items):
    no_dup_items = []
    seen = set()
    for item in items:
        if item not in seen:
            no_dup_items.append(item)
            seen.add(item)
    return no_dup_items
当然,也可以像《Python Cookbook》书上的代码那样,把上面的函数改造成一个生成器。
def dedup(items):
    seen = set()
    for item in items:
        if item not in seen:
            yield item
            seen.add(item)
扩展:由于Python中的集合底层使用哈希存储,所以集合的in和not in成员运算在性能上远远优于列表,所以上面的代码我们使用了集合来保存已经出现过的元素。集合中的元素必须是hashable对象,因此上面的代码在列表元素不是hashable对象时会失效,要解决这个问题可以给函数增加一个参数,该参数可以设计为返回哈希码或hashable对象的函数。
题目004:假设你使用的是官方的CPython,说出下面代码的运行结果。
点评:下面的程序对实际开发并没有什么意义,但却是CPython中的一个大坑,这道题旨在考察面试者对官方的Python解释器到底了解到什么程度。
a, b, c, d = 1, 1, 1000, 1000
print(a is b, c is d)
def foo():
    e = 1000
    f = 1000
    print(e is f, e is d)
    g = 1
    print(g is a)
foo()
结果:
True False
True False
True
上面代码中a is b的结果是True但c is d的结果是False,这一点的确让人费解。这个结果是因为CPython出于性能优化的考虑,把频繁使用的整数对象用一个叫small_ints的对象池缓存起来造成的。small_ints缓存的整数值被设定为[-5, 256]这个区间,也就是说,如果使用CPython解释器,在任何引用这些整数的地方,都不需要重新创建int对象,而是直接引用缓存池中的对象。如果整数不在该范围内,那么即便两个整数的值相同,它们也是不同的对象。
CPython底层为了进一步提升性能还做了一个设定:对于同一个代码块中值不在small_ints缓存范围之内的整数,如果同一个代码块中已经存在一个值与其相同的整数对象,那么就直接引用该对象,否则创建新的int对象。需要大家注意的是,这条规则对数值型适用,但对字符串则需要考虑字符串的长度,这一点可以自行证明。
扩展:如果你用PyPy(另一种Python解释器实现,支持JIT,对CPython的缺点进行了改良,在性能上优于CPython,但对三方库的支持略差)来运行上面的代码,你会发现所有的输出都是True。
题目005:Lambda函数是什么,举例说明的它的应用场景。
点评:这个题目主要想考察的是Lambda函数的应用场景,潜台词是问你在项目中有没有使用过Lambda函数,具体在什么场景下会用到Lambda函数,借此来判断你写代码的能力。因为Lambda函数通常用在高阶函数中,主要的作用是通过传入或返回函数实现代码的解耦合。
Lambda函数也叫匿名函数,它功能简单用一行代码就能实现的小型函数。Python中的Lambda函数只能写一个表达式,这个表达式的执行结果就是函数的返回值,不用写return关键字。Lambda函数因为没有名字,所以也不会跟其他函数发生命名冲突的问题。
面试的时候有可能还会考你用Lambda函数来实现一些功能,也就是用一行代码来实现题目要求的功能,例如:用一行代码实现求阶乘的函数,用一行代码实现求*大公约数的函数等。
fac = lambda x: __import__(‘functools’).reduce(int.__mul__, range(1, x + 1), 1)
gcd = lambda x, y: y % x and gcd(y % x, x) or x
Lambda函数其实*为主要的用途是把一个函数传入另一个高阶函数(如Python内置的filter、map等)中来为函数做解耦合,增强函数的灵活性和通用性。下面的例子通过使用filter和map函数,实现了从列表中筛选出奇数并求平方构成新列表的操作,因为用到了高阶函数,过滤和映射数据的规则都是函数的调用者通过另外一个函数传入的,因此这filter和map函数没有跟特定的过滤和映射数据的规则耦合在一起。
items = [12, 5, 7, 10, 8, 19]
items = list(map(lambda x: x ** 2, filter(lambda x: x % 2, items)))
print(items)    # [25, 49, 361]
当然,用列表的生成式来实现上面的代码更加简单明了,如下所示。
items = [12, 5, 7, 10, 8, 19]
items = [x ** 2 for x in items if x % 2]
print(items)    # [25, 49, 361]

说说Python中的浅拷贝和深拷贝。

说说Python中的浅拷贝和深拷贝。
点评:这个题目本身出现的频率非常高,但是就题论题而言没有什么技术含量。对于这种面试题,在回答的时候一定要让你的答案能够超出面试官的预期,这样才能获得更好的印象分。所以回答这个题目的要点不仅仅是能够说出浅拷贝和深拷贝的区别,深拷贝的时候可能遇到的两大问题,还要说出Python标准库对浅拷贝和深拷贝的支持,然后可以说说列表、字典如何实现拷贝操作以及如何通过序列化和反序列的方式实现深拷贝,*后还可以提到设计模式中的原型模式以及它在项目中的应用。
浅拷贝通常只复制对象本身,而深拷贝不仅会复制对象,还会递归的复制对象所关联的对象。深拷贝可能会遇到两个问题:一是一个对象如果直接或间接的引用了自身,会导致无休止的递归拷贝;二是深拷贝可能对原本设计为多个对象共享的数据也进行拷贝。Python通过copy模块中的copy和deepcopy函数来实现浅拷贝和深拷贝操作,其中deepcopy可以通过memo字典来保存已经拷贝过的对象,从而避免刚才所说的自引用递归问题;此外,可以通过copyreg模块的pickle函数来定制指定类型对象的拷贝行为。
deepcopy函数的本质其实就是对象的一次序列化和一次返回序列化,面试题中还考过用自定义函数实现对象的深拷贝操作,显然我们可以使用pickle模块的dumps和loads来做到,代码如下所示。
import pickle
my_deep_copy = lambda obj: pickle.loads(pickle.dumps(obj))
列表的切片操作[:]相当于实现了列表对象的浅拷贝,而字典的copy方法可以实现字典对象的浅拷贝。对象拷贝其实是更为快捷的创建对象的方式。在Python中,通过构造器创建对象属于两阶段构造,首先是分配内存空间,然后是初始化。在创建对象时,我们也可以基于“原型”的对象来创建新对象,通过对原型对象的拷贝(复制内存)就完成了对象的创建和初始化,这种做法其实更加高效,这也就是设计模式中的原型模式。我们可以通过元类的方式来实现原型模式,代码如下所示。
import copy
class PrototypeMeta(type):
    “””实现原型模式的元类”””
    def __init__(cls, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为对象绑定clone方法来实现对象拷贝
        cls.clone = lambda self, is_deep=True: \
            copy.deepcopy(self) if is_deep else copy.copy(self)
class Person(metaclass=PrototypeMeta):
    pass
p1 = Person()
p2 = p1.clone()                 # 深拷贝
p3 = p1.clone(is_deep=False)    # 浅拷贝
题目007:Python是如何实现内存管理的?
点评:当面试官问到这个问题的时候,一个展示自己的机会就摆在面前了。你要先反问面试官:“你说的是官方的CPython解释器吗?”。这个反问可以展示出你了解过Python解释器的不同的实现版本,而且你也知道面试官想问的是CPython。当然,很多面试官对不同的Python解释器底层实现到底有什么差别也没有概念。所以,千万不要觉得面试官一定比你强,怀揣着这份自信可以让你更好的完成面试。
Python提供了自动化的内存管理,也就是说内存空间的分配与释放都是由Python解释器在运行时自动进行的,自动管理内存功能*大的减轻程序员的工作负担,也能够帮助程序员在一定程度上解决内存泄露的问题。以CPython解释器为例,它的内存管理有三个关键点:引用计数、标记清理、分代收集。
引用计数:对于CPython解释器来说,Python中的每一个对象其实就是PyObject结构体,它的内部有一个名为ob_refcnt 的引用计数器成员变量。程序在运行的过程中ob_refcnt的值会被更新并藉此来反映引用有多少个变量引用到该对象。当对象的引用计数值为0时,它的内存就会被释放掉。
typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;
以下情况会导致引用计数加1:
对象被创建
对象被引用
对象作为参数传入到一个函数中
对象作为元素存储到一个容器中
以下情况会导致引用计数减1:
用del语句显示删除对象引用
对象引用被重新赋值其他对象
一个对象离开它所在的作用域
持有该对象的容器自身被销毁
持有该对象的容器删除该对象
可以通过sys模块的getrefcount函数来获得对象的引用计数。引用计数的内存管理方式在遇到循环引用的时候就会出现致命伤,因此需要其他的垃圾回收算法对其进行补充。
标记清理:CPython使用了“标记-清理”(Mark and Sweep)算法解决容器类型可能产生的循环引用问题。该算法在垃圾回收时分为两个阶段:标记阶段,遍历所有的对象,如果对象是可达的(被其他对象引用),那么就标记该对象为可达;清除阶段,再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收。CPython底层维护了两个双端链表,一个链表存放着需要被扫描的容器对象(姑且称之为链表A),另一个链表存放着临时不可达对象(姑且称之为链表B)。为了实现“标记-清理”算法,链表中的每个节点除了有记录当前引用计数的ref_count变量外,还有一个gc_ref变量,这个gc_ref是ref_count的一个副本,所以初始值为ref_count的大小。执行垃圾回收时,首先遍历链表A中的节点,并且将当前对象所引用的所有对象的gc_ref减1,这一步主要作用是解除循环引用对引用计数的影响。再次遍历链表A中的节点,如果节点的gc_ref值为0,那么这个对象就被标记为“暂时不可达” (
GC_TENTATIVELY_UNREACHABLE) 并被移动到链表B中;如果节点的gc_ref不为0,那么这个对象就会被标记为“可达“ (GC_REACHABLE),对于”可达“对象,还要递归的将该节点可以到达的节点标记为”可达“;链表B中被标记为”可达“的节点要重新放回到链表A中。在两次遍历之后,链表B中的节点就是需要释放内存的节点。
分代回收:在循环引用对象的回收中,整个应用程序会被暂停,为了减少应用程序暂停的时间,Python 通过分代回收(空间换时间)的方法提高垃圾回收效率。分代回收的基本思想是:对象存在的时间越长,是垃圾的可能性就越小,应该尽量不对这样的对象进行垃圾回收。CPython将对象分为三种世代分别记为0、1、2,每一个新生对象都在第0代中,如果该对象在一轮垃圾回收扫描中存活下来,那么它将被移到第1代中,存在于第1代的对象将较少的被垃圾回收扫描到;如果在对第1代进行垃圾回收扫描时,这个对象又存活下来,那么它将被移至第2代中,在那里它被垃圾回收扫描的次数将会更少。分代回收扫描的门限值可以通过gc模块的get_threshold函数来获得,该函数返回一个三元组,分别表示多少次内存分配操作后会执行0代垃圾回收,多少次0代垃圾回收后会执行1代垃圾回收,多少次1代垃圾回收后会执行2代垃圾回收。需要说明的是,如果执行一次2代垃圾回收,那么比它年轻的代都要执行垃圾回收。如果想修改这几个门限值,可以通过gc模块的set_threshold函数来做到。
题目008:说一下你对Python中迭代器和生成器的理解。
点评:很多人面试者都会写迭代器和生成器,但是却无法准确的解释什么是迭代器和生成器。如果你也有同样的困惑,可以参考下面的回答。
迭代器是实现了迭代器协议的对象。跟其他编程语言不通,Python中没有用于定义协议或表示约定的关键字,像interface、protocol这些单词并不在Python语言的关键字列表中。Python语言通过魔法方法来表示约定,也就是我们所说的协议,而__next__和__iter__这两个魔法方法就代表了迭代器协议。生成器是迭代器的语法升级版本,可以用更为简单的代码来实现一个迭代器。
面试中经常会让面试者写生成斐波那契数列的迭代器,下面给出参考代码,其他的迭代器可以如法炮制。
class Fib(object):
    def __init__(self, num):
        self.num = num
        self.a, self.b = 0, 1
        self.idx = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.idx < self.num:
            self.a, self.b = self.b, self.a + self.b
            self.idx += 1
            return self.a
        raise StopIteration()
如果用生成器的语法来改写上面的代码,代码会简单优雅很多。
def fib(num):
    a, b = 0, 1
    for _ in range(num):
        a, b = b, a + b
        yield a
可以通过for-in循环从迭代器对象中取出值,也可以使用next函数取出迭代器对象中的下一个值。
题目009:正则表达式的match方法和search方法有什么区别?
点评:正则表达式是字符串处理的重要工具,所以也是面试中经常考察的知识点。在Python中,使用正则表达式有两种方式,一种是直接调用re模块中的函数,传入正则表达式和需要处理的字符串;一种是先通过re模块的compile函数创建正则表达式对象,然后再通过对象调用方法并传入需要处理的字符串。如果一个正则表达式被频繁的使用,我们推荐后面这种方式,它会减少频繁编译同一个正则表达式所造成的开销。
match方法是从字符串的起始位置进行正则表达式匹配,返回Match对象或None。search方法会扫描整个字符串来找寻匹配的模式,同样也是返回Match对象或None。
题目010:下面这段代码的执行结果是什么。
def multiply():
    return [lambda x: i * x for i in range(4)]
print([m(100) for m in multiply()])
运行结果:
[300, 300, 300, 300]
上面代码的运行结果很容易被误判为[0, 100, 200, 300]。首先需要注意的是multiply函数用生成式语法返回了一个列表,列表中保存了4个Lambda函数,这4个Lambda函数会返回传入的参数乘以i的结果。需要注意的是这里有闭包(closure)现象,multiply函数中的局部变量i的生命周期被延展了,由于i*终的值是3,所以通过m(100)调列表中的Lambda函数时会返回300,而且4个调用都是如此。
如果想得到[0, 100, 200, 300]这个结果,可以按照下面几种方式来修改multiply函数。
方法一:使用生成器,让函数获得i的当前值。
def multiply():
    return (lambda x: i * x for i in range(4))
print([m(100) for m in multiply()])
或者
def multiply():
    for i in range(4):
        yield lambda x: x * i
print([m(100) for m in multiply()])
方法二:使用偏函数,彻底避开闭包现象。
from functools import partial
from operator import __mul__
def multiply():
    return [partial(__mul__, i) for i in range(4)]
print([m(100) for m in multiply()])
温馨提示:Python面试宝典会持续更新,从基础到项目实战的内容都会慢慢覆盖到。虽然每天只更新5个题目,但是每道题扩散出的信息量还是比较大的,希望对找工作的小伙伴所有帮助。

Python中为什么没有函数重载?

Python中为什么没有函数重载?
点评:C++、Java、C#等诸多编程语言都支持函数重载,所谓函数重载指的是在同一个作用域中有多个同名函数,它们拥有不同的参数列表(参数个数不同或参数类型不同或二者皆不同),可以相互区分。重载也是一种多态性,因为通常是在编译时通过参数的个数和类型来确定到底调用哪个重载函数,所以也被称为编译时多态性或者叫前绑定。这个问题的潜台词其实是问面试者是否有其他编程语言的经验,是否理解Python是动态类型语言,是否知道Python中函数的可变参数、关键字参数这些概念。
首先Python是解释型语言,函数重载现象通常出现在编译型语言中。其次Python是动态类型语言,函数的参数没有类型约束,也就无法根据参数类型来区分重载。再者Python中函数的参数可以有默认值,可以使用可变参数和关键字参数,因此即便没有函数重载,也要可以让一个函数根据调用者传入的参数产生不同的行为。
题目012:用Python代码实现Python内置函数max。
点评:这个题目看似简单,但实际上还是比较考察面试者的功底。因为Python内置的max函数既可以传入可迭代对象找出*大,又可以传入两个或多个参数找出*大;*为关键的是还可以通过命名关键字参数key来指定一个用于元素比较的函数,还可以通过default命名关键字参数来指定当可迭代对象为空时返回的默认值。
下面的代码仅供参考:
def my_max(*args, key=None, default=None):
    “””
    获取可迭代对象中*大的元素或两个及以上实参中*大的元素
    :param args: 一个可迭代对象或多个元素
    :param key: 提取用于元素比较的特征值的函数,默认为None
    :param default: 如果可迭代对象为空则返回该默认值,如果没有给默认值则引发ValueError异常
    :return: 返回可迭代对象或多个元素中的*大元素
    “””
    if len(args) == 1 and len(args[0]) == 0:
        if default:
            return default
        else:
            raise ValueError(‘max() arg is an empty sequence’)
    items = args[0] if len(args) == 1 else args
    max_elem, max_value = items[0], items[0]
    if key:
        max_value = key(max_value)
    for item in items:
        value = item
        if key:
            value = key(item)
        if value > max_value:
            max_elem, max_value = item, value
    return max_elem
题目013:写一个函数统计传入的列表中每个数字出现的次数并返回对应的字典。
点评:送人头的题目,不解释。
def count_letters(items):
    result = {}
    for item in items:
        if isinstance(item, (int, float)):
            result[item] = result.get(item, 0) + 1
    return result
也可以直接使用Python标准库中collections模块的Counter类来解决这个问题,Counter是dict的子类,它会将传入的序列中的每个元素作为键,元素出现的次数作为值来构造字典。
from collections import Counter
def count_letters(items):
    counter = Counter(items)
    return {key: value for key, value in counter.items() \
            if isinstance(key, (int, float))}
题目014:使用Python代码实现遍历一个文件夹的操作。
Python标准库os模块的walk函数提供了遍历一个文件夹的功能,它返回一个生成器。可以通过这个生成器来获得文件夹下所有的文件和文件夹。
import os
g = os.walk(‘/Users/Hao/Downloads/’)
for path, dir_list, file_list in g:
    for dir_name in dir_list:
        print(os.path.join(path, dir_name))
    for file_name in file_list:
        print(os.path.join(path, file_name))
说明:os.path模块提供了很多进行路径操作的工具函数,在项目开发中也是经常会用到的。 如果题目明确要求不能使用os.walk函数,那么可以使用os.listdir函数来获取指定目录下的文件和文件夹,然后再通过循环遍历用os.isdir函数判断哪些是文件夹,对于文件夹可以通过递归调用进行遍历,这样也可以实现遍历一个文件夹的操作。
题目015:现有2元、3元、5元共三种面额的货币,如果需要找零99元,一共有多少种找零的方式?
点评:还有一个非常类似的题目:“一个小朋友走楼梯,一次可以走1个台阶、2个台阶或3个台阶,问走完10个台阶一共有多少种走法?”,这两个题目的思路是一样,如果用递归函数来写的话非常简单。
from functools import lru_cache
@lru_cache()
def change_money(total):
    if total == 0:
        return 1
    if total < 0:
        return 0
    return change_money(total – 2) + change_money(total – 3) + change_money(total – 5)
说明:在上面的代码中,我们用lru_cache装饰器装饰了递归函数change_money,如果不做这个优化,上面代码的渐近时间复杂度将会是 ,而如果参数total的值是99,这个运算量是非常巨大的。lru_cache装饰器会缓存函数的执行结果,这样就可以减少重复运算所造成的开销,这是空间换时间的策略,也是动态规划的编程思想。
温馨提示:Python面试宝典会持续更新,从基础到项目实战的内容都会慢慢覆盖到。虽然每天只更新5个题目,但是每道题扩散出的信息量还是比较大的,希望对找工作的小伙伴所有帮助。
友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速