https://developer.apple.com/library/ios/documentation/CoreMotion/Reference/CMStepCounter_class/CMStepCounter_class.pdf
因为个人英语能力有限,对翻译的程度心知肚明。英文文档也非常简单,希望大家看原文。
由于不是土豪,没有设备是支持CMStepCounter的,利用升级后的4S也只是能尝试了一下 [CMStepCounter isStepCountingAvailable];所以Demo得等到苹果跟白菜一样价的时候,我才买得起了。
Overview
CMStepCounter 类提供访问用户携带设备步行的步数接口,步行信息通过内置硬件去采集和存储,让第三方可以通过查询数据来获知用户最近的身体运动情况。第三方可以通过这个类收集当前或任何历史的步行数据。
Tasks
步数计数可用性
开始和停止更新步数计数
获取历史步数计数数据
Class Methods
isStepCountingAvailable
返回布尔值,步数计数在当前设备是否支持
+ (BOOL)isStepCountingAvailable
Return Value
YES 可用 or NO 不可用
Discussion
步数计数并不是所有的iOS设备都支持,使用该方法可以知道当前设备是否支持该技术
Availability
Declared In
CMStepCounter.h
Instance Methods
queryStepCountStartingFrom:to:toQueue:withHandler:
收集并返回某一时间段内的历史步数数据
- (void)queryStepCountStartingFrom:(NSDate *)start to:(NSDate *)end toQueue:(NSOperationQueue *)queuewithHandler:(CMStepQueryHandler)handler
Parameters
start
收集步数数据的开始时间,该参数不能为 nil.
end
收集步数数据的停止时间,该参数不能为nil.
queue
执行指定handler块的操作队列,第三方可以指定一个定制队列或者使用操作队列协助app的主线程。该参数不能为nil
handler
执行处理结果的块方法,该参数不能为nil。更多块方法信息参考CMStepQueryHandler。
Discussion
该方法为异步方法,会立即返回并且把结果分发到指定的handler块中处理。系统最多仅存储最近7天内的有效步数数据。如果在指定时间范围内没有数据,则会传递一个0值到handler块中。
Availability
Declared In
CMStepCounter.h
startStepCountingUpdatesToQueue:updateOn:withHandler:
开始分发当前步数计数数据到第三方应用
- (void)startStepCountingUpdatesToQueue:(NSOperationQueue *)queue updateOn:(NSInteger)stepCounts withHandler:(CMStepUpdateHandler)handler
Parameters
queue
被指定执行特定的handler块的操作队列。第三方可以指定一个定制队列或者使用操作队列协助app的主线程。该参数不能为nil
stepCounts
记录的步伐数据,达到该数值去执行handler块。该数值必须大于0
handler
该块在步伐计数达到或超出数值时会被执行,该参数不能为nil。更多块方法信息参考CMStepQueryHandler。
Discussion
该方法实现对用户步伐数据的追踪,并周期性地唤起块方法去分发结果。当第三方调用了该方法,步伐计数器会重置当前步伐数为0,并开始计数。每次计数到达指定的步伐数时,会执行指定的handler块方法。比如,当设定stepCounts为100时,会在100,200,300等数目时发送更新,激活该块方法。每次发送到该块方法的步伐数目都是从你调用该方法开始的步伐数目总和。
每次超过设定步数值时,指定的处理程序块handler会被执行。如果当超过设定值时第三方应用处在被挂起的状态,那程序块也不会被执行。当第三方应用被唤醒,程序块也不会执行,直到再次超过设定步数值。
可以调用stopStepCountingUpdates方法去停止分发步数计数,当然当步数计数对像被销毁的时候,分发过程也会被停止。
Availability
See Also
Declared In
CMStepCounter.h
stopStepCountingUpdates
停止分发步数计数数据到第三方应用。
- (void)stopStepCountingUpdates
Discussion
调用该方法停止更新步数计数分发,该方法不能停止查询历史数据queryStepCountStartingFrom:to:toQueue:withHandler: 方法.
Availability
See Also
Declared In
CMStepCounter.h
Constants
CMStepQueryHandler
返回查询操作的结果的程序块
typedef void (^CMStepQueryHandler)(NSInteger numberOfSteps, NSError *error);
Discussion
This block takes two parameters:
numberOfSteps
指定时间段内的步数
error
错误信息对象,如果正确为nil
Availability
Declared In
CMStepCounter.h
CMStepUpdateHandler
返回从计数开始到现在的步数,会被周期性调用
typedef void (^CMStepUpdateHandler)(NSInteger numberOfSteps, NSDate *timestamp, NSError *error);
Discussion
This block takes the following parameters:
numberOfSteps
步伐总数
timestamp
当前时间
error
错误信息对像,正确为nil
Availability
Declared In
CMStepCounter.h
在Maven中我们之前进行配置一个项目的依赖时,引用一下jar包,这些jar包第一次都会从一个地方进行下载,这个地方称为仓库,而对于仓库一般有本地仓库和中心仓库之分,但是我们一般在做项目时会在自己的服务器上配置一个私有仓库,那么我们下面就来创建一个私有仓库,这里我们使用的一个工具叫做Nexus。
1、首先到http://www.sonatype.org/nexus/下载对应的nexus安装包:nexus-latest-bundle.zip
2、解压缩得到的压缩包,然后将此压缩包中的bin目录(这里为:D:\docs\nexus-latest-bundle\nexus-2.5.1-01\bin)配置到path环境变量中:
3、接着打开cmd,输入nexus:
这边会让我们执行start、stop、install、uninstall等命令,我们首先执行install命令:
此时就会提示我们安装成功了,并且会在系统服务中生成nexus的服务:
此时我们再修改D:\docs\nexus-latest-bundle\nexus-2.5.1-01\bin\jsw\conf,下的wrapper.conf文件,如下:
1 # Set the JVM executable
2 # (modify this to absolute path if you need a Java that is not on the OS path)
3 wrapper.java.command=java
其中的wrapper.java.command=java修改为本机安装的JDK路径:
1 # Set the JVM executable
2 # (modify this to absolute path if you need a Java that is not on the OS path)
3 wrapper.java.command=C:\Program Files\Java\jdk1.7.0_17\bin\java
然后执行nexus start命令:
4、打开浏览器,输入:http://localhost:8081/nexus就能进行访问了:
5、点击右上角的login进行登陆:用户名:admin,密码:admin123,登陆完成以后就会有很多的命令可以供我们进行操作了
这里我们注意观察type属性中的内容:
- proxy仓库:从远程中央仓库中寻找数据的仓库,代理仓库。
- group仓库:组仓库,方便开发人员用来访问的仓库,这里面包含了其他的所有仓库,这样我们引用仓库时就非常方便了
6、仓库建立完成之后,我们就可以设置项目引用的仓库了,这里我们在user-core项目中引用我们的group仓库:
1 <repositories>
2 <repository>
3 <id>group</id>
4 <name>group repository</name>
5 <url>http://localhost:8081/nexus/content/groups/public/</url> //这里的url我们可以到nexus的控制面板中找到
6 <releases><enabled>true</enabled></releases>
7 <snapshots><enabled>true</enabled></snapshots>
8 </repository>
9 </repositories>
配置完成以后,我们user-aggregation项目中随便的加入一个jar包的依赖,这里加入的是commons-io:
1 <dependency>
2 <groupId>commons-io</groupId>
3 <artifactId>commons-io</artifactId>
4 <version>2.4</version>
5 </dependency>
这样我们可以看到默认的就会到我们自己的本地仓库进行下载了:
有的时候可能不会出现以上的这种情况,程序可能还会自动的从中央仓库进行下载,这时我们如果配置其不让从中央仓库下载的话,我们可以找到maven中的settings文件,然后找到其中的mirrors节点,为我们的中央仓库配置一下镜像:
1 <mirrors>
2 <!-- mirror
3 | Specifies a repository mirror site to use instead of a given repository. The repository that
4 | this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
5 | for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
6 |
7 <mirror>
8 <id>mirrorId</id>
9 <mirrorOf>repositoryId</mirrorOf>
10 <name>Human Readable Name for this Mirror.</name>
11 <url>http://my.repository.com/repo/path</url>
12 </mirror>
13 -->
14 <mirror>
15 <id>central</id>
16 <mirrorOf>central</mirrorOf>
17 <name>Human Readable Name for this Mirror.</name>
18 <url>http://localhost:8081/nexus/content/groups/public/</url> //为中央仓库设置镜像url,这样当访问中央仓库时不是访问中央仓库的地址,而是我们配置的镜像地址,也就是我们自己服务器的地址
19 </mirror>
20 </mirrors>
这样当程序试图访问中央仓库时会自动的转到我们自己的仓库了。
获取ssdt表中所有函数的地址
for (int i = 0; i < KeServiceDescriptorTable->NumberOfServices; i++)
{
KdPrint(("NumberOfService[%d]-------%X\n", i, KeServiceDescriptorTable->ServiceTableBase[i]));
}
需要这样定义
typedef struct _ServiceDescriptorTable {
unsigned int* ServiceTableBase; //System Service Dispatch Table 的基地址
unsigned int* ServiceCounterTable;
//包含着 SSDT 中每个服务被调用次数的计数器。这个计数器一般由sysenter 更新。
unsigned int NumberOfServices;//由 ServiceTableBase 描述的服务的数目。
unsigned char* ParamTableBase; //包含每个系统服务参数字节数表的基地址-系统服务参数表
}*PServiceDescriptorTable;
extern "C" extern PServiceDescriptorTable KeServiceDescriptorTable;
流程:
1、在ssdt_hook()函数中保存要hook函数的(NtOpenProcess)地址
2、将原来ssdt中所要hook的函数地址换成我们自己的函数地址
3、在UnHookSsdt中恢复ssdt中原来的函数地址
当驱动加载成功后,首先执行DriverEntry里面的ssdt_hook()函数,此时我们用CE打开我们保护的进程,
就会执行到我自己定义的函数MyNtOpenProcess()
最后卸载驱动,执行Driver_Unload()里面的UnHookSsdt()函数
作用:
防止被其他进程用OpenProcess打开,例如Cheat Engine
注意:
修改地址的时候,都应该先去掉页面保护,再恢复页面保护
使用了PsLookupProcessByProcessId后,必须用ObDereferenceObject回收!
PsLookupProcessByProcessId获取指定进程的EPROCESS结构,目的就是获取进程名
总结:
ssdt_hook的代码都是这样,
ssdt_hook()函数里面先保存要hook的地址,接着修改为自己定义函数的地址,
UnSsdt_hook()函数里面就是还原地址
而且自己定义函数里面要还原之前那个函数
VOID UnHookSsdt()
{
PageProtectOff();
KeServiceDescriptorTable->ServiceTableBase[122] = O_NtOpenProcess; //恢复ssdt中原来的函数地址
PageProtectOn();
}
NTSTATUS HookSsdt()
{
// 1、在ssdt_hook()函数中保存要hook函数的(NtOpenProcess)地址
O_NtOpenProcess = KeServiceDescriptorTable->ServiceTableBase[122];
PageProtectOff();
// 2、将原来ssdt中所要hook的函数地址换成我们自己的函数地址
KeServiceDescriptorTable->ServiceTableBase[122] = (unsigned int)MyNtOpenProcess;
//
此时我们用CE打开被保护的进程,就会调用我们自己的函数
PageProtectOn();
return STATUS_SUCCESS;
}
效果:
cpp代码:
1 #ifdef __cplusplus
2 extern "C"
3 {
4 #endif
5 #include <ntddk.h>
6 #ifdef __cplusplus
7 }
8 #endif
9
10 typedef struct _ServiceDescriptorTable {
11 unsigned int* ServiceTableBase; //System Service Dispatch Table 的基地址
12 unsigned int* ServiceCounterTable;
13 //包含着 SSDT 中每个服务被调用次数的计数器。这个计数器一般由sysenter 更新。
14 unsigned int NumberOfServices;//由 ServiceTableBase 描述的服务的数目。
15 unsigned char* ParamTableBase; //包含每个系统服务参数字节数表的基地址-系统服务参数表
16 }*PServiceDescriptorTable;
17
18 extern "C" extern PServiceDescriptorTable KeServiceDescriptorTable;
19
20 typedef NTSTATUS(*MYNTOPENPROCESS)(
21 OUT PHANDLE ProcessHandle,
22 IN ACCESS_MASK AccessMask,
23 IN POBJECT_ATTRIBUTES ObjectAttributes,
24 IN PCLIENT_ID ClientId );//定义一个函数指针,用于下面对O_NtOpenProcess进行强制转换
25
26 ULONG O_NtOpenProcess = 0;
27
28 extern "C" NTSTATUS
29 PsLookupProcessByProcessId(
30 IN HANDLE ProcessId,
31 OUT PEPROCESS *Process
32 );
33
34 void PageProtectOff()//关闭页面保护
35 {
36 __asm{
37 cli
38 mov eax,cr0
39 and eax,not 10000h
40 mov cr0,eax
41 }
42 }
43 void PageProtectOn()//打开页面保护
44 {
45 __asm{
46 mov eax,cr0
47 or eax,10000h
48 mov cr0,eax
49 sti
50 }
51 }
52
53 // 判断是否打开的是自己想保护的进程
54 BOOLEAN ProtectProcess(HANDLE ProcessId,char *str_ProtectObjName)
55 {
56 NTSTATUS status;
57 PEPROCESS process_obj;
58
59 if(!MmIsAddressValid(str_ProtectObjName))//这个条件是用来判断目标进程名是否有效
60 {
61 return FALSE;
62 }
63 if(ProcessId == 0)//这个条件是用来排除System Idle Process进程的干扰
64 {
65 return FALSE;
66 }
67 status=PsLookupProcessByProcessId(ProcessId, &process_obj);//这句用来获取目标进程的EPROCESS结构
68 if(!NT_SUCCESS(status))
69 {
70 KdPrint(("我错了,这个是错误号:%X---这个是进程ID:%d\n",status,ProcessId));
71 return FALSE;
72 }
73 if(!strcmp((char *)process_obj + 0x174, str_ProtectObjName))//进行比较
74 {
75 ObDereferenceObject(process_obj);//对象计数器减1,为了恢复对象管理器计数,便于回收
76 return TRUE;
77 }
78 ObDereferenceObject(process_obj);
79 return FALSE;
80 //使用了PsLookupProcessByProcessId后,必须用ObDereferenceObject回收!
81 //PsLookupProcessByProcessId获取指定进程的EPROCESS结构,目的就是获取进程名
82 }
83 NTSTATUS MyNtOpenProcess (
84 __out PHANDLE ProcessHandle,
85 __in ACCESS_MASK DesiredAccess,
86 __in POBJECT_ATTRIBUTES ObjectAttributes,
87 __in_opt PCLIENT_ID ClientId
88 )
89 {
90 if(ProtectProcess(ClientId->UniqueProcess,"notepad.exe")) // 如果打开的进程是目标进程
91 {
92 KdPrint(("%s打开文件失败\n",(char *)PsGetCurrentProcess()+0x174));
93 return STATUS_UNSUCCESSFUL; // 这是一个关键,设置STATUS_UNSUCCESSFUL,CE就会提示打开不成功
94 }
95
96 return ((MYNTOPENPROCESS)O_NtOpenProcess)(ProcessHandle,//处理完自己的任务后,调用原来的函数,让其它进程正常工作
97 DesiredAccess,
98 ObjectAttributes,
99 ClientId);
100 }
101
102
103 VOID UnHookSsdt()
104 {
105 PageProtectOff();
106 KeServiceDescriptorTable->ServiceTableBase[122] = O_NtOpenProcess; //恢复ssdt中原来的函数地址
107 PageProtectOn();
108 }
109 NTSTATUS HookSsdt()
110 {
111 // 1、在ssdt_hook()函数中保存要hook函数的(NtOpenProcess)地址
112 O_NtOpenProcess = KeServiceDescriptorTable->ServiceTableBase[122];
113 PageProtectOff();
114 // 2、将原来ssdt中所要hook的函数地址换成我们自己的函数地址
115 KeServiceDescriptorTable->ServiceTableBase[122] = (unsigned int)MyNtOpenProcess;
116 // 此时我们用CE打开被保护的进程,就会调用我们自己的函数
117 PageProtectOn();
118
119 return STATUS_SUCCESS;
120 }
121 VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
122 {
123 UnHookSsdt();
124 KdPrint(("Driver Unload Success !\n"));
125 }
126
127 extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath )
128 {
129 KdPrint(("Welcome to My Driver\n"));
130 pDriverObject->DriverUnload = DriverUnload;
131 /* 所有函数的地址
132 for (int i = 0; i < KeServiceDescriptorTable->NumberOfServices; i++)
133 {
134 KdPrint(("NumberOfService[%d]-------%X\n", i, KeServiceDescriptorTable->ServiceTableBase[i]));
135 }*/
136 HookSsdt();
137 return STATUS_SUCCESS;
138 }
去年参加校招要到长沙来,这个对于我来说不是特别喜欢(但又必须的来,谁叫咱不是985、211的娃呢),但是对于某些人来说就是福音了。大四还有课,而且学校抓的比较严,所以对于那些想翘课的人来说这个是最好不过的理由了—去参加校招了。所以咱学校规定所以去参加校招的必须要请假,且必须要有相关人员的签字,三天一下,辅导员签字、三到七天系主任签字,一个星期以上院长签字,更多?校长(不知道能不能找到校长呢?反正我是没见校长几面),出了这样的政策确实上课情况好多了!对于这中将请求一级一级地往上传递直到处理请求为止的设计模式就是职责链模式。
上图将学生、辅导员、系主任、院长、校长组成了一个简单的链。在这个链上,学生是申请者,其余的都是请求处理者。职责链可以将请求的处理者组织成一条链,并且将请求沿着链传递,如果某个请求处理者能够处理请求则处理,否则将该请求交由上级处理。
职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了,这就是职责链的设计动机。
一、模式定义
避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止,这就是职责链模式。
在职责链模式中最关键的一点就是客户提交请求后,请求沿着链往下传递直到有一个处理者处理它,在这里客户无需关心它的请求是哪个处理者来处理,反正总有一个处理者会处理它的请求。
在这里客户端和处理者都没有对方明确的信息,同时处理者也不知道职责链中的结构。所以职责链可以简化对象的相互连接,他们只需要保存一个指向其后续者的引用,而不需要保存所有候选者的引用。
在职责链模式中我们可以随时随地的增加或者更改一个处理者,甚至可以更改处理者的顺序,增加了系统的灵活性。处理灵活性是增加了,但是有时候可能会导致一个请求无论如何也得不到处理,它会被放置在链末端,这个既是职责链的优点也是缺点。
二、模式结构
下图是职责链模式的UML结构图:
从上面可以看出职责链包含三个角色:
Handler: 抽象处理者。定义了一个处理请求的方法。所有的处理者都必须实现该抽象类。
ConcreteHandler: 具体处理者。处理它所负责的请求,同时也可以访问它的后继者。如果它能够处理该请求则处理,否则将请求传递到它的后继者。
Client: 客户类。
下面是最典型的具体处理者类。
public class ConcreteHandler extends Handler
{
public void handleRequest(String request)
{
if(请求request满足条件)
{
...... //处理请求;
}
else
{
this.successor.handleRequest(request); //转发请求
}
}
}
三、模式的实现
我们将使用开头那个请假的实例。请假:3天以下辅导员签字、3到5天系主任签字、6到10天院长签字、11-15天校长签字、15天以上不允签字。
首先是请假条:LeaveNode.java
public class LeaveNode {
/** 请假天数 **/
private int number;
/** 请假人 **/
private String person;
public LeaveNode(String person,int number){
this.person = person;
this.number = number;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public String getPerson() {
return person;
}
public void setPerson(String person) {
this.person = person;
}
}
抽象处理者:Leader.java
public abstract class Leader {
/** 姓名 **/
public String name;
/** 后继者 **/
protected Leader successor;
public Leader(String name){
this.name = name;
}
public void setSuccessor(Leader successor) {
this.successor = successor;
}
public abstract void handleRequest(LeaveNode LeaveNode);
}
四个具体处理者:辅导员:Instructor.java
public class Instructor extends Leader{
public Instructor(String name){
super(name);
}
public void handleRequest(LeaveNode LeaveNode) {
if(LeaveNode.getNumber() <= 3){ //小于3天辅导员审批
System.out.println("辅导员" + name + "审批" +LeaveNode.getPerson() + "同学的请假条,请假天数为" + LeaveNode.getNumber() + "天。");
}
else{ //否则传递给系主任
if(this.successor != null){
this.successor.handleRequest(LeaveNode);
}
}
}
}
系主任: DepartmentHead.java
public class DepartmentHead extends Leader{
public DepartmentHead(String name) {
super(name);
}
public void handleRequest(LeaveNode LeaveNode) {
if(LeaveNode.getNumber() <= 7){ //小于7天系主任审批
System.out.println("系主任" + name + "审批" +LeaveNode.getPerson() + "同学的请假条,请假天数为" + LeaveNode.getNumber() + "天。");
}
else{ //否则传递给院长
if(this.successor != null){
this.successor.handleRequest(LeaveNode);
}
}
}
}
院长:Dean.java
public class Dean extends Leader{
public Dean(String name) {
super(name);
}
public void handleRequest(LeaveNode LeaveNode) {
if(LeaveNode.getNumber() <= 10){ //小于10天院长审批
System.out.println("院长" + name + "审批" +LeaveNode.getPerson() + "同学的请假条,请假天数为" + LeaveNode.getNumber() + "天。");
}
else{ //否则传递给校长
if(this.successor != null){
this.successor.handleRequest(LeaveNode);
}
}
}
}
校长:President.java
public class President extends Leader{
public President(String name) {
super(name);
}
public void handleRequest(LeaveNode LeaveNode) {
if(LeaveNode.getNumber() <= 15){ //小于15天校长长审批
System.out.println("校长" + name + "审批" +LeaveNode.getPerson() + "同学的请假条,请假天数为" + LeaveNode.getNumber() + "天。");
}
else{ //否则不允批准
System.out.println("请假天天超过15天,不批准...");
}
}
}
客户端:Client.java
public class Client {
public static void main(String[] args) {
Leader instructor = new Instructor("陈毅"); //辅导员
Leader departmentHead = new DepartmentHead("王明"); //系主任
Leader dean = new Dean("张强"); //院长
Leader president = new President("王晗"); //校长
instructor.setSuccessor(departmentHead); //辅导员的后续者是系主任
departmentHead.setSuccessor(dean); //系主任的后续者是院长
dean.setSuccessor(president); //院长的后续者是校长
//请假3天的请假条
LeaveNode leaveNode1 = new LeaveNode("张三", 3);
instructor.handleRequest(leaveNode1);
//请假9天的请假条
LeaveNode leaveNode2 = new LeaveNode("李四", 9);
instructor.handleRequest(leaveNode2);
//请假15天的请假条
LeaveNode leaveNode3 = new LeaveNode("王五", 15);
instructor.handleRequest(leaveNode3);
//请假20天的请假条
LeaveNode leaveNode4 = new LeaveNode("赵六", 20);
instructor.handleRequest(leaveNode4);
}
}
运行结果:
四、模式的优缺点
优点
1、降低耦合度。它将请求的发送者和接受者解耦。
2、简化了对象。使得对象不需要知道链的结构。
3、增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。
4、增加新的请求处理类很方便。
缺点
1、不能保证请求一定被接收。
2、系统性能将受到一定影响,而且在进行代码调试时不太方便;可能会造成循环调用。
3、可能不容易观察运行时的特征,有碍于除错。
五、模式适用场景
1、有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
2、在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
3、可动态指定一组对象处理请求。
六、总结
1、职责链模式将请求的发送者和接受者解耦了。客户端不需要知道请求处理者的明确信息,甚至不需要知道链的结构,它只需要将请求进行发送即可。
2、职责链模式能够非常方便的动态增加新职责或者删除职责。
3、客户端发送的请求可能会得不到处理。
4、处理者不需要知道链的结构,只需要明白他的后续者是谁就可以了。这样就简化了系统中的对象。