击水湘江

Born To Fight!


  • Home

  • About

  • Tags

  • Archives

聊聊if else

Posted on 2017-09-05

if else
if else 是我们学习C语言开始用的流程控制语句。还记得大学老师说过一句话,任何复杂的业务逻辑都可以用if else去解决。然而就像面向对象中的继承一样,如果用的过多就会造成代码的腐烂。下面我们就来聊聊if else。

为什么太多的if else不好?

我们先看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
   public void OnMessage(Push.PushMessage pushMessage) {
try {
String message = pushMessage.messageContent;
Log.v("keyes", "pushMessage = " + message);
if (message != null) {
JsonObject data = new JsonParser().parse(message).getAsJsonObject();
if (data.get("type") != null) {
String type = data.get("type").getAsString();
if ("100".equals(type)) {//XX消息
EventBus.getDefault().post(new GrabActionEvent());
PopBean bean = PushUtils.dealPushMessage(mContext, message);
IDealWithPush dealWithPush = AppStateUtils.getAppState(mContext);
dealWithPush.dealPush(mContext, bean);
} else if ("106".equals(type)) {//
String nick = data.get("nick").getAsString();
WPushNotify.notification(106, nick, "正在访问您的信息,请立即回复");
} else {
Gson temp = new Gson();
final SystemNotification bean = temp.fromJson(pushMessage.messageContent, SystemNotification.class);
if (bean.getType() == 103) {
//应用在后台,不需要刷新UI,通知栏提示新消息
if (!AppInfoUtils.isRunningForeground(HyApplication.getApplication())) {
WPushNotify.notification(bean);
}
saveDataToDB(bean);
EventBus.getDefault().post(bean);
} else if (bean.getType() == 104) {
List<Activity> list = HyApplication.getInstance().getActivityList();
if (list != null && list.size() > 0) {
Activity activity = list.get(list.size() - 1);
if (!TextUtils.isEmpty(bean.getDescribe())) {
new LogoutDialog(activity, bean.getDescribe());
} else {
new LogoutDialog(activity, activity.getString(R.string.force_exit));
}
}
} else if (bean.getType() == 108) {//沉默用户唤醒
String title = bean.getTitle();
String describe = bean.getDescribe();
WPushNotify.notification(108, title, describe);
}
}
}
}
} catch (Exception e) {
LogUtils.e("push", e.getMessage());
}

Log.i("song", pushMessage.messageContent);

}

这个方法里面仅仅嵌套了好多层if else,看上去会比较复杂难懂,再看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
if(isSkipPPUForQA && StringUtils.isNotBlank(request.getParameter("userId"))){
super.doFilter(request, response, chain);
}else {
try {
if("/app/school/article/share".equals(request.getRequestURI())){
super.doFilter(request, response, chain);
}else{
if(!filterReqUrl(request)) {
long ppuUserId = PassportService.passportService.getLoginUserId(RemoteValid.SAPCE_ONE_HOUR, request, response);
if (ppuUserId < 2) {
response.getWriter().write(this.generateResponse(AppResultStateEnum.PPU_UNVALID.getCodeStr(), "登录认证信息已过期,请重新登录"));
log.error("ppu返回的userId:" + ppuUserId + ",ppu过期,ppu=" + PPUCookieUtil.getPpuCookie(request) + ",url=" + request.getRequestURI());
log.error("imei=" + request.getParameter("imei") + ",version=" + request.getParameter("version") + ",platform=" + request.getParameter("platform"));
} else {
boolean flag = isTouchSingleDeviceLimitWithoutLogin(ppuUserId, request);
if (flag) {
String singleDeviceLoginContent = configComp.getValueByConfigTable(ConfigEnum.APP_SINGLE_DEVICE_LOGIN_CONTENT);
boolean isH5 = "1".equals(request.getParameter("isH5"));
if(isH5){
String jsAjaxHeader = request.getHeader("X-Requested-With");
if("XMLHttpRequest".equals(jsAjaxHeader)){response.getWriter().write(this.generateResponse(AppResultStateEnum.SINGLE_DEVICE_LOGIN.getCodeStr(), singleDeviceLoginContent));
}else {
renderSingleDeviceHtml(response,"/single_device_error");
}
}else {
response.getWriter().write(this.generateResponse(AppResultStateEnum.SINGLE_DEVICE_LOGIN.getCodeStr(), singleDeviceLoginContent));
}
log.error("触发单设备登录限制错误");
} else {
request.addParameter("userId", new String[]{ppuUserId + ""});
super.doFilter(request, response, chain);
}
}
}
}
} catch (Exception e) {
logger.error("业务处理异常,url=",e);
}
}

第一,这个方法中嵌套了七层的if else,层次太多。第二这个方法太长。嵌套层次过多和方法过长都是Bad Smell。那么究竟很多的if else有哪些弊端呢?

  • 僵化:如果再有更多情况的时候,我们需要在原来的地方写更多的if…else if条件。也就是说你需要去改动原来的代码,然后重新编译,重新部署,这是很浪费时间的。并且这违背了面向对象中的开放封闭原则:对扩展开放,对修改封闭。同时由于这个类需要处理各种业务,职责太多,所以也违背了职责单一原则。

  • 效率低下:很多系统的类,比如HashMaps,Properties等,都非常注意基于数据的条件判断。

  • 难阅读:像这种层层if else嵌套的情况,如果其他人需要来看,并且维护这份代码,由于难阅读,他们会感觉吃力。试想下,如果段代码很长,一个屏看不完,那肯定是维护的灾难。

  • 难维护:if else不像switch case,它的每个分支都和其它分支有关系,如果需求变更,在修改某个分支之前要看懂其它所有分支,确保不会对其它分支造成影响。

  • 难调试:很多if else,调试过程中需要一步步跟进,会影响调试效率。

  • 难测试:每次我们写测试用例的Case,针对每个有很多if else的方法,我们要对每个分支都写一个测试,这样下来这个测试用例将会变得非常长。

在任何面向对象语言中,都需要考虑移除分支控制逻辑(if以及switch,case)。移除的常用做法是将这些控制逻辑的方法移到一个类中。 Quora,Simon Hayes

那么我们怎样来解决这种情况,因为遇到不同的情况需要用不同的解决方案,我们逐个来分析:

常见的嵌套类if else处理方式

比如第一个if else的例子,通常

平行类的if else处理方式

如果我们有几个判断条件是平级if,那么我们可以使用命令模式来解决这种问题。比如我们现在有如下的if else:

1
2
3
if (value.equals("A")) { doCommandA() }
else if (value.equals("B")) { doCommandB() }
else if etc.

这时,我们可以使用命令模式来解决,先创建一个接口:

1
2
3
public interface Command {
void exec();
}

然后CommandA和CommandB类实现这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
public class CommandA() implements Command {

void exec() {
// ...
}
}
public class CommandB() implements Command {

void exec() {
// ...
}
}

然后创建一个Map<String,Command>,并且往其中添加Command实例:

1
2
commandMap.put("A", new CommandA());
commandMap.put("B", new CommandB());

然后所有的if/else if,就都会变成:

1
commandMap.get(value).exec();

如果某个Command有任何的改变只需要改动某个具体的类即可,如果有新加的Command,那么只需要添加响应的Command即可。命令模式就是加了一个中间件:命令容器(就是这里的Map,根据情况可能会是List或者其它)来实现解耦。

复杂处理算法的if else

如果我们if之后的代码处理的业务逻辑很相似,并且这种处理算法可能会经常变动,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class IfElseDemo {
public double calculateInsurance(double income, InputType type) {
if (type == smallType) {
return income*0.365;
} else if (type == mediumType) {
return (income-10000)*0.2+35600;
} else if (type == bigType) {
return (income-30000)*0.1+76500;
} else {
return (income-60000)*0.02+105600;
}
}
}

那么我们就可以将每个if分支中的代码单独分离到各个类中,然后再抽出一个父类,这样我们每个条件分支中就不会有很多代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public abstract class InsuranceStrategy {
public double calculateInsuranceVeryHigh(double income) {
return (income - getAdjustment()) * getWeight() + getConstant();
}
public abstract int getConstant();
public abstract double getWeight();
public abstract int getAdjustment();
}

public class InsuranceStrategyMedium extends InsuranceStrategy {
@Override
public int getConstant() {
return 35600;
}
@Override
public double getWeight() {
return 0.2;
}
@Override
public int getAdjustment() {
return 10000;
}
}

/*InsuranceStrategyLow和InsuranceStrategyHigh的处理方式相似,此处略去*/
class IfElseDemo {
private InsuranceStrategy strategy;
public double calculateInsurance(double income, InputType type) {
if (type == smallType) {
strategy = new InsuranceStrategyLow();
} else if (type == mediumType) {
strategy = new InsuranceStrategyMedium();
} else if (type == bigType) {
strategy = new InsuranceStrategyHigh();
} else {
strategy = new InsuranceStrategyVeryHigh();
}
return strategy.calculate(income);
}
}

这样最终不还是有if else吗?是的,最终还是有if else,但是if else的逻辑变得非常清晰,只是用于创建一个新的类。并且我们将经常变化的算法部分封装到了子类中,如果某个子类中的算法变了,只需要变动某个子类(封装变化),然后重新编译就可以了,不需要将整个项目重新编译,部署。

区间类的if else

如果客户端的if条件表示的是不同的范围,然后根据不同范围来选择不同的对象来处理,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Client {

public static void main(String[] args) {

Request request = new Request();
request.addSalaryAmount = 9999;
if (request.addSalaryAmount <= 100){
DivisionManager divisionManager = new DivisionManager();
divisionManager.accept();
}else if (request.addSalaryAmount <= 1000){
Chief chief = new Chief();
chief.accept();
}else if (request.addSalaryAmount <= 10000){
GeneralManager generalManager = new GeneralManager();
generalManager.accept();
}else {
System.out.println("金额太大没人能批准");
}
}
}

上面这个例子中,不同的条件分支是让不同的对象来处理这种条件。并且以后可能Request对象会添加其他的请求属性,比如offWork(请假),并且这种请求属性同样需要DivisionManager,Chief,GeneralManager。然而其中的处理顺序变了,并不是现在的请求等级。可能是先由Chief处理,再有GeneralManager处理,最后有DivisionManager来处理,那怎么办呢?难道还要写一套if else吗?
这时候我们就可以用责任链模式来将这一长串if else嵌套进每一个对象中去,我们可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
interface ManagerCommand{
public void accept(int requestAmount);
}

abstract class CommManager implements ManagerCommand {
public CommManager superior;
public void setSuperior(CommManager superior) {
this.superior = superior;
}
}

public class DivisionManager extends CommManager {

@Override
public void accept(int requestAmount) {

if (requestAmount<100){
System.out.println("部门经理批准");
}else if ( this.superior != null){
this.superior.accept(requestAmount);
}
}
}

public class Chief extends CommManager{

public void accept(int requestAmount) {
if (requestAmount < 1000){
System.out.println("总监同意");
}else if (this.superior != null){
this.superior.accept(requestAmount);
}
}
}
public class GeneralManager extends CommManager{

@Override
public void accept(int requestAmount) {

if (requestAmount<10000){
System.out.println("总经理批准");
}else if (this.superior != null){
this.superior.accept(requestAmount);
}
}
}

最后在Client端调用的时候,我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Client {

public static void main(String[] args) {

Request request = new Request();
request.addSalaryAmount = 999;
DivisionManager divisionManager = new DivisionManager();
Chief chief = new Chief();
GeneralManager generalManager = new GeneralManager();
divisionManager.setSuperior(chief);
chief.setSuperior(generalManager);
divisionManager.accept(request.addSalaryAmount);
}
}

这种写法的好处是:将条件和处理该条件的对象解耦,每个处理条件的对象都不知道其他对象,我们可以随时地增加或者修改处理一个请求的结构。这增加了给对象指派职责的灵活性。

小结:其实上述的每种方式都是利用多态来解决分支带来的僵化,谷歌有一个视频对这个问题阐述得很好。。

参考资料

  1. https://stackoverflow.com/questions/10175805/how-to-avoid-a-lot-of-if-else-conditions
  2. https://stackoverflow.com/questions/271526/avoiding-null-statements?rq=1
  3. https://stackoverflow.com/questions/14136721/converting-many-if-else-statements-to-a-cleaner-approach
  4. https://www.youtube.com/watch?v=4F72VULWFvc
  5. https://www.quora.com/Why-should-Java-programmers-try-to-avoid-if-statements
  6. https://stackoverflow.com/questions/1199646/long-list-of-if-statements-in-swift/1199677#1199677
  7. https://industriallogic.com/xp/refactoring/conditionalWithStrategy.html
  8. 大话设计模式
  9. 重构改善既有代码的设计

理解Foundation框架

Posted on 2017-09-04

本文源于对WWDC:UnderStanding Foundation的总结。

什么是Foundation?

  • Foundation提供了构建基础类的框架
    • 所有应用都是用的基础类型
    • 它们供软件的更高层来组合使用

Dictionry

Dictionary中提供了objectEnumerator和keyEnumerator两个方法,可以直接取到key或者value的Enumerator,然后就直接可以用while循环了。

NSEnumerator *e = [dictionary keyEnumerator];

while(id key = [e nextObject]){

 id value = [e objectForKey:key];
 ....
}

Fast Enumeration

如果想获取某个Dictionary中的key,那么直接用Fast Enumeration就可以:

NSDictionary *someDic = @{@"key":@"value"};
for (id key in someDic) {

    NSLog(@"key:%@",key);   
}

如果想要获取Key及其对应的Value,那么直接是用Block就可以:

[someDic enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {

}];

NSArray排序

对一个NSArray进行排序,有以下几种方法:

  1. C function
  2. Objective-C method
  3. NSSortDescriptor
  4. Blocks

使用Bock遍历数组的方法:

NSMutableArray *names = [NSMutableArray arrayWithObjects:@"11",@"22", nil];
    [names sortUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        NSComparisonResult result;
        NSUInteger lLen = [obj1 length], rLen = [obj2 length];

        if (lLen < rLen) {
            result = NSOrderedAscending;
        }else if (lLen > rLen){   
            result = NSOrderedDescending;
        }else{
            result = NSOrderedSame;
        }        
        return result;
    }];

Collection的过滤

遍历一个Collection的同时再改变它,会引发异常。
Collection的过滤步骤:

  1. 将要筛选的Collection改为可变类型的
  2. 筛选出需要移除的项
  3. 调用可变类型的响应方法进行移除
NSMutableArray *files = [NSMutableArray arrayWithObjects:@"file0",@"file1", nil]; // array of NSString objcects;
NSIndexSet *toRemove = [files indexesOfObjectsPassingTest:^BOOL(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

    if ([obj hasPrefix:@"."]) {return YES;}
    return NO;

}];
[files removeObjectsAtIndexes:toRemove];

Collection更多的特性

  • 查找
  • 对每个元素都调用某个方法
  • NSArray:切开和串联
  • NSSet:交集,合并和子集

Strings

String其实是一个Unicode字符集的数组,你可以将它看做非透明的容器。对它的常用方法有:

  • 比较
  • 查找
  • 转换编码

字符串比较的方法:

- (NSComparisonResult)compare:(NSString *)string; // 1
- (NSComparisonResult)localizedStandardCompare:(NSString *)string NS_AVAILABLE(10_6, 4_0); // 2
- (NSComparisonResult)localizedCompare:(NSString *)string; // 3
- (NSComparisonResult)localizedCaseInsensitiveCompare:(NSString *)string; // 4
- (NSComparisonResult)compare:(NSString *)string options: (NSStringCompareOptions)mask range:(NSRange)rangeOfReceiverToCompare locale:(nullable id)locale; // 5

第二个方法和第三个方法是对那些做了本地化的字符串进行比对。第四个可以对字符串的一部分进行比较,并且可以指定是否是大小写敏感等。如果数组中有字符串需要排序,那么我们可以用到上文中提到的,让数组中的元素分别调用其自身的方法。

NSArray *strings = [NSArray arrayWithObjects:@"Larry",@"Curly",@"Moe", nil];
NSArray *sortedArray = [strings sortedArrayUsingSelector:@selector(localizedCompare:)];

字符串查找

字符串查找的方法如下:

- (NSRange)rangeOfString:(NSString *)searchString;
- (NSRange)rangeOfString:(NSString *)searchString options:(NSStringCompareOptions)mask range:(NSRange)rangeOfReceiverToSearch locale:(nullable NSLocale *)locale NS_AVAILABLE(10_5, 2_0);

*注:如果有特殊字符,比如Ó,那么它在数组中是O,´两个分开存储,是占两个存储单位的,所以它自身rang的length是2。

字符串的查找中还支持正则表达式:

NSString *str = @"Going going gone!";
NSRange found = [str rangeOfString:@"go(\\w*)"
                           options:NSRegularExpressionSearch
                             range:NSMakeRange(0, str.length)];

这样我们就可以得到found的值:found.location = 6, found.length = 5;

字符串编码

字符串和Data之间编码的相互转换:

NSData *someData = [NSData dataWithContentsOfFile:@""];
NSString *inString = [[NSString alloc]initWithData:someData encoding:NSUTF8StringEncoding];
NSString *outString = @"For Windows";
NSData *converted = [outString dataUsingEncoding:NSUTF16StringEncoding];

如果要将某个和文件系统相独立的字符串表示成文件系统调用时所指定的字符串表示,可以使用:

const char *fileName = [outString fileSystemRepresentation];

将它表示成一个C类型的字符串,这个字符串在outString销毁的时候也跟着自动销毁。

更多的特性:

  • 打印格式
  • 遍历逐个子字符串遍历,逐行遍历,逐段遍历
  • 替换某个子字符串
  • 路径补全

NSDateFormatter:

如果不想将一个NSDate转换成字符串的时候出现时间,那么可以将timeStyle设置为:NSDateFormatterNoStyle。

NSDateFormatter *fmt = [[NSDateFormatter alloc]init];
[fmt setTimeStyle:NSDateFormatterNoStyle];
[fmt setDateStyle:NSDateFormatterLongStyle];

Dates和Formatter小结

  • 将NSDate和NSCalendar混合使用来计算时间
  • 当展现日期和数字的时候使用foramtter

数据持久化

将数据以Plist的形式存储

将数据转换成Plist:

NSDictionary *colors = [NSDictionary dictionaryWithObjectsAndKeys:@"Verde",@"Green",@"Rojo",@"Red",@"Amarillo",@"Yellow", nil];
NSError *error = nil;
NSData *plist = [NSPropertyListSerialization dataWithPropertyList:colors format:NSPropertyListXMLFormat_v1_0 options:0 error:&error];
if (!plist) {
    NSLog(@"本地化失败");
}
[plist writeToFile:@"filePath" atomically:YES];

将Plist转化成NSData:

NSData *readData = [NSData dataWithContentsOfURL:urlOfFile];
NSDictionary *newColors = [NSPropertyListSerialization propertyListWithData:readData options:0 format:nil error:&error];

NSFileManager

NSFileManager支持文件的复制,移动,链接,删除等。

NSFileManager *mgr = [[NSFileManager alloc]init];
BOOL res;
res = [mgr copyItemAtURL:src toURL:des error:&error];
res = [mgr moveItemAtURL:src toURL:des error:&error];
res = [mgr linkItemAtURL:src toURL:des error:&error];
res = [mgr removeItemAtURL:src error:&error];

linkItemAtURL就是创建一个硬链接,硬链接就是给一个已经存在的文件重新创建另外一个名字,如果原来文件被删除了,那么硬链接的名字就会失效了。

NSFileManager还可以遍历某个目录下所有的内容:

NSArray *stuff = [mgr contentsOfDirectoryAtURL:dirURL includingPropertiesForKeys:[NSArray array] options:0 error:&error];

如何优雅地使用DateFormatter?

Posted on 2017-08-27

性能对比

之所以要聊DateFormatter是因为某次给项目做性能检测,发现创建DateFormatter太消耗性能,我们来做个对比,新建100000个日期。我们使用两种方式:第一种每次创建日期的时候新建一个NSDateFormatter,第二种共用一个NSDateFormatter,来生成日期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public func testWithMultipleInstantiation() ->CFTimeInterval {

var dateStrings:[String] = []
dateStrings.reserveCapacity(100000)
let startTime = CACurrentMediaTime()
for _ in 0..<100000 {
let df = DateFormatter()
df.dateStyle = .medium
df.timeStyle = .full
dateStrings.append(df.string(from: Date()))
}
let endTime = CACurrentMediaTime()
return endTime - startTime
}

public func testWithSingleInstance() ->CFTimeInterval {

var dateStrings: [String] = []
dateStrings.reserveCapacity(100000)
let startTime = CACurrentMediaTime()
let df = DateFormatter()
df.dateStyle = .medium
df.timeStyle = .full
for _ in 0..<100000 {
dateStrings.append(df.string(from: Date()))
}
let endTime = CACurrentMediaTime()
return endTime - startTime
}

然后我们调用这两个方法:

1
2
print("testWithMultipleInstantiation--\(testWithMultipleInstantiation())")
print("testWithSingleInstance--\(testWithSingleInstance())")

打印结果是:

1
2
testWithMultipleInstantiation--7.83139349098201
testWithSingleInstance--0.742719032976311

从中可以明显看到创建DateFormatter是很消耗性能的,多次创建DateFormatter比单次创建大约要慢11倍。如果我们要用DateFormatter,那么尽量创建一次,然后多次使用。

然后我们再做进一步的实验:创建一次DateFormatter,但是改变这个NSDateFormatter的dateStyle和timeStyle。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public func testWithSingleInstanceChangeFormatter() ->CFTimeInterval {

var dateStrings: [String] = []
dateStrings.reserveCapacity(100000)
let startTime = CACurrentMediaTime()
let df = DateFormatter()
for _ in 0..<100000 {

df.dateStyle = .medium
df.timeStyle = .full
df.dateStyle = .full
df.timeStyle = .medium
dateStrings.append(df.string(from: Date()))
}

let endTime = CACurrentMediaTime()
return endTime - startTime
}

然后调用这个方法:

1
print("ChangeFormatter--\(testWithSingleInstanceChangeFormatter())")

这时输出的结果是:

1
ChangeFormatter--5.77827541399165

从中我们可以看到,其对性能的消耗和多次创建DateFormatter相差并不多。最后我们得到这样一个结论:

  1. 每次使用DateFormatter时都新建是最消耗性能的
  2. 创建一个DateFormatter然后改变其dateStyle和timeStyle等和1中的性能消耗差不多
  3. 为每一种日期类型创建一种DateFormatter并且不改变其dateStyle和timeStyle等属性是性能最优的

解决方案

通过上面的结论,我们发现如果对DateFormatter做成单例,那么就必须保证每个DateFormatter的格式是相同的,因为改变DateFormatter的格式也是很消耗性能的。我们要做多个单例,每种单例是一种formatter,然后分别使用吗?显然太过于麻烦。我们可以使用缓存策略,将每种格式的DateFormatter缓存一份,下次如果有相同格式的Formatter,直接从缓存中取就可以了,这就避免了多次创建和多次改变格式的问题。为了解决这个问题,我使用NSCache做了一个DateFormatter的缓存池:MFDateFormatterPool,已经上传到了GitHub上,分为OC和Swift两个版本,如有问题可以联系我(Swift版稍后会加上)。

其它:NSDateFormatter在IOS7之前是非线程安全的,多线程可能引起崩溃,

延伸阅读:

https://www.raywenderlich.com/31166/25-ios-app-performance-tips-tricks#reuseobjects
http://www.chibicode.org/?p=41
https://stackoverflow.com/questions/18195051/crash-in-nsdateformatter-setdateformat-method

Java为什么会流行?

Posted on 2017-08-26

下面是TIOBE对计算机语言流行度的最新排名:

Language Rate

我们可以看到Java名列第一,并且Java一直排名很靠前。为什么Java会如此流行呢?要了解Java为什么这么流行,我们先从它的起源说起。

Java诞生的前奏

计算机业内一般认为:B语言导致了C语言的诞生,C语言演变出了C++,而C++会被Java语言所打败。是什么导致了Java的诞生?想要解决这个问题,我们先来看看Java的前辈们。

C语言的诞生

C语言的产生是人们追求结构化、高效率、高级语言的结果,它可以替代汇编进行开发,它的出现改变了人们之前的编程方法和思路。

C语言的出现解决了之前语言的各种不足,比如:FORTRAN高效但不适用于编写系统程序。BASIC虽然容易学习,但是功能不够强大。汇编高效,但是学习成本很大,且很难调试。

另外,C语言之前的语言没有考虑结构化设计。它们大量使用GOTO语句来对程序进行控制。这样做的结果是程序极其混乱,各种跳转和条件分支交织在一起极大地影响了程序的可读性。人们解决该问题的愿望非常强烈,且日益迫切。20世界70年代初,计算机革命开始,人们对于软件的需求日益增加,使用当时的语言开发软件已经无法满足需求。人们在此期间进行了很多的尝试,但是没有发明出更好的语言。直到一个新机遇的到来:计算机硬件资源的富余。由于计算机硬件的增多,程序员可以随意的使用计算机,随意进行各种尝试,这就给了他们开发自己工具的机会。后来Ken Thompson发明了B语言,B语言演化到了C语言。1989年美国国家标准化组织制定了C语言的标准,C语言被正式标准化。C语言是由程序员对编程实践的总结而发明出来的,它能够解决早期语言的种种缺陷。

C++的诞生

C语言被用的好好的,为何出现了C++呢?原因是C语言太复杂了。当一个工程项目达到一定规模之后,使用结构化编程方法,编程人员就无法对它的复杂性进行有效管理。20世纪80年代初期,许多工程项目的复杂性都超过了结构化方法的极限。为了解决这个问题,面向对象诞生了。面向对象的特性:继承,封装,多态是用来帮助组织复杂程序的编程方法。因此出现了C++,C++的产生是基于C的,它包含了C的所有特征,属性和优点。

Java出现的时机到了

在20世纪80年代末到90年代初,使用面向对象的C++语言占主导地位。然而,推动计算机语言进化的力量正在酝酿。万维网(WWW)和Internet在随后的几年达到了临界状态,这就促成了编程的另一场革命。

Java诞生

由于嵌入式系统的发展,人们对一种独立于平台的语言更加渴望,这种语言可以嵌入微波炉,遥控器等各种家用电器设备的软件。用作控制器芯片的CPU是多种多样的,但是C和C++只能对特定目标进行编译。比如某个CPU要编译C++代码,那么就要创建一个针对该CPU的C++编译器,而创建编译器是一项耗时耗长,耗资大的工作。为了解决这个问题,Gosling和其他人一直在开发一种可移植,跨平台的语言。该语言能够生成运行于不同环境,不同CPU芯片上的代码。经过不懈的努力,在1991年被James Gosling,Patrick Naughton,Chris Warth,Ed Frank和Mike Sheridan发明出来。第一版花了18个月。刚开始叫Oak,于1995年更名为Java。

Java流行

在万维网(WWW)出现之前Java处于有用、摸摸无闻的用于电子消费品编程的状态。然而由于万维网的出现,Java被推到了计算机语言的设计的最前沿,因为万维网也需要可移植的程序。

因特网是由不同的、分布式的系统组成,其中包含各种类型的计算机,操作系统和CPU。尽管许多类型的平台都可以与因特网连接,但是用户仍然希望他们能够运行同样的程序。

1993年,Java设计小组的成员发现解决嵌入式控制器可移植性的方法,也可以用来解决因特网的代码的可移植性问题。也就是Java不仅可以用来解决小范围的问题,也可以用来解决大范围的问题。这样他们将Java的重心由电子消费品,转移到Internet编程上。

Java对Internet为什么重要

在网路中,在服务器和个人计算机间传递的信息有两大对象:被动的信息和动态的、主动运行的程序。比如阅读电子邮件是被动的数据,被服务器用来正确的显示服务器传递数据的程序是动态的。这中动态性是好的,但是其安全性和可移植行有严重的缺陷。在Java产生以前,赛百空间有一半的对象实体无法进入网络世界,是Java为它们打开了便利之门,而且在这个过程中定义了一种全新的程序形式:applet(小应用程序)。

Java小应用程序

Java可以用来生成两类程序:应用程序(Application)和小应用程序(Java applet)。应用程序不必说,小应用程序是可以再Internet中传输并在兼容Java的Web浏览器中运行的应用程序。小应用程序实际上就是小型的Java程序,它能够像图像文件、声音文件和视频片段那样通过网络动态下载。小程序的特点是,它是动态的智能的程序,可以对用户的输入作出反应,并变化。

安全性

每次当你下载一个程序的时候,你都要冒着被病毒入侵的危险。Java出现之前,很多用户不经常下载可执行的程序文件。即使下载了,在运行它之前也要进行病毒检查。经管如此,很多用户还是担心他们的系统被病毒感染,除此之外,有些恶意程序可以搜索你计算机本地文件系统内容来收集你的私人信息,比如信用卡号码、银行账号和密码等。Java在网络程序和你的计算机之间提供了一道防火墙,消除了用户的顾虑。这道防火墙就是Java运行环境。Java程序被限制在了运行环境中,不允许它访问计算机的其他部分。

可移植性

链接到Internet上的计算机和操作系统不尽相同,要使它们都能动态地下载同一个程序,就需要有能够生成可移植性执行代码的方法。这个方法就是:Java编译器的输出并不是可执行的代码,而是字节码(bytecode)。字节码是一套设计用来在Java运行时环境下执行的高度优化的指令集,该Java运行时系统成为Java虚拟机(JavaVirtual Machine, JVM)。在标准形式下,JVM就是一个字节码解释器。只要某个平台安装了Java虚拟机,它就可以解释Java代码。当然对Java程序进行解释也有助于它的安全性。因为每个Java程序的运行都在Java虚拟机的控制之下,Java虚拟机可以包含这个程序,并且不让它在系统之外产生副作用。

Java虚拟机的增强

尽管Java被设计为解释执行的程序,但是这没有妨碍它将动态字节码编译为本机代码。SUN公司在Java 2发行版中提供了一个字节码编译器–JIT(Just In Time)。它可以根据需要,一部分一部分地将字节码实时编译为可执行代码。它不能将整个Java程序一次性全部编译为可执行的代码,因为Java要执行各种检查,而这些检查只有在运行时才执行。这种编译执行的方法使性能得到较大的提高。

经过上面的探讨我们发现,推动计算机语言发展的因素有两个:

  • 适应正在变化的环境和需求
  • 实现编程艺术的完善和提高

Java之所以流行主要因为万维网的发展和其自身安全性和可移植性的特点。

AVFoundation--媒体捕获

Posted on 2017-08-24

为了管理设备(比如摄像头或者麦克风)捕获的信息,你需要组合一些对象来代表输入和输出,并且使用AVCaptureSession对象来调节它们之间的数据。你最少需要如下步骤:

  • AVCaptureDevice对象来代表输入设备,比如:摄像头或者麦克风
  • 具体的AVCaptureInput子类实例来配置输入设备的port
  • 具体AVCaptureOutput子类实例来输入一个视频文件或者静态图片
  • 一个AVCaptureSession实例来协调输入到输入的数据流

为了给用户预览正在录制的视频,你可以使用AVCaptureVideoPreviewLayer实例来实现。你可以通过一个单一的session来配置各种输入输出:

session work

对于很多应用来说,这就已经够用了。然而对于某些操作来说,比如,你要检测音频频道的power level,你需要考虑某个输入设备的各种端口,以及这些端口是怎样和输出相连接的。
捕获输入和捕获输入之间的关联可以用AVCaptureConnection对象来表示。捕获输入(AVCaptureInput实例)有一到多个输入端口(AVCaptureInputPort实例)。捕获输出(AVCaptureOutPut实例)可以接受一到多个数据源(比如,一个AVCaptureMovieFileOutput对象可以接收视频或音频数据)。

当你往一个session中加入一个输入或者一个输出的时候,这个session可以形成针对所有捕获输入端口和捕获输出的连接。一个捕获输入和捕获输出之间的连接可以被一个AVCaptureConnection对象来表示。

Capture Connection

对于指定的输入或者输出,你可以使用一个capture connection来使能或者关闭其数据流。你也可以使用connection来检测一个音频频道的平均及最高的power level。

注:媒体捕获不支持同时捕获IOS设备的前置和后置摄像头。

使用捕获Session来调节数据流

一个AVCaptureSessin对象是你用来管理数据捕获的关键调节对象。你可以使用一个实例来调节视频音频输入到输出的数据。你可以给这个session添加捕获设备,然后通过调用session的startRuning方法来开始这个数据流,并且通过调用stopRunning方法来结束数据流。

1
2
3
AVCaptureSession *session = [[AVCaptureSession alloc] init];
// Add inputs and outputs.
[session startRunning];

配置Session

使用preset来对session配置你所喜欢的图片质量和分辨率。一个preset是一个常量,它表明了很多种可选配置方案中的一种;在某些情况下实际的配置和具体的设备是相关的。
prest

如果你要对针对某个屏幕大小做配置,你需要在设置前检测其是否支持:

if ([session canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
    session.sessionPreset = AVCaptureSessionPreset1280x720;
} else { 
    // Handle the failure.
}

如果你要使用preset,以便在一个更加细粒度得调整session的参数,或者你要对一个执行中的session做某些改动,那么你需要你需要将你的方法添加在beginConfiguration和commitConfiguration方法中间。beginConfiguration和commitConfiguration可以确保设备的改变作为一组,最小的能见度或者状态的不兼容行。在调用beginConfiguration之后,你可以添加或者移除输出,改变sessionPreset属性,或者单独配置捕捉输入属性。只有在触发了commitConfiguration之后所有的改变才会生效,并且这些改变将会同时生效。

session beginConfiguration];
// Remove an existing capture device.
// Add a new capture device.
// Reset the preset.
[session commitConfiguration];

检测Capture Session的状态

Capture session会在它开始,停止运行或者被打断的时候发出通知,你可以观察这些通知一遍被告知。你可以通过注册AVCaptureSessionRuntimeErrorNotification来观察一个运行时错误。你也可以通过查询session的running属性来判断其是否正在运行,以及其interrupted属性来判断其是否被打断了。除此之外,running属性和interrupted属性都支持KVO,这个会在主线程中被触发。

AVCaptureDevice对象代表一个输入设备

一个AVCaptureDevice对象时对一个物理捕获设备的抽象,该对象可以给AVCaptureSession对象提供输入数据(比如,音频或者视频)。每个对象代表了一种输入设备,比如两个视频输入(前置后置摄像头),一个音频输入(麦克风)。
你可以通过使用AVCaptureDevice类方法devices和devicesWithMediaType来找出当前可用的捕获设备。并且,如果有必要的话你可以找出某个iPhone,iPad或者iPod所提供的特性。尽管如此,可用设备的列表是可以变化的。当前的输入设备可能变得不可用(如果它们被另外的应用所使用),新的输入设备变得可用(如果它们被另外的设备所放弃)。你可以注册AVCaptureDeviceWasConnectedNotification和AVCaptureDeviceWasDisconnectedNotification通知,以便在可用设备变化时候被通知到。你可以使用一个capture input来讲一个输入设备添加到capture session中。

设备属性

你可以查询某个设备的不同属性。使用hasMediaType:和supportsAVCaptureSessionPreset:方法,你可以检测某个设备是否提供某种媒体类型或者支持一种给定的capture session preset。为了给用户提供有用的信息,你可以找出捕获设备的位置(它在检测单元的前面还是后面),以及它本地化后的名字。如果你要给用户展示一个捕获列表来让其选择,那么这种方式是很有用的。

下图展示了后置(AVCaptureDevicePositionBack)和前置(AVCaptureDevicePositionFront)摄像头。
front and back facing camera position

下面的例子遍历了所有的可用设备并且打印出它们的名字(对于视频设备来说,还有它们的位置)。

NSArray *devices = [AVCaptureDevice devices];
  for (AVCaptureDevice *device in devices) {
      NSLog(@"Device name: %@", [device localizedName]);
      if ([device hasMediaType:AVMediaTypeVideo]) {
          if ([device position] == AVCaptureDevicePositionBack) {
              NSLog(@"Device position : back");

} else { 
NSLog(@"Device position : front");
          }
} } 

除此之外,你可以找出设备的模型ID以及其唯一标示。

设备捕获设置

不同的设备有不同的功能,比如,某些设备支持聚焦和flash模式,某些可以支持聚焦到某个兴趣点。
下面的代码段展说明了怎样找到有手电筒模式的视频输入设备,并且它支持一个给定的capture session preset:

NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
NSMutableArray *torchDevices = [[NSMutableArray alloc] init];
for (AVCaptureDevice *device in devices) {
    [if ([device hasTorch] &&
         [device supportsAVCaptureSessionPreset:AVCaptureSessionPreset640x480]) {
        [torchDevices addObject:device];
} } 

如果你发现很多设备满足你的标准,那么你可以让用户从中选择他们喜欢的来使用。为了给用户描述这个设备,你可以使用它的localizedName属性。
你可以用相似的方式来使用不同的特性。指明一种特定的模式方法是固定的,并且你可以查看设备是否支持某种特定模式。在某些情况下,你可以观察一个属性以便属性改变时候得到通知。任何情况下,在某种特定的特性下更改模式前要锁住设备,这在下文的配置设备小结中会有提及。

注:聚焦兴趣点和曝光兴趣点是彼此独立的,它们分别是focus mode和exposure mode。

聚焦模式

有三种聚焦模式:

  • AVCaptureFocusModeLocked:聚焦位置是固定的。当你想在锁住聚焦的情况下,让用户来组成场景那么这是很有用的。
  • AVCaptureFocusModeAutoFocus:摄像头做一次扫描聚焦,然后返回到被锁住的状态。如果你想聚焦选中某个特定的物体,并且对那个物体保持聚焦(尽管那个物体可能不在拍摄场景中心),那么这种模式是很合适的。
  • AVCaptureFocusModeContinuousAutoFocus:在这种模式下摄像头在需要的情况下持续地进行自动对焦。

在利用focusMode属性来设置聚焦模式前,你可以使用isFocusModeSupported:方法来决定某个设备是否支持给定的聚焦模式。
除此之外,某种设备可能会支持对某个兴趣点的聚焦。你可以使用focusPointOfInterestSupported属性来做判断。如果支持的话,你可以使用focusPointOfInterest属性来设置聚焦点。你可以传递一个CGPoint值来指定聚焦的点,其中{0,0}代表左上角,{1,1}在水平模式home键在右侧的情况下。如果设备在竖直模式下,那么这种坐标关系也是适用的。

使用adjustingFocus属性来决定是否某个设备当前正在聚焦。使用KVO的形式,你可以再设备开始和停止聚焦的时候收到通知。

如果你改变了聚焦模式的设置,你可以像下面一样,将它们返回到默认设置:

if ([currentDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) {
    CGPoint autofocusPoint = CGPointMake(0.5f, 0.5f);
    [currentDevice setFocusPointOfInterest:autofocusPoint];
    [currentDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
} 

曝光模式(Exposure Modes)

曝光模式有以下两种:

  • AVCaptureExposureModeContinuousAutoExposure:在需要的情况下设备自动调整曝光水平。
  • AVCaptureExposureModeLocked:在当前水平曝光水平是固定的。

使用isExposureModeSupported:方法来决定是否某个设备支持某种给定的曝光模式,然后使用exposureMode属性来进行设定。

除此之外,某个设备可能会支持兴趣点的曝光。你可以使用exposurePointOfInterestSupported来特使其是否支持。如果支持的话,你可以使用exposurePointOfInterest属性来进行设置。你可以传递一个CGPoint值来指定聚焦的点,其中{0,0}代表左上角,{1,1}在水平模式home键在右侧的情况下。如果设备在竖直模式下,那么这种坐标关系也是适用的。
使用adjustingExposure属性来决定是否某个设备当前正在改变其曝光设置。使用KVO的形式,你可以再设备开始和停止曝光设置的时候收到通知。

如果你改变了曝光设置,那么你可以返回默认设置:

if ([currentDevice
isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) {
    CGPoint exposurePoint = CGPointMake(0.5f, 0.5f);
    [currentDevice setExposurePointOfInterest:exposurePoint];
    [currentDevice setExposureMode:AVCaptureExposureModeContinuousAutoExposure];
} 

闪光灯模式

闪光灯模式又以下三种:

  • AVCaptureFlashModeOff:闪光灯不会开启
  • AVCaptureFlashModeOn:闪光灯总是开启
  • AVCaptureFlashModeAuto:设备会根据周围的光线环境来决定是否开启闪光灯

使用hasFlash方法可以确定某个设备是否有闪光灯。如果返回是YES,然后你可以使用isFlashModeSupported:方法来判断其是否支持某种给定的闪光灯模式,最后使用flashMode属性来对其进行配置。

手电筒模式

在手电筒模式下,为了照亮视频捕捉,闪光灯是在低电耗的情况下持续打开的。一共有三种手电筒模式:

  • AVCaptureTorchModeOff:手电筒关闭
  • AVCaptureTorchModeOn:手电筒总是打开
  • AVCaptureTorchModeAuto:根据需要手电筒选择打开或者关闭

你可以使用hasTorch属性来检测一个设备是否有手电筒。然后你可以使用isTorchModeSupported:方法来检测一个设备是否有给定的手电筒模式,然后通过torchMode属性来设置手电筒模式。
对于有手电筒的设备,如果设备和一个执行中的capture session相连接,那么这个手电筒就会被打开。

视频防抖

依赖于具体的硬件设备,对于操作视频的连接,视频防抖是用的。尽管如此,并非所有的源格式和视频分辨率都支持视频防抖。

使能视频防抖可能会给视频捕捉Pipeline引入多余的延迟。可以使用videoStabilizationEnabled属性来检测视频防抖是否正在使用。enablesVideoStabilizationWhenAvailable属性可以在摄像头支持的情况下让应用自动的使能视频防抖。由于以上的限制,默认情况下视频防抖是被关闭的。

白色平衡

有两种白色平衡模式:

  • AVCaptureWhiteBalanceModeLocked:白平衡模式是固定的。
  • AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance:在需要的情况下,摄像头持续调整白平衡。

你可以使用isWhiteBalanceModeSupported:方法来确定某个设备是否支持一种给定的白平衡,然后使用whiteBalanceMode属性来设置白屏模式。

你可以使用adjustingWhiteBalance属性来决定某个设备是否正在改变其白光模式的设定。你可以通过KVO来观察一个设备开始或者终止其白光设定。

设置设备方向

对于一个AVCaptureConnection,你可以给它设定指定的方向来指明你在AVCaptureOutput(AVCaptureMovieFileOutput,AVCaptureStillImageOutput 以及AVCaptureVideoDataOutput)中图片的朝向。

使用AVCaptureConnectionsupportsVideoOrientation属性来确定设备是否支持改变视频的方向,也可以使用videoOrientation属性来指明你想在输出端口中的指向。下面的代码指明了如何将一个AVCaptureConnection属性设定为AVCaptureVideoOrientationLandscapeLeft:

AVCaptureConnection *captureConnection = <#A capture connection#>;
if ([captureConnection isVideoOrientationSupported])
{
AVCaptureVideoOrientation orientation = AVCaptureVideoOrientationLandscapeLeft; [captureConnection setVideoOrientation:orientation]; 
} 

设置设备

为了给设备设置一个捕获属性,你必须使用lockForConfiguration:属性来获取该设备的锁。这会避免其它应用对该属性做出不能兼容的改变。下面的代码片段说明了怎样在一个设备上改变其聚焦模式(首先确定是否该模式是被支持的),然后尝试锁住该设备来重新配置。如果这个锁是可获取的,那么聚焦模式就被改变,随后这个锁会被立即释放掉。

if ([device isFocusModeSupported:AVCaptureFocusModeLocked]) {
    NSError *error = nil;
    if ([device lockForConfiguration:&error]) {
        device.focusMode = AVCaptureFocusModeLocked;
        [device unlockForConfiguration];
    }
    else {
        // Respond to the failure as appropriate.
}

只要你需要这个可设置的设备属性保持不变,那么你就应该持有这把设备锁。在非必要的情况下持有设备锁可能会降低其它应用的捕获质量。

转换设备

有时你可能想让用户在不同的输入设备间进行切换,比如,从使用前置摄像头到使用后置摄像头。为了避免暂停或者卡顿,你可以在运行状态下配置一个session,然而你需要将你的配置改变放在beginConfiguration和commitConfiguration之间。

AVCaptureSession *session = <#A capture session#>;
[session beginConfiguration];
[session removeInput:frontFacingCameraDeviceInput];
[session addInput:backFacingCameraDeviceInput];
[session commitConfiguration];

当最外面的commitConfiguration被触发时候,所有的改变会同时被执行。这就确保了平滑的转变。

使用Capture Inputs来给一个Session添加捕获设备

你可以使用一个AVCaptureDeviceInput(一个抽象AVCaptureInput类的子类)实例来给一个capture session添加一个捕获设备。

NSError *error;
AVCaptureDeviceInput *input =
        [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
if (!input) {
    // Handle the error appropriately.
}

你可以使用addInput:来个一个session添加输入。如果合适的话,你可以使用canAddInput来确定某个捕获输入是否和现存的session是兼容的。

AVCaptureSession *captureSession = <#Get a capture session#>;
  AVCaptureDeviceInput *captureDeviceInput = <#Get a capture device input#>;
  if ([captureSession canAddInput:captureDeviceInput]) {
      [captureSession addInput:captureDeviceInput];
  }
  else {
      // Handle the failure.
} 

如果想知道如何重新配置一个运行中的session,你可以查看上文中的Configuring a session中的细节。

一个AVCaptureInput可以给媒体数据提供多个流。比如,一个输入设备可以提供视频或者音频数据。每个输入提供的媒体流都是用AVCaptureInputPort来表示的。一个捕获session使用一个AVCaptureConnection对象来决定一组AVCaptureInputPort对象和一个AVCaptureOutput之间的映射。

使用捕获输出来获取一个Session的输出

为了从一个捕获session中获取输出,你需要添加一个或者多个output。一个output是一个AVCaptureOutput子类的实例。你使用:

  • AVCaptureMovieFileOutput来输出一个视频文件
  • 如果你想处理正在被捕获的视频的frame,比如,你要创建你自己的ViewLayer,那么你可以使用AVCaptureVideoDataOutput
  • 如果你要处理正在被捕获的音频你可以使用AVCaptureAudioDataOutput
  • 如果你要捕获静态图片以及元数据你可以使用AVCaptureStillImageOutput

使用addOutput:你可以给某个捕获session添加outputs。通过使用canAddOutput,你可以检测是否某个捕获输出和现存session相兼容。在某个session运行的过程中,你可以根据需要添加或者移除输出。

AVCaptureSession *captureSession = <#Get a capture session#>;
AVCaptureMovieFileOutput *movieOutput = <#Create and configure a movie output#>;
if ([captureSession canAddOutput:movieOutput]) {
    [captureSession addOutput:movieOutput];
}
else {
    // Handle the failure.
} 

保存视频文件

你可以使用AVCaptureMovieFileOutput对象来将某个视频数据保存到文件中。(AVCaptureMovieFileOutput是AVCaptureFileOutput的子类,它定义了很多基本操作)。你可以对一个视频文件输出的很多方面做以配置,比如录制的最大时长,文件大小。如果可用的磁盘空间小于指定的值你可以禁止视频的录制。

AVCaptureMovieFileOutput *aMovieFileOutput = [[AVCaptureMovieFileOutput alloc]
init];
CMTime maxDuration = <#Create a CMTime to represent the maximum duration#>;
aMovieFileOutput.maxRecordedDuration = maxDuration;
aMovieFileOutput.minFreeDiskSpaceLimit = <#An appropriate minimum given the quality of the movie format and the duration#>; 

视频输出的分辨率和比特率取决于一个捕获session的sessionPreset值。视频编码通常是H.264格式,音频编码通常是AAC格式。实际的数值可能根据设备的不同而改变。

开始录制

使用startRecordingToOutputFileURL:recordingDelegate:你可以开始录制一个QuickTime视频。你需要提供一个基于文件的URL以及一个代理。这个URL一定不能和已存在文件相同,因为视频文件输出不会覆盖已存在的资源。同时,你也需要获得往某个指定的路径下写文件的权限。代理必须遵守AVCaptureFileOutputRecordingDelegate协议,并且必须要实现captureOutput:didFinishRecordingToOutputFileAtURL:fromConnections:error:方法。

AVCaptureMovieFileOutput *aMovieFileOutput = <#Get a movie file output#>;
NSURL *fileURL = <#A file URL that identifies the output location#>;
[aMovieFileOutput startRecordingToOutputFileURL:fileURL recordingDelegate:<#The
delegate#>];

在实现captureOutput:didFinishRecordingToOutputFileAtURL:fromConnections:error:代理方法的时候,代理可以往相册中写入录制的视频。这也可以检测可能出现的错误。

确保文件被成功写入

在实现captureOutput:didFinishRecordingToOutputFileAtURL:fromConnections:error:方法的时候,为了确保文件是否被成功的写入了,你不仅仅需要检查error,还需要检查error的user info字典中AVErrorRecordingSuccessfullyFinishedKey的值:

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput
        didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
        fromConnections:(NSArray *)connections
        error:(NSError *)error {
    BOOL recordedSuccessfully = YES;
    if ([error code] != noErr) {
        // A problem occurred: Find out if the recording was successful.
        id value = [[error userInfo]
objectForKey:AVErrorRecordingSuccessfullyFinishedKey];
        if (value) {
            recordedSuccessfully = [value boolValue];
} } 
    // Continue as appropriate...

你需要检测user info字典中的keyAVErrorRecordingSuccessfullyFinishedKeykey的值,因为尽管有错误,文件还是可以被存储成功。这个错误可能标志着你达到了某个视频录制的限制,比如,AVErrorMaximumDurationReached或者AVErrorMaximumFileSizeReached。其它可能导致录制停止的原因有:

  • 磁盘占满了:AVErrorDiskFull
  • 录制设备断开连接了:AVErrorDeviceWasDisconnected
  • session被中止了(比如,收到了一个电话):AVErrorSessionWasInterrupted

给文件添加元数据

任何时候你都可以给一个视频文件设置元数据,甚至是在录制的时候。在某些场景下这时很有用的,比如当视频录制开始的时候信息不可以拿到,这可能因为位置信息造成的。某个输出文件的元数据是由一组AVMetaDataItem对象来表示的;你使用它可变子类的一个实例,AVMutableMetadataItem来创建一个你自己的元数据。

VCaptureMovieFileOutput *aMovieFileOutput = <#Get a movie file output#>;
NSArray *existingMetadataArray = aMovieFileOutput.metadata;
NSMutableArray *newMetadataArray = nil;
if (existingMetadataArray) {
    newMetadataArray = [existingMetadataArray mutableCopy];
}
else {
    newMetadataArray = [[NSMutableArray alloc] init];
} 
AVMutableMetadataItem *item = [[AVMutableMetadataItem alloc] init];
item.keySpace = AVMetadataKeySpaceCommon;
item.key = AVMetadataCommonKeyLocation;
CLLocation *location - <#The location to set#>;
item.value = [NSString stringWithFormat:@"%+08.4lf%+09.4lf/"
    location.coordinate.latitude, location.coordinate.longitude];
[newMetadataArray addObject:item];
aMovieFileOutput.metadata = newMetadataArray;

处理视频帧

一个AVCaptureVideoDataOutput对象使用代理来处理视频帧。你可以通过setSampleBufferDelegate:queue:方法来设置代理。除了设置代理,你还可以指定该代理方法被调用的队列。你必须使用一个串行队列来确保传递给代理的帧是按照合适顺序进行的。你可以使用队列来改变既定的调度和处理视频帧的优先权。参考SquareCam来作为一个实现的例子。

在代理方法里,帧作为一个CMSampleBufferRef类型的实例来被表示captureOutput:didOutputSampleBuffer:fromConnection:。默认情况下,缓冲是以视频最有效的格式被发发出的。你可以使用videoSettings属性来指明一种定制的输出格式。视频的设定属性是一个字典;目前为止,唯一支持的key是kCVPixelBufferPixelFormatTypeKey。

推荐的像素格式是通过availableVideoCVPixelFormatTypes属性来返回的,同时availableVideoCodecTypes属性返回支持的数值。Core Graphics和OpenGL都和BGRA格式配合的很好。

AVCaptureVideoDataOutput *videoDataOutput = [AVCaptureVideoDataOutput new];
NSDictionary *newSettings =
                @{ (NSString *)kCVPixelBufferPixelFormatTypeKey :
@(kCVPixelFormatType_32BGRA) };
videoDataOutput.videoSettings = newSettings;
 // discard if the data output queue is blocked (as we process the still image
[videoDataOutput setAlwaysDiscardsLateVideoFrames:YES];)
// create a serial dispatch queue used for the sample buffer delegate as well as
when a still image is captured
// a serial dispatch queue must be used to guarantee that video frames will be
delivered in order
// see the header doc for setSampleBufferDelegate:queue: for more information
videoDataOutputQueue = dispatch_queue_create("VideoDataOutputQueue",
DISPATCH_QUEUE_SERIAL);
[videoDataOutput setSampleBufferDelegate:self queue:videoDataOutputQueue];
AVCaptureSession *captureSession = <#The Capture Session#>;
if ( [captureSession canAddOutput:videoDataOutput] )
     [captureSession addOutput:videoDataOutput];

处理视频时考虑的性能因素

对于你的应用来说,你应该给这个session设置最低的可用分辨率。将输出设定到一个比需求更高的分辨率会浪费处理循环以及消耗更多的电量。
你必须确保你实现了captureOutput:didOutputSampleBuffer:fromConnection方法,以便在创建一帧的时候可以处理一个采样缓冲。如果这耗费过长的时间,并且你保持了这个视频帧率,那么AV Foundation对象就会停止传送帧,不尽停止给你的代理传递帧,还会停止向其它的输出传递帧,比如preview layer。
你可以使用捕获视频数据输出的minFrameDuration属性来确定你在没有被停止的情况下消耗更少的帧率,以便你有足够的时间来处理这一帧。你也许也要确保alwaysDiscardsLateVideoFrames属性也被设置成YES(默认情况下也是YES)。这个属性确保了任何延迟了的帧都会被丢弃而不是移交给你进行处理。换句话说,如果你正在录制视频,并且如果输出帧有些延迟不会造成影响,并且你想获取所有的帧,那么你可以将这个属性的值设定为NO。这并不意味这不会掉帧(也就是说,帧仍然会掉),但是它不会掉的那么早或者由于效率原因掉帧。

捕获静态图片

如果你想捕获静态图片以及元数据,那么你可以使用AVCaptureStillImageOutput来输出。这个图片的分辨率取决于这个session的preset以及这个设备。

像素和编码格式

不同的设备支持不同的图片格式。分别使用availableImageDataCVPixelFormatTypes和availableImageDataCodecTypes你可以找出某个设备支持的pixel和codec类型。每个方法会返回指定设备所支持数据的数组。你可以设置outputSettings字典来设置你所想要的图片格式,比如:

AVCaptureStillImageOutput *stillImageOutput = [[AVCaptureStillImageOutput alloc]
init];
NSDictionary *outputSettings = @{ AVVideoCodecKey : AVVideoCodecJPEG};
[stillImageOutput setOutputSettings:outputSettings];

如果你要获取一张JPEG格式的图片,那么你通常不需要指定你自己的压缩格式。相反,因为它的压缩格式是硬件驱动的,所以你应该让静态图片输出来给你压缩。尽管你改变了图片的元数据,如果你要得到一张图片的数据表示,那么你可以使用jpegStillImageNSDataRepresentation:来得到没有被再次压缩数据的NSData对象。

捕获一张图片

当你想捕获一张图片的时候,你可以调用输出的captureStillImageAsynchronouslyFromConnection:completionHandler:方法。第一个参数是你为了捕获要用的连接。你需要找到这个连接,它的输入端是一个,它的输入端口正在搜集视频:

 AVCaptureConnection *videoConnection = nil;
  for (AVCaptureConnection *connection in stillImageOutput.connections) {
       for (AVCaptureInputPort *port in [connection inputPorts]) {
          if ([[port mediaType] isEqual:AVMediaTypeVideo] ) {
              videoConnection = connection;
break; } 
} 
      if (videoConnection) { break; }
     }

captureStillImageAsynchronouslyFromConnection:completionHandler:方法的第二个参数是一个具有两个参数的block:一个包含了图片数据的CMSampleBuffer类和一个error。采样缓冲自身包含了比如EXIF字典等元数据作为其附属。如果你想你可以改变这个附属,但是要注意在像素和编码格式中提到的对JPEG图片的优化。

[stillImageOutput captureStillImageAsynchronouslyFromConnection:videoConnection
completionHandler:
    ^(CMSampleBufferRef imageSampleBuffer, NSError *error) {
        CFDictionaryRef exifAttachments =
            CMGetAttachment(imageSampleBuffer, kCGImagePropertyExifDictionary,
NULL);
        if (exifAttachments) {
            // Do something with the attachments.
        }
        // Continue as appropriate.
    }];

展示录制的内容

你可以给用户提供视频录制的预览(利用preview layer),或者音频录制的预览(通过监听音频channel)。

视频预览

使用AVCaptureVideoPreviewLayer对象,你可以给用户提供一个录制内容的预览。AVCaptureVideoPreviewLayer是CALayer的子类。为了展示预览,你不需要输出任何东西。

在视频展示给用户之前,使用AVCaptureVideoDataOutput类可以让客户端程序具有获取视频像素的能力。

和捕获输出不一样,一个视频预览layer保持一个对其关联session的一个强引用。这就确保了在该layer试图播放视频的时候session不会被释放。下面的代码展示了初始化一个预览layer的方式。

AVCaptureSession *captureSession = <#Get a capture session#>;
CALayer *viewLayer = <#Get a layer from the view in which you want to present the
 preview#>;
AVCaptureVideoPreviewLayer *captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:captureSession]; 
[viewLayer addSublayer:captureVideoPreviewLayer];

一般来说,这个preview layer在渲染树中和其它的CALayer对象是一样的。你可以像处理其它layer一样对图片进行伸缩,旋转等。其中有一个不同点是:为了指明来自摄像头的图片怎样旋转,你需要设置layer的orientation属性。除此之外,你可以通过查询supportsVideoMirroring属性来决定某个设备是否支持视频镜像。如果有需要你可以设置videoMirrored属性,尽管当automaticallyAdjustsVideoMirroring属性默认情况下是YES,这个镜像值基于session的配置而自动被设定的。

视频重力模式

这个preview layer支持三种重力模式,你可以通过videoGravity属性来设置:

  • AVLayerVideoGravityResizeAspect:这会按照固定的比例保存视频,当视频不能铺满整个可用屏幕大小的时候会留下黑条。
  • AVLayerVideoGravityResizeAspectFill:这会保质视频的比例,但是会铺满整个屏幕,并且在需要的时候剪裁视频。
  • AVLayerVideoGravityResize:这会拉伸视频来铺满整个可用的屏幕空间,尽管这样可能让图片扭曲。

给预览添加点击捕获功能

在视频连接的时候,使用preview layer来实现点击捕获,你需要小心。你必须要考虑预览方向和layer的重力,以及这个预览可能被镜像的可能性。参考示例代码中IOS的AVCam项目来实现这个功能。

展示音频等级(Audio Level)

为了在一个捕获连接中的音频通道中检测平均power和峰值power峰值,你可以使用AVCaptureAudioChannel对象。因为音频等级不是KVO的,所以你必须按照你需要的频率来访问其数值来更新UI(比如,每秒钟10次)。

AVCaptureAudioDataOutput *audioDataOutput = <#Get the audio data output#>;
NSArray *connections = audioDataOutput.connections;
if ([connections count] > 0) {
    // There should be only one connection to an AVCaptureAudioDataOutput.
    AVCaptureConnection *connection = [connections objectAtIndex:0];
    NSArray *audioChannels = connection.audioChannels;
    for (AVCaptureAudioChannel *channel in audioChannels) {
        float avg = channel.averagePowerLevel;
        float peak = channel.peakHoldLevel;
        // Update the level meter user interface.
} } 

综合:像UIImage对象一样捕获视频帧

下面简明的代码实例向你展示了怎样捕获一个视频并且将你得到的视频帧转换成UIImage对象。它有以下的功能:

  • 创建一个AVCaptureSession对象来协调一个AV输入的数据流到一个输出
  • 对你想要的输入类型找到AVCaptureDevice对象
  • 给该设备创建一个AVCaptureDeviceInput对象
  • 创建一个AVCaptureVideoDataOutput对象来产生视频帧
  • 对AVCaptureVideoDataOutput对象实现一个代理对象来处理视频帧
  • 实现一个方法来将该代理收到的CMSampleBuffer转换成一个UIImage对象

创建并且配置一个捕获Session

你可以使用AVCaptureSession对象来把来自一个AV输入设备的数据流来转换为一个输出。创建一个session,并且配置它来生成中等分辨率的视频帧:

AVCaptureSession *session = [[AVCaptureSession alloc] init];
session.sessionPreset = AVCaptureSessionPresetMedium;

创建配置设备及设备输入

捕获设备是由AVCaptureDevice对象表示的,该类提供了获取你想要的输入类型的方法。一个设备有一个或者多个端口,这些端口使用AVCaptureInput对象来配置。通常情况下,在它的默认配置上,你使用捕获输入。

找到一个视频捕获设备,然后利用该设备创建一个设备输入并且将其添加到session里面。如果一个合适的设备不能够被加载,那么deviceInputWithDevice:error:方法将会返回一个引用的错误。

AVCaptureDevice *device =
        [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error = nil;
AVCaptureDeviceInput *input =
        [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
if (!input) {
    // Handle the error appropriately.
}
[session addInput:input];

创建和配置视频输出数据

你使用一个AVCaptureVideoDataOutput对象来处理正在被捕获而未被压缩的帧。通常情况下,你可以配置一个输出的很多方面。比如,对于视频来说,你可以通过videoSettings属性来表明像素格式以及通过设置minFrameDuration属性来设置帧率的峰值。

创建和配置一个视频数据的输出并将其添加到session中,通过将minFrameDuration属性值设置为1/15来将帧率峰值设置为15fps:

AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
[session addOutput:output];
output.videoSettings =
                @{ (NSString *)kCVPixelBufferPixelFormatTypeKey :
@(kCVPixelFormatType_32BGRA) };
output.minFrameDuration = CMTimeMake(1, 15);=

数据输出对象使用代理来提视频帧。这个代理必须遵守AVCaptureVideoDataOutputSampleBufferDelegate协议。当你设置数据输出代理的时候,你也必须提供一个回调将要被触发的队列。

dispatch_queue_t queue = dispatch_queue_create("MyQueue", NULL);
[output setSampleBufferDelegate:self queue:queue];
dispatch_release(queue);

你使用该队列改变给定的优先权来传递和处理视频帧。

实现帧缓存的代理方法

在该代理类里,实现captureOutput:didOutputSampleBuffer:fromConnection:方法,这个方法会在一个采样缓存被写入的时候被调用。视频数据的输出对象以CMSampleBuffer类型的形式来传递帧,因此,你需要将一个CMSampleBuffer对象转换为一个UIImage对象。这个转换的方法会在Converting CMSampleBuffer to UIImage Object中说明。

- (void)captureOutput:(AVCaptureOutput *)captureOutput
          didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
          fromConnection:(AVCaptureConnection *)connection {

      UIImage *image = imageFromSampleBuffer(sampleBuffer);
     // Add your code here that uses the image.
 }

这个代理方法被触发的队列就是你在setSampleBufferDelegate:queue:中指明的队列;如果你要更新UI,那么你必须在主线程中执行相关的代码。

开始和终止录制

在配置完capture session之后,你应该确保根据用户的偏好设置里你有录制视频的权限。

NSString *mediaType = AVMediaTypeVideo
[AVCaptureDevice requestAccessForMediaType:mediaType completionHandler:^(BOOL
granted) {
    if (granted)
    {
        //Granted access to mediaType
        [self setDeviceAuthorized:YES];
    }
else { 
        //Not granted access to mediaType
        dispatch_async(dispatch_get_main_queue(), ^{
        [[[UIAlertView alloc] initWithTitle:@"AVCam!"
message:@"AVCam doesn't have permission to use Camera, please change privacy settings" 
}); } 
}]; 
                   delegate:self
          cancelButtonTitle:@"OK"
          otherButtonTitles:nil] show];
[self setDeviceAuthorized:NO];

如果摄像头的session被配置完成并且用户隐私设定允许使用摄像头,你可以调用startRunning方法来执行session在串行队列上的启动,以便主队列不会被阻塞(这可以让UI更加快速的被响应)。看iOS的AVCam作为一个实现的例子。

[session startRunning];

通过调用stopRunning方法,你可以停止录制视频。

高帧率的视频捕获

在选中的硬件上,iOS7.0引入了高帧率的视频捕获(也称作”SloMo”视频)。所有的AV Foundation框架都支持高帧率的内容。
你可以使用AVCaptureDeviceFormat类来确定某个设备的捕获能力。该类也有返回诸如:所支持的媒体类型,帧率,视图的field,最大放大倍数以及视频防抖是否被支持等参数。

  • 捕获支持在60fps下全720p分辨率,其中包括视频防抖,可掉P帧(H264编码视频的特性,这种特性可以让视频播放很流畅,及时在老设备上也可以这样。)
  • 视频播放可以提高音频对慢速和快速播放的支持,这就让音频的捕获时间(time pitch)可以在更低或者更高的速度下被保存。
  • 在可变合成的中,可以支持缩放编辑。
  • 在支持60fps视频的时候,输出提供了两种选择。可变的帧率,慢速或快速的移动,都会被保存或者视频帧率将会变得更小,比如30fps。

播放

一个AVPlayer实例可以通过setRate:方法来自动设置大多数的播放速度。该数值会作为播放速度的一个放大倍数。1.0数值表示正常的播放,0.5数值表示以一半的速度播放,5.0数值表示以正常速度5倍的速度来播放。

AVPlayerItem对象支持audioTimePitchAlgorithm属性。使用Time Pitch Algorithm Settings常量,该属性让你可以指明音频在不同帧率下的播放方式。
下表展示了所支持的time pitch算法,数量,该算法是否会造成音频在某个指定帧率下停止,以及每个算法支持的帧率范围。

Time Pitch Algorithm

编辑

编辑的时候,你可以使用AVMutableComposition类来建立临时的编辑。

  • 使用composition类方法来创建一个新的AVMutableComposition实例
  • 使用insertTimeRange:ofAsset:atTime:error方法来插入你的视频asset
  • 使用scaleTimeRange:toDuration来设定某个合成中某个部分的时间比例

输出

使用AVAssetExportSession类来导出60fps的视频输出一个asset。该内容可以用以下两种方式输出:

  • 使用AVAssetExportPresetPassthroughpreset来避免视频的重编码。通过对标志为60fps的媒体片段进行时间重置,这些媒体片段就会减速或者加速。
  • 为了最大播放兼容,使用恒定帧率输出。设定视频合成的frameDuration属性为30fps。你也可以通过设定导出session的audioTimePitchAlgorithm属性来指定捕获时间(time pitch)。

录制

使用AVCaptureMovieFileOutput类,你可以捕获高帧率的视频,它自动支持高帧率捕获。它会自动选择正确的H264捕获水平。

为了使用定制的录制,你必须使用AVAssetWriter类,这需要额外的设置。

assetWriterInput.expectsMediaDataInRealTime=YES;

该设置可以确保捕获可以和输入的数据保持同步。

AVFoundation--视频编辑

Posted on 2017-08-24

AV Foundation框架提供了很多类来方便对视频和音频的资源处理。AV Foundation框架的核心API是合成。一个合成仅仅是对一种或多种不同媒体资源track的组合。AVMutableComposition类提供了一个插入,移除以及管理track临时顺序的接口。你可以使用一个mutable composition来将一个新的asset和已经存在的asset集合组合在一起。如果你仅仅需要将很多的asset按序组装到一起,这些就够了。如果你想在合成的过程中执行任何定制的视频,音频处理,你需要分别加入音频混合(audio mix)和视频合成。

Mutable Composition

使用AVMutableAudioMix类,你可以在合成的过程中定制音频。同时,你可以指定一个音轨(audio track)的最大音量,或者设定一个音量坡度(volume ramp)。

AudioMix

对于视频合成中的video track如果要编辑的话,你可以使用AVMutableVideoComposition类来进行处理。对于单一的视频合成来说,你可以对输出的视频文件指明渲染的大小,比例,以及时长。通过一个视频的合成指令(AVMutableVideoCompositionInstruction类提供),你可以改变视频的背景色并且使用layer指令。在合成过程中,你可以使用这些layer指令(AVMutableVideoCompositionLayerInstruction类)来对video track实现旋转,旋转坡度(transform ramps),透明度,透明度坡度(opacity ramp)。视频合成类也可以让你使用核心动画来产生响应的效果(使用animationTool属性)。

Video Composition

使用AVAssetExportSession,你可以将音频混合(audio mix)和视频合成同时组合到你的合成中去。用你的composition初始化这个export session,然后将音频混合和视频混合分别赋值给audioMix和videoComposition属性即可。

Audio Mix And Video Composition

创建一个Composition

使用AVMutableComposition类来创建你自己的composition。为了添加媒体数据到你的composition,你必须通过AVMutableCompositionTrack类来添加一个或者多个composition tracks。最简单的情况就是用video track和audio track来创建一个mutable composition。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
AVMutableComposition *mutableComposition = [AVMutableComposition composition];
// Create the video composition track.
AVMutableCompositionTrack *mutableCompositionVideoTrack = [mutableComposition
addMutableTrackWithMediaType:AVMediaTypeVideo
preferredTrackID:kCMPersistentTrackID_Invalid];
// Create the audio composition track.
AVMutableCompositionTrack *mutableCompositionAudioTrack = [mutableComposition
addMutableTrackWithMediaType:AVMediaTypeAudio
preferredTrackID:kCMPersistentTrackID_Invalid];
```

### 初始化一个Composition Track时的选项

当你要往一个composition中添加一个新的track的时候,你必须提供一个media type和一个track ID。尽管视频和音频是最常用的media type,你还可以设置其它的media type,比如`AVMediaTypeSubtitle`(字幕)`AVMediaTypeText`(文本)。

每一个和试听数据相关的track都有一个唯一标示叫做 track ID。如果你将这个track ID赋值为`kCMPersistentTrackID_Invalid`,那么在关联的时候系统会自动给你创建一个唯一标识。

## 给一个Composition添加音视频数据

一旦你使用一个或者多个track创建一个composition,那么你就可以开始将你的媒体数据media数据添加到合适的tracks上。为了将媒体数据添加到一个composition track,你需要使用AVAsset对象(媒体数据存储在这个对象内部)。你可以在某个track上使用mutable composition track接口来将多个具有相同隐含媒体类型的`track`放到一起。接下来的例子说明了怎样将两种video asset track按序放到某种composition track中:

```objc

// You can retrieve AVAssets from a number of places, like the camera roll for
example.
AVAsset *videoAsset = <#AVAsset with at least one video track#>;
AVAsset *anotherVideoAsset = <#another AVAsset with at least one video track#>;

// Get the first video track from each asset.
AVAssetTrack *videoAssetTrack = [[videoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
AVAssetTrack *anotherVideoAssetTrack = [[anotherVideoAsset
tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];

// Add them both to the composition.
[mutableCompositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero,videoAssetTrack.timeRange.duration)
ofTrack:videoAssetTrack atTime:kCMTimeZero error:nil];

[mutableCompositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero,anotherVideoAssetTrack.timeRange.duration) ofTrack:anotherVideoAssetTrack atTime:videoAssetTrack.timeRange.duration error:nil];

```

### 检索兼容的Composition Track

如果可能的话,你应该对每一种媒体类型仅有一个composition track。可兼容asset track的统一可以将资源的利用降低到最少。当序列地展示媒体数据的时候,你应该将任何相同类型的媒体数据都放到同一个compsition track中。你可以查询一个mutable composition来找出和你需要的asset track相兼容的任何composition track:

```objc
AVMutableCompositionTrack *compatibleCompositionTrack = [mutableComposition
mutableTrackCompatibleWithTrack:<#the AVAssetTrack you want to insert#>];
if (compatibleCompositionTrack) {
// Implementation continues.
}
```

*注:将多个视频片段放到同一个composition track,在过个媒体片段转换时可能会隐性的造成掉帧现象,这种情况在嵌入式设备中尤其明显。对你的视频片段选择合适的合成track完全取决于你App的设计以及其将要展示的平台*

### 生成音量坡度(Volume Ramp)

在你的composition中,一个简单的`AVMutableAudioMix`对象可以单独对所有的音频track进行常用的音频处理。在你合成的过程中,使用`audioMix`类方法来创建一个audio mix,然后使用`AVMutableAudioMixInputParameters`实例来将这个audio mix和特定的track相关联。接下来的例子将会说明说明如何在一个指定的`audio track`上设置一个音量坡度来让音量在合成的过程中缓慢的消失。

```objc
AVMutableAudioMix *mutableAudioMix = [AVMutableAudioMix audioMix];
// Create the audio mix input parameters object.
AVMutableAudioMixInputParameters *mixParameters = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:mutableCompositionAudioTrack];
// Set the volume ramp to slowly fade the audio out over the duration of the
composition.
[mixParameters setVolumeRampFromStartVolume:1.f toEndVolume:0.f
timeRange:CMTimeRangeMake(kCMTimeZero, mutableComposition.duration)];
// Attach the input parameters to the audio mix.
mutableAudioMix.inputParameters = @[mixParameters];
```

### 运用透明度坡度(Opacity Ramp)

视频合成指令也可以用于视频和成Layer指令。一个`AVMutableVideoCompsitionLayerInstruction`对象可以对某个composition内部的某个video track使用旋转,旋转坡度,透明度,透明度坡度。某个视频合成指令的`layerInstructions`数组中layer指令的顺序决定了合成指令的过程中,来自source track的视频帧怎样被分层堆放,怎样被合成。在接下来的代码片段说明了怎样设置透明度坡度来在第一个视频结束切换第二个视频的时候产生缓慢消失的效果。

```objc
AVAsset *firstVideoAssetTrack = <#AVAssetTrack representing the first video segment played in the composition#>;
AVAsset *secondVideoAssetTrack = <#AVAssetTrack representing the second video
segment played in the composition#>;

// Create the first video composition instruction.
AVMutableVideoCompositionInstruction *firstVideoCompositionInstruction =
[AVMutableVideoCompositionInstruction videoCompositionInstruction];

// Set its time range to span the duration of the first video track.
firstVideoCompositionInstruction.timeRange = CMTimeRangeMake(kCMTimeZero,
firstVideoAssetTrack.timeRange.duration);

// Create the layer instruction and associate it with the composition video track.
AVMutableVideoCompositionLayerInstruction *firstVideoLayerInstruction =
[AVMutableVideoCompositionLayerInstruction
videoCompositionLayerInstructionWithAssetTrack:mutableCompositionVideoTrack];

// Create the opacity ramp to fade out the first video track over its entire duration.
[firstVideoLayerInstruction setOpacityRampFromStartOpacity:1.f toEndOpacity:0.f
timeRange:CMTimeRangeMake(kCMTimeZero, firstVideoAssetTrack.timeRange.duration)];

// Create the second video composition instruction so that the second video track isn't transparent.
AVMutableVideoCompositionInstruction *secondVideoCompositionInstruction =
[AVMutableVideoCompositionInstruction videoCompositionInstruction];

// Set its time range to span the duration of the second video track.
secondVideoCompositionInstruction.timeRange =
CMTimeRangeMake(firstVideoAssetTrack.timeRange.duration,
CMTimeAdd(firstVideoAssetTrack.timeRange.duration,
secondVideoAssetTrack.timeRange.duration));

// Create the second layer instruction and associate it with the composition video track.
AVMutableVideoCompositionLayerInstruction *secondVideoLayerInstruction =
[AVMutableVideoCompositionLayerInstruction
videoCompositionLayerInstructionWithAssetTrack:mutableCompositionVideoTrack];

// Attach the first layer instruction to the first video composition instruction.
firstVideoCompositionInstruction.layerInstructions = @[firstVideoLayerInstruction];

// Attach the second layer instruction to the second video composition instruction.
secondVideoCompositionInstruction.layerInstructions = @[secondVideoLayerInstruction];

// Attach both of the video composition instructions to the video composition.
mutableVideoComposition.instructions = @[firstVideoCompositionInstruction,
secondVideoCompositionInstruction];
```

### 加入核心动画效果

通过使用`animationTool`属性,你可以在合成视频的时候添加核心动画效果。通过这个animation tool,你可以完成给视频添加水印,添加标题或者动画覆盖。在视频合成过程中,核心动画有两种不同的使用方式:你可以在它自己composition track的视频帧中添加一个Core Animation layer,或者你可以直接使用(Core Animation layer)来渲染核心动画效果。接下来的代码展示了怎样使用第二种方式在视频的中间来添加一个水印。

```objc
CALayer *watermarkLayer = <#CALayer representing your desired watermark image#>;
CALayer *parentLayer = [CALayer layer];
CALayer *videoLayer = [CALayer layer];
parentLayer.frame = CGRectMake(0, 0, mutableVideoComposition.renderSize.width,
mutableVideoComposition.renderSize.height);
videoLayer.frame = CGRectMake(0, 0, mutableVideoComposition.renderSize.width,
mutableVideoComposition.renderSize.height);
[parentLayer addSublayer:videoLayer];
watermarkLayer.position = CGPointMake(mutableVideoComposition.renderSize.width/2,
mutableVideoComposition.renderSize.height/4);
[parentLayer addSublayer:watermarkLayer];
mutableVideoComposition.animationTool = [AVVideoCompositionCoreAnimationTool
videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer
inLayer:parentLayer];
```

## 综合:综合各种Assets并且保存到Camera Roll中

接下来的代码例子说明了如何将两个视频asset track和一个音频asset track综合来创建一个简单的video file。它的主要功能有

* 创建一个`AVMutableComposition`对象并且添加很多的`AVMutableCompositionTrack`对象
* 给可兼容的composition track通过`AVAssetTrack`对象添加时间范围
* 检查一个video asset的`preferredTransform`属性来确定视频的方向
* 在一个合成过程中使用`AVMutableVideoCompositionLayerInstruction`对象来对一个视频track实现旋转效果
* 给一个video composition对象设置合适的`renderSize`和`frameDuration`属性值。
* 在导出视频文件的时候使用合成结合另一个视频合成
* 将视频文件导出到`Camera Roll`文件中

*注:为了展示最关键的代码,该实例略去了一个完整的应用所需要的功能点,比如:内存管理,注销观察者(对KVO的观察或者对某个通知的监听)。为了能够很好的使用AV Foundation,你需要对Cocoa有丰富的经验,以便处理可能遗漏的功能点。*

### 创建Composition

为了将几个不同的assets创建放到一起,你可以使用一个`AVMutableComposition`对象。创建这个composition并且将一个音频和视频track添加进去。

```objc
AVMutableComposition *mutableComposition = [AVMutableComposition composition];
AVMutableCompositionTrack *videoCompositionTrack = [mutableComposition
addMutableTrackWithMediaType:AVMediaTypeVideo
preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *audioCompositionTrack = [mutableComposition
addMutableTrackWithMediaType:AVMediaTypeAudio
preferredTrackID:kCMPersistentTrackID_Invalid];
```

### 添加Assets

一个空的composition没有什么作用。添加两个video asset track和audio asset track到这个composition中。

```objc
AVAssetTrack *firstVideoAssetTrack = [[firstVideoAsset
tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
AVAssetTrack *secondVideoAssetTrack = [[secondVideoAsset
tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
[videoCompositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero,
firstVideoAssetTrack.timeRange.duration) ofTrack:firstVideoAssetTrack
atTime:kCMTimeZero error:nil];
[videoCompositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero,
secondVideoAssetTrack.timeRange.duration) ofTrack:secondVideoAssetTrack
atTime:firstVideoAssetTrack.timeRange.duration error:nil];
[audioCompositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, CMTimeAdd(firstVideoAssetTrack.timeRange.duration, secondVideoAssetTrack.timeRange.duration)) ofTrack:[[audioAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] atTime:kCMTimeZero error:nil];
```

*注:假设你有两个assets,它们每一个至少包含了一个video track,第三个asset中至少包含了一个audio track。这个视频可以从Camera Roll中获取,audio track可以从音乐库或者视频自己的audio tack中获取。*

### 检测视频方向

一旦你将音频和视频track添加到这个composition中,你需要确保两个video track的方向正确的。默认情况下,所有的视频track假定都是水平模式的。如果你的video track在竖直模式拍摄,那么当它们输出的时候是不能被正确输出的。同样的,如果你试图将一个竖直模式的视频和一个水平模式的视频合成一个视频,那么这个输出的session就会失败。

```objc
BOOL isFirstVideoPortrait = NO;
CGAffineTransform firstTransform = firstVideoAssetTrack.preferredTransform;

// Check the first video track's preferred transform to determine if it was recorded in portrait mode.
if (firstTransform.a == 0 && firstTransform.d == 0 && (firstTransform.b == 1.0 ||
firstTransform.b == -1.0) && (firstTransform.c == 1.0 || firstTransform.c ==
-1.0)) {
isFirstVideoPortrait = YES;
}
BOOL isSecondVideoPortrait = NO;
CGAffineTransform secondTransform = secondVideoAssetTrack.preferredTransform;

// Check the second video track's preferred transform to determine if it was
recorded in portrait mode.
if (secondTransform.a == 0 && secondTransform.d == 0 && (secondTransform.b == 1.0
|| secondTransform.b == -1.0) && (secondTransform.c == 1.0 || secondTransform.c
== -1.0)) {
isSecondVideoPortrait = YES;
}
if ((isFirstVideoAssetPortrait && !isSecondVideoAssetPortrait) ||
(!isFirstVideoAssetPortrait && isSecondVideoAssetPortrait)) {
UIAlertView *incompatibleVideoOrientationAlert = [[UIAlertView alloc]
initWithTitle:@"Error!" message:@"Cannot combine a video shot in portrait mode
with a video shot in landscape mode." delegate:self cancelButtonTitle:@"Dismiss"
otherButtonTitles:nil];
[incompatibleVideoOrientationAlert show];
return;
}
```

### 使用视频合成层指令

一旦你知道视频片段有可兼容的视频方向,那么你就可以对每个视频段使用必要的图层指令来,并且将这些图层指令添加到视频合成中。

```objc
BOOL isFirstVideoPortrait = NO;
CGAffineTransform firstTransform = firstVideoAssetTrack.preferredTransform;
// Check the first video track's preferred transform to determine if it was recorded in portrait mode.
if (firstTransform.a == 0 && firstTransform.d == 0 && (firstTransform.b == 1.0 ||
firstTransform.b == -1.0) && (firstTransform.c == 1.0 || firstTransform.c ==
-1.0)) {
isFirstVideoPortrait = YES;
}
BOOL isSecondVideoPortrait = NO;
CGAffineTransform secondTransform = secondVideoAssetTrack.preferredTransform;
// Check the second video track's preferred transform to determine if it was
recorded in portrait mode.
if (secondTransform.a == 0 && secondTransform.d == 0 && (secondTransform.b == 1.0
|| secondTransform.b == -1.0) && (secondTransform.c == 1.0 || secondTransform.c
== -1.0)) {
isSecondVideoPortrait = YES;
}
if ((isFirstVideoAssetPortrait && !isSecondVideoAssetPortrait) ||
(!isFirstVideoAssetPortrait && isSecondVideoAssetPortrait)) {
UIAlertView *incompatibleVideoOrientationAlert = [[UIAlertView alloc]
initWithTitle:@"Error!" message:@"Cannot combine a video shot in portrait mode
with a video shot in landscape mode." delegate:self cancelButtonTitle:@"Dismiss"
otherButtonTitles:nil];
[incompatibleVideoOrientationAlert show];
return;
}

所有的AVAssetTrack对象都有一个preferredTransform属性,该属性中包含了这个asset track对象的方向信息。只要这个asset track在屏幕上展示,那么这个transform都将会被使用。在上面的代码中,这个layer instruction transform被设置成了这个asset track的transform,以便于这个视频在新的合成中一旦调整渲染尺寸,仍然可以正常展示。

设置渲染尺寸和帧时长

为了将视频的方向固定,你必须响应地调整renderSize属性。你也应该对frameDuration属性挑选一个合适的值。比如每秒钟30次(或者30帧每秒)。默认情况下,renderScale属性被设置为1.0,这在本次合成过程中是合适的。

  CGSize naturalSizeFirst, naturalSizeSecond;
  // If the first video asset was shot in portrait mode, then so was the second one
   if we made it here.
  if (isFirstVideoAssetPortrait) {
  // Invert the width and height for the video tracks to ensure that they display
  properly.
      naturalSizeFirst = CGSizeMake(firstVideoAssetTrack.naturalSize.height,
  firstVideoAssetTrack.naturalSize.width);
      naturalSizeSecond = CGSizeMake(secondVideoAssetTrack.naturalSize.height,
  secondVideoAssetTrack.naturalSize.width);
} else { 
  // If the videos weren't shot in portrait mode, we can just use their natural
  sizes.
      naturalSizeFirst = firstVideoAssetTrack.naturalSize;
      naturalSizeSecond = secondVideoAssetTrack.naturalSize;
  }
  float renderWidth, renderHeight;
  // Set the renderWidth and renderHeight to the max of the two videos widths and
  heights.
  if (naturalSizeFirst.width > naturalSizeSecond.width) {
      renderWidth = naturalSizeFirst.width;
} else { 
      renderWidth = naturalSizeSecond.width;
  }
  if (naturalSizeFirst.height > naturalSizeSecond.height) {
      renderHeight = naturalSizeFirst.height;
} else { 
      renderHeight = naturalSizeSecond.height;
  }

 mutableVideoComposition.renderSize = CGSizeMake(renderWidth, renderHeight);
  // Set the frame duration to an appropriate value (i.e. 30 frames per second for
  video).
 mutableVideoComposition.frameDuration = CMTimeMake(1,30);

导出合成,并且将它保存到Camera Roll中

在该过程的最后一个步骤设计到将整个合成保存到一个单独的视频文件,并且将视频存储到Camera Roll中。你可以使用AVAssetExportSession对象来创建这个新的video文件,然后你将它传递到输出文件期望的URL中。然后使用ALAssetLibrary对象来保存这个结果视频文件到Camera Roll中。

// Create a static date formatter so we only have to initialize it once.
  static NSDateFormatter *kDateFormatter;
  if (!kDateFormatter) {
      kDateFormatter = [[NSDateFormatter alloc] init];
      kDateFormatter.dateStyle = NSDateFormatterMediumStyle;
      kDateFormatter.timeStyle = NSDateFormatterShortStyle;
} 

// Create the export session with the composition and set the preset to the highest quality. 
  AVAssetExportSession *exporter = [[AVAssetExportSession alloc]
  initWithAsset:mutableComposition presetName:AVAssetExportPresetHighestQuality];

// Set the desired output URL for the file created by the export process.
exporter.outputURL = [[[[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil 
create:@YES error:nil] URLByAppendingPathComponent:[kDateFormatter stringFromDate:[NSDate date]]] URLByAppendingPathExtension:CFBridgingRelease(UTTypeCopyPreferredTagWithClass((CFStringRef)AVFileTypeQuickTimeMovie, 
   kUTTagClassFilenameExtension))];

// Set the output file type to be a QuickTime movie.
  exporter.outputFileType = AVFileTypeQuickTimeMovie;
  exporter.shouldOptimizeForNetworkUse = YES;
  exporter.videoComposition = mutableVideoComposition;

// Asynchronously export the composition to a video file and save this file to the camera roll once export completes. 
 [exporter exportAsynchronouslyWithCompletionHandler:^{
      dispatch_async(dispatch_get_main_queue(), ^{
          if (exporter.status == AVAssetExportSessionStatusCompleted) {

 ALAssetsLibrary *assetsLibrary = [[ALAssetsLibrary alloc] init];
              if ([assetsLibrary
  videoAtPathIsCompatibleWithSavedPhotosAlbum:exporter.outputURL]) {

[assetsLibrary writeVideoAtPathToSavedPhotosAlbum:exporter.outputURL completionBlock:NULL]; 
         } 
      } 
   }); 
}]; 

SDWebImage学习笔记(二)

Posted on 2017-08-23

本文是根据SDWebImage,Tag2.0到Tag2.6所做的总结。

注意判空

为了安全起见,方法中每个参数的值的异常我们都需考虑到,我们要有一个guard,然后直接返回,否则在某些特殊情况下,比如这个参数是服务端返回的,某次服务端没有返回,就会崩溃,加了guard之后就不会了,在swift中加入了guard关键字来做:

1
2
3
4
guard x > 0 else {
// 变量不符合条件判断,则返回
return
}

因为在OC中没有这样的判断,所以我们需要添加如下的代码:

1
2
3
4
5
if (!url)
{
self.image = nil;
return;
}

针对某个机型做接口适配

如果某个机型的手机需要做一些特殊处理,我们可以这么做:

1
2
3
4
5
6
7
8
9
10
11
 #ifdef __IPHONE_4_0
UIDevice *device = [UIDevice currentDevice];
if ([device respondsToSelector:@selector(isMultitaskingSupported)] && device.multitaskingSupported)
{
// When in background, clean memory in order to have less chance to be killed
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}
#endif

这样一来就只有iPhone4会执行里面的代码(__IPHONE_4_0是系统自带的宏)。

多线程中如何使用NSFileManager

如果某个方法是在后台线程中执行的,并且这个线程中使用了NSFileManager,为了避免资源竞争,需要使用[[NSFileManager alloc] init]的方式进行初始化:

1
2
// Can't use defaultManager another thread
NSFileManager *fileManager = [[NSFileManager alloc] init];

存储图片时候不改变其图片类型

如果我们在存储图片的时候要存储成某种格式的图片类型,那么可以使用:

1
[fileManager createFileAtPath:[self cachePathForKey:key] contents:UIImageJPEGRepresentation(image, (CGFloat)1.0) attributes:nil];

如果我们不想改变存储时候的数据类型,那么可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
   [fileManager createFileAtPath:[self cachePathForKey:key] contents:data attributes:nil];
```

来完成。

## 在NSOperationQueue中添加立即执行的任务

如果要在NSOperationQueue中添加要立即执行的任务,那么可以这样做:

```objc
[cacheOutQueue addOperation:[[[NSInvocationOperation alloc] initWithTarget:self selector:@selector(queryDiskCacheOperation:) object:arguments]
```


## 头文件引入过多

如果我们的头文件引入过多,那么我们需要将这些头文件都放到一个新的文件中去,然后引入那个单独的文件即可,比如SDWebImageCompat文件的存在就是为了解决这个问题:

```objc
#import <TargetConditionals.h>
#if !TARGET_OS_IPHONE
#import <AppKit/AppKit.h>
#ifndef UIImage
#define UIImage NSImage
#endif
#ifndef UIImageView
#define UIImageView NSImageView
#endif
#else
#import <UIKit/UIKit.h>
#endif

从数组中找到指向同一块内存空间的指针

如果要从数组中找到数值相同的指针,我们可以使用:

1
2
3
4
5
6
NSUInteger idx = [cacheDelegates indexOfObjectIdenticalTo:delegate];
if (idx == NSNotFound)
{
// Request has since been canceled
return;
}

这种形式,但是要注意没有找到时候的处理。

UIImage的解压缩

PNG和JPEG等只是一种图片压缩格式,PNG是无损压缩,含有alpha通道,JPEG是有损压缩,没有alpha通道。我们每次展示图片的时候都需要先将UIImage进行解压缩。解压缩默认是在主线程中进行的,如果图片较大则会造成很大的性能消耗,如果在IO线程中进行则会提高性能,框架中使用的方式是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CGImageRef imageRef = image.CGImage;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(NULL,
CGImageGetWidth(imageRef),
CGImageGetHeight(imageRef),
8,
// Just always return width * 4 will be enough
CGImageGetWidth(imageRef) * 4,
// System only supports RGB, set explicitly
colorSpace,
// Makes system don't need to do extra conversion when displayed.
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
CGColorSpaceRelease(colorSpace);
if (!context) return nil;

CGRect rect = (CGRect){CGPointZero, CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)};
CGContextDrawImage(context, rect, imageRef);
CGImageRef decompressedImageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);

UIImage *decompressedImage = [[UIImage alloc] initWithCGImage:decompressedImageRef];
CGImageRelease(decompressedImageRef);
return [decompressedImage autorelease];

关于UIImage的解压缩,可以参考相关文章:
怎样避免UIImage解压缩造成的性能消耗?,IOS中的图片解压缩,UIImage解压缩的两种形式,谈谈IOS中图片的解压缩

怎样获取某个目录下的文件大小?

1
2
3
4
5
6
7
8
9
10
11
12
-(int)getSize
{
int size = 0;
NSDirectoryEnumerator *fileEnumerator = [[NSFileManager defaultManager] enumeratorAtPath:diskCachePath];
for (NSString *fileName in fileEnumerator)
{
NSString *filePath = [diskCachePath stringByAppendingPathComponent:fileName];
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
size += [attrs fileSize];
}
return size;
}

Struct初始化

在某些情况下,Struct可以使用如下的方法进行初始化:

1
CGRect rect = (CGRect){CGPointZero, { CGImageGetWidth(imageRef), CGImageGetHeight(imageRef) }};

调用方法是需要注意

在某种特殊情况下,调用方法前要判断是否该对象具有某个方法,以免引起崩溃:

1
2
-    if ([((NSHTTPURLResponse *)response) statusCode] >= 400)
+ if ([response respondsToSelector:@selector(statusCode)] && [((NSHTTPURLResponse *)response) statusCode] >= 400)

因为在上述代码中response可能不是HTTP的response。

ImageIO实现图片边下边展示效果

实现渐下渐放效果是在SDWebImageDownloader.m文件的- (void)connection:(NSURLConnection *)aConnection didReceiveData:(NSData *)data{}方法中。其中的关键代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 /// Create the image
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#ifdef TARGET_OS_IPHONE
// Workaround for iOS anamorphic image
if (partialImageRef)
{
const size_t partialHeight = CGImageGetHeight(partialImageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (bmContext)
{
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
CGImageRelease(partialImageRef);
partialImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
}
else
{
CGImageRelease(partialImageRef);
partialImageRef = nil;
}
}

然后根据CGImageRef生成对应的UIImage并且将结果返回给delegate:

1
2
UIImage *image = [[UIImage alloc] initWithCGImage:partialImageRef];
[delegate imageDownloader:self didUpdatePartialImage:image];

怎样在UIKit中更好的使用ValueType

Posted on 2017-08-14

ValueType怎样用到UIView中?

如果两个类没有相同的父类,我们又想对它们写一个相同的逻辑,怎么办?我们可以让它们遵守同一个协议,该协议中有它们共有的属性,然后就可以复用了。这样我们就依赖Protocol而不是SuperClassl来建立一个多态。
分享代码不必使用继承了:
UIView的组合不是最优的,因为UIView的创建需要很长的时间,并且需要开辟堆空间,需要处理事件。

我们接下来看看怎样利用ValueType将Layout和具体的类进行分开。因为我们可能会用UIView也可能用SKNode来做其ContentView。
按照之前的情况,我们可能这样写:

class DecoratingLayoutCell : UITableViewCell {
   var content: UIView
   var decoration: UIView
   // Perform layout...
} 

为了将具体的Cell和其Layout分开,我们创建一个Struct:

struct DecoratingLayout {
   var content: UIView
   var decoration: UIView   
mutating func layout(in rect: CGRect) {
   // Perform layout...
   } 
} 

然后在Cell中我们就可以这样写:

class DreamCell : UITableViewCell {
   ...
   override func layoutSubviews() {

   var decoratingLayout = DecoratingLayout(content: content, decoration:decoration)
   decoratingLayout.layout(in:bounds)
} 

同时,我们可以对UIView也用同样的Layout(封装变化)。

class DreamDetailView : UIView {
   ...
   override func layoutSubviews() {
      var decoratingLayout = DecoratingLayout(content: content, decoration:decoration)
   decoratingLayout.layout(in:bounds)
} } 

将Layout分开之后还非常有利于单元测试,比如我们想要测试我们的Layout创建出来的两个View是否符合我们的预期,我们可以这样:

 let child1 = UIView()
   let child2 = UIView()
   var layout = DecoratingLayout(content: child1, decoration: child2)
   layout.layout(in: CGRect(x: 0, y: 0, width: 120, height: 40))
} 
XCTAssertEqual(child2.frame, CGRect(x: 0, y: 5, width: 35, height: 30))
XCTAssertEqual(child2.frame, CGRect(x: 35, y: 5, width: 70, height: 30))

因为我们的Layout代码量很小并且和其它代码是隔离的(没有继承,不是Reference Type),那么这样我们就可以很好的做局部分析。

有一天,我们要对SKNode也做同样的操作。所以我们也需要NodeDecoratingLayout:

struct ViewDecoratingLayout {
   var content: UIView
   var decoration: UIView   
mutating func layout(in rect: CGRect) {
   // Perform layout...
   } 
} 
struct NodeDecoratingLayout {
   var content: SKNode
   var decoration: SKNode   
mutating func layout(in rect: CGRect) {
   // Perform layout...
   content.frame = ....
   decoration.frame = ...
   } 
} 

我们可以看到这和VIewDecoratingLayout是一样的。所以我们要将它封装起来,怎么封装?UIView和SKNode没有共同的父类。因为这两个结构体都有一个属性,那么我们就用一个Layout的Protocol来解决这个问题:

protocol Layout {
   var frame: CGRect {get set}
}
struct DecoratingLayout {
   var content: Layout
   var decoration: Layout
   mutating func layout(in rect: CGRect) {
      content.frame = ...
      decoration.frame = ...
   }
} 

然后为了让UIView和SKNode都是用这个Layout,我们只用Protocol Extension。

extension UIView : Layout{}
extension SKNode : Layout{}

我们用Protocol和Extension实现了多态,而没有使用继承!

这样我们的Layout就不必依赖于UIKit了。

但是上面的代码有一个Bug,我们不能保证content和decoration是相同的类型,他们可能一个是UIView,一个是SKNode。我们可以使用泛型来解决这个问题。我们可以给DecoratingLayout添加泛型:

struct DecoratingLayout<Child: Layout> {
   var content: Child
   var decoration: Child
   mutating func layout(in rect: CGRect) {
      content.frame = ...
      decoration.frame = ...
   }
}

这样我么就可以保证了,content和decoration有相同的类型。使用泛型,可以让编译器对代码有一个更好的理解,然后会对代码做很多的优化。

但是如果出现了更加相似的布局,我们该怎样重用我们的代码呢?比如:

SimilarLayout

重用代码,我们最先想到的是继承,但是继承会造成Override及变量的变更,这样我们很难从子类中就明确的推断出代码的含义,那么我们用组合来做这个事情,面对上面的第二张图,我们通常的做法是,创建两个UIView:

Composition

但是这样做有弊端的:类对象是很消耗性能的:开辟堆空间,内存管理,除此之外,UIView还有:绘制,事件处理。然而Struct是几乎不消耗性能,并且由于其实Value Type,所以它可以做更好的封装,你不需要关系别人对其进行修改。

我们可以使用如下方式:

struct CascadingLayout<Child : Layout> {
   var children: [Child]
   mutating func layout(in rect: CGRect) {
... } 
} 
struct DecoratingLayout<Child : Layout> {
   var content: Child
   var decoration: Child
   mutating func layout(in rect: CGRect) {
      content.frame = ...
      decoration.frame = ...
   }
} 

这两种方式组合,这里我们不需要Layout协议有frame属性,我们只需要其有layout()方法即可

protocol Layout {
 mutating func layout(in rect: CGRect)
}

然后,我们的DecoratingLayout和CascadingLayout遵守该协议即可:

struct DecoratingLayout<Child : Layout, ...> : Layout {}
struct CascadingLayout<Child : Layout> : Layout {}

这之后我们就可以用组合的方式实现上文提到的UI效果了:

// Composition of Values
let decoration = CascadingLayout(children: accessories)
var composedLayout = DecoratingLayout(content: content, decoration: decoration)
composedLayout.layout(in: rect)

为了实现这种UIView的叠加效果,我们给Layout协议添加contents属性:

protocol Layout {
  mutating func layout(in rect: CGRect)
  var contents:[Layout]{get}  // 这里可以是UIView或者SKNode
}

但是这回出现和之前所说的一样的Bug,我们没有办法保证这个contents属性里是否既有UIView还有SKNode,为了解决这个问题,我们加上一个关联属性:

protocol Layout {
  mutating func layout(in rect: CGRect)
  associatedtype Content
  var contents:[Content]{get}  // 这里可以是UIView或者SKNode
}

然后在DecoratingLayout中,我们这样做:

struct DecoratingLayout<Child : Layout> : Layout {
   ...
   mutating func layout(in rect: CGRect)
   typealias Content = Child.Content
   var contents: [Content] { get }

如果我们要对其中的内容做以限制,那么我们可以这样做:

struct DecoratingLayout<Child:Layout, Decoration:Layout where Child.Content == Decoration.Content> : Layout {
    var content: Child
    var decoration: Decoration
    mutating func layout(in rect: CGRect)
    typealias Content = Child.Content
    var contents: [Content] { get }
}

这样我们的单元测试就不必依赖于具体的UIView或者是SKNode:

  func testLayout() {
   let child1 = TestLayout()
   let child2 = TestLayout()
   var layout = DecoratingLayout(content: child1, decoration: child2)
   layout.layout(in: CGRect(x: 0, y: 0, width: 120, height: 40))
   XCTAssertEqual(layout.contents[0].frame, CGRect(x: 0, y: 5, width: 35, height: 30))
   XCTAssertEqual(layout.contents[1].frame, CGRect(x: 35, y: 5, width: 70, height: 30))
}

struct TestLayout : Layout {
   var frame: CGRect
   ...  
}

ValueType怎样用到Controller中

我们增加了Favoriate功能之后,我们的撤销功能消失了。为了隔离我们的Model,我们将其变为一个Struct,并且拥有之前的属性:

class DreamListViewController : UITableViewController {
   var model: Model
... 
} 
struct Model : Equatable {
    var dreams: [Dream]
    var favoriteCreature: Creature
} 

然后将Model和View隔离的做法极其容易出bug,因为我们的Model的任何改变都要对应相应View的位置。
我们可以将其合并起来:

/// Diffs the model changes and updates the UI based on the new model.
    private func modelDidChange(diff: Model.Diff) {
        // Check to see if we need to update any rows that present a dream.
        if diff.hasAnyDreamChanges {
            switch diff.dreamChange {
                case .inserted?:
                    let indexPath = IndexPath(row: diff.from.dreams.count, section: Section.dreams.rawValue)
                    tableView.insertRows(at: [indexPath], with: .automatic)

                case .removed?:
                    let indexPath = IndexPath(row: diff.from.dreams.count - 1, section: Section.dreams.rawValue)
                    tableView.deleteRows(at: [indexPath], with: .automatic)

                case .updated(let indexes)?:
                    let indexPaths = indexes.map { IndexPath(row: $0, section: Section.dreams.rawValue) }
                    tableView.reloadRows(at: indexPaths, with: .automatic)

                case nil: break
            }
        }

如果我们的UI有各种状态,并且我们要处理这种状态,这时,该怎样处理才比较好呢?比如我们的这个App中有:展示、选择、分享三种状态。这时如果遇到状态的切换就会遇到问题,因为Cell是复用的,所以就会出现忘记清除前一种Cell的状态而引起的Bug。
这种情况出现的原因就是我们的ViewController持有了这些不同的状态,如果状态逐渐变多,那么就会有非常复杂的业务逻辑需要处理。

class DreamListViewController : UITableViewController {
    var isInViewingMode: Bool
    var sharingDreams: [Dream]?
    var selectedRows: IndexSet?
    ... 
} 

因为UI只能出现一种状态,所以每次我们切换状态,我们就需要给其它的两种状态进行置空,这时就很容易引起Bug,为了解决这个问题,我们可以使用Enum来讲这些状态进行封装。

enum State {
    case viewing
    case sharing(dreams:[Dream])
    case selecting(selectedRows: IndexSet)
}

这样我们就可以避免因为状态转换而产生的Bug了。

Vapor制作JSON Model

Posted on 2017-08-13

上篇博客中我们讲到了怎样配置数据库,以及怎样将数据库中的数据读出并且返回给客户端,本文将说明怎样将数据库中的数据用Model来表示,并且怎样对Model进行各种操作。

建立Class

我们建立以个简单的User类,如下:

final class User {
 var id: String
    var name: String
    var age: Int
    var gender: String
    var imageUrl: String
    var oatch: String

    init(id: String, name: String, age:Int, gender: String, imageUrl: String, oatch: String) 
    {

        self.id = id
        self.age = age
        self.name = name
        self.gender = gender
        self.imageUrl = imageUrl
        self.oatch = oatch
    }
}

遵守NodeRepresentable和NodeRepresentable协议

从之前返回JSON时,JSONS的生成过程JSON(node:someNode),那么Node是什么呢?其实就是一个遵守NodeRepresentable的对象,我们让User遵守NodeRepresentable协议,兵且实现其makeNode方法,这样就可以传入JSON(node:user)了:

func makeNode(in context: Context?) throws -> Node {

        return try Node(node:[
            "id":id,
            "name":name,
            "age":age,
            "gender":gender,
            "imageUrl":imageUrl,
            "oatch":oatch
            ])
    }

我们这样不调用JSON的方法,只使用我们的Model就可以创建出来JSON对象呢?我们只需要遵守JSONRepresentable对象即可,然后添加其需要实现的协议方法。

func makeJSON() throws -> JSON {
    return try JSON(node: self)
}

然后我们就可以使用user.makeJSON()来创建JSON对象了。

Swift后端Vapor安装数据库

Posted on 2017-08-11

MySQL

Vapor中使用Fluent作为数据库的驱动,它现在可支持的数据库类型有:MySQL,SQL lite,MongoDB,PostgreSQL。因为MySQL用得较多,我们先来学习它。

安装过程

安装MySQL-Provider

在Package.swift文件中加入

.Package(url: "https://github.com/vapor/mysql-provider.git", majorVersion: 2)

然后执行执行vapor clean和rm -rf .build Package.pins,最后执行vapor update和vapor build。
安装完MySql之后报错mysql/mysql.h file not found以及Could not build Objective-C module CMySQL,这时因为MySql数据库需要更新,执行下面的指令

brew update && brew install mysql vapor/tap/cmysql pkg-config

然后再执行vapor xcode就可以运行成功了。

配置

在Droplet中添加驱动

我们要往Config对象中添加相应的Provider,如下所示

import MySQLProvider
let config = try Config()
try config.addProvider(MySQLProvider.Provider.self)
let drop = try Droplet(config)

配置Fluent

在fluent.json文件中加入如下的配置

{
    "//": "The underlying database technology to use.",
    "//": "memory: SQLite in-memory DB.",
    "//": "sqlite: Persisted SQLite DB (configure with sqlite.json)",
    "//": "Other drivers are available through Vapor providers",
    "//": "https://github.com/search?q=topic:vapor-provider+topic:database",
    "driver": "mysql",
}

配置MySQL

在Config文件夹下面新建文件mysql.json,并添加如下内容

{
    "hostname": "localhost",
    "user": "root",
    "password": "yourPassword",
    "database": "yourDatabase"
    "poort": "3306"
}      

也可以将证书作为url传入MySQL。

{
"url": "http://root:password@172.0.0.1/hello"
}

多份读取(Read Replicas)

多份读取可以通过配置hostname或者是readReplicas接口数组来进行配置。在mysql.josn中加入:

{
   {
    "master": "master.mysql.foo.com",
    "readReplicas": ["read01.mysql.foo.com", "read02.mysql.foo.com"],
    "user": "root",
    "password": "password",
    "database": "hello"
   }
}

Tip:也可以将readReplicas用字符串表示,多个字符串用逗号分隔开。

驱动

你可以在Routes中得到MySQL Driver(前提是在你自己的MySQL数据库中创建了your_table)表。

import MySQLProvider
get("mysql") { req in

           let mysqlDriver = try self.mysql()
           let user = try mysqlDriver.raw("SELECT * FROM your_table")
           let reusltJon = try JSON(node: user)
           return reusltJon

       }

然后在浏览器中输入http://localhost:8080/mysql,如果看到输出了相应的JSON传就证明安装成功了。

配置缓存

在Config/droplet.json里面可以配置fluent缓存,这里fluent缓存走的是mysql:

{
 "driver": "fluent"
}

下次,当启动Droplet的时候,如果出现:

Database prepared

就说明安装成功了。

帮助

  1. 如果运行出现
The current hash key "0000000000000000" is not secure.
Update hash.key in Config/crypto.json before using in production.
Use `openssl rand -base64 <length>` to generate a random string.
The current cipher key "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" is not secure.
Update cipher.key in Config/crypto.json before using in production.
Use `openssl rand -base64 32` to generate a random string.

这说明我们需要运行openssl rand -base64 <length>,以及openssl rand -base64 32来产生新的hash key和cipher key,并且将原来的数值替换掉。

  1. MySql更改密码:

请参考:

  1. https://stackoverflow.com/questions/2101694/mysql-how-to-set-root-password-to-null
  2. https://stackoverflow.com/questions/30692812/mysql-user-db-does-not-have-password-columns-installing-mysql-on-osx
  3. https://sraji.wordpress.com/2011/08/10/how-to-reset-mysql-root-password/
  4. https://stackoverflow.com/questions/9624774/after-mysql-install-via-brew-i-get-the-error-the-server-quit-without-updating
  5. https://stackoverflow.com/questions/9624774/after-mysql-install-via-brew-i-get-the-error-the-server-quit-without-updating/9704993#comment16367803_9704993

需要注意的是:sudo mysqld_safe --skip-grant-tables执行完之后,要重新打开一个终端执行。

1234…6
击水湘江

击水湘江

努力让明天的自己爱上今天的自己!

56 posts
10 tags
© 2019 击水湘江
Powered by Hexo
|
Theme — NexT.Muse v6.0.3