利他才能利己


  • 首页

  • 标签

  • 归档

  • 搜索

iOS 多线程: 初步认识

发表于 2018-02-12 | 分类于 iOS |

几乎每个编程语言或者平台都会遇到多线程的问题, 说明多线程是一个非常重要且开发者必须了解和掌握的.

多线程也是面试官比较喜欢问的问题, 例如:

  • 进程和线程的区别, Android 是否支持多进程?
  • 线程池如何实现的?
  • 锁机制?
  • 多线程之间如何通信?

谈及 iOS 中的多线程,一般说的是 pthread,NSthread,GCD,NSOperation 这四种, 用的最多也最方便的就是 GCD 了. 关于这四者, 后续会为大家一一分享.

phtread 是跨平台的, C/C++ 中都有它的声影, GCD 和 NSOperation 都是常用的,NSOperation 是基于 GCD 的. GCD 的核心概念是将一个任务添加到队列,指定任务执行的方法,然后执行, NSOperation 则是直接将一个操作添加到队列中.

该系列文章来跟大家分享关于 iOS 中的多线程.

  • iOS 多线程: 初步认识(本篇)

进程和线程

进程和线程的定义, 大家可以自行到维基百科上面去查.

这里只说二者的区别.

  • 一个程序至少有一个进程, 一个进程至少有一个线程如主线程.

  • 多线程程序的并发性高.

  • 进程在执行过程中拥有独立的内存单元,而多线程是共享内存的,从而极大地提高了程序的运行效率.

  • 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口. 线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制.

  • 操作系统并没有将多个线程看做多个独立的应用, 多线程的意义在于一个应用程序中,有多个执行部分可以同时执行, 从而实现了进程的调度和管理以及资源分配.

队列

iOS 中, 队列主要分为:

  • 全局队列
  • 主队列.
  • 串行队列.
  • 并发队列.
  • Concurrent:
    tasks are dequeued in FIFO order, but run concurrently and can finish in any order.
  • Serial: .
    tasks execute one at a time in FIFO order

并发: 任务以 FIFO 从序列中移除,然后并发运行,可以按照任何顺序完成.

串行: 任务以FIFO从序列中一个一个执行. 一次只调度一个任务.

在 iOS 中, 并发不一定会开启多个线程, 串行也不一定只开启一个线程. 因为这里会牵扯到是异步还是同步执行.

主队列, 即 mainQueue.

Returns the default queue that is bound to the main thread.

会关联主线程.

全局队列, 即 globalQueue.

The well-known global concurrent queues may not be modified.

全局队列中执行不一定会开启新线程.

同步和异步

在 iOS 的 GCD 中, 还有同步和异步执行的区别.

同步

同步执行代码块, 诸如 dispatch_async 中执行的.

异步

异步执行代码块, 诸如 dispatch_sync 中执行的.

创建队列的方法

创建主队列

1
dispatch_queue_t dispatch_get_main_queue(void);

创建全局队列

1
dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);

参数 identifier 用来表示优先级. 对应的优先级为:

  • DISPATCH_QUEUE_PRIORITY_HIG
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

如果传入 0 标示 DISPATCH_QUEUE_PRIORITY_DEFAULT.

参数 flags 是一个保留参数, API 文档要求传入 0, 非0值可能会导致返回结果为 NULL.

自定义队列

dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);

参数 label 是一个字符串.

参数 attr 用来标示是串行还是并行队列. 可以从 DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_CONCURRENT 二者中取值.

如果该参数传入 NULL, 默认是 DISPATCH_QUEUE_SERIAL 串行队列.

串行队列中的线程

串行队列到底是开了一个线程, 还是开了多个线程, 我们一探究竟.

先看例子1

1
2
3
4
5
6
7
8
dispatch_queue_t serialQueue = dispatch_queue_create("com.veryitman", DISPATCH_QUEUE_SERIAL);

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

dispatch_async(serialQueue, ^{
NSLog(@"dispatch_async. DISPATCH_QUEUE_SERIAL CurrentThread: %@", [NSThread currentThread]);
});
}

1

可以看出, 只开启一个新线程.

再看例子2

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t serialQueue = dispatch_queue_create("com.veryitman", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{
NSLog(@"dispatch_async. DISPATCH_QUEUE_SERIAL CurrentThread: %@", [NSThread currentThread]);
sleep(2);
});

dispatch_async(serialQueue, ^{
NSLog(@"dispatch_sync. DISPATCH_QUEUE_SERIAL CurrentThread: %@", [NSThread currentThread]);
});

1

可以看出, 也只开启一个新线程.

改造一下例子2, 将其中的一个异步改为同步

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t serialQueue = dispatch_queue_create("com.veryitman", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{
NSLog(@"dispatch_async. DISPATCH_QUEUE_SERIAL CurrentThread: %@", [NSThread currentThread]);
sleep(2);
});

dispatch_sync(serialQueue, ^{
NSLog(@"dispatch_sync. DISPATCH_QUEUE_SERIAL CurrentThread: %@", [NSThread currentThread]);
});

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
/// 异步全局队列和主队列
{
dispatch_async(dispatch_get_main_queue(), ^{
// 1
NSLog(@"dispatch_async. mainQueue. isMainThread: %i", [NSThread isMainThread]);
});

dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 0
NSLog(@"dispatch_async. globalQueue. isMainThread: %i", [NSThread isMainThread]);
});
}

/// 异步串行和并行队列
{
dispatch_async(dispatch_queue_create("", DISPATCH_QUEUE_SERIAL), ^{
// 0
NSLog(@"dispatch_async. DISPATCH_QUEUE_SERIAL. isMainThread: %i", [NSThread isMainThread]);
});

dispatch_async(dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT), ^{
// 0
NSLog(@"dispatch_async. DISPATCH_QUEUE_CONCURRENT. isMainThread: %i", [NSThread isMainThread]);
});
}

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
/// 同步全局队列和主队列
{

#if 0
// 会死锁
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"dispatch_sync. mainQueue. isMainThread: %i", [NSThread isMainThread]);
});
#endif

dispatch_sync(dispatch_get_global_queue(0, 0), ^{
// 1
NSLog(@"dispatch_sync. globalQueue. isMainThread: %i", [NSThread isMainThread]);
});
}

/// 同步串行和并行队列
{
dispatch_sync(dispatch_queue_create("", DISPATCH_QUEUE_SERIAL), ^{
// 1
NSLog(@"dispatch_sync. DISPATCH_QUEUE_SERIAL. isMainThread: %i", [NSThread isMainThread]);
});

dispatch_sync(dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT), ^{
// 1
NSLog(@"dispatch_sync. DISPATCH_QUEUE_CONCURRENT. isMainThread: %i", [NSThread isMainThread]);
});
}

1

得出结论

  • 任何队列的同步执行, 都没有开启新线程, 在主线程中执行.
  • 主队列的同步执行会造成死锁.
  • 主队列的异步执行, 没有开启新线程. 在主线程中执行.
  • 串行和并行以及全局队列的异步执行, 都会开启新线程.

参考

  • 进程和线程的区别

  • 进程与线程的一个简单解释

C/C++: 如何相互调用

发表于 2018-02-12 | 分类于 C/C++ |

最近在看人工智能相关的知识,无意中发现了一个巨牛的 人工智能教程,分享一下给大家。

教程不仅是零基础,通俗易懂,而且非常风趣幽默,像看小说一样!觉得太牛了,所以分享给大家。点 这里 可以直接看教程。

简介

C++ 中调用 C 比较简单. 但是 C 调用 C++ 稍微复杂一些.

C 调用 C++ 分为可以调用 C++ 类中的函数和普通 cpp 中的函数. 无论是哪种函数, 我们都可以使用封装了 C++ 的文件作为适配供给 C 来使用.

下面看具体的例子.

C++ 调用 C

首先创建 CFile.h 和 CFile.c 文件.

CFile.h

1
2
3
4
5
6
7
8
#ifndef CFile_h
#define CFile_h

#include <stdio.h>

extern void start_c(int cmd);

#endif /* CFile_h */

CFile.c

1
2
3
4
5
6
#include "CFile.h"

void start_c(int cmd) {

printf("start_c by cmd: %i\n", cmd);
}

在 C++ 文件中调用 C 代码, 示例如下:

main.cpp

1
2
3
4
5
6
7
8
9
10
extern "C" {
#include "CFile.h"
}

int main(int argc, const char * argv[]) {

start_c(1);

return 0;
}

这里可以看到导入 C 文件的方式:

1
2
3
extern "C" {
#include "CFile.h"
}

如果直接导入, 如:

1
2
3
4
5
6
7
8
#include "CFile.h"

int main(int argc, const char * argv[]) {

start_c(1);

return 0;
}

编译报错:

1
2
3
4
Undefined symbols for architecture x86_64:
"start_c(int)", referenced from:
_main in main.o
ld: symbol(s) not found for architecture x86_64

这里的 extern "C" 告诉编译器, 要按照 C 的链接约定,而不是 C++ 的链接约定.

C 编译器不支持 extern "C".

C 调用普通 C++ 的函数

C 调用 C++ 有点曲折.

CPPFile.hpp

1
2
3
4
5
6
#ifndef CPPFile_hpp
#define CPPFile_hpp

void start_cpp(int cmd);

#endif /* CPPFile_hpp */

CPPFile.cpp

1
2
3
4
5
6
7
8
9
#include "CPPFile.hpp"
#include <iostream>

using namespace std;

void start_cpp(int cmd) {

cout << "start_cpp by cmd: " << cmd << endl;
}

这里需要写一个中间的 cpp(CPPAdapter.cpp)文件, 提供方法给 C 来使用.

注意: 这里没有 CPPAdapter.hpp 头文件.

CPPAdapter.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "CPPFile.hpp"

#ifdef __cplusplus
extern "C" {
#endif

void adapter_start_cpp(int cmd) {

//调用 CPPFile 中的方法
start_cpp(cmd);
}

#ifdef __cplusplus
}
#endif

然后在 C 中调用 C++ 的代码:

CFile.c

1
2
3
4
5
6
7
8
9
10
#include "CFile.h"

//声明函数
extern void adapter_start_cpp(int cmd);

void start_c(int cmd) {

//调用 c++ 代码
adapter_start_cpp(5);
}

C 调用 C++ 类中的方法

和上面例子的原理一样的.

CPPClassFile.hpp

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

#include <iostream>
#include <string>

using namespace std;

class Person {
private:
string name;

public:
Person();
~Person();

int setName(string name);
};

#endif /* CPPClassFile_hpp */

CPPClassFile.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "CPPClassFile.hpp"

Person::Person() {

cout << "Person()" << endl;
}

Person::~Person() {

cout << "~Person()" << endl;
}

int Person::setName(string name) {

this->name = name;

cout << "Set name: " << name << endl;

return 0;
}

CPPAdapter.cpp

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
#include "CPPFile.hpp"
#include "CPPClassFile.hpp"

#ifdef __cplusplus
extern "C" {
#endif

void adapter_start_cpp(int cmd) {

//调用 CPPFile 中的方法
start_cpp(cmd);
}

int adapter_set_name(const char *cName) {

Person *person = new Person();
int ret = person->setName(cName);
delete person;

return ret;
}

#ifdef __cplusplus
}
#endif

在 C 中可以调用了, 如下代码:

CFile.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "CFile.h"

///声明 CPPFile 中的方法
extern void adapter_start_cpp(int cmd);

///声明 CPPClassFile 中的方法
extern int adapter_set_name(const char *cName);

void start_c(int cmd) {

printf("start_c by cmd: %i\n", cmd);

//调用 CPPFile 中的方法
adapter_start_cpp(5);

//调用 CPPClassFile 中的方法
adapter_set_name("www.veryitman.com");
}

extern “C”

extern "C" 中的 “C” 并不表示 C 语言,”C” 表示的是一种链接约定.

extern "C" 指令描述的是一种链接约定,它并不影响调用函数的定义,即使做了该声明,对函数类型的检查和参数转换仍要遵循 C++ 的标准,而不是 C 的标准.

不同的编程语言(编译型)链接特性是不同的,这也决定了它们编译后的链接符号的不同.

如函数 void function(int d),C 语言会把它编译成类似 _function 这样的符号,C 链接器只要找到该函数符号就可以链接成功.

C++ 会把这个函数编译成类似 _function_int 或 _xxx_functionIntxxx 这样的符号,即在符号上增加了类型信息,这也解释了为什么 C++ 可以实现函数重载了.

那么,对于用 C 编译器编译成的库,用 C++ 直接链接势必会出现不能识别符号的问题,用 extern "C" 就可以解决, 正如上面的例子.

简单来说, extern "C" 的作用就是让编译器知道要以 C 语言的方式编译和链接函数.

__cplusplus 宏

__cplusplus 宏是 C++ 编译器默认定义的.

类似如下的代码:

1
2
3
4
5
6
7
8
9
#ifdef __cplusplus
extern "C"{
#endif

void fun(int, size_t);

#ifdef __cplusplus
}
#endif

在 C++ 中, 编译器将 fun 按照 C 的链接约定来编译, 而如果是 C 编译器, 直接按照 C 的链接约定来编译即可.

__cplusplus 是在 C++ 编译器中默认定义的,C语言不支持 extern "C".

上面的代码很实用, 也是一种编程技巧.

iOS APNs: Background Fetch

发表于 2018-02-08 | 分类于 iOS |

在前面的几篇文章中, 给大家介绍了跟推送相关的内容.

今天跟大家聊一聊 iOS7 新加入的 Background Fetch 功能.

该系列博客:

iOS APNs: 远程推送
iOS APNs: 本地推送
iOS APNs: 处理数据
iOS APNs: 静默推送
iOS APNs: Background Fetch(本篇)

简介

从 Background Fetch 字面上来看, 大致意思是可在后台获取数据.

在 iOS7 中,Apple 为开发者提供了可以在后台更新应用程序界面和内容的 API, 即 Background Fetch.

该特性允许开发者在一个周期间隔后进行特定的动作,如获取网络内容, 更新 UI 操作等.

同时在 iOS7 中, 也加入了另一个特性, 就是博文 iOS APNs: 静默推送 里面说的静默推送.

这些都是对 iOS 后台多任务的补充和完善, Apple 一直在优化系统从而增强用户体验.

开启 Background Fetch

1.需要 APP 支持

如下图所示, 勾选即可.

1

2.设置 fetch 时间间隔

在 AppDelegate 中设置.

1
2
3
4
5
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 

// 设置 fetch 时间间隔
[application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
}

UIApplicationBackgroundFetchIntervalMinimum 是系统自定义的时间. 如果不设置, 默认是 UIApplicationBackgroundFetchIntervalNever, 这样就不会让 Background Fetch 生效了.

可以查看对 UIApplicationBackgroundFetchIntervalNever 的官方解释.

1
2
3
UIApplicationBackgroundFetchIntervalNever

A fetch interval large enough to prevent fetch operations from occurring.

当然, 你也可以设置自己定义的时间的间隔, 但是基本没有什么作用, 因为这个时间间隔完全由系统来决定, 系统会根据你的电量以及使用频率来决定.

所以, 可以理解为 setMinimumBackgroundFetchInterval 方法只是开启了 Background Fetch 功能.

3.开启了系统后台更新功能

在 iOS 系统中, 设置/通用/后台应用刷新中一定要开启对应 APP 的功能.

判断是否已经开启该功能的示例代码:

1
2
3
4
if ([[UIApplication sharedApplication] backgroundRefreshStatus] != UIBackgroundRefreshStatusAvailable) {

//不可用提示用户
}

处理回调

在实际的应用环中,Background Fetch 事件是由系统管理的,我们开发者是无法预先知道 Fetch 事件达到的时机的.

网上很多说可以使用 Xcode 模拟这个事件, 估计也是之前的系统和 Xcode 版本, 现在的 Xcode9 貌似不行了.

但是, 你的手机在后台等几分钟, 有时候会被系统触发该事件, 触发后, 对应的系统回调(在 AppDelegate 中)会被调用.

1
2
3
4
5
6
7
8
9
10
// Background fetch 回调
- (void)application:(UIApplication *)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {

MZLOG(@"AppDelegate. Background fetch.");

//可进行对应的耗时操作, 如下载等.

completionHandler(UIBackgroundFetchResultNoData);
}

Background Fetch 会为我们的 App 争取更多的后台时间, 但是一般是几十秒左右, 不会太多. 所以, 不要在回调中做太多耗时的操作.

iOS APNs: 静默推送

发表于 2018-02-06 | 分类于 iOS |

最近在看人工智能相关的知识,无意中发现了一个巨牛的 人工智能教程,分享一下给大家。

教程不仅是零基础,通俗易懂,而且非常风趣幽默,像看小说一样!觉得太牛了,所以分享给大家。点 这里 可以直接看教程。


今天跟大家介绍一下 ios 的静默推送功能。

静默推送,简单来说就是通过推送实现用户无感知的消息通知。

该系列博文:

iOS APNs: 远程推送
iOS APNs: 本地推送
iOS APNs: 处理数据
iOS APNs: 静默推送(本篇)

普通推送

例如微信,好友给你发送了一条消息,你的微信APP 在后台, 此时会收到带声音的一个消息提示。

这条推送机油有文字又有声音,点开这个推送可以直接打开 APP.

从程序的角度来讲,进入 App 后会调用下面的方法:

iOS10.0.x 和其之前的系统调用下面的方法:

1
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo

iOS10.0.x 之后的系统,会调用如下的方法:

1
2
3
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler

静默推送的异同

1.没有弹出框

没有推送的弹框出现,用户毫无感知的接收到了这个推送消息. 有 润物细无声 的感觉。

该推送既没有文字又没有声音。

2.需要用户授权允许推送

这个跟普通推送一样,都需要用户授权才可以推送。

3.会执行代理方法

静默推送来的时候,也能执行上面的回调方法。执行上面方法的场景如下:

1.App 在前台.
2.App 在后台, 但是没有被 kill 掉.

4.iOS7 以后才有的

对于 iOS 之前是没有静默推送的。

5.可以延长后台时间

收到静默推送后,在应用程序挂起之前有 30 秒左右可以执行相关的操作。

6.推送频率的控制

静默的推送是苹果推送服务器控制推送频率的。

苹果推送服务根据后台推送任务的能耗(用电量,接收情况)来控制后台推送的推送频率和延迟时间。


总之, 静默推送是普通推送的一种变种而已。

无论是静默推送还是普通推送,开发者证书里面必须开通 push 相关的功能,并且也需要用户开启允许推送的授权。

这个在 iOS APNs: 远程推送 里面已经介绍过,不在这里赘述。

推送的格式

普通推送的格式,大致是这个样子:

1
2
3
4
5
6
7
8
{
"aps":
{
"alert":"Testing.. (15),
"badge":1,
"sound":"default"
}
}

静默推送是不允许带 alert badge sound 等字段的,但是必须包含 "content-available":1.

例如下面的形式都可以:

形式1

1
2
3
4
5
6
{
"aps":
{
"content-available":1
}
}

形式2

1
2
3
4
5
6
7
{
"aps":
{
"custom":"open_profile_page"
"content-available":1
}
}

其中 custom 是自己定义的,大家根据需求自行扩展即可。

形式3

1
2
3
4
5
6
7
{
"aps" : {
"content-available" : 1
},
"acme1" : "bar",
"acme2" : 42
}

这个来自苹果开发者 文档 。

使用场景

具体的使用场景, 需要结合本身的业务和功能特点来说。

静默推送的前提是 APP 没有被杀死,可以通过回调函数来执行相关的代码。

另外静默推送不会打扰用户,比如用户正在游戏中战斗,你频繁的使用普通推送给他,估计他都要疯了.。

我个人使用静默推送的场景大致是这样的,APP 将要挂起的时候, 请求服务器去发一条静默推送,然后使用本地通知的方式唤起用户来打开 APP,从而保证 APP 处于保活的状态,至少静默推送可以增加被后台挂起的时间。

关于本地通知的内容,可以参考之前的文章 iOS APNs: 本地推送.

更多的使用场景等大家在业务中去挖掘。

Hi 2018, 我又来了

发表于 2018-02-02 | 分类于 随笔 |

好久没有更新自己的博客了, 以前坚持一周至少两三篇博文的我, 从 2017 年 10 月份到现在居然停止了。

其实, 这是有原因的…

数据丢了

2017 年 10 月 15 日晚, 我亲手把自己的之前写的文章(原数据)给弄丢了,我自己搭建的博客系统是使用 GitHub + Hexo 部署和发布的。

事情大致经过是这样的…

Mac 操作系统提示, 有更新的系统可以使用了, 问是否立即安装, 在公司的时候, 我心想, 更新后, 自己安装的一些库, 估计也要重新安装, 嫌麻烦就直接跳过了。

过了几天, 我晚上下班回家, 系统又提示我更新. 当时, 我就没有忍住, 直接更新了, 毕竟早晚都要更新的, 再说了 MacOS 还是比较让人放心的。

趁电脑更新系统的时候, 我就去洗了澡, 回来发现系统更新已经完成了。

心中想正好可以把之前没有完成的博客去完善一下, 谁知道…天不遂我愿呀! 再去使用 Hexo 的时候, 提示我失败, 居然无法使用了. 这可把我急的够呛, 一气之下(脑子估计当时进水了), 直接删了 Hexo 目录, 是那种程序员式的毫不留情的删除。

1
rm -fr ./hexo

潇洒的操作后, 重新安装 Hexo, 又折腾了两个晚上, 最终也是安装好了, 可以写东东了。

但是发现从 Github 上面 clone 后的文件, 无法还原成自己当初的 MarkDown 文件了。

雪花那个飘啊飘, 北风那个萧呀萧…

一切都晚了! ^^

唯一值得庆幸的是, 原来自己的网站博文还在那里。

还原数据

从哪里跌倒, 就从哪里爬起来, 我决定把之前的数据使用 人工智能 的方式还原回来。

😆我就是那个 人工, 手动的将 HTML 转换为 MarkDown 文档, 压根没有什么 智能。

好歹那也是自己熬过多少个日夜的成果呀, 没有功劳也有苦劳, 我不忍心就这样让它丢掉, 含着泪也要把它们给找回来!

自己下半年也比较忙, 周末都在加班, 只能每天晚上回家抽些时间还原一点, 有时候太困, 就推迟到下个晚上. 大概用了 3 个月左右的时间, 基本还原回来了。

每篇文章基本都有配图, 都有跳转链接, 恢复起来, 还是需要一点时间和毅力的, 中途我也放弃过, 但是想想它们就要这样的被我抹杀了, 实在太可怜了, 就坚持下来了。

反思和总结

还原之前的数据, 比写新文章还累, 主要是心累.

刚开始的时候, 我每篇都想一步到位的还原, 发现坚持几个晚上, 效率很低, 很容易放弃。

最后, 我调节了一下心态, 分 迭代 去做。

第一个阶段, 把 HTML 文章拷贝到 MarkDown 工具中, 不做任何的修饰和修改.

第二个阶段, 梳理 MarkDown 文档, 把缺失的图片补上去。

第三个阶段, 把文章中的链接尽量补全。

经过这三个阶段后, 文章基本已经复原了, 只是格式上看起来有点糟糕。

第四个阶段, 整理文章的排版, 对比已经发布的文章, 查漏补缺。

第四个阶段结束后, 文章基本都复活了, 接下来就是部署和发布了。

第五个阶段, 整理 Hexo, 配置相关的主题。

第六个阶段, 本地预览和再次校验文章, 统一发布。

每个阶段, 我都给自己一个小目标, 不断的去完善, 最后将各个阶段的成果串联起来, 就完成了. 当自己遇到困难的时候, 不妨分解一下任务, 不断的去完成每个小任务, 每个完成的小任务都是对自己最大的奖励。

整个还原的过程, 又让我重新梳理了一下之前的文章, 包括目录规划和文章中的措辞, 也对图片进行了压缩和处理. 收获颇多。

如果当初自己不那么冲动, 如果当初自己备份了文件, 应该就不会有这么一个艰辛的历程了。

数据备份很重要, 冲动是要付出代价的。

现在我把原数据保存在云盘中, 本地也保留一份, 定期更新到云盘中. 这样就可以防止数据丢失了。

祝大家在 2018 发发发, 身体健康, 幸福安康!

推荐

推荐给 Mac 用户一个图片压缩工具 imageoptim.

GCC: 静态库

发表于 2017-10-08 | 分类于 C/C++ |

概要

如果你对 GCC 的编译 C语言的流程不清楚, 建议在阅读本文之前先去看一下 GCC: 编译C语言的流程 这篇文章, 篇幅短小精悍, 阅读后, 至少可以扫扫盲😜.

本篇博文用到的一些基础知识点:

1.GCC

gcc -c 选项含义:

1
Only run preprocess, compile, and assemble steps

是 -c 选项只是进行了预处理, 编译, 汇编的阶段, 不会进行链接的操作.

2.静态库

Linux 上的静态库,本质是一些目标文件的归档文件.

3.静态库和共享库区别

[1].使用共享库可以节省内存.

比如 libc,系统中几乎所有的进程都映射 libc 到自己的进程地址空间,而 libc 的只读部分在物理内存中只需要存在一份,就可以被所有进程共享,这就是“共享库”这个名称的由来了.

[2].使用共享库可以很方便地升级库文件而不需要重新编译应用程序.

[3].共享库占用更少的体积.

在运行时做动态链接.而在链接静态库时, 链接器会把静态库中的目标文件取出来和可执行文件真正链接在一起.

创建步骤

创建静态库的步骤如下:

[1]. 写源文件.
[2]. 通过 gcc -c x.c 生成目标文件.
[2]. 归档. 用 ar 归档目标文件,生成静态库.
[3]. 写头文件, 便于使用者知道怎么使用该静态库.

使用静态库时,在源码中包含对应的头文件,链接时记得链接自己的库.

下面结合具体例子, 展开讲.

写源文件

目录结构
1

示例文件都是很简单的代码, 附录可以查看完整示例.

生成目标文件

将 libs 目录下面的(c)源文件进行预处理, 编译和汇编.
注意这里没有进行链接.

1
gcc -c libs/person.c libs/eat.c libs/play.c libs/sleep.c

执行城后, 会生成对应的 .o 文件.

归档

libperson.a 是要生成的库文件.

库文件都以 lib 开头, 静态库以. a 为后缀. 所以一般是 lib+ 名字.a

1
ar rs libperson.a person.o sleep.o play.o eat.o

ar: 类似于 tar, 用来对文件进行库打包.

r 选项: 将其后面的文件列表添加到文件包(libperson.a)中, 如果 libperson.a 不存在就创建它, 如果 libperson.a 已经存在且里面有同名的目标文件就进行替换操作.

s 选项: 为静态库创建索引.这个索引会被链接器使用.

ranlib 命令也可以为静态库创建索引. 所以上面的命令可以等效为下面的两个命令.

1
2
ar r libperson.a person.o sleep.o play.o eat.o
ranlib libperson.a

写头文件

写一个 person.h 文件, 便于调用者查看库如何使用.

person.h

1
2
3
4
5
6
7
#ifndef _PERSON_H
#define _PERSON_H
extern void init(int pUid);
extern void eat();
extern void play();
extern void sleep();
#endif

使用静态库

1
gcc main.c -L. -lperson -Ilibs -o main

-L 选项: 告诉编译器去哪里找库文件, 这里的 _L. 表示在当前目录.
如果不用 -L 选项, 即使库文件在当前目录, 编译器也不会去找, 所以该选项不能少.

报错信息如下:

1
2
ld: library not found for -lperson
clang: error: linker command failed with exit code 1 (use -v to see invocation)

-lperson: 告诉编译器要链接 libperson.a 库.

-I: 告诉编译器到哪里找头文件.
如果不指定头文件的查找目录, 也会报错:

1
2
3
4
main.c:2:10: fatal error: 'person.h' file not found
#include "person.h"
^~~~~~~~~~
1 error generated.

此时的目录结构:
1

链接成功后, 可以执行生成的 main (可执行)文件.

1
./main
1
2
3
uid: 101 eating
uid: 101 playing
uid: 101 has sleep

有趣的实验

猜想一下如果有两个库一个是共享库, 一个是静态库, 而且二者除了后缀不一样, 名字都一样如 libperson.a 和 libperson.so, 那么调用方如何来选择对应的库文件呢?

我们把 GCC: 共享库 里面生成的共享库 libperson.so 放到当前的目录, 重新编译链接 main.c 文件.

1
gcc main.c -L. -lperson -Ilibs -o main

再次执行 ./main, 得到结果是这样的:

1
2
3
From sharedlib.uid: 101 eating
From sharedlib.uid: 101 playing
From sharedlib.uid: 101 has sleep

以上的结果, 说明链接器会优先选择共享库其次才是静态库.

Linux(MacOS 也一样) 的 GCC 默认链接动态库,只有当动态库不存在时,才去链接静态库.
若是需要强制指定静态库需要指定选项 -static.但是在 MacOS 上面不支持该选项.

1
gcc -static main.c -L. -lperson -Ilibs -o main

附录

示例完整代码

main.c

1
2
3
4
5
6
7
8
#include <stdio.h>
#include "person.h"
int main() {
init(101);
eat();
play();
sleep();
}

person.h

1
2
3
4
5
6
7
#ifndef _PERSON_H
#define _PERSON_H
extern void init(int pUid);
extern void eat();
extern void play();
extern void sleep();
#endif

person.c

1
2
3
4
int uid;
void init(int pUid) {
uid = pUid;
}

eat.c

1
2
3
4
5
#include <stdio.h>
extern int uid;
void eat() {
printf("uid: %i eating\n", uid);
}

play.c

1
2
3
4
5
#include <stdio.h>
extern int uid;
void play() {
printf("uid: %i playing\n", uid);
}

sleep.c

1
2
3
4
5
#include <stdio.h>
extern int uid;
void sleep() {
printf("uid: %i has sleep\n", uid);
}

GCC 系列博文

GCC: 编译 C 语言的流程

GCC: Homebrew 安装 GCC 和 Binutils

GCC: 共享库

GCC: 静态库

GCC: 共享库

发表于 2017-10-07 | 分类于 C/C++ |

基本概念

共享库: 在程序执行期间需要调用到共享库的时候才加载到内存里面,可以被多个程序共享使用.

在 MacOS 上面创建和使用共享库要比在 Linux 上面简单.
在 Linux 上面还会牵扯到共享库路径问题, 在 Mac 上面就没有这样的问题.

在 MacOS 上面使用的 GCC 其本质是 LLVM. 你可以在命令行:

1
gcc -v

可以看到对应的输出结果:

1
2
3
4
5
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/include/c++/4.2.1
Apple LLVM version 9.0.0 (clang-900.0.37)
Target: x86_64-apple-darwin16.7.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

创建共享库步骤

[1].写源文件.这里以 C文件为基础.
[2].将源文件生成目标文件.
[3].创建共享库.
[4].写头文件, 供调用方使用.

下面结合实例来分享一下如何在 Mac 上面创建共享库.

写源文件

目录结构:
1

对应的完整代码可以看文末的附录.

生成目标文件

执行如下命令:

1
gcc -Wall -c -fPIC libs/eat.c libs/play.c libs/sleep.c libs/person.c

注意: -fPIC 选项一定要加.

PIC(position independent code), 产生位置无关码

生成共享库

这里把共享库暂且称之为 libperson.so.

执行下面命令创建:

1
gcc -shared -fPIC eat.o play.o sleep.o person.o -o libperson.so

写头文件

这里头文件主要是 person.h

1
2
3
4
5
6
7
#ifndef _PERSON_H
#define _PERSON_H
extern void init(int pUid);
extern void eat();
extern void play();
extern void sleep();
#endif

使用共享库

main.c 是使用方, 编译链接即可.

1
gcc main.c -L. -lperson -Ilibs -o main

生成可执行文件 main, 执行结果:

1
2
3
From sharedlib.uid: 101 eating
From sharedlib.uid: 101 playing
From sharedlib.uid: 101 has sleep

注意:
-L 选项, 告诉编译器去哪里找库文件, 这里的 _L. 表示在当前目录.

-lperson, 表示 libperson.so 库.

-Ilibs 告诉编译器头文件所在的目录.

附录

示例完整代码

main.c

1
2
3
4
5
6
7
8
#include <stdio.h>
#include "person.h"
int main() {
init(101);
eat();
play();
sleep();
}

person.h

1
2
3
4
5
6
7
#ifndef _PERSON_H
#define _PERSON_H
extern void init(int pUid);
extern void eat();
extern void play();
extern void sleep();
#endif

person.c

1
2
3
4
int uid;
void init(int pUid) {
uid = pUid;
}

eat.c

1
2
3
4
5
#include <stdio.h>
extern int uid;
void eat() {
printf("From sharedlib.uid: %i eating\n", uid);
}

play.c

1
2
3
4
5
#include <stdio.h>
extern int uid;
void play() {
printf("From sharedlib.uid: %i playing\n", uid);
}

sleep.c

1
2
3
4
5
#include <stdio.h>
extern int uid;
void sleep() {
printf("From sharedlib.uid: %i has sleep\n", uid);
}

GCC 系列博文

GCC: 编译 C 语言的流程

GCC: Homebrew 安装 GCC 和 Binutils

GCC: 共享库

GCC: 静态库


扫码关注,你我就各多一个朋友~

GCC: Homebrew 安装 GCC 和 Binutils

发表于 2017-10-07 | 分类于 C/C++ |

文章背景

前段时间,有个博客上面的小伙伴给我发来邮件,问我关于 GCC 相关的知识,对于 GCC 我也只是凭着之前的了解,回复他了。书到用时方恨少呀!

这位小伙伴还在读大学,自学能力比较强,自己出了完成课时之外,还会在网上找资料自学。

我记得自己在大学的时候,对 GCC 一窍不通,只知道 Turbo-C 和那些被强制记住的关键字。

最后告诉他, 我会坚持把自己以前学习过相关 GCC 的知识,以博客的形式输出。于是。就有了 GCC 系列 的文章,希望能够帮到大家。

概要

这篇博客主要分享 macOS 下安装 GCC 和 Binutils 工具。

macOS 终端下输入 gcc -v, 其实是链接指向了 LLVM 编译器, 不是 GNU 的 GCC.

1
2
3
4
5
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/include/c++/4.2.1
Apple LLVM version 9.0.0 (clang-900.0.37)
Target: x86_64-apple-darwin16.7.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

安装 GCC

关于 GCC, 可以参考 维基百科.

执行下面命令, 即可开始安装:

1
brew tap homebrew/versions

然后搜索 gcc

1
brew search gcc

搜索结果中会显示 gcc 的版本,可以选择安装一个,建议安装最新版。

这里以安装 gcc49 为例。

1
brew install gcc49

brew 会自动为你安装相关依赖,如 gmp4、mpfr2、libmpc08、isl011、cloog018.

这里安装需要等待一段时间…

看到下面的内容, 表示安装完毕.

1
2
3
Pouring gcc49-4.9.2_1.yosemite.bottle.1.tar.gz

/usr/local/Cellar/gcc49/4.9.2_1: 1138 files, 164M

安装成功后, 安装目录在:

1
/usr/local/Cellar/gcc49/4.9.2_1

你在终端直接输入 gcc -v,其实还是 LLVM,那么如何使用我们 刚安装的 gcc?

1
2
3
cd /usr/local/Cellar/gcc49/4.9.2_1/bin

ls -l

可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c++-4.9
cpp-4.9
g++-4.9
gcc-4.9
gcc-ar-4.9
gcc-nm-4.9
gcc-ranlib-4.9
gcov-4.9
x86_64-apple-darwin14.3.0-c++-4.9
x86_64-apple-darwin14.3.0-g++-4.9
x86_64-apple-darwin14.3.0-gcc-4.9
x86_64-apple-darwin14.3.0-gcc-4.9.2
x86_64-apple-darwin14.3.0-gcc-ar-4.9
x86_64-apple-darwin14.3.0-gcc-nm-4.9
x86_64-apple-darwin14.3.0-gcc-ranlib-4.9

Ok,你应该明白了, 在命令行输入 gcc-4.9 -v 就可以使用我们刚安装的 GCC 了,包括 g++ 等工具.

以后使用 GNU 的 GCC 输入 gcc-4.9 就好了。

安装 Binutils

关于 Binutils 可以参考 维基百科.

执行下面命令, 直接安装:

1
brew install binutils

同理:

1
cd /usr/local/Cellar/binutils/2.25/bin

可以看到你需要的工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gaddr2line
gar
gc++filt
gcoffdump
gdlltool
gdllwrap
gelfedit
gnlmconv
gnm
gobjcopy
gobjdump
granlib
greadelf
gsize
gsrconv
gstrings
gstrip
gsysdump
gwindmc
gwindres

个人感受

在 macOS 上面,即使安装了 GNU 的 GCC,也没有在 Linux 上面用的那么爽,很多 GCC 的命令选项在 macOS 上面无法使用,但不得不承认 LLVM 是比较优秀的编译器。

遇到问题

gcc-4.9 编译代码报错,报错内容如下:

1
2
3
4
main.c:9:19: fatal error: stdio.h: No such file or directory
#include <stdio.h>
^
compilation terminated.

卸载 gcc 重新安装最新版本的 gcc 即可,如果你不知道最新的 gcc 版本是哪个,可以 search 一下。

1
brew search gcc

显示结果大致如下:

1
2
3
4
5
6
gcc        
gcc@5
gcc@7
gcc@4.9
gcc@6
i386-elf-gcc

这里以安装 gcc@7 为例,命令如下:

1
2
3
4
5
// 卸载原来的
brew uninstall gcc

// 安装最新版本
brew install gcc@7

GCC 系列博文

GCC: 编译 C 语言的流程

GCC: Homebrew 安装 GCC 和 Binutils

GCC: 共享库

GCC: 静态库


扫码关注,你我就各多一个朋友~

GCC: 编译C语言的流程

发表于 2017-10-03 | 分类于 C/C++ |

再看下文之前,先普及一下 Linux 和 gcc 的历史,下面的这段内容来自于网络。

早在 20 世纪 70 年代,UNIX 系统是开源而且免费的。但是在 1979 年时,AT&T 公司宣布了对 UNIX 系统的商业化计划,随之开源软件业转变成了版权式软件产业,源代码被当作商业机密,成为专利产品,人们再也不能自由地享受科技成果。

于是在 1984 年,Richard Stallman 面对于如此封闭的软件创作环境,发起了 GNU 源代码开放计划并制定了著名的 GPL 许可协议。

1987 年时,GNU 计划获得了一项重大突破—gcc编译器发布,这使得程序员可以基于该编译器编写出属于自己的开源软件。

随之,在 1991 年10 月,芬兰赫尔辛基大学的在校生 Linus Torvalds 编写了一款名为 Linux 的操作系统。该系统因其较高的代码质量且基于 GNU GPL 许可协议的开放源代码特性,迅速得到了 GNU 计划和一大批黑客程序员的支持。

随后 Linux 系统便进入了如火如荼的发展阶段。

1994 年 1 月,Bob Young 在 Linux 系统内核的基础之上,集成了众多的源代码和程序软件,发布了红帽系统并开始出售技术服务,这进一步推动了 Linux 系统的普及。

1998 年以后,随着 GNU 源代码开放计划和 Linux 系统的继续火热,以 IBM 和 Intel 为首的多家 IT 企业巨头开始大力推动开放源代码软件的发展。

到了 2017 年年底,Linux 内核已经发展到了 4.13 版本,并且 Linux 系统版本也有数百个之多,但它们依然都使用 Linus Torvalds 开发、维护的Linux 系统内核。RedHat 公司也成为了开源行业及 Linux 系统的带头公司。

声明

这篇博文 makefile: gcc 工作流程 早期被我放在 CSDN 上面过.时隔多年, 仍不能忘怀, 现乔迁至此.

本文比较简单, 比较适合初学者.
我只是试图以实例的方式给大家讲解一下 GCC 编译链接 C语言程序的流程, 并没有高深的知识.

编译流程

C 语言经过预处理、编译、连接最后生成可执行文件.

在 Linux 的 GCC 下面,看看其工作流程. 流程图如下:

1

预处理,将包含的 *.h 文件或者一些预处理语句(如 #define)处理一下,然后将 *.c 文件生成 *.i 的中间文件.

注意了 GCC 不会自动生成这样一个文件,如果需要,自己加 -E 参数即可.

编译、汇编阶段生成 *.o 目标文件,最后经过链接生成可执行文件.

注意:可执行文件不一定是 exe 文件.

实例

下面以一个实例 hello.c,说明这个过程.

1

源文件很简单,这里只是为了说明问题.

在命令行,执行下面命令完成预处理:

1
gcc  -E  hello.c  -o  hello.i

执行之后,你可以打开 hello.i 文件,可以看到很多 extern 以及将 MAX 解释为 20:
1

在命令行,执行下面命令, 完成编译阶段:

1
gcc  -S  hello.i  -o hello.s

这时可以生成 *.s 文件,其实就是一些汇编语句,如下所示:
1

那麽,我们就可以进入汇编阶段了,执行:

1
gcc  -c  hello.s  -o hello.o

打开这个 hello.o 文件,都是一些机器码.

最后一个连接生成可执行文件:

1
gcc hello.o -o hello

在当前目录就可以生成 hello 这个可执行文件.

执行 ./hello,就会输出:
1

到此,GCC 编译连接生成可执行文件完毕!

当前目录下面的文件:
1

以上的步骤,其实一句命令即可:

1
gcc  hello.c  -o  hello

此时目录内容如下:
1

自己动手感受一下,你会收获更多!

GCC 系列博文

GCC: 编译 C 语言的流程

GCC: Homebrew 安装 GCC 和 Binutils

GCC: 共享库

GCC: 静态库

iOS APNs: 处理数据

发表于 2017-08-07 | 分类于 iOS |

简介

博文: iOS 细说推送: 远程推送 和 iOS 细说推送: 本地推送
分别介绍了 iOS 的两种典型推送, iOS 还有很多类型的推送, 如静默推送, VOIP 推送(iOS8)等, 后面会慢慢介绍给大家.

今天跟大家聊聊关于如何处理推送的数据(payload).

该系列博客:

iOS APNs: 远程推送
iOS APNs: 本地推送
iOS APNs: 处理数据(本篇)
iOS APNs: 静默推送
iOS APNs: Background Fetch

推送的代理回调时机

还记得 AppDelegate 里面关于推送的几个代理方法吧.
~ 估计你也忘记了, 没关系, 我们再一起整理一下.

1
2
3
4
5
6
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {

MZLOG(@"markApp push. RemotePush userInfo: %@", userInfo);

// 可以根据推送内容决定下一步的行为
}

该方法在接收到 RemotePush 的时候, 调用时机:

1.APP 在前台运行的, RemotePush 被推送过来了.
2.APP 在后台运行, 无论是否被挂起只要没有被杀死, 点击推送内容.

以上两种情况均可以进入 didReceiveRemoteNotification 方法.

1
2
3
4
5
6
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {

MZLOG(@"App push. LocalPush notification: %@", notification);

// 可以根据推送内容决定下一步的行为
}

该方法接收到 LocalPush 的时候, 调用时机:

1.APP 在前台运行的, LocalPush 被推送过来了.
2.APP 在后台运行, 无论是否被挂起只要没有被杀死, 点击推送内容.

以上两种情况均可以进入 didReceiveLocalNotification 方法.

这两个回调方法, 分别接收 RemotePush 和 LocalPush 的推送消息.

有些人会问了, 假如这个时候我的应用在后台, 被系统杀死了或者被用户双击 Home 键杀死了, 此时远程推送过来了, 或者状态栏里面有本地推送的消息, 我点击推送消息, 这两个方法会被调用吗?

类似这种效果, 上面是 RemotePush, 下面是 LocalPush.

1

我肯定的告诉大家, 应用被杀死的情况下, 点击推送内容, didReceiveRemoteNotification 和 didReceiveLocalNotification 都不会被调用.

这个时候, AppDelegate 中的另一个方法上场了.

1
2
- (BOOL)application:(UIApplication *)application 
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

看一下 didFinishLaunchingWithOptions 的具体实现, 示例代码如下:

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
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

/// 将应用图标的 badge 清零
{
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:1];
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
[[UIApplication sharedApplication] cancelAllLocalNotifications];
}

if (nil != launchOptions) {
/// 处理 LocalPush
{
// 这里可以得到 UILocalNotification 对象
id localPushNotify = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];

MZLOG(@"AppDelegate localPush: %@", localPushNotify);

if (nil != localPushNotify) {
if ([localPushNotify isKindOfClass:[UILocalNotification class]]) {
// 获取 userinfo 数据
NSDictionary *userInfo = [(UILocalNotification *)localPushNotify userInfo];
MZLOG(@"AppDelegate localPush of UILocalNotification: %@", userInfo);

// 根据 userInfo 的内容处理如页面跳转等
}
}
}

/// 处理 RemotePush
{
NSDictionary *remotePushNotify = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
MZLOG(@"AppDelegate remotePush: %@", remotePushNotify);

if (nil != remotePushNotify) {
NSDictionary *remoteAps = [remotePushNotify objectForKey:@"aps"];
MZLOG(@"AppDelegate remotePush. The aps' info, alert: %@, badge: %@, sound: %@",
[remoteAps objectForKey:@"alert"],
[remoteAps objectForKey:@"badge"], [remoteAps objectForKey:@"sound"]);
// 根据推送的内容处理如页面跳转等
}
}
}

MZLOG(@"AppDelegate. launchOptions: %@", launchOptions);

return YES;
}

对应的打印输出内容, 如下

本地推送的内容:

1
2
3
 AppDelegate localPush of UILocalNotification: {
"user_info_key" = "user_info_value_json_str";
}

远程推送的内容:

1
2
3
4
5
6
7
aps = {
alert = "Testing.. (2)";
badge = 1;
sound = default;
};

The aps' info, alert: Testing.. (19), badge: 1, sound: default

自定义推送内容

对于推送的数据格式, 是苹果规定的格式, 我们可以在其基础上添加我们需要的数据.

在介绍下面内容之前, 先了解一下什么是 payload.

payload 是推送通知的一部分,每一条推送通知都包含一个 Payload.
它包含了系统提醒用户通知到达的方式,还可以添加自定义的数据, 即通知主要传递的数据为 payload.

Payload 本身为 JSON 格式的字符串,它内部必须要包含一个键为 aps 的字典.也就是说 payload 是整个字符串.

关于 payload 的限制

在早期的推送中, payload 不能超过 256bytes, 中间还经历过推送的 payload 最大为 2KB.
现在苹果支持最大为5KB(VOIP 推送), 官方文档有说, 如下:

1
2
3
4
For regular remote notifications, the maximum size is 4KB (4096 bytes)
For Voice over Internet Protocol (VoIP) notifications, the maximum size is 5KB (5120 bytes)
NOTE
If you are using the legacy APNs binary interface to send notifications instead of an HTTP/2 request, the maximum payload size is 2KB (2048 bytes)

上面是 官方文档 的原文, 大概意思是:

现在 APNs 支持 payload 为 4KB, 如果是 VoIP 推送的话, 支持 5KB. 但是, 如果你使用传统的 APNs 而不是使用 HTTP/2 的话, 最大支持 2KB. 也就是说, 推送的 payload 大小和 iOS 系统无关, 而是和协议有关.

可以使用下面代码, 查看 payload 长度

1
payload.getBytes().length

我们正常的推送内容是这样的, 正常格式:

1
{"aps":{"alert":"Testing.. (21)","badge":1,"sound":"default"}}

如果想对推送内容做自定义, 可以自己添加额外的数据, 如:

1
{"aps":{"alert":"Testing.. (21)","badge":1,"sound":"default"}, "ext":{"key":"value"}}

其中, ext 就是额外添加的数据模型.

在 APP 收到这样的数据模型时, 可以对应不同的行为.

大家可以根据自己的业务需求, 自行定义数据格式.

自定义的数据格式, 注意内容的长度, 在不同的 iOS 系统上面, 对于推送的内容是有长度限制的.

再唠叨几句

关于 iOS7 以后添加的方法

1
2
3
- (void)application:(UIApplication *)application 
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler

这个方法也是在 AppDelegate 中实现的.

1
2
- (void)application:(UIApplication *)application 
didReceiveRemoteNotification:(NSDictionary *)userInfo

这个方法在 iOS3 就已经有了, 可谓是历史悠久.我姑且称之为该方法为 FatherMethod, 上面那个新加入的称之为 SonMethod.

注意事项:

[1]. 实现了 FatherMethod, 即使你实现了 SonMethod, SonMethod 也不会被调用.
[2]. SonMethod 比 FatherMethod 多了一个参数 completionHandler, 这个在后面会说到.主要用于 Background Mode 中的 Background Fetch.
[3]. 推送被调用的时机, 除了 FatherMethod 的时机外, 还多了一种时机, 就是在 APP 被杀死后, 点击推送内容打开 APP, 此时 APP 再次回到前台的时候, 该方法也会被调用(didFinishLaunchingWithOptions也会调用), 所以在处理远程推送内容的时候, 要注意这一点.

针对注意事项[3], 目前我能给出的有两种解决方案:

第一, 判断是否是第一次启动 APP, 如果是第一次启动 APP, 统一在didFinishLaunchingWithOptions 中处理, 不在 SonMethod 中处理.

第二, 远程推送统一在 SonMethod 中处理, didFinishLaunchingWithOptions 中只处理在 APP 被杀死的情况下, 用户点击推送本地的内容.

我个人采用的是第二中方案.

感谢

在推送工具 NWPusher 和 Xcode 的 Window/Devices/Console 帮助下, 才得以完善博文.

NWPusher 用来发送推送内容.

Window/Devices/Console 用于查看输出日志.

必看文档

Creating the Remote Notification Payload

Payload Key Reference

<1…141516…20>

193 日志
16 分类
163 标签
© 2024 veryitman