热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

面向切面编程之aspects源码解析及应用

1.背景(这篇文章在微信读书Weread团队博客中也有发表)最近在做项目的打点统计的时候,发现业务逻辑和打点逻辑经常耦合在一起,这样一方面影响了正常的业务逻辑,同时也很容易搞乱打点

1. 背景

(这篇文章在微信读书Weread团队博客中也有发表)

最近在做项目的打点统计的时候,发现业务逻辑和打点逻辑经常耦合在一起,这样一方面影响了正常的业务逻辑,同时也很容易搞乱打点逻辑,而且要查看打点情况的时候也很分散,因此想着如何将两者解耦,并将打点逻辑集中起来。其实在 web 编程时候,这种场景很早就有了很成熟的方案,也就是所谓的 aop 编程(面向切面编程),其原理也就是在不更改正常的业务处理流程的前提下,通过生成一个动态代理类,从而实现对目标对象嵌入附加的操作。在 ios 中,要想实现相似的效果也很简单,利用 oc 的动态性,通过 swizzling method 改变目标函数的 selector 所指向的实现,然后在新的实现中实现附加的操作,完成之后再回到原来的处理逻辑。想明白这些之后,我就打算动手实现,当然并没有重复造轮子,我在 github 发现了一个基于 swizzling method 的开源框架 Aspects 。这个库的代码量比较小,总共就一个类文件,使用起来也比较方便,比如你想统计某个 controller 的 viewwillappear 的调用次数,你只需要引入 Aspect.h 头文件,然后在合适的地方初始化如下代码即可。

- (void)addKvLogAspect {
//想法tab打开
[self wr_Aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^{
KVLog_ReviewTimeline(ReviewTimeline_Open_Tab);
}error:NULL];
}

这篇文章主要是介绍 aspect 源码以及其思路,以及我在实际应用中遇到的一些问题。对 swizzling method 不了解的同学可以先去网上了解一下,下面的内容是基于大家对 swizzling method 有一定的了解的基础上的。

2. 基本原理

我们知道 oc 是动态语言,我们执行一个函数的时候,其实是在发一条消息:[ receiver message ],这个过程就是根据 message 生成 selector,然后根据 selector 寻找指向函数具体实现的指针 IMP,然后找到真正的函数执行逻辑。这种处理流程给我们提供了动态性的可能,试想一下,如果在运行时,动态的改变了 selector 和 IMP 的对应关系,那么就能使得原来的 [ receiver message ] 进入到新的函数实现了。

那么具体怎么实现这样的动态替换了?

直观的一种方案是提供一个统一入口,如 commonImp,将所有需要 hook 的函数都指向这个函数,然后在这里,提取相关信息进行转发,JSPatch 实现原理详解对此方案的可行性有进行分析,对于64位机器可能会有点问题。另外一个方法就是利用 oc 自己的消息转发机制进行转发,aspect 的大体思路,基本上是顺着这个来的。为了更好的解释这个过程,我们先来看一下消息具体是怎么找到对应的 imp 的,见下图(此图并非原创)。

《面向切面编程之 aspects 源码解析及应用》

从上面我们可以发现,在发消息的时候,如果 selector 有对应的 IMP,则直接执行,如果没有,oc 给我们提供了几个可供补救的机会,依次有 resolveInstanceMethod、forwardingTargetForSelector、forwardInvocation。Aspects 之所以选择在 forwardInvocation 这里处理是因为,这几个阶段特性都不太一样:resolvedInstanceMethod 适合给类/对象动态添加一个相应的实现,forwardingTargetForSelector 适合将消息转发给其他对象处理,相对而言,forwardInvocation 是里面最灵活,最能符合需求的。因此 aspects 的方案就是,对于待 hook 的 selector,将其指向 objc_msgForward / _objc_msgForward_stret,同时生成一个新的 aliasSelector 指向原来的 IMP,并且 hook 住 forwardInvocation 函数,使他指向自己的实现。按照上面的思路,当被 hook 的 selector 被执行的时候,首先根据 selector 找到了 objc_msgForward / _objc_msgForward_stret,而这个会触发消息转发,从而进入 forwardInvocation。同时由于 forwardInvocation 的指向也被修改了,因此会转入新的 forwardInvocation 函数,在里面执行需要嵌入的附加代码,完成之后,再转回原来的 IMP。

3. 源码分析

3.1 数据结构

介绍完大致思路之后,下面将从代码层来来具体分析。从头文件中可以看到使用aspects有两种使用方式

  1. 类方法

  2. 实例方法

+ (id)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;

两者的主要原理基本差不多,这里不做一一介绍,只是以实例方法为例进行说明。在介绍之前,先介绍里面几个重要的数据结构:

AspectOptions

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
AspectPositiOnAfter= 0, /// Called after the original implementation (default)
AspectPositiOnInstead= 1, /// Will replace the original implementation.
AspectPositiOnBefore= 2, /// Called before the original implementation.
AspectOptiOnAutomaticRemoval= 1 <<3 /// Will remove the hook after the first execution.
};

这里表示了 block 执行的时机,也就是额外操作的执行时机,在我的应用场景中就是打点逻辑的执行时机,它可以在原始函数执行之前,也可以是执行之后,甚至可以完全替换掉原来的逻辑。

AspectsContainer

一个对象或者类的所有的 Aspects 整体情况

// Tracks all aspects for an object/class.
@interface AspectsContainer : NSObject
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition;
- (BOOL)removeAspect:(id)aspect;
- (BOOL)hasAspects;
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end

AspectIdentifier

一个 Aspect 的具体内容

@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id)info;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end

这里主要包含了单个的 aspect 的具体信息,包括执行时机,要执行 block 所需要用到的具体信息:包括方法签名、参数等等

AspectInfo

一个 Aspect 执行环境,主要是 NSInvocation 信息。

@interface AspectInfo : NSObject
- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation;
@property (nonatomic, unsafe_unretained, readonly) id instance;
@property (nonatomic, strong, readonly) NSArray *arguments;
@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;
@end

3.2 代码流程

有了上面的了解,我们就能更好的分析整个 apsects 的执行流程。添加一个 aspect 的关键流程如下图所示:

《面向切面编程之 aspects 源码解析及应用》

从代码来看,要想使用 aspects ,首先要添加一个 aspect ,可以通过上面介绍的类/实例方法。关键代码实现如下:

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
...
__block AspectIdentifier *identifier = nil;
aspect_performLocked(^{
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {//1判断能否hook
...//2 记录数据结构
aspect_prepareClassAndHookSelector(self, selector, error);//3 swizzling
}
});
return identifier;
}

这个过程基本和上面的流程图一致:这里重点介绍几个关键部分

1)判断能否被 hook:对于对象实例而言,这里主要是根据黑名单,比如 retain forwardInvocation 等这些方法在外部是不能被 hook(对于类对象还要确保同一个类继承关系层级中,只能被 hook 一次,因此这里需要判断子类,父类有没有被 hook,之所以做这样的实现,主要是为了避免出现死循环的出现,这里有相关的讨论)。如果能够 hook,则继续下面的步骤。

2)swizzling method:这是真正的核心逻辑

3.2.1 swizzling method

swizzling method 主要有两部分,一个是对对象的 forwardInvocation 进行 swizzling,另一个是对传入的 selector 进行 swizzling.

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
Class klass = aspect_hookClass(self, error); //1 swizzling forwardInvocation
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {//2 swizzling method
...//
}
}

3.2.1.1 swizzling forwardInvocation

aspect_hookClass 函数主要 swizzling 类/对象的 forwardInvocation 函数,aspects 的真正的处理逻辑都是在 forwradInvocation 函数里面进行的。对于对象实例而言,源代码中并没有直接 swizzling 对象的 forwardInvocation 方法,而是动态生成一个当前对象的子类,并将当前对象与子类关联,然后替换子类的 forwardInvocation 方法(这里具体方法就是调用了 object_setClass(self, subclass) ,将当前对象 isa 指针指向了 subclass ,同时修改了 subclass 以及其 subclass metaclass 的 class 方法,使他返回当前对象的 class。,这个地方特别绕,它的原理有点类似 kvo 的实现,它想要实现的效果就是,将当前对象变成一个 subclass 的实例,同时对于外部使用者而言,又能把它继续当成原对象在使用,而且所有的 swizzling 操作都发生在子类,这样做的好处是你不需要去更改对象本身的类,也就是,当你在 remove aspects 的时候,如果发现当前对象的 aspect 都被移除了,那么,你可以将 isa 指针重新指回对象本身的类,从而消除了该对象的 swizzling ,同时也不会影响到其他该类的不同对象)。对于每一个对象而言,这样的动态对象只会生成一次,这里 aspect_swizzlingForwardInvocation 将使得 forwardInvocation 方法指向 aspects 自己的实现逻辑 ,具体代码如下:

static Class aspect_hookClass(NSObject *self, NSError **error) {
...
//生成动态子类,并swizzling forwardInvocation方法
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
aspect_swizzleForwardInvocation(subclass);//swizzling forwardinvation方法
objc_registerClassPair(subclass);
...
object_setClass(self, subclass);//将当前self设置为子类,这里其实只是更改了self的isa指针而已
return subclass;
}
...
static void aspect_swizzleForwardInvocation(Class klass) {
...
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@")
}
...
}

由于子类本身并没有实现 forwardInvocation ,隐藏返回的 originalImplementation 将为空值,所以也不会生成 NSSelectorFromString(AspectsForwardInvocationSelectorName) 。

3.2.1.2 swizzling selector

当 forwradInvocation 被 hook 之后,接下来,将对传入的 selector 进行 hook ,这里的做法是,将 selector 指向了转发 IMP ,同时生成一个 aliasSelector ,指向了原来的 IMP ,同时为了放在重复 hook ,做了一个判断,如果发现 selector 已经指向了转发 IMP ,那就就不需要进行交换了,代码如下

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
...
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
...
SEL aliasSelector = aspect_aliasForSelector(selector);//generator aliasSelector
if (![klass instancesRespondToSelector:aliasSelector]) {
__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
}
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);// point to _objc_msgForward
...
}
}

3.2.2 handle ForwardInvocation

基于上面的代码分析知道,转发最终的逻辑代码最终转入 ASPECTS_ARE_BEING_CALLED 函数的处理中。这里,需要处理的部分包括额外处理代码(如打点代码)以及最终重新转会原来的 selector 所指向的函数,其实现代码如下:

static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
...
// Before hooks. 原来逻辑之前执行
aspect_invoke(classContainer.beforeAspects, info);
aspect_invoke(objectContainer.beforeAspects, info);
// Instead hooks.
BOOL respOndsToAlias= YES;
if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {//是否需要替换掉原来的路基
aspect_invoke(classContainer.insteadAspects, info);
aspect_invoke(objectContainer.insteadAspects, info);
} else {
Class klass = object_getClass(invocation.target);
do {
if ((respOndsToAlias= [klass instancesRespondToSelector:aliasSelector])) {
[invocation invoke];//根据aliasSelector找到原来的逻辑并执行
break;
}
}while (!respondsToAlias && (klass = class_getSuperclass(klass)));
}
// After hooks. 原来逻辑之后执行
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);
// If no hooks are installed, call original implementation (usually to throw an exception)
if (!respondsToAlias) {//找不到aliasSelector的IMP实现,没有找到原来的逻辑,进行消息转发
invocation.selector = originalSelector;
SEL originalForwardInvocatiOnSEL= NSSelectorFromString(AspectsForwardInvocationSelectorName);
if ([self respondsToSelector:originalForwardInvocationSEL]) {
((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
} else {
[self doesNotRecognizeSelector:invocation.selector];
}
}
...
}

依次处理 before/instead/after hook 以及真正函数实现。如果没有找到原始的函数实现,还需要进行转发操作。

4. 遇到的问题

以上就是 apsects 的实现了,接下来会介绍在实际应用过程中遇到的一些问题以及我的解决方案。

4.1 jspatch不兼容

原因

我们的项目中引入了 jspatch 作为我们的 hot fix方案。 jspatch 也会 hook 住对象的 forwradInvocation 方法,并且 swizzling 相应的 method,使其指向转发 IMP,由于 aspects 也是基于这两者实现的,那么会不会导致问题呢(其实类似的问题也会发生在对象提前被 kvo 了,会不会有影响)?

回过头去看3.2.1 我们先是 hook了 类的 forwardInvocation 使其指向了 __ASPECTS_ARE_BEING_CALLED__,然后在 swizzling method 那里, aspect 有做一个判断,如果传入的 selector 指向了转发 IMP,那么我们什么也不做。因此可想而知,如果传入的 selector 先被 jspatch hook,那么,这里我们将不会再处理,也就不会生成 aliasSelector 。

这会导致什么问题了?设想一下,当 selector 被触发的时候,由于 selector 指向了转发 IMP,因此会进入消息转发过程,同时由于 forwardInvocation 被 aspects 所 hook,最终会进入到 aspects 的处理逻辑 ASPECTS_ARE_BEING_CALLED 中来。让我们回过头去看看3.2.2中的分析,由于找不到 aliasSelector 的 IMP 实现,因此会在此进行消息转发。而在3.2.1.1的分析中我们知道,子类并没有实现 NSSelectorFromString(AspectsForwardInvocationSelectorName),所以这里的流程就会进入 doesNotRecognizeSelector,从而抛出异常。

解决方案

出现上诉问题的原因在于,当 aliasSelector 没有被找到的时候,我们没能将消息正常的转发,也就是没有实现一个 NSSelectorFromString(AspectsForwardInvocationSelectorName),使得消息有机会重新转发回去的方法。因此解决方案也就呼之欲出了,我的做法是在对子类的 forwardInvocation 方法进行交换而不仅仅是替换,实现逻辑如下,强制生成一个 NSSelectorFromString(AspectsForwardInvocationSelectorName) 指向原对象的 forwardInvocation 的实现。

static Class aspect_hookClass(NSObject *self, NSError **error) {
...
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
...
IMP originalImplementation = class_replaceMethod(subclass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
class_addMethod(subclass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
} else {
Method baseTargetMethod = class_getInstanceMethod(baseClass, @selector(forwardInvocation:));
IMP baseTargetMethodIMP = method_getImplementation(baseTargetMethod);
if (baseTargetMethodIMP) {
class_addMethod(subclass, NSSelectorFromString(AspectsForwardInvocationSelectorName), baseTargetMethodIMP, "v@:@");
}
}
...
}

注意如果 originalImplementation 为空,那么生成的 NSSelectorFromString(AspectsForwardInvocationSelectorName) 将指向 baseClass 也就是真正的这个对象的 forwradInvocation,这个其实也就是 jspatch hook 的方法。同时为了保证 block 的执行顺序(也就是前面介绍的 before hooks / instead hooks / after hooks ),这里需要将这段代码提前到 after hooks 执行之前进行。这样就解决了 forwardInvocation 在外面已经被 hook 之后的冲突问题。

4.2 remove操作

4.2.1 单个aspect remove

单个 aspect 的 remove 貌似有个问题,先来看看源码。

if (aspect_isMsgForwardIMP(targetMethodIMP)) {
SEL aliasSelector = aspect_aliasForSelector(selector);
Method originalMethod = class_getInstanceMethod(klass, aliasSelector);
IMP originalIMP = method_getImplementation(originalMethod);
if (originalIMP) {
class_replaceMethod(klass, selector, originalIMP, typeEncoding);
}
}

当你对某个 aspect 执行 remove 操作的时候,它会直接 replace 这个 selector 的 IMP,这个操作是对整个类的所有实例都生效的,这会导致什么问题呢?

以类 A 为例,你先进入了 A 的一个实例 A1,hook 住了方法 selector1,然后,并没有销毁这个实例的时候,通过其他路径又进入类 A 的另一个实例 A2,当然也 hook 了 selector1,然后这个时候,如果你 A2 中执行了这个 aspect 的 remove 操作,按照上面的逻辑,类 A 的 selector1 将会恢复正常,可像而知,当你退回 A1 的时候, A1 的 aspect 将会失效。这里其实我的解决思路很简单,因为在执行 remove 操作的时候,其实和这个对象相关的数据结构都已经被清除了,即使不去恢复 selector1 的执行,在进入 ASPECTS_ARE_BEING_CALLED 由于这个没有响应的 aspects,其实会直接跳到原来的处理逻辑,并不会有其他附加影响。

4.2.2 整个对象aspects remove

还有一个问题就是, aspects 的 remove 操作只能支持单个的 remove 操作,不支持一次性删除一个对象的所有 aspects 。这里,也做了一个扩展,对原来的 aspects 进行扩展,实现了一次性 remove 一个对象所有 aspects 的方法。


推荐阅读
  • 本文介绍了ASP.NET Core MVC的入门及基础使用教程,根据微软的文档学习,建议阅读英文文档以便更好理解,微软的工具化使用方便且开发速度快。通过vs2017新建项目,可以创建一个基础的ASP.NET网站,也可以实现动态网站开发。ASP.NET MVC框架及其工具简化了开发过程,包括建立业务的数据模型和控制器等步骤。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • Go语言实现堆排序的详细教程
    本文主要介绍了Go语言实现堆排序的详细教程,包括大根堆的定义和完全二叉树的概念。通过图解和算法描述,详细介绍了堆排序的实现过程。堆排序是一种效率很高的排序算法,时间复杂度为O(nlgn)。阅读本文大约需要15分钟。 ... [详细]
  • 在springmvc框架中,前台ajax调用方法,对图片批量下载,如何弹出提示保存位置选框?Controller方法 ... [详细]
  • 本文介绍了前端人员必须知道的三个问题,即前端都做哪些事、前端都需要哪些技术,以及前端的发展阶段。初级阶段包括HTML、CSS、JavaScript和jQuery的基础知识。进阶阶段涵盖了面向对象编程、响应式设计、Ajax、HTML5等新兴技术。高级阶段包括架构基础、模块化开发、预编译和前沿规范等内容。此外,还介绍了一些后端服务,如Node.js。 ... [详细]
  • ScrollView嵌套Collectionview无痕衔接四向滚动,支持自定义TitleView
    本文介绍了如何实现ScrollView嵌套Collectionview无痕衔接四向滚动,并支持自定义TitleView。通过使用MainScrollView作为最底层,headView作为上部分,TitleView作为中间部分,Collectionview作为下面部分,实现了滚动效果。同时还介绍了使用runtime拦截_notifyDidScroll方法来实现滚动代理的方法。具体实现代码可以在github地址中找到。 ... [详细]
  • 本文介绍了在MFC下利用C++和MFC的特性动态创建窗口的方法,包括继承现有的MFC类并加以改造、插入工具栏和状态栏对象的声明等。同时还提到了窗口销毁的处理方法。本文详细介绍了实现方法并给出了相关注意事项。 ... [详细]
  • MVC设计模式的介绍和演化过程
    本文介绍了MVC设计模式的基本概念和原理,以及在实际项目中的演化过程。通过分离视图、模型和控制器,实现了代码的解耦和重用,提高了项目的可维护性和可扩展性。详细讲解了分离视图、分离模型和分离控制器的具体步骤和规则,以及它们在项目中的应用。同时,还介绍了基础模型的封装和控制器的命名规则。该文章适合对MVC设计模式感兴趣的读者阅读和学习。 ... [详细]
  • Asp.net Mvc Framework 七 (Filter及其执行顺序) 的应用示例
    本文介绍了在Asp.net Mvc中应用Filter功能进行登录判断、用户权限控制、输出缓存、防盗链、防蜘蛛、本地化设置等操作的示例,并解释了Filter的执行顺序。通过示例代码,详细说明了如何使用Filter来实现这些功能。 ... [详细]
  • Android源码中的Builder模式及其作用
    本文主要解释了什么是Builder模式以及其作用,并结合Android源码来分析Builder模式的实现。Builder模式是将产品的设计、表示和构建进行分离,通过引入建造者角色,简化了构建复杂产品的流程,并且使得产品的构建可以灵活适应变化。使用Builder模式可以解决开发者需要关注产品表示和构建步骤的问题,并且当构建流程发生变化时,无需修改代码即可适配新的构建流程。 ... [详细]
  • 本文介绍了一些Java开发项目管理工具及其配置教程,包括团队协同工具worktil,版本管理工具GitLab,自动化构建工具Jenkins,项目管理工具Maven和Maven私服Nexus,以及Mybatis的安装和代码自动生成工具。提供了相关链接供读者参考。 ... [详细]
  • iOS Swift中如何实现自动登录?
    本文介绍了在iOS Swift中如何实现自动登录的方法,包括使用故事板、SWRevealViewController等技术,以及解决用户注销后重新登录自动跳转到主页的问题。 ... [详细]
  • 本文介绍了在iOS开发中使用UITextField实现字符限制的方法,包括利用代理方法和使用BNTextField-Limit库的实现策略。通过这些方法,开发者可以方便地限制UITextField的字符个数和输入规则。 ... [详细]
  • IOS开发之短信发送与拨打电话的方法详解
    本文详细介绍了在IOS开发中实现短信发送和拨打电话的两种方式,一种是使用系统底层发送,虽然无法自定义短信内容和返回原应用,但是简单方便;另一种是使用第三方框架发送,需要导入MessageUI头文件,并遵守MFMessageComposeViewControllerDelegate协议,可以实现自定义短信内容和返回原应用的功能。 ... [详细]
author-avatar
执笔W写下我们的故事
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有