Try and try again!The worst result is failure!


  • 首页

  • 归档

  • 分类

  • 搜索

iOS 中 runtime 简单介绍

发表于 2016-06-22 | 分类于 iOS |

runtime 总是一副很神秘的样子,就像盖着盖头的新娘,今天就揭下盖头看看新娘(runtime)究竟神秘样子!当然即使揭下盖头也只能看看新娘(runtime)的外貌,究竟内心是什么样的人,还需要长时间的相处去发现。

什么 runtime

runtime简称运行时,是一个c和汇编写的动态库,为 C 添加了面相对象的能力并创造了 Objective-C。也是系统运行时的一些机制,其中最主要的是消息机制。

1
2
C语言:函数的调用在编译时会决定调用哪个函数
OC语言:动态调用,也就是说编译时并不能决定真正调用哪个函数,只有在运行时才会根据函数名找到相应的函数调用。

怎样使用 runtime

说了一些没用的废话,那么 runtime 是怎样来使用的呢?下面就简单介绍一下。

常用的头文件

1
2
#import <objc/runtime.h> 包含对类、成员变量、属性、方法的操作
#import <objc/message.h> 包含消息机制

发送消息

OC 中方法调用的本质就是让对象发送消息。

比如在 OC 中这样调用方法

1
2
3
4
5
6
7
8
9
10
11
12
// 实例方法
Person *p = [[Person alloc]init];
[p eat];

// 本质:让对象发送消息(在编译时会通过runtime 转换成下面的代码)
objc_msgSend(p, @selector(eat));

// 类方法
[Person eat];

// 本质:底层会把类名调用转换成对象调用,仍然是对象发送消息
objc_msgSend([Person class], @selector(eat));

消息传递的关键是编译器构建每个类和对象时采用的数据结构,每个类必须包含2个必要元素:

1.一个指向父类的指针

2.一个调度表(dispatch table)将类的selector(方法名) 与方法的实际内存地址关联起来。

每个对象都有一个指向其所属类的指针 isa 。

当对象发送消息时,objc_msgSend() 方法根据对象的 isa 指针找到对象的类,然后在类的调度表(dispatch table)中查找 selector ,如果没有找到,objc_msgSend() 方法通过指向父类的指针找到父类,然后在父类的调度表中查找 selector,以此类推直到找到 NSObject 类。一旦找到 selector ,objc_msgSend()方法根据调度表中 selector 的内存地址调用实现。

上面的方法每次都要查找整个调度表,如果类的方法很多,执行起来会消耗较多的时间。为了保证消息的发送与执行速度,系统会将使用过的 selector 会 方法的内存地址缓存起来,每个类都有一个缓存区域,包含当前类使用过的 selector 和父类中使用过的 selector。查找调度表之前,会先查找对象所属类的缓存。

当 selector 没有被实现时,会面临2中情况:

1
2
1. 使用 [p eat] 调用
2. 使用 [p performSelector:@selector(eat)]; 调用

第一种情况编译器会报错,第二种情况需要运行时才能确定对象能否接受到消息,这时会进入消息转发流程:

  • 消息转发流程

1、动态方法解析
接收到未知消息时(假设person的eat方法尚未实现),runtime会调用+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法),比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *s = NSStringFromSelector(sel);
if ([s isEqualToString:@"eat"]) {
class_addMethod([Person class], @selector(eat), (IMP)eatFunc, "v@:");
}

return [super resolveInstanceMethod:sel];
}

void eatFunc(id self, SEL _cmd)
{
NSLog(@"hello");
}

简单介绍一下 class_addMethod 方法的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 
参数说明:

cls:被添加方法的类,这里不要使用object_getClass(self),而是使用 [Person class] 来获取当前类

name:未实现的方法名,比如我的eat方法未实现,这里就用eat

imp:转发的函数,函数名随便命名,比如这里就是eatFunc

types:一个定义该函数返回值类型和参数类型的字符串

这里的type为“v@:”
v :表示返回值为void ,若是 i 则表示 返回值类型为 int
@ :参数 id(self)
: : SEL(_cmd)

2、备用接收者
如果以上方法没有做处理,runtime会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法。
如果该方法返回了一个非nil(也不能是self)的对象,而且该对象实现了这个方法,那么这个对象就成了消息的接收者,消息就被分发到该对象。
适用情况:通常在对象内部使用,让内部的另外一个对象处理消息,在外面看起来就像是该对象处理了消息。
比如:person 让女朋友 personGirlFriend 来接收这个消息

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString * s = NSStringFromSelector(aSelector);
if ([s isEqualToString:@"eat"]) {
return self. personGirlFriend;
}
return [super forwardingTargetForSelector:aSelector];
}

3、完整消息转发
在- (void)forwardInvocation:(NSInvocation *)anInvocation方法中选择转发消息的对象,其中anInvocation对象封装了未知消息的所有细节,并保留调用结果发送到原始调用者。
比如:person将消息完整转发給父母 PersonParent来处理

1
2
3
4
5
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([PersonParent instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:self. personParent];
}
}

4、如果在以上三个方法都没有处理未知消息,则会引发异常。

遍历对象属性

OC 中成员变量的实质什么?方法的实质又是什么?类的实质又是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

/// An opaque type that represents a category.
typedef struct objc_category *Category;

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;

-------------------------------------------------
从上面可以看出:
成员变量的实质是一个指向 objc_ivar 结构体的指针。
方法的实质是一个指向 objc_method 结构体的指针
属性变量的实质是一个指向 objc_property 结构体的指针

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

类的实质就是一个指向 objc_class 结构体的指针
  • 使用方法
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
// 对成员变量的一些操作
- (void)getIvarList:(id)object
{
u_int count;
// class_copyIvarList 获取所有成员变量(包括property属性)
Ivar *lists = class_copyIvarList(object_getClass(object), &count);

for (int i=0; i<count; i++) {
// 成员变量
Ivar list = lists[i];
// 变量名
const char *name = ivar_getName(list);
// 变量类型
const char *type = ivar_getTypeEncoding(list);
// 获取指定名称的成员变量
Ivar sList = class_getClassVariable(object_getClass(object), "listName");
// 获取某个成员变量的值
id value = object_getIvar(object_getClass(object), sList);
// 为某个成员变量赋值
id reValue = (id)@"RENAME";
object_setIvar(object_getClass(object), sList, reValue);
}
// 需要手动释放
free(lists);
}

// 对属性列表的一些操作
- (void)getProperty:(id)object
{
u_int count;
// class_copyPropertyList 获取属性列表
objc_property_t *properties = class_copyPropertyList(object_getClass(object), &count);

for (int i=0; i<count; i++) {
// 属性
objc_property_t property = properties[i];
// 属性名
const char *name = property_getName(property);
// 属性类型
const char *type = property_getAttributes(property);
// 获取属性的修饰词
u_int outCount;
objc_property_t *types = property_copyAttributeList(property, &outCount);
}
// 需要手动释放
free(properties);
}
  • 使用实例1
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
// 将json 数据转换成model

// 方法一:
- (void)method1:(NSDictionary *)dic
{
// 必须类型一一对应,不然有可能报错,比如字典中name是@(1234)NSNumber类型,model中name是NSString类型,如果调用name.length 程序就会报错。
// 使用系统方法
[self setValuesForKeysWithDictionary:dic];

}

// 针对上面说的问题的解决方案

// 创建一个NSNumber 的类目

// 解决方案一:runtime(方法转移)
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *s = NSStringFromSelector(sel);
if ([s isEqualToString:@"length"]) {
class_addMethod([NSNumber class], @selector(length), (IMP)lengthFunc, "v@:");
}

return [super resolveInstanceMethod:sel];
}

int lengthFunc(id self, SEL _cmd)
{
NSString *s = [NSString stringWithFormat:@"%@",self];
return (int)s.length;
}

// 解决方案二:添加一个叫length的方法
- (int)length
{
NSString *s = [NSString stringWithFormat:@"%@",self];
return (int)s.length;
}


// 方法二:
- (void)method2:(NSDictionary *)dic
{
NSMutableDictionary *keys = [NSMutableDictionary dictionary];
u_int count;
objc_property_t *propertyLists = class_copyPropertyList(object_getClass(self), &count);

for (int i=0; i<count; i++) {

objc_property_t propertyList = propertyLists[i];
NSString *propertyName = [NSString stringWithCString:property_getName(propertyList) encoding:NSUTF8StringEncoding];
NSString *propertyType = [NSString stringWithCString:property_getAttributes(propertyList) encoding:NSUTF8StringEncoding];

// propertyType 的各个值的意义
//属性类型 name值:T value:变化
//编码类型 name值:C(copy) &(strong) W(weak) 空(assign) 等 value:无
//非/原子性 name值:空(atomic) N(Nonatomic) value:无
//变量名称 name值:V value:变化

// 裁剪类型字符串,不适用基本数据类型,只适用NSString,NSNumber,etc
NSRange range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringFromIndex:range.location + range.length];
range = [propertyType rangeOfString:@"\""];
// 裁剪到哪个角标,不包括当前角标
propertyType = [propertyType substringToIndex:range.location];

[keys setObject:propertyType forKey:propertyName];

}

free(propertyLists);

for (NSString *key in [keys allKeys]) {

if ([dic objectForKey:key]) {

NSString *type = [keys objectForKey:key];
if ([type isEqualToString:@"NSString"]) {

NSString* value = [NSString stringWithFormat:@"%@",[dic objectForKey:key]];
[self setValue:value forKey:key];

}
if ([type isEqualToString:@"NSNumber"])
{
NSNumber *value = [dic objectForKey:key];
// __NSCFString,__NSCFNumber,__NSCFBoolean,__NSCFArray,__NSCFDictionary
if ([value isKindOfClass:NSClassFromString(@"__NSCFString")]) {
// @"12"
value = value;
}else if ([value isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
// @(12),@(12.3),etc
value = @([value floatValue]);
}else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")])
{ // @(YES),@(NO)
value = @([value boolValue]);
}
[self setValue:value forKey:key];
}
}
}
}

// 方法三:找一个比较好的第三方库。
  • 使用实例2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 有时想查看model各个属性的值,但是调试时打印出来的却是一个地址,那么怎样解决这个问题呢?重写debugDescription方法。

- (NSString *)debugDescription
{
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
u_int count;
objc_property_t *properties = class_copyPropertyList([self class], &count);

for (int i = 0; i<count; i++) {
objc_property_t property = properties[i];
NSString *name = @(property_getName(property));
id value = [self valueForKey:name]?:@"nil";
[dictionary setObject:value forKey:name];
}

free(properties);

return [NSString stringWithFormat:@"<%@: %p> %@",[self class],self,dictionary];
}

添加属性与方法

该如何给系统的类(比如UIButton)添加一个属性或者方法呢?你可能想到了继承。但是如果不能用继承呢?你也许还会想到分类,但是分类只能添加方法不能添加属性啊,不要着急runtime 来了。

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
// 如何为系统的类添加属性

方案一:继承

方案二:分类+runtime(只用分类不能添加属性)

// 创建UIButton 的分类

// .h 文件
@property (nonatomic, strong) NSString *url;

// .m 文件
- (void)setUrl:(NSString *)url
{
// 关联对象
objc_setAssociatedObject(self, @selector(url), url, OBJC_ASSOCIATION_COPY);
}

- (NSString *)url{
// 获取关联对象
return objc_getAssociatedObject(self, @selector(url));
}

// 然后就可以访问buton 的 url 属性了,如果需要移除关联对象,将关联对象的值设为nil 即可。

// 当然如果只是在一个类中使用button 的 url 属性, 那么没必要建一个分类,直接在调用类中实现:

// 设置关联
objc_setAssociatedObject(button, @"link", @"这是链接测试", OBJC_ASSOCIATION_COPY);
// 获取关联的值
NSString *link = objc_getAssociatedObject(button, @"link");

// 添加方法:继承,分类,runtime(见上面的描述)

方法交换

有些时候我们希望有自己的方法取代系统的方法(按照自己的方法实现,或者防止程序崩溃),比如防止数组越界或者传入值为nil 时,系统的崩溃。

改变系统方法的实现的方式有:继承,分类(类目),这里再说一种方法的交换。具体需要那种方式自己去判断。

方法交换的相关函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 通过方法名获取方法
class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)

// 获取一个方法的实现
method_getImplementation(<#Method m#>)

// 获取一个OC实现的编码类型
method_getTypeEncoding(<#Method m#>)

// 添加一个方法
class_addMethod(__unsafe_unretained Class cls, <#SEL name#>, <#IMP imp#>, <#const char *types#>)

// 用一个方法的实现替换
class_replaceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)

// 交换2个方法的实现
method_exchangeImplementations(<#Method m1#>, <#Method m2#>)
  • 使用实例
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
// 防止数组越界
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

Method method1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method method2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(z_objectAtIndex:));
method_exchangeImplementations(method1, method2);
});
}

- (id)z_objectAtIndex:(NSUInteger)index
{
NSLog(@"~~~~~~~:%ld",count);
if (index >= self.count) {
return @"haha";
}else
{
return [self z_objectAtIndex:index];
}
}

// 上面的写法能解决数组越界的问题,但是z_objectAtIndex: 方法会调用很多次,而且个一段时间就会调用几次,不清楚为什么。是就是这样的机制,还是写法有误?


// 替换imageNamed 方法
+ (void) load
{
Method method1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
Method method2 = class_getClassMethod([UIImage class], @selector(z_imageNamed:));

method_exchangeImplementations(method1, method2);
// IMP imp1 = method_getImplementation(method1);
// IMP imp2 = method_getImplementation(method2);
// method_setImplementation(method1, imp2);
// method_setImplementation(method2, imp1);
}

+ (UIImage *)z_imageNamed:(NSString *)name
{
NSLog(@"修改名字");
return [self z_imageNamed:@"name"];
}
// 这里的z_imageNamed:就只会调用一次。

如何防止button的连点

总结

经过上面的学习,只能说对 runtime 有了一些了解,能够简单的使用一下,如果想要更深入的了解 runtime 还需要持续不断的深入学习。

参考博客:

Objective-C对象模型及应用

objc/runtime 探索

iOS开发教程之Objc Runtime笔记

NSLog使用 与 LLDB命令

发表于 2016-06-20 | 分类于 iOS |

调试是程序开发的重要的功能组成部分,可以用来发现程序出存在的问题,快速定位及解决,调试也可以辅助更好的理解程序。事实上,调试最初就是为了解决问题而产生的,调试的英文为debug, 而bug就是程序中存在的问题,debug就是解决掉这些问题。

1.调试的功能:

在调试中,程序的每一步的执行均是可控的,可以通过单步执行,设置断点等,控制程序的运行节奏,并在每次暂停时,都可以查看当前有效变量的具体值。

2.对初学者的意义:

调试中可以很明确的看到程序的执行过程,以及每一步产生的变化,对于初学者,这样直观的体验自然要比读代码更容易理解,而看到现象之后,再回想理论也更容易。

3.对编程者的意义:

当程序的运行结果与期望不符时,可以通过调试,查看每步的具体执行及结果,因此可以定位出到底是哪个操作或语句与预期的不同,从而快速定位,再针对性分析代码,实现快速解决问题的目的。

NSLog 使用

在XCode做开发调试时往往需要打印一些调试信息做debug用

NSLog 性能问题

它的运行会占用时间和设备资源。当打印信息的地方多了之后在模拟器上跑可能不会有什么问题,因为模拟器用的是电脑的硬件,但是当应用跑在设备上时这些输出语句会在很大程度上影响应用的性能,而且输出的数据也可能会暴露出App里的保密数据,所以发布正式版时需要把这些输出全部屏蔽掉,针对这种问题可以写一些宏来控制这些调试信息的输出。

解决方案:

简单粗暴的解决方案:在APP release前,将所有的NSLog注释掉,简单有效,但副作用是:下次你要调试时,又得将NSLog一个个取消注释。

正确的解决方案:你以release模式编译的程序不会用NSLog输出,而你以debug模式编译的程序将执行NSLog的全部功能。 在release版本禁止输出NSLog内容。

如何实现?

  1. 在 xxx.pch (预编译文件) 中添加以下代码:
1
2
3
4
5
6
7
8
//用宏指令做一个判断,如果DEBUG为真,则编译#ifdef到#endif宏定义,否则编译器就不编译;
#ifdef DEBUG
#define NSLog(...) NSLog(__VA_ARGS__)
#define debugMethod() NSLog(@"%s", __func__)
#else
#define NSLog(...)
#define debugMethod()
#endif
  1. 设置DEBUG

在 “Target > Build Settings > Preprocessor Macros > Debug” 里有一个”DEBUG=1”。

设置为Debug模式下,点击Product –> Scheme –> Edit Scheme

设置run –> info –> Build Configuration成Debug时,就可以打印nslog了。

设置Release,发布app版本的时候就不会打印了,提高了性能。

如何自定义输入

在 xxx.pch 中添加以下代码:

1
2
3
4
5
#ifdef DEBUG
#define NSLog(format, ...) fprintf(stderr, "class:%s \nline: %d \nmethod:%s \nmessage:%s \n%s \n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],__LINE__, __func__,[[NSString stringWithFormat:format, ##__VA_ARGS__] UTF8String], [@"----------------------------------------------" UTF8String]);
#else
#define NSLog(format, ...)
#endif

可以根据个人需要调整上面代码,主要就是这几个宏 ##__VA_ARGS , \FILE , \LINE 和\FUNCTION__。

1
2
3
4
5
1) __VA_ARGS__ 是一个可变参数的宏,很少人知道这个宏,这个可变参数的宏是新的C99规范中新增的,目前似乎只有gcc支持(VC6.0的编译器不支持)。宏前面加上##的作用在于,当可变参数的个数为0时,这里的##起到把前面多余的","去掉的作用,否则会编译出错, 你可以试试。
2) __FILE__ 宏在预编译时会替换成当前的源文件名
3) __LINE__宏在预编译时会替换成当前的行号
4) __FUNCTION__宏在预编译时会替换成当前的函数名称
5) __func__ 打印当前函数或方法,c字符串

将Log日志重定向输出到文件

对于真机,日志没法保存,不好分析问题。所以有必要将日志保存到应用的Docunment目录下,并设置成共享文件,这样才能取出分析。

首先是日志输出,分为c的printf和标准的NSLog输出,printf会向标准输出(sedout)打印,而NSLog则是向标准出错(stderr),我们需要同时让他们都将日志打印到一个文件中。

1
2
3
//例子:
freopen("xx.log","a+",stdout);
freopen("xx.log","a+",stderr);

具体做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 将NSlog打印信息保存到Document目录下的文件中
- (void)redirectNSlogToDocumentFolder
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [paths objectAtIndex:0];
NSString *logFilePath = [documentDirectory stringByAppendingPathComponent:@"log.txt"];

// 先删除已经存在的文件
NSFileManager *defaultManager = [NSFileManager defaultManager];
[defaultManager removeItemAtPath:logFilePath error:nil];

// 将log输入到文件
freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stdout);
freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);
}

此函数要在- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions中调用。

当连接Mac调试的时候把这些注释掉,否则log只会输入到文件中,而不能从xcode的监视器中看到。

最后配置共享文件夹:

在应用程序的Info.plist文件中添加UIFileSharingEnabled键(Application supports iTunes file sharing 键),并将键值设置为YES。将您希望共享的文件放在应用程序的 Documents目录。一旦设备插入到用户计算机,iTunes 9.1就会在选中设备的Apps标签中找到自己的应用,查看共享内容,找到 log.txt 文件。

断言NSAssert()的使用

NSAssert()只是一个宏,用于开发阶段调试程序中的Bug,通过为NSAssert()传递条件表达式来断定是否属于Bug,满足条件返回真值,程序继续运行,如果返回假值,则抛出异常,并切可以自定义异常描述。NSAssert()是这样定义的:

1
#define NSAssert(condition, desc)

condition是条件表达式,值为YES或NO;desc为异常描述,通常为NSString。当conditon为YES时程序继续运行,为NO时,则抛出带有desc描述的异常信息。NSAssert()可以出现在程序的任何一个位置。具体事例如下:

生成一个LotteryEntry对象时,传入的NSDate不能为nil,加入NSAssert()判断。对象初始化源码如下:

1
2
3
4
5
6
7
8
9
10
- (id)initWithEntryDate:(NSDate *)theDate {
self = [super init];
if (self) {
NSAssert(theDate != nil, @"Argument must be non-nil");
entryDate = theDate;
firstNumber = (int)random() % 100 + 1;
secondNumber = (int)random() % 100 + 1;
}
return self;
}

接下来则是生成对象时传入一个值为nil的NSDate,看断言是否运行。

LotteryEntry *nilEntry = [[LotteryEntry alloc] initWithEntryDate:nil];

断言效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
2013-01-17 20:49:12.486 lottery[3951:303] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Argument must be non-nil'

*** First throw call stack:
(
0 CoreFoundation 0x00007fff90c590a6 __exceptionPreprocess + 198
1 libobjc.A.dylib 0x00007fff8fd2a3f0 objc_exception_throw + 43
2 CoreFoundation 0x00007fff90c58ee8 +[NSException raise:format:arguments:] + 104
3 Foundation 0x00007fff88dae6a2 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 189
4 lottery 0x0000000100001929 -[LotteryEntry initWithEntryDate:] + 249
5 lottery 0x0000000100001794 main + 932
6 libdyld.dylib 0x00007fff8d83f7e1 start + 0
)
libc++abi.dylib: terminate called throwing an exception

移除NSAssert比较简单,我们需要在target中选择build settings, 找到 preprocessor macros(预处理宏)项目,配置它的release为 NS_BLOCK_ASSERTIONS。

具体操作步骤为: 双击release的空白处,此时会弹出对话框,点击对话框中的+添加NS_BLOCK_ASSERTIONS。

Xcode 的LLDB 调试

先说一下Xcode怎样添加断点,以及调试区域在哪里,话不多说,请看图:

当代码走到断点处,会进入调试模式,在Xcode右下方的调试区域,有 (lldb) 显示。

常用 lldb 命令

  • po 或 p 命令

调试器中最常用到的命令是 p (用于输出基本类型)或者 po (用于输出 Objective-C 对象),示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 调试命令
po username

// 运行结果
admin

// 调试命令
p username

// 运行结果
(__NSCFConstantString *) $1 = 0x000000010c0f32a0 @"admin"

/**
1. 每次查询的结果会保存在一些持续变量中($[0-9]+),这样你可以在后面的查询中直接使用这些值。比如现在我接下来要重新取回 $1 的值:p $1
运行结果:(__NSCFConstantString *) $1 = 0x000000010c0f32a0 @"admin"

2. 0x000000010c0f32a0 是username 的地址
**/

po 或 p 命令远没你想的那么简单,他还有执行代码的功能,比如:

1
2
3
4
// 执行命令
po [self.view setBackgroundColor:[UIColor redColor]]

然后你就发现当前视图的背景变成红色了。
  • expression 或 expr 命令

常用于在调试过程中修改变量的值,而不用重新启动程序。

1
2
3
4
5
6
7
8
9
10
11
// 执行命令
expression username = @"hello"

// 执行结果
(NSTaggedPointerString *) $0 = 0xa00006f6c6c65685 @"hello"

// 执行命令
po username

// 执行结果
hello

除了上面的用途,还可以新声明一个变量,比如:

1
2
3
4
5
6
7
8
// 执行命令
expression NSString * $a = @"xxxx" // $符号不要丢了

// 执行命令
po $a

// 执行结果
xxxx
  • call 命令

call 命令和 po 命令的功能相似,有的地方说“在不需要显示输出,或是方法无返回值时使用call”,但是使用 po 命令系统会向编代码一样提示你方法名或变量名,但是 call 命令不会啊,没想出有什么理由放弃 po 而使用 call 。

  • bt 命令

bt 命令用来打印主线程的堆栈信息,bt all 可以打印所有线程的堆栈信息。

  • image 命令

image 命令可用于寻址,有多个组合命令。比较实用的用法是用于寻找栈地址对应的代码位置。

比如以下代码:

1
2
NSArray *array = @[@"1",@"2"];
NSLog(@"%@",array[3]);

这段代码会报数组越界的错误,错误信息如下:

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
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 1]'
*** First throw call stack:
(
0 CoreFoundation 0x0000000107d49d85 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x00000001077bddeb objc_exception_throw + 48
2 CoreFoundation 0x0000000107c32934 -[__NSArrayI objectAtIndex:] + 164
3 demo 0x00000001072b51f6 -[ViewController viewDidLoad] + 390
4 UIKit 0x000000010829a984 -[UIViewController loadViewIfRequired] + 1198
5 UIKit 0x000000010829acd3 -[UIViewController view] + 27
6 UIKit 0x0000000108170fb4 -[UIWindow addRootViewControllerViewIfPossible] + 61
7 UIKit 0x000000010817169d -[UIWindow _setHidden:forced:] + 282
8 UIKit 0x0000000108183180 -[UIWindow makeKeyAndVisible] + 42
9 UIKit 0x00000001080f7ed9 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 4131
10 UIKit 0x00000001080fe568 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1769
11 UIKit 0x00000001080fb714 -[UIApplication workspaceDidEndTransaction:] + 188
12 FrontBoardServices 0x000000010ab688c8 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 24
13 FrontBoardServices 0x000000010ab68741 -[FBSSerialQueue _performNext] + 178
14 FrontBoardServices 0x000000010ab68aca -[FBSSerialQueue _performNextFromRunLoopSource] + 45
15 CoreFoundation 0x0000000107c6f301 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
16 CoreFoundation 0x0000000107c6522c __CFRunLoopDoSources0 + 556
17 CoreFoundation 0x0000000107c646e3 __CFRunLoopRun + 867
18 CoreFoundation 0x0000000107c640f8 CFRunLoopRunSpecific + 488
19 UIKit 0x00000001080faf21 -[UIApplication _run] + 402
20 UIKit 0x00000001080fff09 UIApplicationMain + 171
21 demo 0x00000001072b5bdf main + 111
22 libdyld.dylib 0x000000010a52392d start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)

可能出错的地址是0x00000001072b51f6(可以根据执行文件名判断,或者最小的栈地址)。为了进一步精确定位,我们可以输入以下的命令:

1
2
3
4
5
6
7
8
// 执行命令
image lookup --address 0x00000001072b51f6

// 运行结果
Address: demo[0x00000001000011f6] (demo.__TEXT.__text + 390)
Summary: demo`-[ViewController viewDidLoad] + 390 at ViewController.m:31

告诉我们出错位置是 ViewController.m 类的 31 行。
  • 简称和别名

有些调试命令比较长,使用起来比较麻烦,我们可以自定义别名。比如给image lookup –address 添加了一个 ila 的别名。

1
2
3
4
5
6
7
8
9
// 执行命令
command alias ila image lookup --address %1

// 执行命令
ila 0x00000001072b51f6

// 运行结果
Address: demo[0x00000001000011f6] (demo.__TEXT.__text + 390)
Summary: demo`-[ViewController viewDidLoad] + 390 at ViewController.m:31

等等,还没有结束,还有一个非常重要的命令 help,如果你对expr 命令不了解,可以使用help expr 来查看更多关于expr 命令的信息。

调试技巧

请看图:

双击断点,选择编辑选项,开始编辑断点。

1
2
3
4
5
Condition:此处是判断断点的执行条件,当(BOOL)[username isEqualToString:@"admin"]为 YES 时,执行断点。

Action:此处是执行的调试命令,比如 expr username = @"hello" 等改变变量值的命令,或者格式化输出 po [NSString stringWithFormat:@"username:%@ \npassword:%@",username,password],当然还可以执行其他命令。

Options:此处勾选,断点处不会进入调试模式。

总结

调试方法的灵活使用,可以减轻开发负担,同时也能更加准确的找到问题所在。这里只是平时常用的几个命令,想要更多的命令,还需要自己取学习。

参考网站 The LLDB Debugger

iOS Bug 汇总

发表于 2016-05-18 |

1. objc[xxxx]: Cannot form weak reference to instance (xxxx) of class xxxx. It is possible that this object was over-released, or is in the process of deallocation.

1
2
3
4
5
解决方案:

1. 不要在 dealloc 方法中,使用 weak self

2. A控制器(包含scrollView及其子类,并设置了其代理),B控制器(webView,并设置了webView.scrollView.delegate = self),当A push 到 B,然后B pop回 A时,会引起程序崩溃。解决方案就是再B将要退出时把webView.scrollView.delegate = nil。

2. object file was built for newer iOS version (8.1) than being linked (7.0)

1
2
3
4
5
解决方案:

1. 查看是否在导入.h文件时,误导入了.m文件。

2. 在Build Settings -> other lingkr Flags 中添加 -w

3. The operation couldn’t be completed. (LaunchServicesError error 0.)

1
2
3
4
5
解决办法:
第1种方法.点击当前的模拟器,点击IOS Simulator->Reset Content and Settings...->Reset,然后会重置模拟器,再编译代码可登录模拟器成功
第2种方法.点击Xcode->Product->Clean
第3种方法.将模拟器上的当前运行的app删除,再重新加载(长按模拟器的屏幕,点击当前报错的app 删除)
第4种方法.在info.plist文件中修改bundle的版本号

4. Could not find Developer Disk Image

1
2
3
是由于Xcode支持的最高版本低于真机的系统版本,才会导致此类问题发生。
解决方案一:升级xcode到最新版本
解决方案二:添加 真机系统版本的资源包,到路径/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport 中

iOS时间格式转换总结

发表于 2016-05-18 | 分类于 iOS |

在iOS程序开发中,有时需要将时间转换成我们需要的格式。如果服务器返回的时间格式是时间戳,转换起来相对比较简单;但是如果给的不是时间戳转换起来可能稍微麻烦一点。返回的什么格式可以协商,返回的时间的样式要统一,不要这里返回时间戳,那里返回标准时间。

UTC、GMT和时间戳简介

GMT

格林尼治标准时间(Greenwich Mean Time,GMT)是指位于英国伦敦郊区的皇家格林尼治天文台的标准时间,即本初子午线时间。

由于地球在它的椭圆轨道里的运动速度不均匀,这个时刻可能与实际的太阳时有误差,最大误差达16分钟。

地球每天的自转是有些不规则的,而且正在缓慢减速,因此格林尼治时间已经不再被作为标准时间使用。

UTC

协调世界时间(又称世界标准时间),与 GMT 一样都是本初子午线时间,只不过 UTC 是经过协调后的世界时间,比 GMT 更加准确。

全球共有二十四个时区,格林尼治时间指本初子午线时间(东经和西经交界处),北京位于东八区。

时区差东为正,西为负。因此,把东八区时区差记为 +0800。

时间戳

时间戳是指格林尼治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。

代码部分

时间处理类简介

NSDate

用于描述一个特定的时间点,可以获取时间,但没法单独获取年,月,日等信息。 提供方法来获得时间(过去,当前,未来都可以),计算两个时间之间的间隔,比较时间,描述时间内容。

1
2
// 获取当前时间
NSDate *date = [NSDate date];

NSDateFormatter

用于描述 date 的显示格式,配合 NSDate 使用。可以设置时间显示格式,日期格式,时间格式,还有时区等等,最方便的是,你还可以设置AM/PM,月份,星期的显示内容。

NSDateComponents

可以得到我们比较熟悉的年月日时分秒信息,在一些计算显示场合很有用处,用NSDate类是无法单独来获得每一个元素信息的,必须使用这个NSDateComponents类。

NSCalendar

用来描述一种特定的日历。同时还提供了大部分基于时间计算的接口,并且把NSDate类和NSDateComponents类联系了起来。

字符说明

(:)

时间分隔符。在某些区域设置中,可以使用其他字符表示时间分隔符。时间分隔符在格式化时间值时分隔小时、分钟和秒。格式化输出中用作时间分隔符的实际字符由您的应用程序的当前区域性值确定。

(/)

日期分隔符。在某些区域设置中,可以使用其他字符表示日期分隔符。日期分隔符在格式化日期值时分隔日、月和年。格式化输出中用作日期分隔符的实际字符由您的应用程序的当前区域性确定。

(%)

用于表明不论尾随什么字母,随后字符都应该以单字母格式读取。也用于表明单字母格式应以用户定义格式读取。有关更多详细信息,请参见下面的内容。

d

将日显示为不带前导零的数字(如 1)。如果这是用户定义的数字格式中的唯一字符,请使用 %d。

dd

将日显示为带前导零的数字(如 01)。

EEE

将日显示为缩写形式(例如 Sun)。

EEEE

将日显示为全名(例如 Sunday)。

M

将月份显示为不带前导零的数字(如一月表示为 1)。如果这是用户定义的数字格式中的唯一字符,请使用 %M。

MM

将月份显示为带前导零的数字(例如 01/12/01)。

MMM

将月份显示为缩写形式(例如 Jan)。

MMMM

将月份显示为完整月份名(例如 January)。

gg

显示时代/纪元字符串(例如 A.D.)

h

使用 12 小时制将小时显示为不带前导零的数字(例如 1:15:15 PM)。如果这是用户定义的数字格式中的唯一字符,请使用 %h。

hh

使用 12 小时制将小时显示为带前导零的数字(例如 01:15:15 PM)。

H

使用 24 小时制将小时显示为不带前导零的数字(例如 1:15:15)。如果这是用户定义的数字格式中的唯一字符,请使用 %H。

HH

使用 24 小时制将小时显示为带前导零的数字(例如 01:15:15)。

m

将分钟显示为不带前导零的数字(例如 12:1:15)。如果这是用户定义的数字格式中的唯一字符,请使用 %m。

mm

将分钟显示为带前导零的数字(例如 12:01:15)。

s

将秒显示为不带前导零的数字(例如 12:15:5)。如果这是用户定义的数字格式中的唯一字符,请使用 %s。

ss

将秒显示为带前导零的数字(例如 12:15:05)。

S(SS,SSS)

显示毫秒

aa(a)

对于使用 12 小时制的区域设置,对中午之前任一小时显示大写的 AM,对中午到 11:59 P.M 之间的任一小时显示大写的 PM。

对于使用 24 小时制的区域设置,不显示任何字符。

y

将年份 (0-9) 显示为不带前导零的数字。如果这是用户定义的数字格式中的唯一字符,请使用 %y。

yy

以带前导零的两位数字格式显示年份(如果适用)。

yyy

以四位数字格式显示年份。

yyyy

以四位数字格式显示年份。

z

显示不带前导零的时区偏移量(如 -8)。如果这是用户定义的数字格式中的唯一字符,请使用 %z。

zz

显示带前导零的时区偏移量(例如 -08)

zzz

显示完整的时区偏移量(例如 -08:00)

格式显示

M/d/yy —–> 12/7/58

d-MMM —–> 7-Dec

d-MMMM-yy —–> 7-December-58

d MMMM —–> 7 December

MMMM yy —–> December 58

hh:mm tt —–> 08:50 PM

h:mm:ss t —–> 8:50:35 P

H:mm —–> 20:50

H:mm:ss —–> 20:50:35

M/d/yyyy H:mm —–> 12/7/1958 20:50

yyyy-MM-dd HH:mm:ss.SSS —–> 2014-09-10 09:12:10.000

yyyy-MM-dd HH:mm:ss —–> 2014-09-10 09:12:10

yyyy-MM-dd —–> 2014-09-10

MM dd yyyy —–> 09 10 2014

EEE, yyyy-MM-dd HH:mm:ss —–> Tue 2014-09-10 09:12:10

使用实例

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
// 获取当前时间
NSDate *date = [NSDate date];

// 根据时间戳获取时间
NSDate *date = [NSDate dateWithTimeIntervalSince1970:timeInterval];

// 距离当前时间 timeInterval 秒的时间
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:timeInterval];

// 将 Jul 22, 2014 9:00:00 AM 格式的时间转换成时间戳
NSString *time = @"Jul 22, 2014 9:00:00 AM";
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"MMM dd, yyyy hh:mm:ss aa"];
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
// 使用系统默认的会有8个小时的时差
// [dateFormatter setTimeZone:[NSTimeZone systemTimeZone]];
// 使用这种方式也可以
// [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:8]];
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]]; // 或GMT
NSDate *date = [dateFormatter dateFromString:time];
NSLog(@"date:%@",date);
NSTimeInterval timeInterval = [date timeIntervalSince1970];
NSLog(@"%f",timeInterval);

// 将20110826134106转换成特定的时间字符串
NSString *time = @"20110826134106";
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyyMMddHHmmss"];
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];
NSDate *date = [dateFormatter dateFromString:time];
NSLog(@"date:%@",date);
[dateFormatter setDateFormat:@"EEE, yyyy-MM-dd HH:mm:ss"];
NSString *string = [dateFormatter stringFromDate:date];
NSLog(@"string:%@",string);

// 将Wed, 05 May 2011 10:50:00 +0800转换成特定的时间字符串
NSString *time = @"Wed, 05 May 2011 10:50:00 +0800";
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"EEE, dd MM yyyy HH:mm:ss ZZZ"];
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
[dateFormatter setTimeZone:[NSTimeZone systemTimeZone]];
NSDate *date = [dateFormatter dateFromString:time];
NSLog(@"date:%@",date);
[dateFormatter setDateFormat:@"EEE, yyyy-MM-dd HH:mm:ss"];
NSString *string = [dateFormatter stringFromDate:date];
NSLog(@"string:%@",string);

// 获取年、月、日、星期等
NSString *time = @"2016-06-01 11:00:00";
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];
NSDate *date = [dateFormatter dateFromString:time];
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *comps = [[NSDateComponents alloc] init];
NSInteger unitFlags = NSYearCalendarUnit |NSMonthCalendarUnit |NSDayCalendarUnit |NSWeekdayCalendarUnit |NSHourCalendarUnit |NSMinuteCalendarUnit |NSSecondCalendarUnit;
comps = [calendar components:unitFlags fromDate:date];
NSInteger week = [comps weekday];
NSInteger year = [comps year];
NSInteger month = [comps month];
NSInteger day = [comps day];

// 判断日期是否在本周内
-(BOOL)isDateThisWeek:(NSDate *)date {

NSDate *start;
NSTimeInterval extends;

NSCalendar *cal=[NSCalendar autoupdatingCurrentCalendar];
NSDate *today=[NSDate date];

BOOL success= [cal rangeOfUnit:NSWeekCalendarUnit startDate:&start interval: &extends forDate:today];

if(!success)
return NO;

NSTimeInterval dateInSecs = [date timeIntervalSinceReferenceDate];
NSTimeInterval dayStartInSecs= [start timeIntervalSinceReferenceDate];

if(dateInSecs > dayStartInSecs && dateInSecs < (dayStartInSecs+extends)){
return YES;
}
else {
return NO;
}
}

比较两个日期相差多少时,一般使用时间戳进行比较。(如果你有更好的方法,欢迎分享一下)

NSDateFormatter创建比较慢,最好使用单例。

iOS 开发中一些实用小代码

发表于 2016-05-13 | 分类于 iOS |

本文介绍了 iOS 实际开发过程中的一些实用代码段。

代码段

添加 pch 文件

pre-Compile Header(预编译头文件),一般存放一些使用频率较高的代码或文件。尽量不要存放太多内容,如果预编译的代码太多的话,会增加程序的启动时间。

Command + N 打开新建文件窗口,选择 iOS –> Other –> PCH File 创建 pch 文件,如图:

点击 Next

点击 Create

PCH 文件创建好之后需要配置一下,如图:

将图片中的 Precompile Prefix Header 右边的NO改为Yes。预编译后的pch文件会被缓存起来,可以提高编译速度。

同时编辑 Prefix Header ,如图:

这是相对路径的写法,pch指的是项目名,pch.pch指的是项目中pch文件的文件名和后缀。

到这里 PCH 文件已经创建并配置完成,最后一步是把需要预编译的代码放到 pch.pch 中,如图:

这里我设置了两个宏定义(屏幕宽和屏幕高)并引入了一个Common.h的头文件。

然后你就发现 kSCREEN_WIDTH 和 kSCREEN_HEIGHT 变成全局的了。

正则表达式

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
// 验证邮箱
+ (BOOL)validateEmail:(NSString *)email
{
NSString *pattern = @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}";
NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", pattern];
BOOL isMatch = [pred evaluateWithObject:email];
return isMatch;
}

// 验证手机号
+ (BOOL)validateMobile:(NSString *)mobile
{
// 以1开头,第二位是3,5,7,8的11位整数
NSString *pattern = @"^1[3578]\\d{9}$";
NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", pattern];
BOOL isMatch = [pred evaluateWithObject:mobile];
return isMatch;
}

// 验证用户名
+ (BOOL)validateNickName:(NSString *)userName
{
//2-10个中英文、数字和下划线
//NSString *pattern = @"[\u4e00-\u9fa5_a-zA-Z0-9]{2,10}";
// \S匹配任意可见字符
NSString *pattern = @"\\S{0,10}";
NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", pattern];
BOOL isMatch = [pred evaluateWithObject:userName];
return isMatch;
}

// 验证密码
+ (BOOL)validatePassword:(NSString *)password
{
//6-20位的数字字母组合-- ^表示已...开头,$表示已...结尾
NSString *pattern = @"^[a-zA-Z0-9]{6,18}+$";
NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", pattern];
BOOL isMatch = [pred evaluateWithObject:password];
return isMatch;
}

参考网站:IOS常用正则表达式

点击状态栏不返回顶部问题

1
@property(nonatomic) BOOL  scrollsToTop __TVOS_PROHIBITED;          // default is YES.

官方文档说:当一个控制器只有一个 scrollView 或 scrollView 的子类(tableView,collectionView等),并且把这个属性设置为 YES,点击状态栏才能回到顶部;当有2个或以上的scrollView,系统不知道你需要操作的是那个scrollView,因此需要将其他的scrollView的这个属性设置为 NO。

导航栏和状态栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 隐藏导航栏
[self.navigationController setNavigationBarHidden:YES animated:YES];

// 隐藏状态栏
[[UIApplication sharedApplication] setStatusBarHidden:YES];

// 设置导航栏是否半透明
self.navigationController.navigationBar.translucent = YES;

// 导航栏变为透明
[self.navigationController.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:0];
// 让黑线消失的方法
self.navigationController.navigationBar.shadowImage=[UIImage new];

// 状态栏的网络活动风火轮是否旋转
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES ; //默认值是NO。

for循环和while循环

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

for (NSString *str in array) {

}

// 先执行i = 0(只执行一次);再执行循环体,最后执行i++;
for (int i=0; i<10; i++) {
// 循环体
}

// 等同于for循环
// 当循环到结尾然后需要从头再次循环时使用,for循环不可以。
int i = 0;
while (i < 10) {
if(i == 9)
{
i = 0;
}
NSLog(@"i: %d", i);
i ++;
}

截取屏幕图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//创建一个基于位图的图形上下文并指定大小为CGSizeMake(200,400) 
UIGraphicsBeginImageContext(CGSizeMake(200,400));

//renderInContext 呈现接受者及其子范围到指定的上下文
//self.view.layer 表示要截屏的视图
[self.view.layer renderInContext:UIGraphicsGetCurrentContext()];

//返回一个基于当前图形上下文的图片
UIImage *aImage = UIGraphicsGetImageFromCurrentImageContext();

//移除栈顶的基于当前位图的图形上下文
UIGraphicsEndImageContext();

//以png格式返回指定图片的数据
imageData = UIImagePNGRepresentation(aImage);

图片压缩

方案一:文件体积变小,但是像素没变,长宽尺寸不变,可能导致图片质量下降,并且有一个压缩最小值。

1
NSData *data=UIImageJPEGRepresentation(image, 0.0f);

方案二:裁剪图片,图片像素数减少,长宽尺寸减小,图片体积也减少

1
2
3
4
5
6
7
8
9
10
- (NSData *)imageWithImage:(UIImage*)image
scaledToSize:(CGSize)newSize;
{
UIGraphicsBeginImageContext(newSize);
[image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 有时需要两种方案配合使用
return UIImageJPEGRepresentation(newImage, 0.8);
}

上传图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager POST:url parameters:parameters constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {

NSData *imgData = UIImageJPEGRepresentation(image, 0.5);
/**
* name:部分是服务器用来解析的字段
* fileName:则是直接上传上去的图片,注意一定要加 .jpg或者.png,(这个根据你得到这个imgData是通过jepg还是png的方式来获取决定)。
* mimeType:值也要与上面的类型对应,网上看到有的说直接写成 @"image/*", 据说也是可以的, 没验证过。
*/
[formData appendPartWithFileData:imgData name:@"avatar" fileName:@"avatar.jpg" mimeType:@"image/jpeg"];

} success:^(AFHTTPRequestOperation *operation, id responseObject) {

} failure:^(AFHTTPRequestOperation *operation, NSError *error) {

}];

UITableViewWrapperView 和UITableView 的 frame 不同

1
2
3
4
5
6
7
8
解决方案
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];

self.tableView.contentInset = UIEdgeInsetsZero;
self.tableView.scrollIndicatorInsets = UIEdgeInsetsZero;
}

iOS 自定义TextView/TextField光标颜色、长度或高度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1. TextView/TextField光标颜色可通过设置tintColor属性进行修改:

self.textView.tintColor = [UIColor redColor];

2. TextView/TextField自定义光标长度或高度, 可通过重写父类方法caretRectForPosition:实现, 具体设置如下:

@interface CustomTextView : UITextView

重写父类方法:

- (CGRect)caretRectForPosition:(UITextPosition *)position
{
CGRect originalRect = [super caretRectForPosition:position];
originalRect.origin.x = 8;
originalRect.origin.y = 10;
originalRect.size.height = self.font.lineHeight;
originalRect.size.width = 1;

return originalRect;
}

输入框明文和密文切换问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1. 明文和密文切换末尾空白

// 在转换方法中添加以下代码
- (void)switchAction:(id)sender {
self.passwordTextField.secureTextEntry = !self.passwordTextField.secureTextEntry;
NSString* text = self.passwordTextField.text;
self.passwordTextField.text = @" ";
self.passwordTextField.text = text;
}

2. 明文和密文切换再次输入会清空输入内容

// 在代理方法中添加以下代码

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string{

NSString * toBeString = [textField.text stringByReplacingCharactersInRange:range withString:string];
if (textField == _passwordTF && textField.isSecureTextEntry ) {
textField.text = toBeString;
return NO;
}
return YES;
}

shareSDK问题

1
2
3
4
5
6
7
8
9
10

// 新浪微博分享--解决does multipart has image?问题, "说明传的图片的地址新浪微博解析不出来,会失败,可能是你的图片的url(测试地址,内网),新浪访问不了,再就是将ip换成域名即可。"
// 我解决的办法是:将设置参数方法的第三个参数image,由传链接的形式,改为直接用UIImage类型的对象

NSData * image_data = [[NSData alloc]initWithContentsOfURL:[NSURL URLWithString:_imageUrl]];
UIImage *sina_image = [UIImage imageWithData:image_data];
id<ISSCAttachment> _image = [ShareSDK pngImageWithImage:sina_image];

// 第三方登录--QQ
// 当客户端未安装时,不会调用网页版QQ。关于QZone网页授权:1,以前申请的应用可以网页授权,但现在新申请的网页应用不可以网页授权。2,ShareSDK之前因为用户反馈,有网页授权会通不过审核,所以不得已去掉了网页授权。

textView与textField限制输入字数问题

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

// 一般采用这个方法
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
//判断输入的字符,是否超过界限
NSString *str = [NSString stringWithFormat:@"%@%@", textView.text, text];
if (str.length > kMaxLen)
{
textView.text = [str substringToIndex:kMaxLen];
return NO;
}
return YES;
}

// 但是,在汉语拼音输入状态下,如果有键盘联想汉字输入,则无法回调到该函数,所以可以一直联想输入下去,做不到限制字符数量的目的,为了解决这个问题,我们在textViewDidChange进行限制。

- (void)textViewDidChange:(UITextView *)textView
{
if ( textView.text.length > kMaxLen)
{
textView.text = [textView.text substringToIndex:kMaxLen];
}
}

// 此时,如果在拼音状态下输入会有crash ,因为汉语拼音状态下,联想出的汉字等待用户选择,在用户选择前的状态下,不能改变textView.text。解决办法:

- (void)textViewDidChange:(UITextView *)textView
{
if (textView.markedTextRange == nil && textView.text.length > kMaxLen)
//加上 textView.markedTextRange == nil判断,当此属性为nil时,代表不在这种联想输入等待确定状态。
{
textView.text = [textView.text substringToIndex:kMaxLen];
}
}

修改tableView的分割线

1
2
3
4
5
 if ([tableView respondsToSelector:@selector (setSeparatorInset:)]) {

[tableView setSeparatorInset:UIEdgeInsetsZero];
[tableView setSeparatorColor:kGetColor ( 18.0 , 18.0 , 18.0 )];
}

UITableView 的plain样式分组名悬浮问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma mark - 禁止session滚动
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
-
if (scrollView == _tableView)
{
CGFloat sectionHeaderHeight = 25; //sectionHeaderHeight

if (scrollView.contentOffset.y<=sectionHeaderHeight&&scrollView.contentOffset.y>=0) {

scrollView.contentInset = UIEdgeInsetsMake(-scrollView.contentOffset.y, 0, 0, 0);

} else if (scrollView.contentOffset.y>=sectionHeaderHeight) {

scrollView.contentInset = UIEdgeInsetsMake(-sectionHeaderHeight, 0, 0, 0);

}else
{
scrollView.contentInset = UIEdgeInsetsMake(64, 0, 0, 0);
}
}
}

iOS 支持 ipv6 问题

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
1.AFNetworking 换成最新版本

2.友盟等与网络有关的第三方SDK换成最新版本

3.服务器请求地址不要使用ip,要使用域名

4.如果用到底层API需要单独处理,尽量使用NSURLSession

5.检测网络状况的类在ios8一下可能会有点问题

```

### 判断字符串是否为空

```bash
+ (BOOL)isEmpty:(NSString *)str
{
if (!str) {
return YES;
}else
{
NSCharacterSet *set=[NSCharacterSet whitespaceAndNewlineCharacterSet];

NSString *trimedString=[str stringByTrimmingCharactersInSet:set];

if (trimedString.length==0) {
return YES;
}else
{
return NO;
}
}
}

URLDecode 解码

1
2
3
4
5
6
+ (NSString *)URLDecodedString:(NSString *)str
{
NSString *decodedString=(__bridge_transfer NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (__bridge CFStringRef)str, CFSTR(""), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));

return decodedString;
}

textView 根据输入内容改变高度(最多显示5行)

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
- (void)textViewDidChange:(UITextView *)textView
{
NSString *textViewText = [textView.text stringByReplacingOccurrencesOfString:@" " withString:@""];

//判断输入的字符是否是空格
if (![self isEmpty:textViewText]) {

CGFloat h = [inputTextView.layoutManager usedRectForTextContainer:inputTextView.textContainer].size.height;
inputTextView.contentSize = CGSizeMake(inputTextView.contentSize.width, h+20);
CGFloat five_h = hight_text_one*5.0f;
h = h>five_h?five_h:h;
CGRect frame = inputTextView.frame;
CGFloat diff = toolBar.frame.size.height - inputTextView.frame.size.height;
if (frame.size.height == h+20) {
if (h == five_h) {
[inputTextView setContentOffset:CGPointMake(0, inputTextView.contentSize.height - h - 20) animated:NO];
}
return;
}
frame.size.height = h+20;
inputTextView.frame = frame;
[inputTextView setContentOffset:CGPointZero animated:YES];

}else
{
// 处理输入为空格的情况
}
}

“Request failed: unacceptable content-type: text/plain(text/html)” 解决方案

1
2
3
4
5
6
7
此时需要修改AFNetworking可接收的Content-Type,前往AFNetworking源代码目录找到AFURLResponseSerialization.m文件将里面的代码:
self.acceptableContentTypes =[NSSetsetWithObjects:@"application/json",@"text/json",@"text/javascript",nil];
修改为:

self.acceptableContentTypes =[NSSet setWithObjects:@"application/json",@"text/json",@"text/javascript",@"text/plain" , nil];

self.acceptableContentTypes =[NSSet setWithObjects:@"application/json",@"text/json",@"text/javascript",@"text/html",nil];

UILabel的行间距,字间距以及高度计算

1
2


apple 内购流程梳理

发表于 2016-05-06 | 分类于 iOS |

第一次接触内购,网上搜了很多资料,大多数都是讲的基本的用法,不能用来处理一些特殊情况,但是我们需要解决的往往都是一些特殊情况。经过一段时间的努力和时间,终于大体了解了内购的基本流程,这里整理一下内购的代码,以便以后使用或他人借鉴。

准备部分

没有开发者帐号以及证书没有配置好的请自行百度配置。

创建一个 AppID 和 BoundleID

进入苹果的Apple Developer 创建一个 AppID 和 BoundleID, 进入开发者中心后,点击 Account 选项 ,在点击 Certificates, IDs & Profiles ,在点击 App IDs 如图

最后点击页面上的加号创建AppID(AppID 和 BoundleID 的命名按照苹果的要求来就好了,最后选择需要的服务,要勾选上内购)。

新建一个 App 应用

进入苹果的itunes Connect,登录后点击 我的 App 然后创建一个新的应用,如图

按照要求填写上面申请好的AppID 和 BoundleID,填写好以后,点击 创建 就有一个新应用了。

添加付费项目

点击添加的应用,进入下一个页面后点击 功能,然后添加 App 内购项目如图

一般都是消耗型项目吧,这里我选择的是消耗型项目。

然后是填写具体的内购信息,不知道该填什么的可以随便填一些信息,看看效果,如图

测试的时候照片可以不上串,产品Id 是唯一的(比较重要)。

添加测试账户

总不能用正式帐号去测试吧,那样要花好多钱的,这里我们用测试帐号去测试内购流程。

进入苹果的itunes Connect,登录后点击 用户和职能 然后在点击 沙箱技术测试员,最后添加测试帐号,如图

这里的邮箱可以随便填写,不用去验证,填完后准备工作就完成了。

最后,注意一下测试内购时,把 App Store 里的正式帐号注销掉,并且不要用测试帐号去登录 App Store。

代码部分

在正式编码前,我们先来了解一下内购的大体流程,请看图

这是从网上找的一张图,很清楚的描述了内购的流程,下面的代码我会按照这张图的流程来进行。

在工程里导入storekit.framework,在内购类导入#import \

客户端代码

.h文件

//
//  StoreAPI.h
//

//内购支付类
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

// 加入 SKPaymentTransactionObserver 和 SKProductsRequestDelegate 监听机制
@interface StoreAPI : NSObject<SKPaymentTransactionObserver,SKProductsRequestDelegate>

+ (instancetype)shareInstance;

/*
 请求商品,购买页面点击购买时调用的方法
 @param:type  商品id
 */
- (void)requestProductData:(NSString *)type;

//恢复购买
- (void)restoreProduct;

@end

.m文件

//
//  StoreAPI.m
//

#import "StoreAPI.h"

//购买验证URL
#define kAppStoreVerifyURL @"https://buy.itunes.apple.com/verifyReceipt" //实际购买验证URL
#define kSandboxVerifyURL @"https://sandbox.itunes.apple.com/verifyReceipt" //开发阶段沙盒验证URL

static StoreAPI *storeAPI = nil;
@implementation StoreAPI
{
    NSString *productId;        
}

//单例
+ (instancetype)shareInstance
{
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{

          storeAPI = [[TDStoreAPI alloc]init];
         // 添加观察者,观察内购的状态
        [[SKPaymentQueue defaultQueue] addTransactionObserver:storeAPI];
    });
        return storeAPI;  
}

// 请求商品
// 这里的产品Id是服务端传过来的,也可以保存在手机上(不利于修改)
- (void)requestProductData:(NSString *)type
{
    productId = type;

    //判断设备是否与支持内购
    if (![SKPaymentQueue canMakePayments]) {
        NSLog(@"不允许程序内付费");
        return;
    }

    // 创建请求 
    NSArray *product=[[NSArray alloc]initWithObjects:type, nil];
    NSSet *nsset=[NSSet setWithArray:product];
    SKProductsRequest *request=[[SKProductsRequest alloc]initWithProductIdentifiers:nsset];
    request.delegate=self;
    [request start];
}

#pragma mark - SKProductsRequestDelegate
//收到产品返回的信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    NSArray *product=response.products;
    NSLog(@"ProductID:%@",response.invalidProductIdentifiers);
    NSLog(@"产品付费数量:%lu",(unsigned long)[product count]);

    if ([product count]==0) {
        NSLog(@"没有商品");
        return;
    }

    SKProduct *p=nil;

    for (SKProduct *pro in product) {

        NSLog(@"%@",[pro description]);
        NSLog(@"%@",[pro localizedTitle]);
        NSLog(@"%@",[pro localizedDescription]);
        NSLog(@"%@",[pro price]);
        NSLog(@"%@",[pro productIdentifier]);

        if ([pro.productIdentifier isEqualToString:productId]) {

            p=pro;
        }
    }

    SKPayment *payment=[SKPayment paymentWithProduct:p];

    // 将支付请求添加进支付队列
    [[SKPaymentQueue defaultQueue]addPayment:payment];
}

//请求失败(代理方法)
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
    // 在这里可以做一些购买失败的弹框之类的处理
    NSLog(@"--错误--:%@",error);
}

//请求结束
- (void)requestDidFinish:(SKRequest *)request
{
    NSLog(@"反馈信息结束");
}

#pragma mark SKPaymentTransactionObserver

//监听购买结果(嗯,这个方法很重要!!!)
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    //记录了所有的购买记录
    for (SKPaymentTransaction *tran in transactions) {

        switch (tran.transactionState) {
            case SKPaymentTransactionStatePurchased:
            {
                NSLog(@"交易完成");   
                [self completeTransaction:tran];
            }
                break;

            case SKPaymentTransactionStatePurchasing:
            {
                NSLog(@"商品添加进列表");
            }
                break;

            case SKPaymentTransactionStateRestored:
            {
                [self restoreTransaction:tran];
                NSLog(@"已经购买过商品");                    
            }
                break;
            case SKPaymentTransactionStateFailed:
            {
                NSLog(@"交易失败");
                [self failedTransaction:tran];               
            }
                break;     
            default:
                break;
        }   
    }
}

//恢复购买(自定义方法)
- (void)restoreProduct{
   [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

//交易成功(自定义方法)
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
    //从沙盒中获取交易凭证并且拼接成请求体数据
    NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData;
    // 获取凭证的方法在ios7.0以后和以前有点不一样
    if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_7_0)
    {
        receiptData = [NSData dataWithContentsOfURL:receiptUrl];
    }else
    {
        receiptData = transaction.transactionReceipt;
    }
    //BASE64 编码
    NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];

    // 保存交易记录
    [self recordTransaction:transaction];
    [self provideContent:transaction.payment.productIdentifier];

    NSString * productIdentifier = transaction.payment.productIdentifier;

    if ([productIdentifier length] > 0) {
        // 向自己的服务器验证购买凭证
        [self verifyPurchaseWithPaymentTransaction:receiptString];
    }

    // Remove the transaction from the payment queue.
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

//交易失败(自定义方法)
- (void)failedTransaction:(SKPaymentTransaction *)transaction {
    if(transaction.error.code != SKErrorPaymentCancelled) {
        NSLog(@"购买失败");
      } else {
        NSLog(@"用户取消交易");
    }
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

//恢复交易
- (void) restoreTransaction: (SKPaymentTransaction *)transaction
{
    NSLog(@" 交易恢复处理");
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];

}

// 恢复购买完成
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
    NSLog(@"恢复成功");
//    if (queue.transactions.count == 0) {

//    }else{

//    }
}

//记录交易
- (void)recordTransaction:(SKPaymentTransaction *)transaction {
    // Optional: Record the transaction on the server side...
    NSLog(@"Hello World");
}

//下载
- (void)provideContent:(NSString *)productIdentifier {
    NSLog(@"Hello World");
}


//验证购买凭证
-(void)verifyPurchaseWithPaymentTransaction:(NSString *)receiptString{

    // 这里的代码是客户端自己向苹果验证的步骤(服务端的和这里相似)
    //发送网络POST请求,对购买凭据进行验证---测试阶段对沙盒进行验证
    NSURL *url = [NSURL URLWithString:kSandboxVerifyURL];

    //国内访问苹果服务器比较慢 timeoutInterval需要长一点
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0f];
  request.HTTPMethod = @"POST";

    if (receiptString.length < 20) {
        return;
    }
    NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据
   NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
   request.HTTPBody = bodyData;
   //提交验证请求,并获得官方的验证JSON结果
    NSError *error;
    NSData *result = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&error];

    if (error) {
        NSLog(@"验证失败");
        return;
    }
   NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:result options:NSJSONReadingAllowFragments error:&error];
  //  如果status的值为0, 就说明该receipt为有效的。 否则就是无效的。
   if ([dic[@"status"] intValue] == 0) {
       NSLog(@"验证成功");           
    }else
    {
        NSLog(@"验证失败");      
   }
}

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

@end

服务器端处理

接收ios端发过来的购买凭证。
判断凭证是否已经存在或验证过,然后存储该凭证。
将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。
如果需要,修改用户相应的会员权限。
考虑到网络异常情况,服务器的验证应该是一个可恢复的队列,如果网络失败了,应该进行重试。

注意事项

  1. 观察者应该在程序启动的时候就创建好,因为程序重新启动后,苹果会继续未完成的订单,这样就不会漏掉之前的交易信息。

  2. 苹果不会提供等待的提示弹框,这需要自己去实现。

  3. 由于攻击者可以伪造支付成功的凭证,所以需要增加服务器验证。客户端将支付凭证发给服务端,由服务端向苹果的服务器进行二次验证,确保凭证的有效性。

  4. 向苹果提交的是正式版的,但是苹果审核App时,会在sandbox 环境购买,所以产生的凭证需要连接苹果的测试服务器验证,但是提交的App连接的是线上的服务器,会造成验证失败的问题。解决方案是:如果返回的code 是 21007,那么再向苹果的测试服务器验证一次,如果code 是 0,表示验证成功。

  5. 考虑到断网的情况,客户端发送的购买凭证应该进行本地的持久化存储,如果程序退出,崩溃或断网,可以重新向服务器发送凭证,当服务器返回验证成功后可以删除相应的凭证。

  6. 由于苹果那边充值成功后,服务器还需要进行验证(连接的是国外的服务器,响应比较慢),所以充值的金额不能及时到帐,需要等几秒。(或许是我没有发现更好地解决方法)

  7. 如果向服务器发送购买凭证过程中断网或因其他原因失败,导致服务器没有收到购买凭证,或许需要监听网络状况,联网后再次发送也可以等程序下次启动时再次发送,看具体要求吧。

参考资料

唐巧大神的博客

李华明Himi 的博客

iOS应用内置付费详尽攻略(全文)

iOS应用程序内购/内付费

iOS内购IAP(In App Purchases)入门

iOS开发内购教程In App Purchase 需要了解的

IAP相关资源收集及开发总结

iOS内置付费开发笔记

iOS证书说明和发布内购流程整理

iOS- 给App添加内购& 验证购买iOS7新特性

应用内购(In-App Purchase)常见问题解答

hexo 安装与配置

发表于 2016-04-27 | 分类于 工具 |

总想弄个自己的博客,写一些东西(或感悟、或经验、或代码、或计划)。选择困难症的我纠结到底是用CSDN还是新浪抑或网易……还有很多,都挑花眼了,各有各的有点,也各有各的不足。那么到头来我为什么选择了Hexo + GitHub 来创建自己的博客呢?因为它没广告啊!!!当然还可以根据自己的喜好选择喜欢的主题,同时看着自己的作品是不是有一种满足感呢?最后,作为一个程序猿用别的博客怎么能体现程序猿的与众不同呢?

Hexo 简单介绍

hexo 是一个基于Node.js的静态博客框架,可以方便的生成静态网页并且托管在github 上。hexo是一个开源的博客框架,我们可以任意使用修改,但是很多功能要自己去实现,没有计算机基础的人使用起来可能比较困难。

准备

前面提到hexo是基于Node.js的博客框架,所以安装Hexo之前需要先Node.js 和设置 github(托管代码)

GitHub 设置

  • 首先你要注册一个github帐号
  • 创建一个库(new repository)命名为YourSiteName.github.io/
  • 根据自己的喜好简单设置一下github生成的静态网页

请参考

下载Git

安装Node.js

下载Node.js 并安装

Hexo 安装

  • 安装
1
$ npm install -g hexo-cli
  • 初始化

安装完成后,建一个文件夹(如hexo),执行以下指令(在hexo路径下),Hexo 即会自动在目标文件夹建立网站所需要的所有文件

1
$ hexo init
  • 查看博客
1
2
$ hexo g
$ hexo s

然后在浏览器中输入localhost:4000 就可以看到你的博客了。

如果安装过程中出现错误可以尝试在命令行前加上 sudo

详情请参考

Hexo 基本使用

既然博客已经安装好了,那么怎么写博客呢?博客是基于MarkDown语法的,不了解MarkDown的请点击这里。

编辑器MAC环境下免费的有Mou 和MacDown 比较给力,其他的不熟悉。

  • 创建文件
1
$ hexo new "Hexo使用"

或者直接进入hexo\source\_posts路径下添加 xxx.md 类型的文件,然后打开文件编辑

title: Hexo使用    // 标题
categories: hexo    // 分类
tags: [hexo,blog]  //标签,多个用逗号隔开
---

#这里是正文,用markdown编写
  • 预览
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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
$ hexo g  
$ hexo s
$ 访问localhost:4000预览效果。(退出server用Ctrl+c)
```

- 提交到git

编辑hexo\\_config.yml,把下面的 abc 换成你的用户名。

``` bash
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
type: git
repository: https://github.com/abc/abc.github.io
```

保存文件并运行

``` bash
$ hexo g
$ hexo s
$ hexo d
```

然后在浏览器输入abc.github.io 就能看到 Git 的博客发生了变化。

## Hexo 配置

Okay

Hexo 的基本使用已经结束了,下面是怎样装饰你的博客了,如果你对默认的样式很满意,后面的可以跳过了。

我这里使用的主题是[next](http://theme-next.iissnan.com/)

Hexo 有两个配置文件,在hexo路径下的 _config.yml 文件是 `站点配置文件` ,在 hexo\themes\hexo-theme-next 路径下的 _config.yml 文件是`主题配置文件`。

### 配置文件

- 站点配置文件

# Hexo Configuration
## Docs: https://hexo.io/docs/configuration.html
## Source: https://github.com/hexojs/hexo/

# Site 这里的配置可以参考我的博客
title: Try、Try Again!The worst result is failure! // 博客title
subtitle: // 副标题
description: 以出以入,以就鲜絜,似善化。其万折也必东,似志。是故君子见大水必观焉。 -- 荀子·宥坐 // 描述
author: 东折 // 作者
language: zh-Hans // 使用语言
timezone:
email: zhd_dong@163.com // 邮箱

# URL 这里没有设置
## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/'
url: http://yoursite.com
root: /
permalink: :year/:month/:day/:title/
permalink_defaults:

# Directory 这里没有设置
source_dir: source
public_dir: public
tag_dir: tags
archive_dir: archives
category_dir: categories
code_dir: downloads/code
i18n_dir: :lang
skip_render:

# Writing 这里没有设置
new_post_name: :title.md # File name of new posts
default_layout: post
titlecase: false # Transform title into titlecase
external_link: true # Open external links in new tab
filename_case: 0
render_drafts: false
post_asset_folder: false
relative_link: false
max_open_file: 100
future: true
highlight:
enable: true
line_number: true
auto_detect: false
tab_replace:

# Category & Tag
default_category: uncategorized
category_map:
tag_map:


#archive: 1
#category: 1
#tag: 1

# Date / Time format
## Hexo uses Moment.js to parse and display date
## You can customize the date format as defined in
## http://momentjs.com/docs/#/displaying/format/
date_format: YYYY-MM-DD
time_format: HH:mm:ss

# Pagination
## Set per_page to 0 to disable pagination
per_page: 10 // 这里设置一页显示多少条博客
pagination_dir: page

# Extensions
## Plugins: https://hexo.io/plugins/
## Themes: https://hexo.io/themes/
theme: hexo-theme-next // 在这里设置想要的主题(主题对应的文件夹的名称)

duoshuo_shortname: yulncbd // 这里添加多说评论

# Deployment // 这里设置Git 的相关信息
## Docs: https://hexo.io/docs/deployment.html
deploy:
type: git
repository: https://github.com/YulncBd/YulncBd.github.io // 这里将YulncBd替换成自己用户名


`站点配置文件` 主要配置了博客简介,git,多说评论等一些信息。自己可以尝试一下其他的设置,看看什么效果。设置完成后执行一下命令查看结果

hexo g
hexo s
hexo d

到用浏览器打开 `localhost:4000` 查看效果,再到 `你的用户名.github.io` 查看Git 上你博客的变化。

- 主题配置文价

可以打开 zh-Hans.yml 文件查看对应的汉字设置

# ---------------------------------------------------------------
# Site Information Settings
# ---------------------------------------------------------------

# Put your favicon.ico into `hexo-site/source/` directory.
favicon: /favicon.ico

# Set default keywords (Use a comma to separate)
keywords: "Hexo, NexT"

# Set rss to false to disable feed link.
# Leave rss as empty to use site's feed link.
# Set rss to specific value if you have burned your feed already.
rss: false

# Specify the date when the site was setup
since: 2016



# ---------------------------------------------------------------
# Menu Settings
# ---------------------------------------------------------------

// 这里设置导航栏(添加了分类 和 标签,具体设置后面在介绍)
# When running the site in a subdirectory (e.g. domain.tld/blog), remove the leading slash (/archives -> archives)
menu:
home: /
archives: archives
categories: categories
tags: tags
#about: about
#tags: /tags
#commonweal: /404.html

// 这里设置导航栏对应的图片
# Enable/Disable menu icons.
# Icon Mapping:
# Map a menu item to a specific FontAwesome icon name.
# Key is the name of menu item and value is the name of FontAwsome icon. Key is case-senstive.
# When an question mask icon presenting up means that the item has no mapping icon.
menu_icons:
enable: true
#KeyMapsToMenuItemKey: NameOfTheIconFromFontAwesome
home: home
about: user
categories: th
tags: tags
archives: archive
commonweal: heartbeat
search: search




# ---------------------------------------------------------------
# Scheme Settings
# ---------------------------------------------------------------

// 这里设置主题的样式(可以都选择看一下效果)
# Schemes
#scheme: Muse
#scheme: Mist
scheme: Pisces


# ---------------------------------------------------------------
# Font Settings
# - Find fonts on Google Fonts (https://www.google.com/fonts)
# - All fonts set here will have the following styles:
# light, light italic, normal, normal intalic, bold, bold italic
# - Be aware that setting too much fonts will cause site running slowly
# - Introduce in 5.0.1
# ---------------------------------------------------------------
font:
enable: true

# Uri of fonts host. E.g. //fonts.googleapis.com (Default)
host:

# Global font settings used on <body> element.
global:
# external: true will load this font family from host.
external: true
family: Lato

# Font settings for Headlines (h1, h2, h3, h4, h5, h6)
# Fallback to `global` font settings.
headings:
external: true
family:

# Font settings for posts
# Fallback to `global` font settings.
posts:
external: true
family:

# Font settings for Logo
# Fallback to `global` font settings.
# The `size` option use `px` as unit
logo:
external: true
family:
size:

# Font settings for <code> and code blocks.
code:
external: true
family:


# ---------------------------------------------------------------
# Sidebar Settings
# ---------------------------------------------------------------

// 这里设置一些网站链接
# Social Links
# Key is the link label showing to end users.
# Value is the target link (E.g. GitHub: https://github.com/iissnan)
social:
简书: http://www.jianshu.com/
GitHub: https://github.com/
知乎: http://www.zhihu.com
豆瓣: https://www.douban.com/

// 这里设置链接的图片
# Social Links Icons
# Icon Mapping:
# Map a menu item to a specific FontAwesome icon name.
# Key is the name of the item and value is the name of FontAwsome icon. Key is case-senstive.
# When an globe mask icon presenting up means that the item has no mapping icon.
social_icons:
enable: true
# Icon Mappings.
# KeyMapsToSocalItemKey: NameOfTheIconFromFontAwesome
GitHub: github
Twitter: twitter
Weibo: weibo

// 这里设置头像
# Sidebar Avatar
# in theme directory(source/images): /images/avatar.jpg
# in site directory(source/uploads): /uploads/avatar.jpg
avatar: /uploads/avatar.png


# Table Of Contents in the Sidebar
toc:
enable: true

# Automatically add list number to toc.
number: true


# Creative Commons 4.0 International License.
# http://creativecommons.org/
# Available: by | by-nc | by-nc-nd | by-nc-sa | by-nd | by-sa | zero
#creative_commons: by-nc-sa
#creative_commons:

// 这里设置导航居左还是居右
sidebar:
# Sidebar Position, available value: left | right
position: left
#position: right

# Sidebar Display, available value:
# - post expand on posts automatically. Default.
# - always expand for all pages automatically
# - hide expand only when click on the sidebar toggle icon.
# - remove Totally remove sidebar including sidebar toggler.
display: post
#display: always
#display: hide
#display: remove



# ---------------------------------------------------------------
# Misc Theme Settings
# ---------------------------------------------------------------

# Custom Logo.
# !!Only available for Default Scheme currently.
# Options:
# enabled: [true/false] - Replace with specific image
# image: url-of-image - Images's url
custom_logo:
enabled: false
image:

// 这里设置代码段的背景色
# Code Highlight theme
# Available value:
# normal | night | night eighties | night blue | night bright
# https://github.com/chriskempson/tomorrow-theme
highlight_theme: night bright


# Automatically scroll page to section which is under <!-- more --> mark.
scroll_to_more: true

// 这里设置博客显示信息
# Automatically Excerpt. Not recommand.
# Please use <!-- more --> in the post to control excerpt accurately.
auto_excerpt:
enable: true // 博客能否收起
length: 200 // 博客收起后显示高度
excerpt_link: read_more #替换为中文 // 阅读全文


# Wechat Subscriber
wechat_subscriber:
enabled: false
qcode: /path/to/your/wechatqcode ex. /uploads/wechat-qcode.jpg
description: hello ex. subscribe to my blog by scanning my public wechat account



# ---------------------------------------------------------------
# Third Party Services Settings
# ---------------------------------------------------------------

# MathJax Support
mathjax:
enable: true
cdn: //cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML


# Swiftype Search API Key
#swiftype_key:

# Baidu Analytics ID
#baidu_analytics:

# Duoshuo ShortName
#duoshuo_shortname:

# Disqus
#disqus_shortname:

# Baidu Share
# Available value:
# button | slide
#baidushare:
## type: button

# Share
#jiathis:
#add_this_id:

# Share
#duoshuo_share: true

# Google Webmaster tools verification setting
# See: https://www.google.com/webmasters/
#google_site_verification:


# Google Analytics
#google_analytics:

# CNZZ count
#cnzz_siteid:

// 设置多说的信息
# Make duoshuo show UA
# user_id must NOT be null when admin_enable is true!
# you can visit http://dev.duoshuo.com get duoshuo user id.
duoshuo_info:
ua_enable: true
admin_enable: false
user_id: 0
#admin_nickname: Author
Author: hello

// 设置Facebook信息
# Facebook SDK Support.
# https://github.com/iissnan/hexo-theme-next/pull/410
facebook_sdk:
enable: false
app_id: #<app_id>
fb_admin: #<user_id>
like_button: #true
webmaster: #true


# Show number of visitors to each article.
# You can visit https://leancloud.cn get AppID and AppKey.
leancloud_visitors:
enable: false
app_id: #<app_id>
app_key: #<app_key>

# Show PV/UV of the website/page with busuanzi.
# Get more information on http://ibruce.info/2015/04/04/busuanzi/
busuanzi_count:
# count values only if the other configs are false
enable: false
# custom uv span for the whole site
site_uv: true
site_uv_header: <i class="fa fa-user"></i>
site_uv_footer:
# custom pv span for the whole site
site_pv: true
site_pv_header: <i class="fa fa-eye"></i>
site_pv_footer:
# custom pv span for one page only
page_pv: true
page_pv_header: <i class="fa fa-file-o"></i>
page_pv_footer:

# Tencent analytics ID
# tencent_analytics:

# Enable baidu push so that the blog will push the url to baidu automatically which is very helpful for SEO
baidu_push: false



#! ---------------------------------------------------------------
#! DO NOT EDIT THE FOLLOWING SETTINGS
#! UNLESS YOU KNOW WHAT YOU ARE DOING
#! ---------------------------------------------------------------

# Motion
use_motion: true

# Fancybox
fancybox: true


# Script Vendors.
# Set a CDN address for the vendor you want to customize.
# For example
# jquery: https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js
# Be aware that you should use the same version as internal ones to avoid potential problems.
vendors:
# Internal path prefix. Please do not edit it.
_internal: vendors

# Internal version: 2.1.3
jquery:

# Internal version: 2.1.5
fancybox:

# Internal version: 1.0.6
fastclick:

# Internal version: 1.9.7
lazyload:

# Internal version: 1.2.1
velocity:

# Internal version: 1.2.1
velocity_ui:

# Internal version: 0.7.9
ua_parser:


# Assets
css: css
js: js
images: images

# Theme version
version: 5.0.1

// 这里设置的是打赏的信息
# Donate 文章末尾显示打赏按钮
reward_comment: 如果觉得写的不错,那就打赏一下吧! // 文字提示
wechatpay: /uploads/weixin.png //微信图片(本地图片或网络图片)
#alipay: 此处为支付宝向我付款二维码图片的相对或者是绝对URL


### 分类和标签

- 添加分类

```bash
cd hexo // 进入博客路径
hexo new page categories // 创建分类页面

然后进入到hexo\source\ categories路径下打开index.md文件,编辑保存。

---
title: categories
date: 2016-04-26 16:17:21
 type: "categories"
---

在主题配置文件里设置

menu:
    home: /
    archives: archives
    categories: categories   // 如果有问题在前面加一个反斜杠
    tags: tags
    #about: about
    #commonweal: /404.html
  • 添加标签
1
2
cd hexo   // 进入博客路径
hexo new page tags // 创建分类页面

然后进入到hexo\source\ tags路径下打开index.md文件,编辑保存。

---
title: tags
date: 2016-04-26 16:17:21
 type: "tags"
---

在主题配置文件里设置

menu:
    home: /
    archives: archives
    categories: categories   
    tags: tags  // 如果有问题在前面加一个反斜杠
    #about: about
    #commonweal: /404.html

最后发表一篇文章(设置好分类和标签),执行一下命令查看结果

1
2
3
hexo g
hexo s
hexo d

头像

要在 source 路径下创建文件夹 uploads ,然后将要设置的图片拷到这里,设置如下

avatar: /uploads/avatar.png   // 在主题配置文件里设置(也可以用网络链接)

打赏

在主题配置文件底部添加以下代码:

// 这里设置的是打赏的信息
# Donate 文章末尾显示打赏按钮
reward_comment: 如果觉得写的不错,那就打赏一下吧! // 文字提示
wechatpay: /uploads/weixin.png  //微信图片(本地图片或网络图片)
#alipay: 此处为支付宝向我付款二维码图片的相对或者是绝对URL

图片的设置同头像设置一致。

多说评论

额,以为很麻烦,所以一开始没有设置,后来有时间想设置一下,发现 so easy!

先到多说 去申请一个站点,步骤如下:

1.登录后在首页选择 我要安装。

2.创建站点,填写表格,多说域名这一栏填写的即是你的 duoshuo_shortname,如图:

3.创建站点完成后在 站点配置文件 中新增 duoshuo_shortname 字段,值设置成上一步中的值。

不用别的操作,只有3步就搞定。

总结

以上的设置只是一些基本的设置,先把博客用起来,后面在慢慢的去了解,去深入。关于404公益页面,域名,统计,搜索等功能暂未实现,感觉博客没必要弄那么多不必要的功能,如果你感兴趣可以去研究一下。

Welcome To Hexo

发表于 2016-04-25 | 分类于 hexo |

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

MacDown中MarkDown的使用规范总结

发表于 2016-04-08 | 分类于 工具 |

Created by 东折 on 8 April 2016

标题

在MarkDown中,你只需要在文本前面加上#即可。同理,你可以增加二级标题、三级标题、四级标题、五级标题、六级标题,总共六级,只需增加#即可。标题字号相应降低,例如:

#一级标题
##二级标题
###三级标题
####四级标题
#####五级标题
######六级标题

注:#和「一级标题」之间建议保留一个字符的空格,这是最标准的MarkDown写法

列表

  • 在MarkDown中,你只需要在文字卡面加上-或+就可以了,例如:

    - 文本一
    - 文本二
    - 文本三
    
  • 如果你希望有序列表,在文字前面加上1. 2. 3. 就可以了,例如:

    1. 文本一
    2. 文本二
    3. 文本三
    
  • 效果如下(前面的>符号表示引用),如果「- 文本」不是放在行首,显示的就是空心圆 :

    • 文本一
    • 文本二
    • 文本三
    1. 文本一
    2. 文本二
    3. 文本三

注:-和1. 和文本之间保留一个空格,但是后面要跟代码时,代码需要两个制表符(Tab)或8个空格,才能正常显示。

链接、超链接、图片和代码

  • 在MarkDown中,插入链接时,只需要使用 [使用文本](链接地址)这样的语法就可以,例如:

    MarkDown

  • 在MarkDown中,插入超链接(网址、邮箱等)时,只需要将超链接用<>包括起来即可,例如:

    123456789@163.com

    http://www.jianshu.com

  • 在MarkDown中,插入图片时,只需要使用![](图片链接地址)这样的语法就可以,例如:

  • 在MarkDown中,建立代码区块,只要在文本前添加制表符或者4个空格就可以,例如:

    - (void)hello
    {
          nslog(@"hello world");
    }
    

注:插入图片的语法和链接的语法很像,只是前面多了一个 !。这里的[],(),!符号都是英文符号。

表格

格式如下:

  • 行名居中,数据居左

    | Tables | Are | Cool |
    | —— |——-| —–|
    | col 3 is | right-aligned | $1600 |
    | col 2 is | centered | $12 |
    | zebra stripes | are neat | $1 |

    [^emphasize]
    
  • 行名居中,数据居中

    | Tables | Are | Cool |
    | :—–: |:——-:| :—–:|
    | col 3 is | right-aligned | $1600 |
    | col 2 is | centered | $12 |
    | zebra stripes | are neat | $1 |

  • 行名居左,数据居左

    | Tables | Are | Cool |
    | :—— |:——-| :—–|
    | col 3 is | right-aligned | $1600 |
    | col 2 is | centered | $12 |
    | zebra stripes | are neat | $1 |

  • 行名居右,数据居右

    | Tables | Are | Cool |
    | ——: |——-:| —–:|
    | col 3 is | right-aligned | $1600 |
    | col 2 is | centered | $12 |
    | zebra stripes | are neat | $1 |

注:两边的竖线可有可无,但是要有都有,要无都无。

其他

  • 回车

    一个回车表示换行

    两个回车表示换段

  • 引用

    在Markdown中你只需要在你希望引用的文字前面添加>就可以,例如:

    一盏灯,一片黄昏;

    注:> 和 文本之间建议保留一个空格

  • 粗体与斜体

    在MarkDown中,用两个包含一段文字就是粗体,用一个包含一段文字就是斜体,例如:

    **粗体**
    
    *斜体*
    
    ***粗斜体***
    
  • 横线

    三个星号或者三个减号或者三个下划线即可生成一条虚横线,例如:

    ***
    ---
    ___
    

    一个减号或两个减号,可以生成一条实横线,例如:

    -
    --
    
  • 删除线

  • 文本突出显示

    只需要用``将文本包含起来,即可。例如:

    突出显示文本

  • 文本注释

    只需使用[^注释]这样的格式,就可以,但是要在文章最后面也要有相同的[^注释]去解释相应的内容,例如:

    [^hello]
    
  • 特殊符号

    文本中需要插入特殊符号时,需要在特殊符号前加上反斜杠,例如:

    #hello

    注:这里的特殊符号并不只是只单一的特殊符号,而是当特殊符号与文本或特殊符号组合形成特殊的格式时,如果需要显示特殊符号,需要加上\。如上面的#与hello组合会将hello显示为标题,如果此时需要显示#hello,需要在#前添加\。

总结

以上的语法只是MarkDown的基本语法,在实际使用中往往是多个语法混合使用,具体的规则要在具体的使用中去体会。个人觉得用MarkDown来记笔记要比使用简单地文本方便得多,而且写出的东西也更加易读。

Okey,happy writing!

东折

东折

君子见大水必观焉,何也?其万折也必东,似志 !

9 日志
3 分类
10 标签
© 2017 东折
由 Hexo 强力驱动
主题 - NexT.Pisces