无 Sdk 实现支付宝分享

·

3 min read

这两天试着为 MonkeyKing 添加了分享到支付宝好友的功能,自己也小小地体验了一次“逆向工程”,在此小记,以为备忘。

MonkeyKing 的目的是帮助 iOS 开发者在不集成 SDK 的情况下实现社交分享功能。要为它添加 分享到支付宝好友 的功能就需要知道支付宝官方 SDK 做了些什么,并自己使用代码来进行相应的操作。很遗憾支付宝的 SDK 并不是开源的, 所以我们就需要一些特殊手段来一探究竟。

方法

OpenShare 的作者写过一篇详细的 文章 对如何监控我们自己的 App 与官方客户端之间的通信做了介绍,我的操作主要便是依据这篇文章进行的,在此仅对其做个补充。

过程

获取数据

首先,我们要监控官方 Demo 与官方客户端直接互相传递了哪些数据。 在我们的 APP 中,和官方的客户端进行通信主要的方式有两种:

  1. 通过 UIApplication.sharedApplication 的 openURL 方法

  2. 通过 UIPasteboard 进行数据传递

我们可以通过 Method Swizzling为 openURL 方法及 pasteboard 相关方法加上一些自己的处理,打印出 openURL 所打开的 URL 地址以及官方 Demo 及客户端在 pasteboard 中传递的数据(详见这里) 。

数据处理

URL

在添加 Method Swizzling 之后, 我们点击官方 Demo 中的 发送文本信息到支付宝,可以看到 openURL 方法打开的 URL 是这个样子的:

----------open url: 0----------
alipayshare://platformapi/shareService?action=sendReq&shareId=2016012101112529

最后的那一串数字是我们的 appID, 若我们直接调用 openURL 打开这个链接,是可以从我们的 APP 跳转到支付宝的,但除此之外,什么都没有发生,这是因为我们还没有给支付宝客户端提供数据进行处理。

发送的数据

我们应该提供给支付宝客户端的数据也可以通过 Swizzling 之后的方法打印出来,大概是长这个样子的:

----------swizzlePasteboardSetData: 1----------
PasteboardName: com.apple.UIKit.pboard.general
type: com.alipay.openapi.pb.req.2016012101112529
 dict{
    "$archiver" = NSKeyedArchiver;
    "$objects" =     (
        "$null",
                {
            "$class" = "<CFKeyedArchiverUID 0x7f961ae1fd00 [0x1043bc7b0]>{value = 20}";
            "NS.keys" =             (
                "<CFKeyedArchiverUID 0x7f961ae25c20 [0x1043bc7b0]>{value = 2}",
                "<CFKeyedArchiverUID 0x7f961ae27b00 [0x1043bc7b0]>{value = 3}"
            );
            "NS.objects" =             (
                "<CFKeyedArchiverUID 0x7f961ae10d90 [0x1043bc7b0]>{value = 4}",
                "<CFKeyedArchiverUID 0x7f961ae1b010 [0x1043bc7b0]>{value = 11}"
            );
        },
        app,
        req,
                {
            "$class" = "<CFKeyedArchiverUID 0x7f961ac2e2f0 [0x1043bc7b0]>{value = 10}";
            appKey = "<CFKeyedArchiverUID 0x7f961ac78b90 [0x1043bc7b0]>{value = 6}";
            bundleId = "<CFKeyedArchiverUID 0x7f961ac38200 [0x1043bc7b0]>{value = 7}";
            name = "<CFKeyedArchiverUID 0x7f961ac7ca10 [0x1043bc7b0]>{value = 5}";
            scheme = "<CFKeyedArchiverUID 0x7f961ac0b140 [0x1043bc7b0]>{value = 8}";
            sdkVersion = "<CFKeyedArchiverUID 0x7f961ac7c3c0 [0x1043bc7b0]>{value = 9}";
        },
        APSocialSDKDemo,
        2016012101112529,
        "com.nixWork.China",
        ap2016012101112529,
        "1.0.1.150917",
                {
            "$classes" =             (
                APSdkApp,
                NSObject
            );
            "$classname" = APSdkApp;
        },
                {
            "$class" = "<CFKeyedArchiverUID 0x7f961ac7cc50 [0x1043bc7b0]>{value = 19}";
            message = "<CFKeyedArchiverUID 0x7f961ac7c5b0 [0x1043bc7b0]>{value = 13}";
            scene = "<CFKeyedArchiverUID 0x7f961ac7ae20 [0x1043bc7b0]>{value = 18}";
            type = "<CFKeyedArchiverUID 0x7f961ac7cc30 [0x1043bc7b0]>{value = 12}";
        },
        0,
                {
            "$class" = "<CFKeyedArchiverUID 0x7f961ac7db30 [0x1043bc7b0]>{value = 17}";
            mediaObject = "<CFKeyedArchiverUID 0x7f961ac7c2a0 [0x1043bc7b0]>{value = 14}";
        },
                {
            "$class" = "<CFKeyedArchiverUID 0x7f961ac7b5d0 [0x1043bc7b0]>{value = 16}";
            text = "<CFKeyedArchiverUID 0x7f961ac7b5b0 [0x1043bc7b0]>{value = 15}";
        },
        WeWillWeWillRockYou,
                {
            "$classes" =             (
                APShareTextObject,
                NSObject
            );
            "$classname" = APShareTextObject;
        },
                {
            "$classes" =             (
                APMediaMessage,
                NSObject
            );
            "$classname" = APMediaMessage;
        },
        0,
                {
            "$classes" =             (
                APSendMessageToAPReq,
                APBaseReq,
                NSObject
            );
            "$classname" = APSendMessageToAPReq;
        },
                {
            "$classes" =             (
                NSMutableDictionary,
                NSDictionary,
                NSObject
            );
            "$classname" = NSMutableDictionary;
        }
    );
    "$top" =     {
        root = "<CFKeyedArchiverUID 0x7f961ae26090 [0x1043bc7b0]>{value = 1}";
    };
    "$version" = 100000;
}

从上面的信息可以看出,官方 Demo 粘贴数据的 pasteboard 的信息, 以及具体的数据。这些数据看起来像是一个包含了我们 APP 信息及所发送文本(“WeWillWeWillRockYou”)的大字典。那么我们下一步需要做的事情就很明白了–自己拼接出这个大字典。

但问题来了,其中的"<CFKeyedArchiverUID 0x7f961ac7c5b0 [0x1043bc7b0]>{value = 13}" 是什么东西呢?

经过搜索,我们可以找到这样的解释:

CFKeyedArchiverUID is the “8th property list object” for supporting NSKeyedArchiver.

于是我们就要考虑把我们拿到的数据转换成 property list 看个究竟了。

我们在 swizzlePasteboardSetData 方法中将获得的 Data 写入 plist 文件中

- (void)swizzlePasteboardSetData {
    SEL swizzlePasteboardSetDataSEL=@selector(setData:forPasteboardType:);
    void (*swizzlePasteboardSetDataIMP)(id,SEL,id,id)=(void(*)(id,SEL,id,id))[UIPasteboard instanceMethodForSelector:swizzlePasteboardSetDataSEL];

    static int count=0;
    void (^mypasteboardSetData)(id SELF,NSData *data,NSString *type)=^(id SELF,NSData *data,NSString *type){

        NSLog(@"\\n----------swizzlePasteboardSetData: %d----------\\nPasteboardName: %@\\ntype: %@\\n dict%@\\n",count++,[((UIPasteboard *)SELF) name], type,[NSPropertyListSerialization propertyListWithData:data options:0 format:0 error:nil]);

        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *documentsDirectory = paths[0];
        NSString *plistPath = [documentsDirectory stringByAppendingPathComponent:@"textData.plist"];

        NSLog(@"plistPath %@", plistPath);

        [[NSFileManager defaultManager] createFileAtPath:plistPath contents:nil attributes:nil];
        [data writeToFile:plistPath atomically:YES];
        swizzlePasteboardSetDataIMP(SELF,swizzlePasteboardSetDataSEL,data,type);
    };
    class_replaceMethod([UIPasteboard class], swizzlePasteboardSetDataSEL, imp_implementationWithBlock(mypasteboardSetData), NULL);
}

然后使用模拟器打开 Demo, 运行之后就可以进入模拟器的 Documents 文件夹中找到输出的 textData.plist 文件,但是这个 .plist 文件是 binary 格式的,而非 XML 格式,我们需要在终端中使用

plutil -convert xml1 textData.plist

来将其转换成 XML 格式。打开后就可以看到我们真正需要拼接的字典长什么样啦:

在我们自己的代码中,根据这个 plist 文件的结构拼接出 dictionary 之后,使用

guard let data =  try? NSPropertyListSerialization.dataWithPropertyList(dictionary, format: .XMLFormat_v1_0, options: 0) else {
    // ...
    return
}

UIPasteboard.generalPasteboard().setData(data, forPasteboardType: "com.alipay.openapi.pb.req.\\(appID)")

即可将数据贴入剪贴板,再用

openURL(URLString: "alipayshare://platformapi/shareService?action=sendReq&shareId=\\(appID)")

便可跳转到支付宝进行分享了。

需要注意的是,plist 文件中我们看到的 CF$UID 对应的数字其实是个索引,比如 Item4appKeyCF$UID 对应 6, 那么 Item6 就是我们的 appKey, 因此,这些数字以及整个 $objects 数组的顺序必须保证准确。

另外,分享图片/URL 等所需要的字典和分享文本的字典稍有不同,要注意正确拼接。

分享后回调的数据

在分享结束后,无论成功失败,支付宝都会向 type 为 com.alipay.openapi.pb.resp.\(account.appID) 的 pasteboard (注意这个 pasteboard 和 发送数据的 pasteboard 不同)中加入回调数据,通过比较分享成功与失败返回结果的不同,我们可以找到哪一项说明了分享结果,取出后进行对应处理即可。