iOS 必知必会 - APNs篇
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 必知必会 - APNs篇相关的知识,希望对你有一定的参考价值。
参考技术A导语:
由于移动设备内存、CPU、电量的局限性,ios 不允许 APP 的进程常驻后台(事实上可以申请后台运行一段时间,最长约 10 分钟),这样当用户主动杀掉 APP,或者 APP 进入后台超过约定时长时,就意味着该 APP 进程的结束。这在很大程度上保障了前台 APP 的流畅性,也延长了手机的使用时长,获得了较好的用户体验。但是这也意味着,服务器无法主动和用户交互(如推送实时消息等)。为了解决这个限制,苹果推出了 APNs,允许设备和服务器分别与苹果的推送通知服务器保持长连接状态。
iOS 的通知分为本地通知和远程通知。本地通知是由本地应用触发的,一般是基于时间的一种通知形式,如闹钟、待办事件等的提醒。远程通知是由开发商通过自己的服务器推送的一种通知形式,而 APNs 就是远程通知功能的核心。
关于远程推送,记住以下两点就够了:
这里就很清楚了,其实 APNs 的本质就是 服务器和客户端之间的中介 。当服务器需要给客户端推送消息时,先将消息发送给苹果服务器,再由苹果服务器找到对应设备推送下去。
那为什么还要走中介,不直接发送呢?因为这样做一个设备(即所有 APP )只需要和苹果的服务器建立一条长连接,而不需要每个 APP 都和服务器建立一条长连接。
可能有些人还是不太明白 APNs 的意义,觉得也只是将多个长连接变成了统一的一个长连接而已,有必要那么做吗?
很有必要!
我们来看下 android 的推送现状就明白了。
Android 事实上也有类似于 APNs 的一套用于推送的服务,简称 GCM,即 Google Cloud Messaging。但由于 GCM 需要谷歌服务器的支持,在国内由于「墙」的原因基本不能使用。这下就热闹了,国内出现了一大堆第三方推送服务商,如华为推送、小米推送、极光推送等。APP 通过集成这些推送服务来实现推送功能,而这些推送服务为了保持自己的长连接不被杀死,采用了各种保活、唤醒手段,这也是 Android 手机使用不流畅的真凶。之前也有看到「 工信部要求国内安卓统一消息推送标准 」的新闻,工信部都这么重视,可见统一推送的意义非凡。
想要了解具体区别,可以参考这篇文章 「 国内 90%以上的 iOS 开发者,对 APNs 的认识都是错的 」。
不言而喻,当然是尽早升级 HTTP/2 协议了。
参考:
(完)
Linux 编程之信号篇:异常监控必知必会
文章目录
为什么要了解信号
信号是 UNIX 系统进程管理非常重要的一环,下面这些场景都需要通过信号实现:
- 进程接收内核的通知(比如内核通知进程 用户输入了信息)
- 系统终止一个进程
- 管理父子进程(比如通知父进程子进程退出了)
- 进程间通信
信号在 Android 系统中的地位也非常重要。通过了解信号,我们可以实现对应用运行状态的监听,最实际的用途,就是监听应用发生崩溃、ANR 并上报。
因此,我们有必要掌握信号的使用和基本原理,从而对关注的信号进行处理。
什么是信号
信号是一种软中断, 是一种通知方式。
当收到内核或者其他进程发送的信号后,接收信号会从当前执行的代码转移到之前注册的信号处理函数(如果注册了的话),当信号处理函数执行完成后,恢复执行之前的代码(如果没有退出的话)。
上一句话括号里之所以有“如果注册了的话”,是因为在接收到信号后,进程可以有多种选择:
- 忽略
- 使用默认的处理函数
- 自己注册一个信号处理函数
当进程通过 syscall 指定要忽略某些信号后,再接收到这些信号,会直接无视,继续执行;如果是没有忽略的信号,就会执行默认或者注册的处理函数。
另外,我们也可以通过信号传递一些数据,这一般用于进程间通信。
信号有哪些
在 UNIX 系统中,信号有大概 31 个,具体定义在 asm/signal.h
中(使用 adb shell kill -l 也能看到这些信号),这里我们仅看下常见的几种:
- SIGKILL(9)/SIGSTOP(19):被系统强制杀死/停止**(不能被捕获或者忽略)**
- SIGQUIT(3): Android 进程 ANR 时会收到这个信号
- SIGABRT(6): 主动 abort() 后触发,
- SIGALRM(14):主动 alarm() 后触发
- SIGSEGV(11):段错误,访问无效内存或者无权访问内存
- SIGBUS(7):硬件或对齐错误
- SIGFPE(8):算数异常,包括溢出、除以 0 等
- SIGCHLD(17):子进程被终止时,内核给父进程发送
- SIPROF(27):向没有读取端的管道写入,一般用作调试
可以看到,信号的名称都是 SIGxxx,另外每个信号也有唯一的整数值,在我们注册相关监听时,都是使用整数,名称只是为了直观理解其类型。
还有一个值为 0 的空信号,这个仅用于测试是否有权限发送信号。
如何发送/捕获信号
老 API:
- kill()
- signal()
推荐 API:
- sigqueue()
- sigaction()
推荐理由:可以发送的信息更多
发送
kill
我们可以通过 adb 使用 kill: adb shell kill -9 pid
。
在代码中使用 kill 的话,也很简单,它的函数签名:
int kill(pid_t __pid, int __signal);
- pid > 0,会给该进程发送信号
- pid = 0,会给调用进程的【进程组中的每个进程】发送信号
- pid = -1,会向调用进程发送信号
- pid < -1,会给 pid 绝对值的进程组发送信号
返回值:0 成功;-1 失败。
- killpg
- raise
raise
raise 用于给自己发信号,就是简化版的 kill:
int raise(int __signal);
//等于 kill(getpid(), signo);
返回值:0 成功;其他值失败。
killpg
给一个进程组发送信号:
int killpg(int __pgrp, int __signal);
等同于 kill(-1* pgrp, signal)
需要注意的是,发送信号,需要权限,用户只能给 UID 相同的进程发送信号,除非是 ROOT 设备。
sigqueue
sigqueue 可以发送待附加信息的信号,方法签名如下:
int sigqueue(pid_t __pid, int __signal, const union sigval __value)
typedef union sigval
int sival_int;
void __user * sival_ptr;
sigval_t;
sigqueue 只能给指定的进程发送信号,不能发给一个进程组。
使用案例:
char* msg = static_cast<char *>(calloc(12, sizeof(char)));
msg = "zhangshixin";
union sigval value;
value.sival_ptr = msg;
int ret = sigqueue(getpid(), signo, value);
LOG(" send signal by sigqueue, signo:%d , ret: %d, msg: %s", signo, ret, msg);
监听
signal
signal 函数比较简单,第一个参数是要监听的信号,第二个参数是一个信号处理函数:
#if __ANDROID_API__ >= __ANDROID_API_L__
sighandler_t signal(int __signal, sighandler_t __handler)
通过宏可以看出,
signal
只支持 5.0 级以上版本的设备。
写一个例子:
//信号处理函数必须返回 void
void my_signal_handler(int signo)
LOG("my_signal_handler called, sig_no: %d", signo);
void my_signal_handler_2(int signo)
LOG("my_signal_handler_2 called, sig_no: %d, signame: %s, name by sys_siglist: %s",
signo, strsignal(signo), sys_siglist[signo]);
void old_signal_listen_test()
//监听 SIGPROF,返回该信号之前注册的信号处理函数
auto ret = signal(SIGPROF, my_signal_handler);
ret = signal(SIGPROF, my_signal_handler_2);
LOG("register signal handler ret: %p, my_signal_handler: %p", ret, my_signal_handler);
if (ret == SIG_ERR)
fprintf(stderr, "register signal handler failed!");
return;
//忽略
signal(SIGFPE, SIG_IGN);
//只在接收到可捕获的信号时才返回,返回值返回 -1
int pause_ret = pause();
LOG("pause returned, ret: %d", pause_ret);
//使用默认处理
ret = signal(SIGPROF, SIG_DFL);
LOG("set SIGPROF use default handler, ret: %p", ret);
上面的例子中,我们做了三件事:
- 注册了两次信号处理函数,然后打印出第二次注册时,返回值的地址
- 忽略了 SIGFPE 信号
- 调用 pause,阻塞当前代码
- 在收到信号后,设置 SIGPROF 信号继续使用默认处理方式
通过运行结果可以看到,的确是返回的第一次注册的信号处理函数的地址:
zsx_linux: register signal handler ret: 0x788d755264, my_signal_handler: 0x788d755264
注册成功后,我们可以在 adb shell 中发送一个 SIGPROF 信号进行测试(需要 ROOT 手机):
admin@C02ZL010LVCK debug % adb shell
ursa:/ $ top | grep performance
25263 shell 20 0 8.8M 2.2M 1.8M S 0.0 0.0 0:00.02 grep performance
25236 u0_a124 10 -10 4.3G 122M 73M S 0.0 1.5 0:00.57 top.shixinzhang.performance
ursa:/ $ su
:/ # kill
kill
usage: kill [-s signame | -signum | -signame] job | pid | pgrp ...
kill -l [exit_status ...]
1|:/ # kill -27 25236
kill -27 25236
kill 命令的格式:kill -signum pid
发送后,我们在信号处理函数里的日志就如期打印了:
zsx_linux: my_signal_handler_2 called, sig_no: 27, signame: Profiling timer expired, name by sys_siglist: Profiling timer expired
zsx_linux: pause returned, ret: -1
zsx_linux: set SIGPROF use default handler, ret: 0x788d756360
上面的测试代码还有 2 个细节:
pause()
是 Linux 提供的测试信号 API,调用它会立刻阻塞,在接收到可捕获的信号时才返回strsignal(signo)
和sys_siglist[signo]
可以获取信号的名称,一般推荐用后者
需要注意的是,不是所有信号都可以捕获并处理的,比如注册 SIGKILL SIGSTOP 就直接会返回 SIG_ERR;注册 SIGUSR1 虽然 OK,但发送这些信号,也不会执行我们注册的信号处理函数 (可能被阻塞了)。
sigaction
https://man7.org/linux/man-pages/man2/sigaction.2.html
sigaction 比 signal 更为强大,它可以获取到信号发送者的信息和进程状态。
#include <signal.h>
int sigaction(int signum, const struct sigaction *restrict act,
struct sigaction *restrict oldact);
第一个参数是要注册的信号,第二个参数要信号处理相关的结构体,第三个参数是之前设置的信号处理行为。
sigaction 结构体内容如下:
struct sigaction
union
sighandler_t sa_handler; //和 signal 一样的信号处理函数
void (*sa_sigaction)(int, struct siginfo*, void*); //可以获取到更详细信息的信号处理函数
;
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
;
sa_flags 支持这些参数:
能额外获取到的信息:
__SIGINFO struct int si_signo; int si_errno; int si_code; union __sifields _sifields;
union __sifields
struct
__kernel_pid_t _pid;
__kernel_uid32_t _uid;
_kill;
struct
__kernel_pid_t _pid;
__kernel_uid32_t _uid;
sigval_t _sigval;
_rt;
//...
;
__sifields 内容非常多,这里我做了精简,重点关注信号发送者的信息、信号产生的原因和传递的数据。
使用例子:
void my_sa_sigaction(int signo, struct siginfo* info, void* context)
LOG("my_sa_sigaction called ! signo: %d", signo);
auto _signo = info->si_signo;
//错误原因
auto sig_code = info->si_code;
auto killer_pid = info->_sifields._kill._pid;
auto killer_uid = info->_sifields._kill._uid;
auto syscall = info->_sifields._sigsys._syscall;
LOG("_signo: %d", _signo);
LOG("sig_code: %d, send by sigqueue? %d", sig_code, sig_code == SI_QUEUE);
LOG("killer_pid: %d , killer_uid: %d", killer_pid, killer_uid);
LOG("syscall: %d", syscall);
auto value = context;
LOG("value: %s", value);
//获取发送的数据
sigval_t sig_value = info->_sifields._rt._sigval;
LOG("sig_value %s", sig_value.sival_ptr);
//比 signal 更为强大,可以获取的信息更多
void sigaction_test()
struct sigaction sig_action;
//设置信号的处理方式,SA_SIGINFO 表示使用信息更多的信号处理函数
sig_action.sa_flags = SA_SIGINFO;
//设置执行信号处理函数时,要阻塞的信号集(避免重入)
//目前正在被处理的信号,也是被阻塞的
// sig_action.sa_mask = SIGPROF;
sig_action.sa_sigaction = my_sa_sigaction;
//成功时返回 0
int ret = sigaction(SIGPROF, &sig_action, nullptr);
if (ret)
LOG("sigaction set failed! ");
LOG("set sigaction ret: %d", ret);
使用 sigqueue 发送信号后,输出结果
zsx_linux: my_sa_sigaction called ! signo: 27
zsx_linux: _signo: 27
zsx_linux: sig_code: -1, send by sigqueue? 1
zsx_linux: killer_pid: 2919 , killer_uid: 10124
zsx_linux: sig_value zhangshixin
信号处理函数的注意事项
实现信号处理函数,有 2 点需要注意:
- 最好不要访问、修改全局数据
- 不调用不可重入函数
第一点建议的原因:由于信号处理函数是异步调用,可能在程序执行数据读写的过程中被切换到信号处理函数,如果在信号处理函数中访问、修改这部分数据,会导致结果不可预期。
第二点中,可重入的函数就是说多次调用不会有异常,建议在信号处理函数中仅使用可重入的函数。
Linux 信号可重入的安全函数列表见 https://man7.org/linux/man-pages/man7/signal-safety.7.html
信号阻塞
有些情况下,在用户程序或者信号处理函数中,可能需要对挂起某些信号,即使收到这些信号,也暂不处理,继续执行任务,等执行完当前任务,再开始接收信号。
Linux 提供了这种机制,称为信号的阻塞(Block)和解除阻塞(UnBlock)。
Linux 中管保存当前进程阻塞的信号数据,叫做“信号掩码”,signal mask。
获取、修改信号掩码的 API 是 sigprocmask
:
int sigprocmask(int __how, const sigset_t* __new_set, sigset_t* __old_set);
https://man7.org/linux/man-pages/man2/sigprocmask.2.html
我们可以通过构造一个信号集(即多个信号),然后调用 sigprocmask
:
- 如果传入的参数是 SIG_BLOCK,就是把信号集里的信号,也都阻塞了(其余之前设置的不变)
- 如果传入的参数是 SIG_UNBLOCK,就是把信号集里的信号,解除阻塞(其余之前设置的不变)
- 如果传入的参数是 SIG_SETMASK,就是粗暴地把进程的阻塞的信号,设置成当前的信号集合(之前设置的不要了)
可以通过传入一个空的 set,获取 old_set,然后判断当前进程是否阻塞某个信号:
void process_block_signal_mask_test()
//信号集
sigset_t set, fill_set, old_set;
//1.初始化空信号集,把所有信号都排除
int ret = sigemptyset(&set);
LOG("sigemptyset ret: %d", ret);
//把所有信号都包括在内
sigfillset(&fill_set);
//添加 SIGQUIT 到信号集
// ret = sigaddset(&set, SIGQUIT);
// LOG("sigaddset ret: %d", ret);
ret = sigismember(&set, SIGQUIT);
LOG("check sigismember, sig: %d ret: %d", SIGQUIT, ret);;
ret = sigismember(&fill_set, SIGQUIT);
LOG("check [fill_set] sigismember, sig: %d ret: %d", SIGQUIT, ret);;
//2.可以传入空 set,只为看当前的信号掩码有哪些
ret = sigprocmask(SIG_BLOCK, &set, &old_set);
//检查是否默认阻塞 SIGQUIT,答案是是的
LOG("check process mask, SIGQUIT ismember: %d", sigismember(&old_set, SIGQUIT));
sigset_t pending_set;
//3.获取当前进程待处理的信号
if (sigpending(&pending_set))
LOG("sigpending failed");
LOG("sigpending ret: %d, pending_set: %ld", ret, pending_set);
//4.恢复旧的信号掩码,这个函数和 pause 一样会阻塞,直到收到信号
ret = sigsuspend(&old_set);
LOG("sigsuspend ret: %d, old_set: %ld", ret, old_set);
输出结果:
zsx_linux: sigemptyset ret: 0
//空集合,不包含 SIGQUIT
zsx_linux: check sigismember, sig: 3 ret: 0
//满集合,包含 SIGQUIT
zsx_linux: check [fill_set] sigismember, sig: 3 ret: 1
//进程初始化时,把 SIGQUIT 信号屏蔽了
zsx_linux: check process mask, SIGQUIT ismember: 1
上面我们看到,在我的 demo app 里,默认就把 SIGQUIT 屏蔽了,具体实现代码在 Runtime::init 里,会调用到 Runtime::BlockSignals:
void Runtime::BlockSignals()
SignalSet signals;
signals.Add(SIGPIPE);
// SIGQUIT is used to dump the runtime's state (including stack traces).
signals.Add(SIGQUIT);
// SIGUSR1 is used to initiate a GC.
signals.Add(SIGUSR1);
signals.Block();
void Block()
if (pthread_sigmask64(SIG_BLOCK, &set_, nullptr) != 0)
PLOG(FATAL) << "pthread_sigmask failed";
可以看到,Runtime::BlockSignals 会调用 pthread_sigmask64 设置这三个信号阻塞,这里也解答了为什么前面我们注册了 SIGUSR1 信号但在测试时没有收到该信号。
pthread_sigmask 和 sigprocmask 基本一样,但 POSIX.1 标准里没有定义 sigprocmask 在多线程环境下的行为,所以可能在不同的系统上实现有些差异。
一般建议使用 pthread_sigmask
注意:前面提到无法注册的 SIGKILL 和 SIGSTOP,同样无法被阻塞。系统强制执行,无法修改。
我们可以通过 sigpending
获取当前进程被阻塞了、待处理的信号列表:
int sigpending(sigset_t* __set);
总结
通过本文,我们可以对 Linux 的信号机制有个基本的认识,知道信号的发送和注册方式,了解阻塞信号的方法,以及通过信号传递数据。
Thanks
- 《Linux 系统编程》
- https://man7.org/linux/man-pages/man7/signal.7.html
- https://man7.org/linux/man-pages/man2/sigaction.2.html
- https://man7.org/linux/man-pages/man7/signal-safety.7.html
- https://man7.org/linux/man-pages/man3/pthread_sigmask.3.html
以上是关于iOS 必知必会 - APNs篇的主要内容,如果未能解决你的问题,请参考以下文章