通过命令行上传ipa到appstore

搞持续集成自动化打包上传到appstore遇到这个问题,记录一下。

其实主要就一条到命令:
xcrun altool --upload-app -f xxxx.ipa -u "yanqizhou@126.com" -p "*******"

这里的密码不是App id的登录密码,是一个App专用的密码,如果使用登录密码会报错:Please sign in with an app-specific password. You can create one at appleid.apple.com

  1. 登录 https://appleid.apple.com/
  2. 在安全栏有一个App 专用密码,点击生成一个
  3. 在命令中使用第二部生成的密码登录

上传完会看到提示:
No errors uploading 'xxxx.ipa'


iOS 14下面图片无法加载,包括weex、YYAnimateView、SDAnimatedImageView

升级xcode12后,编译运行App发现大片大片的图片加载不出来,包括weex的图片和YYAnimateView的图片都有问题。

经过一番研究之后,发现是iOS 14下UIKit对 displayLayer:的处理机制有所变化。
displayLayer:CALayerDelegate的代理方法。在iOS 14之前,UIKit在调用这个方法之前就会去渲染UIImageView.image
而在iOS 14,UIKit则是先去调用代理方法,如果你实现了displayLayer:这个方法,那么UIKit就不会再去渲染了。

如果改成下面这样就可以正常加载了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)displayLayer:(CALayer *)layer {
UIImage *currentFrame = _curFrame;
if (currentFrame) {
layer.contentsScale = currentFrame.scale;
layer.contents = (__bridge id)currentFrame.CGImage;
} else {
// If we have no animation frames, call super implementation. iOS 14+ UIImageView use this delegate method for rendering.
if ([UIImageView instancesRespondToSelector:@selector(displayLayer:)]) {
[super displayLayer:layer];
}
}
// if (_curFrame) {
// layer.contents = (__bridge id)_curFrame.CGImage;
// }
}

参考:

  1. https://github.com/apache/incubator-weex/issues/3265
  2. https://github.com/ibireme/YYWebImage/issues/242
  3. https://github.com/SDWebImage/SDWebImage/issues/3040

iOS14剪切板探究,淘宝实现方法分析和实现Demo

随着iOS 14的发布,剪切板的滥用也被大家所知晓。只要是APP读取剪切板内容,系统都会在顶部弹出提醒,而且这个提醒不能够关闭。这样,大家在使用APP的过程中就能够看到哪些APP使用了剪切板。

正好我们自己的应用也使用了剪切板,升级了iOS 14之后弹的着实让人心烦。就想着怎么处理一下,翻了一下UIPasteboard的文档,发现相关的内容并不多。
读取UIPasteboardstringstringsURLURLsimageimagescolorcolors的时候会触发系统提示。
使用hasStringshasURLshasImageshasColors等方法的时候不会触发系统提示。
那么思路就是尽可能少的去调用会触发系统提示的方法,根据其他方法去判断确实需要读取的时候再去调用那些方法。
根据我们自己的情况,只有判断hasStringsYES就去读取,又不能清剪切板,其实还是有点不尽人意,然后又看到这个属性:

1
@property(readonly, nonatomic) NSInteger changeCount;
1
2
3
The number of times the pasteboard’s contents have changed.

Whenever the contents of a pasteboard changes—specifically, when pasteboard items are added, modified, or removed—UIPasteboard increments the value of this property. After it increments the change count, UIPasteboard posts the notifications named UIPasteboardChangedNotification (for additions and modifications) and UIPasteboardRemovedNotification (for removals). These notifications include (in the userInfo dictionary) the types of the pasteboard items added or removed. Because UIPasteboard waits until the end of the current event loop before incrementing the change count, notifications can be batched. The class also updates the change count when an app reactivates and another app has changed the pasteboard contents. When users restart a device, the change count is reset to zero.

然后就又加了一个条件,记录一下真正读取剪切板时的changeCount,如果下次读取的时候没有发生变化则不读取。
这样一来效果就好多了,应用运行生命周期内,基本上只会弹出一次提示。

但是,后来看到淘宝更牛逼,命中了真正分享出来的内容才会弹,其他则不会弹。然后就研究了一下淘宝的分享内容和新的API文档,大概得到了答案。
先看下淘宝的分享内容:

1
9👈幅治内容 Http:/T$AJg8c4IfW3q$打開👉绹..寶👈【贵州茅台酒 茅台 飞天53度酱香型白酒收藏 500ml*1单瓶装送礼高度】

组成部分:数字+文字+链接的形势,再结合iOS 14提供的新API:

1
2
detectPatternsForPatterns:completionHandler:
detectPatternsForPatterns:inItemSet:completionHandler:

UIPasteboardDetectionPattern系统只提供了三种:

  1. 数字 UIPasteboardDetectionPatternNumber
  2. 链接 UIPasteboardDetectionPatternProbableWebURL
  3. 搜索UIPasteboardDetectionPatternProbableWebSearch

大胆猜测,淘宝应该是通过判断是否符合数字和链接的规则来判断是否命中分享内容。

然后就写了一个demo来验证,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
UIPasteboard *board = [UIPasteboard generalPasteboard];

[board detectPatternsForPatterns:[NSSet setWithObjects:UIPasteboardDetectionPatternProbableWebURL, UIPasteboardDetectionPatternNumber, UIPasteboardDetectionPatternProbableWebSearch, nil]
completionHandler:^(NSSet<UIPasteboardDetectionPattern> * _Nullable set, NSError * _Nullable error) {

BOOL hasNumber = NO, hasURL = NO;
for (NSString *type in set) {
if ([type isEqualToString:UIPasteboardDetectionPatternProbableWebURL]) {
hasURL = YES;
} else if ([type isEqualToString:UIPasteboardDetectionPatternNumber]) {
hasNumber = YES;
}
}

if (hasNumber && hasURL) {

dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"温馨提示" message:[NSString stringWithFormat:@"%@\n%@", [board string], @"符合淘宝的读取标准"] preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:nil];
[alert addAction:cancelAction];
[self presentViewController:alert animated:YES completion:nil];
});
}
}];

然后构造了一个看起来符合条件1 http:/123abc的串去反向验证了一下发现demo和淘宝的结果的表现是一致的,都弹了系统提示。

具体的demo可去github下载。


iOS14 debug安装的带有flutter应用从桌面图标重新启动时闪退

刚刚升级遇到的问题,希望能够帮助到遇到相同问题的人。

  1. 用xcode12连接iOS设备调试安装应用
  2. 拔掉数据线
  3. 从桌面图标点击进入App必闪退

后来发现是flutter的原因,目前有下面两种个解决方案:

  1. 使用flutter的release模式 flutter run --release
  2. 使用flutter的beta版本v1.22

参考文章:

  1. https://medium.com/flutter/supporting-ios-14-and-xcode-12-with-flutter-15fe0062e98b
  2. https://github.com/flutter/flutter/issues/66175

用JavaScript实现树的前序、中序、后续查询以及高度计算

用JavaScript实现树的前序、中序、后续查询以及高度计算

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
class TreeNode {
constructor (value) {
this.value = value;
this.left = this.right = null;
}
}

var maxDepth = function(troot) {
if (!troot) {
return 0;
}

const left = maxDepth(troot.left);
const right = maxDepth(troot.right);
return Math.max(left, right) + 1;
}

var preOrder = function(root) {
if (!root) {
return;
}
console.log(root.value);
preOrder(root.left);
preOrder(root.right);
}

var preOrderWithoutRescursion = function(root) {
let stack = [];
let pointNode = root;

while(pointNode || stack.length > 0) {
while(pointNode) {
console.log(pointNode.value);
stack.push(pointNode);
pointNode = pointNode.left;
}

if (stack.length > 0) {
pointNode = stack.pop();
pointNode = pointNode.right;
}
}
}

var midOrder = function(root) {
if (!root) {
return;
}
midOrder(root.left);
console.log(root.value);
midOrder(root.right);
}

var midOrderWithoutRescursion = function(root) {
let stack = [];
let pointNode = root;

while(pointNode || stack.length > 0) {
while(pointNode) {
stack.push(pointNode);
pointNode = pointNode.left;
}

if (stack.length > 0) {
pointNode = stack.pop();
console.log(pointNode.value);
pointNode = pointNode.right;
}
}
}

var postOrder = function(root) {
if (!root) {
return;
}
postOrder(root.left);
postOrder(root.right);
console.log(root.value);
}

var postOrderWithoutRescursion = function(root) {
let stack = [];
let pointNode = root;
let right = root;

while(pointNode || stack.length > 0) {
if (pointNode) {
stack.push(pointNode);
pointNode = pointNode.left;
} else {
pointNode = stack.length > 0 ? stack[stack.length - 1] : null;
if (pointNode.right && pointNode.right != right) {
pointNode = pointNode.right;
} else{
pointNode = stack.pop();
console.log(pointNode.value);
right = pointNode;
pointNode = null;
}
}
}
}

var levelOrder = function(root) {
if (!root) {
return;
}

let queue = [];
queue.unshift(root);

while(queue.length > 0) {
let pointNode = queue.pop();
console.log(pointNode.value);

if (pointNode.left) {
queue.unshift(pointNode.left)
}

if (pointNode.right) {
queue.unshift(pointNode.right);
}
}
}

var tree = new TreeNode(3);
tree.left = new TreeNode(9);
tree.right = new TreeNode(20);
tree.right.left = new TreeNode(15);
tree.right.right = new TreeNode(7);

let depth = maxDepth(tree);
console.log("depth:"+depth);

console.log("pre order:");
preOrder(tree);

console.log("mid order:");
midOrder(tree)

console.log("post order:");
postOrder(tree)

console.log("pre order without recursion");
preOrderWithoutRescursion(tree);

console.log("mid order without recursion");
midOrderWithoutRescursion(tree);

console.log("post order without recursion");
postOrderWithoutRescursion(tree);

console.log("level order");
levelOrder(tree);

运行结果:

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
depth:3
pre order:
3
9
20
15
7
mid order:
9
3
15
20
7
post order:
9
15
7
20
3
pre order without recursion
3
9
20
15
7
mid order without recursion
9
3
15
20
7
post order without recursion
9
15
7
20
3
level order
3
9
20
15
7

NSObject中类方法load和initialize

+load和+initialize是NSObject的两个类方法,这两个类方法会在类被使用时主动调用,但是调用时机和调用顺序却截然不同。

先看看苹果的官方文档里面

Initialize

###Initializing a Class
class func initialize()
Initializes the class before it receives its first message.

he runtime sends initialize() to each class in a program just before the class, or any class that inherits from it, is sent its first message from within the program. Superclasses receive this message before their subclasses.
The runtime sends the initialize() message to classes in a thread-safe manner. That is, initialize() is run by the first thread to send a message to a class, and any other thread that tries to send a message to that class will block until initialize() completes.
The superclass implementation may be called multiple times if subclasses do not implement initialize()—the runtime will call the inherited implementation—or if subclasses explicitly call [super initialize]. If you want to protect yourself from being run multiple times, you can structure your implementation along these lines:

1
2
3
4
5
+ (void)initialize {
if (self == [ClassName self]) {
// ... do the initialization ...
}
}

Because initialize() is called in a blocking manner, it’s important to limit method implementations to the minimum amount of work necessary possible. Specifically, any code that takes locks that might be required by other classes in their initialize() methods is liable to lead to deadlocks. Therefore, you should not rely on initialize() for complex initialization, and should instead limit it to straightforward, class local initialization.

initialize() is invoked only once per class. If you want to perform independent initialization for the class and for categories of the class, you should implement load() methods.

  1. initialize方法是在该类接收到第一个消息之前调用.
  2. 父类的 initialize 方法先于子类的 initialize 方法调用.
  3. 如果子类中没有实现 initialize 方法,或者子类显示调用父类实现 [super initialize], 那么则会调用其父类的实现。也就是说,父类的 initialize 可能会被调用多次。
  4. 在应用程序生命周期里,runtime 只会向每个类发送一次 initialize 消息. 所以如果category中实现了initialize方法,那么原来类中的则不会被调用.

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface SuperObject : NSObject
@end

@implementation SuperObject
+ (void)initialize {
NSLog(@"%s", __FUNCTION__);
}
@end

@interface SubObject : SuperObject
@end

@implementation SubObject
+ (void)initialize {
NSLog(@"%s", __FUNCTION__);
}
@end

int main(int argc, char * argv[]) {
@autoreleasepool {
[SubObject class];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

输出:

1
2
+[SuperObject initialize]
+[SubObject initialize]

就算把main函数中的[SubObject class]改为:

1
2
[SubObject class];
[[SubObject alloc] init];

输出还是一样。

添加一个category:

1
2
3
4
5
6
7
8
@interface SubObject (category)
@end

@implementation SubObject (category)
+ (void)initialize {
NSLog(@"%s", __FUNCTION__);
}
@end

输出则为(category中方法覆盖了原类中的方法):

1
2
+[SuperObject initialize]
+[SubObject(category) initialize]

load

###class func load()
Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.

The load() message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.
The order of initialization is as follows:

1. All initializers in any framework you link to.
2. All +load methods in your image.
3. All C++ static initializers and C/C++ __attribute__(constructor) functions in your image.
4. All initializers in frameworks that link to you.

In addition:

* A class’s +load method is called after all of its superclasses’ +load methods.
* A category +load method is called after the class’s own +load method.

In a custom implementation of load() you can therefore safely message other unrelated classes from the same image, but any load() methods implemented by those classes may not have run yet.

  1. 在类或者category被添加到runtime的时候调用,该调用发生在main函数之前。
  2. 父类load方法先于子类调用,类本身load方法先于category中调用。

看下面的例子:

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
@interface SuperObject : NSObject
@end

@implementation SuperObject
+ (void)initialize {
NSLog(@"%@ , %s", [self class], __FUNCTION__);
}
+ (void)load {
NSLog(@"%@, %s", [self class], __FUNCTION__);
}
@end

@interface SubObject : SuperObject
@end

@implementation SubObject
+ (void)initialize {
NSLog(@"%@, %s", [self class], __FUNCTION__);
}
+ (void)load {
NSLog(@"%@, %s", [self class], __FUNCTION__);
}
@end

@interface SubObject (category)
@end

@implementation SubObject (category)
+ (void)initialize {
NSLog(@"%@, %s", [self class], __FUNCTION__);
}
+ (void)load {
NSLog(@"%@ %s", [self class], __FUNCTION__);
}
@end

int main(int argc, char * argv[]) {
@autoreleasepool {
SubObject *subObject = [[SubObject alloc] init];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

输出结果(这里有点地方要解释一下,按照文档的说法,load应该咋initialize之前,这里实在之后,原是因为,load方法中调用了class方法,从而触发了initialize方法,所以出现了这种情况):

1
2
3
4
5
SuperObject , +[SuperObject initialize]
SuperObject, +[SuperObject load]
SubObject, +[SubObject(category) initialize]
SubObject, +[SubObject load]
SubObject +[SubObject(category) load]

再看一个例子,子类中不实现initialize方法:

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
@interface SuperObject : NSObject
@end

@implementation SuperObject
+ (void)initialize {
NSLog(@"%@ , %s", [self class], __FUNCTION__);
}

@end

@interface SubObject : SuperObject
@end

@implementation SubObject
+ (void)load {
NSLog(@"%@, %s", [self class], __FUNCTION__);
}
@end

int main(int argc, char * argv[]) {
@autoreleasepool {
SubObject *subObject = [[SubObject alloc] init];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

输出结果如下(子类自动调了父类的initialize):

1
2
3
SuperObject , +[SuperObject initialize]
SubObject , +[SuperObject initialize]
SubObject, +[SubObject load]

不需要在loadinitialize方法中显式的去调用父类的方法。


Object-C Two-Stage Creation:alloc、init

在其他语种中例如JAVA,C++,Ruby等这些语言中new是很常见的,但是在object-c中,大家最常见到的和最常用的初始化方法就是[[ClassName alloc] init]而不是[ClassName new] 去初始化对象。

先来看看他们的方法实现:

1
2
3
4
5
6
7
8
+ new {
id newObject = (*_alloc)((Class)self, 0);
Class metaClass = self->isa;
if (class_getVersion(metaClass) > 1)
return [newObject init];
else
return newObject;
}
1
2
3
4
5
6
7
+ alloc {
return (*_zoneAlloc)((Class)self, 0, malloc_default_zone());
}

- init {
return self;
}

从上面的实现方法来看,其实就是把new拆分成了allocinit,所以在Cocoa的文档中,这种创建对象的方法也被叫做 Two-Stage Creation

首先我们看看Cocoa这么设计的初衷是什么

  • 使用初始化方法的时候,不需要管内存分配的方式
  • 子类化一个对象时,避免需要实现太多的初始化方法
  • 简化临时实例的创建和使用

怎么理解呢,举个例子:
假如有这样一个对象,处于一些特殊原因,有5中分配内存的方式和10种初始化的方式,如果alloc和init在一起的,那么就需要5*10种构造方法为每一种内存分配方式下提供10中初始化方式。如果alloc和init分开的话,就只需要分别提供5种内存分配方式和10中初始化的方式就可以了。

###alloc

NSObject这个基类提供了两种内存分配的方法:+(id)alloc+(id)allocWithZone:(NSZone *)aZone,这两个方法在被继承的对象中几乎从不被重写。+(id)alloc 其实也是调用了 +(id)allocWithZone:(NSZone *)aZone 传入了一个默认的zone参数(等下我们再详细说下zone)。这两个方法会返回一个指向一个重新分配的内存块的指针并且做了下面几件事情:

  1. 把对象的retain count设为1.
  2. 把对象的isa变量指向对象的class
  3. 初始化所有的成员变量为默认值(0、nil,0.0 etc.)

####Zones

这个从头说起,每一个Cocoa的应用都有大量的可寻址内存。

当设备上所有物理内存都被占用了,这时有应用还在动态的向操作系统申请内存。这个时候,操作系统就会把内存中的一些内容拷到硬盘(swapping)上从而释放出一部分内存来满足这个要求。

然后应用运行的过程中要需要用到拷到硬盘上去的那部分数据了,操作系统又会把内存中另外一块内容拷到硬盘上,然后把之前拷出去的换回来供应用使用。

由于这样的操作是比较耗时的,所以这样操作过多时就会造成抖动(thrashing)。

如果经常一起使用的两个object存在内存中比较“远”的块中,那么造成抖动的机会就会大大的增加。

考虑一下这种情况:

  1. 应用需要使用到的其中一个object不在内存中,这个时候就需要把硬盘中的块儿换回来。
  2. 换回来之后,发现这个object要访问的另外一个object又不在内存中。
  3. 这时就需要这时就需要继续从硬盘中换回数据。
  4. 最坏的情况就是,换第二部分的时候又把第一次的给换出去了。

OK,那么zone就是用来保证需要一起使用的object尽量分配的互相靠近的位置,保证,要么都在内存中,要么都在硬盘中,避免上面的情况发生。

不过在OS X 10.5的Object-C 2.0之后,如果你使用了ARC,那么allocWithZone:的zone参数将被忽略,Apple不在鼓励使用这个方法,这些操作将默认有Cocoa来实现。

###init

当内存分配完成之后,就要调用object的实例方法进行初始化了。这些初始化方法通常以init开头,返回值为id类型。

  • 一个对象可以有很多初始化方法,每一个初始化方法可以带不同的参数,通常其中有一个初始化方法会作为为对象的指定初始化方法(The Designated Initializer)。比如NSObject的指定初始化方法是-(id)init,UIView的默认初始化方法是-(id)initWithFrame:(CGRect)frame
  • 任何一个初始化方法都可以成为指定的初始化方法,但是必须要明确的说明。
  • 一般情况下,指定的初始化方法是那个带参数最多的初始化方法。
  • 其他的初始化方法通过调用指定的初始化方法来实现。

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface MYCircle : NSObject
{
NSPoint center;
float radius;
}

// Designated Initializer
- (id)initWithCenter:(NSPoint)aPoint radius:(float)aRadius;
@end


@implementation MYCircle

// Designated Initializer
- (id)initWithCenter:(NSPoint)aPoint radius:(float)aRadius
{
self = [super init];
if(nil != self) {
center = aPoint;
radius = aRadius;
}
return self;
}
@end

这里的self=[super init]是至关重要的一步。

  1. [super init]可能会失败,如果失败了,接下来如果继续操作的话是不可预期的。
  2. super可能会重新+alloc然后返回一个跟self完全不同的object,这种情况下接下来的操作也是有问题的。

回到刚才的问题,在这个例子中- (id)initWithCenter:(NSPoint)aPoint radius:(float)aRadius;被标记为指定的初始化方法。这种情况下,应该重写继承的父类的指定初始化方法-(id)init (通过NS_DESIGNATED_INITIALIZER标记)

1
2
3
4
5
6
7
8
9
10
11
12
13
- (id)init
// Overriden inherited Designated Initializer - (id)init
{
static const float MYDefaultRadius = 1.0f;

// call Designated Initializer with default arguments
return [self initWithCenter:NSZeroPoint radius:MYDefaultRadius];
}

- (id)initWithRadius:(float)aRadius
{
return [self initWithCenter:NSZeroPoint radius:aRadius];
}

基本上要遵循一下几点:

  1. 确保指定初始化方法要调用父类的指定初始化方法。
  2. 把父类的制定初始化方法的返回值赋给self。
  3. 如果父类的指定初始化返回为nil时,不要继续去操作示例变量了。
  4. 如果标记了新的指定初始化方法,那么一定要重写所继承的父类的指定初始化方法(例子总的init)。
  5. 继承的时候,确保每一个初始化方法,如果本身不是指定初始化方法,那么就要调用指定初始化方法去实现。

###Creating Temporary Instances

临时变量的创建就是把alloc和init合并到一起然后返回一个临时的实例对象。

MRC情况下:

1
2
3
4
5
6
+ (id)stringWithString:(NSString *)aString

{
return [[[self alloc] initWithString:aString] autorelease];

}

ARC情况下:

1
2
3
4
5
6
+ (id)stringWithString:(NSString *)aString

{
return [[self alloc] initWithString:aString];

}

这样在创建和使用临时实例的时候就非常方便了。


【译】在应用的icon上加上编译相关信息

#####加那些信息

  • 版本号
  • 代码分支名字
  • 最后一次commit的hash值

######版本号
我们可以使用PlistBuddy工具直接从项目文件info.plist中取出版本号

1
version=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_FILE}"`

(当然了,你可以通过这个工具取出info.plist中的任何值,把CFBundleVersion改为其他的key就行了,key值可以在xcode中查看)

######代码分支名字和commit的hash值
这两个值我们可以通过git的命令行git rev-parse来获取

1
2
commit=`git rev-parse --short HEAD`
branch=`git rev-parse --abbrev-ref HEAD`

######怎么把这些信息放到icon上去?
ImageMagic是一个操作图片的命令行工具,这个工具有一堆牛逼的功能

首先要安装ImageMagick和ghostscript(fonts)工具,可以使用brew来快速安装

1
2
brew install imagemagick
brew install ghostscript

我们可以使用convert工具,通过配置imagemagic的caption参数把这些文字信息放到icon上,顺便设置底部对齐以及默认高度。

1
2
3
convert -background '#0008' -fill white -gravity center -size ${width}x40 \
caption:"${version} ${branch} ${commit}" \
${base_file} +swap -gravity south -composite "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/${target_file}"

#####xcode 项目的配置
通过几部简单的配置把这些步骤加到xcode编译中

  1. 把项目中的Icon的文件名Icon改为Icon_base(Icon@2x.png to Icon@2x_base.png)
  2. 在编译目标的build phase中添加下面的脚本:
    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
    commit=`git rev-parse --short HEAD`
    branch=`git rev-parse --abbrev-ref HEAD`
    version=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_FILE}"`

    function processIcon() {
    export PATH=$PATH:/usr/local/bin
    base_file=$1
    base_path=`find ${SRCROOT} -name $base_file`

    if [[ ! -f ${base_path} || -z ${base_path} ]]; then
    return;
    fi

    target_file=`echo $base_file | sed "s/_base//"`
    target_path="${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/${target_file}"

    if [ $CONFIGURATION = "Release" ]; then
    cp ${base_file} $target_path
    return
    fi

    width=`identify -format %w ${base_path}`

    convert -background '#0008' -fill white -gravity center -size ${width}x40\
    caption:"${version} ${branch} ${commit}"\
    ${base_path} +swap -gravity south -composite ${target_path}
    }

    processIcon "Icon_base.png"
    processIcon "Icon@2x_base.png"
    processIcon "Icon-72_base.png"
    processIcon "Icon-72@2x_base.png"

#####结尾

关于这个脚本的几个小点:

  1. 自动跳过不存在的icon文件
  2. 通过sed去掉文件名末尾的_base字符串
  3. release版本则不会添加这些信息到icon上
  4. xcode貌似吧path信息弄乱了,所以在这个脚本使用中添加了usr/local/bin路径

脚本和例子看这里

原文地址:http://merowing.info/2013/03/overlaying-application-version-on-top-of-your-icon/


ffmpeg常用基本命令[整理]

主要参数: -i 设定输入流 -f 设定输出格式 -ss 开始时间 视频参数: -b 设定视频流量,默认为200Kbit/s -r 设定帧速率,默认为25 -s 设定画面的宽与高 -aspect 设定画面的比例 -vn 不处理视频 -vcodec 设定视频编解码器,未设定时则使用与输入流相同的编解码器 音频参数: -ar 设定采样率 -ac 设定声音的Channel数 -acodec 设定声音编解码器,未设定时则使用与输入流相同的编解码器 -an 不处理音频

1.分离视频音频流

1
2
ffmpeg -i input_file -vcodec copy -an output_file_video  //分离视频流
ffmpeg -i input_file -acodec copy -vn output_file_audio  //分离音频流

2.视频解复用

1
2
ffmpeg –i test.mp4 –vcodec copy –an –f m4v test.264
ffmpeg –i test.avi –vcodec copy –an –f m4v test.264

3.视频转码

1
2
3
4
ffmpeg –i test.mp4 –vcodec h264 –s 352*278 –an –f m4v test.264              //转码为码流原始文件
ffmpeg –i test.mp4 –vcodec h264 –bf 0 –g 25 –s 352*278 –an –f m4v test.264 //转码为码流原始文件
ffmpeg –i test.avi -vcodec mpeg4 –vtag xvid –qsame test_xvid.avi //转码为封装文件
//-bf B帧数目控制,-g 关键帧间隔控制,-s 分辨率控制

4.视频封装

1
ffmpeg –i video_file –i audio_file –vcodec copy –acodec copy output_file

5.视频剪切

1
2
3
ffmpeg –i test.avi –r 1 –f image2 image-%3d.jpeg        //提取图片
ffmpeg -ss 0:1:30 -t 0:0:20 -i input.avi -vcodec copy -acodec copy output.avi //剪切视频
//-r 提取图像的频率,-ss 开始时间,-t 持续时间

6.视频录制

1
ffmpeg –i rtsp://192.168.3.205:5555/test –vcodec copy out.avi

7.YUV序列播放

1
ffplay -f rawvideo -video_size 1920x1080 input.yuv

8.YUV序列转AVI

1
ffmpeg –s w*h –pix_fmt yuv420p –i input.yuv –vcodec mpeg4 output.avi

流媒体相关应用:

1、将文件当做直播送至live

1
ffmpeg -re -i localFile.mp4 -c copy -f flv rtmp://server/live/streamName

2、将直播媒体保存至本地文件

1
ffmpeg -i rtmp://server/live/streamName -c copy dump.flv

3、将其中一个直播流,视频改用h264压缩,音频不变,送至另外一个直播服务流

1
ffmpeg -i rtmp://server/live/originalStream -c:a copy -c:v libx264 -vpre slow -f flv rtmp://server/live/h264Stream

4、将其中一个直播流,视频改用h264压缩,音频改用faac压缩,送至另外一个直播服务流

1
ffmpeg -i rtmp://server/live/originalStream -c:a libfaac -ar 44100 -ab 48k -c:v libx264 -vpre slow -vpre baseline -f flv rtmp://server/live/h264Stream

5、将其中一个直播流,视频不变,音频改用faac压缩,送至另外一个直播服务流

1
ffmpeg -i rtmp://server/live/originalStream -acodec libfaac -ar 44100 -ab 48k -vcodec copy -f flv rtmp://server/live/h264_AAC_Stream

6、将一个高清流,复制为几个不同视频清晰度的流重新发布,其中音频不变

1
ffmpeg -re -i rtmp://server/live/high_FMLE_stream -acodec copy -vcodec x264lib -s 640×360 -b 500k -vpre medium -vpre baseline rtmp://server/live/baseline_500k -acodec copy -vcodec x264lib -s 480×272 -b 300k -vpre medium -vpre baseline rtmp://server/live/baseline_300k -acodec copy -vcodec x264lib -s 320×200 -b 150k -vpre medium -vpre baseline rtmp://server/live/baseline_150k -acodec libfaac -vn -ab 48k rtmp://server/live/audio_only_AAC_48k

7、功能一样,只是采用-x264opts选项

1
ffmpeg -re -i rtmp://server/live/high_FMLE_stream -c:a copy -c:v x264lib -s 640×360 -x264opts bitrate=500:profile=baseline:preset=slow rtmp://server/live/baseline_500k -c:a copy -c:v x264lib -s 480×272 -x264opts bitrate=300:profile=baseline:preset=slow rtmp://server/live/baseline_300k -c:a copy -c:v x264lib -s 320×200 -x264opts bitrate=150:profile=baseline:preset=slow rtmp://server/live/baseline_150k -c:a libfaac -vn -b:a 48k rtmp://server/live/audio_only_AAC_48k

8、将当前摄像头及音频通过DSSHOW采集,视频h264、音频faac压缩后发布

1
ffmpeg -r 25 -f dshow -s 640×480 -i video=”video source name”:audio=”audio source name” -vcodec libx264 -b 600k -vpre slow -acodec libfaac -ab 128k -f flv rtmp://server/application/stream_name

9、将一个JPG图片经过h264压缩循环输出为mp4视频

1
ffmpeg -i INPUT.jpg -an -vcodec libx264 -coder 1 -flags +loop -cmp +chroma -subq 10 -qcomp 0.6 -qmin 10 -qmax 51 -qdiff 4 -flags2 +dct8x8 -trellis 2 -partitions +parti8x8+parti4x4 -crf 24 -threads 0 -r 25 -g 25 -y OUTPUT.mp4

10、将普通流视频改用h264压缩,音频不变,送至高清流服务(新版本FMS live=1)

1
ffmpeg -i rtmp://server/live/originalStream -c:a copy -c:v libx264 -vpre slow -f flv “rtmp://server/live/h264Stream live=1〃

视频采集相关:

1.采集usb摄像头视频命令:

1
2
3
4
5
ffmpeg -t 20 -f vfwcap -i 0 -r 8 -f mp4 cap1111.mp4

./ffmpeg -t 10 -f vfwcap -i 0 -r 8 -f mp4 cap.mp4

具体说明如下:我们采集10秒,采集设备为vfwcap类型设备,第0个vfwcap采集设备(如果系统有多个vfw的视频采集设备,可以通过-i num来选择),每秒8帧,输出方式为文件,格式为mp4。

2.最简单的抓屏:

1
ffmpeg -f gdigrab -i desktop out.mpg

3.从屏幕的(10,20)点处开始,抓取640x480的屏幕,设定帧率为5 :

1
ffmpeg -f gdigrab -framerate 5 -offset_x 10 -offset_y 20 -video_size 640x480 -i desktop out.mpg

4.ffmpeg从视频中生成gif图片:

1
ffmpeg -i capx.mp4 -t 10 -s 320x240 -pix_fmt rgb24 jidu1.gif

5.ffmpeg将图片转换为视频:

1
http://blog.sina.com.cn/s/blog_40d73279010113c2.html

音频转换:

1.转换amr到mp3:

1
ffmpeg -i shenhuxi.amr amr2mp3.mp3

2.转换amr到wav:

ffmpeg -acodec libamr_nb -i shenhuxi.amr amr2wav.wav
3.转换mp3到wav:

1
ffmpeg -i DING.mp3 -f wav test.wav

4.转换wav到amr:

1
ffmpeg -i test.wav -acodec libamr_nb -ab 12.2k -ar 8000 -ac 1 wav2amr.amr

5.转换wav到mp3:

1
ffmpeg -i test.wav -f mp3 -acodec libmp3lame -y wav2mp3.mp3

MySQL server has gone away 问题

导入数据遇到的:ERROR : (2006, 'MySQL server has gone away') 意思就是连接断开了。

主要看下面几个参数:

1.是否重启过,如果uptime太短就是最近重启过

1
2
3
4
5
6
7
MariaDB [(none)]> show global status like 'uptime';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Uptime | 68600 |
+---------------+-------+
1 row in set (0.93 sec)

2.查看无操作多久后会自动关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
MariaDB [(none)]> show global variables like '%timeout';
+----------------------------+----------+
| Variable_name | Value |
+----------------------------+----------+
| connect_timeout | 10 |
| delayed_insert_timeout | 300 |
| innodb_lock_wait_timeout | 50 |
| innodb_rollback_on_timeout | OFF |
| interactive_timeout | 28800 |
| lock_wait_timeout | 31536000 |
| net_read_timeout | 30 |
| net_write_timeout | 60 |
| slave_net_timeout | 3600 |
| thread_pool_idle_timeout | 60 |
| wait_timeout | 28800 |
+----------------------------+----------+
11 rows in set (0.01 sec)

如果wait_timeout太短就在my.ini或者my.cnf中修改一下:

1
2
3
wait_timeout=2880000
interactive_timeout = 2880000
max_allowed_packet = 512M

3.进程主动kill掉,主要就是这个了,由于是导入数据,肯定就是这个max_allowed_packet太小了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MariaDB [(none)]> show global status like 'com_kill';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_kill | 10 |
+---------------+-------+
1 row in set (0.00 sec)
MariaDB [(none)]> show global variables like 'max_allowed_packet';
+--------------------+---------+
| Variable_name | Value |
+--------------------+---------+
| max_allowed_packet | 1048576 |
+--------------------+---------+
1 row in set (0.01 sec)

那就设置大一点儿就行了,这个也可以在上面的那个文件里修改:

1
2
3
4
5
6
7
8
9
10
MariaDB [(none)]> set global max_allowed_packet=1024*1024*16;
Query OK, 0 rows affected (0.03 sec)

MariaDB [(none)]> show global variables like 'max_allowed_packet';
+--------------------+----------+
| Variable_name | Value |
+--------------------+----------+
| max_allowed_packet | 16777216 |
+--------------------+----------+
1 row in set (0.00 sec)