Unity 多平台原生SDK接入速览(一):微信开放平台

2,589 阅读7分钟

该系列将记录我对于五个平台(微信、QQ、Facebook、Twitter、微博)的原生SDK的调研,重点关注登录和分享。P.S. 当前并没有 iOS 设备,因此文章都是以 Android 平台的接入为主,使用的 IDE 为 Android Studio。

ZeroyiQ:Unity 多平台原生SDK接入速览(一):微信开放平台

ZeroyiQ:Unity 多平台原生SDK接入速览(二):QQ互联

ZeroyiQ:Unity 多平台原生SDK接入速览(三):Facebook

ZeroyiQ:Unity 多平台原生SDK接入速览(四):Twitter

ZeroyiQ:Unity 多平台原生SDK接入速览(五):微博

一、前言

微信开放平台,当前(2020-6-24)注册账户必须要填写企业信息,还需要应用审核。请优先解决账户和审核问题,获取到应用 AppID 和 Secret。

二、SDK接入

1. 配置环境

项目 build.gradle 中添加依赖。

dependencies {
    api 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:+'
}

2. 设置权限

AndroidManifest.xml 中设置,如果使用到扫码登录,或者 mta(腾讯移动分析) 才需要添加以下权限。

<!-- 扫码登录 需要权限-->
<uses-permission android:name="android.permission.INTERNET" />

<!-- mta 需要权限-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

3. 初始化

要使微信能响应我们的程序,必须向微信注册我们的应用(AppID)。

private static final String WX_ID = "应用ID(需要替换)";
// IWXAPI 是第三方app和微信通信的openApi接口
private static IWXAPI WXAPI;
    
private void init() {
    // 通过WXAPIFactory工厂,获取IWXAPI的实例
    WXAPI = WXAPIFactory.createWXAPI(activity, WX_ID, true);

    // 将应用的appId注册到微信
    WXAPI.registerApp(WX_ID);

    //建议动态监听微信启动广播进行注册到微信
    activity.registerReceiver(new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            WXAPI.registerApp(WX_ID);
            }
        }, new IntentFilter(ConstantsAPI.ACTION_REFRESH_WXAPP));
}

4. 发送请求

现在我们就可以通过 通过 IWXAPI 的 sendReq 和 sendResp 两个方法来发送请求了。

boolean sendReq(BaseReq req);

sendReq 是第三方 app 主动发送消息给微信,发送完成之后会切回到第三方 app 界面。

boolean sendResp(BaseResp resp);

sendResp 是微信向第三方 app 请求数据,第三方 app 回应数据之后会切回到微信界面。

5. 接收请求

在与包名相同的路径下新增一个 wxapi 目录,并在该目录下新增一个 WXEntryActivity 类,该类继承自 Activity ,实现 IWXAPIEventHandler 接口。

img

WXEntryActivity

AndroidManifest.xml 中配置该 Activity,需要填入我们自己的包名

        <activity
            android:name=".wxapi.WXEntryActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:launchMode="singleTask"
            android:taskAffinity="包名"
            android:theme="@android:style/Theme.Translucent.NoTitleBar" />

WXEntryActivity 中添加 Intent 的传递

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        WeChat.WXAPI.handleIntent(getIntent(), this);
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
        WeChat.WXAPI.handleIntent(intent, this);
    }

IWXAPIEventHandler 有 onReq 和 onResp 两个回调方法。要注意的是通过 sendReq 发送的请求,将由 onResp 回调回来;通过 sendResp 发送的请求,将由 onReq 回调回来。

三、登录

1. 发起登录请求

在注册完 OpenSdk 后,发起登录请求。

    /**
     * 登录微信
     * @param context 上下文
     * @param api 微信 OpenAPI
     * @param wechatCode 回调接口
     */
    public static void loginWeChat(Context context, IWXAPI api, WeChatCode wechatCode) {
        //判断是否安装了微信客户端
        if (!api.isWXAppInstalled()) {
            ToastUtils.show(context.getApplicationContext(),R.string.wechat_error_unInstalled);
            return;
        }
        mWeChatCode = wechatCode;

        // 发送授权登录信息,来获取code
        SendAuth.Req req = new SendAuth.Req();

        // 应用的作用域,获取个人信息
        req.scope = "snsapi_userinfo";

        /**
         * 用于保持请求和回调的状态,授权请求后原样带回给第三方
         * 为了防止csrf攻击(跨站请求伪造攻击),后期改为随机数加session来校验
         */
        Random random = new Random();
        WeChat.WXState = WeChat.WX_STATE_ROOT + random.nextInt(1000);
        req.state = WeChat.WXState;

        // 发送请求
        api.sendReq(req);
    }

    /**
     * 返回code的回调接口
     */
    public interface WeChatCode {
        void getResponse(String code);
    }

2. 接收回调,获得 code

WXEntryActivity 的 onResp 方法中接收回调。

     /**
     * 第三方应用发送到微信的请求处理后的响应结果,会回调到该方法
     * @param baseResp 回调 response
     */
    @Override
    public void onResp(BaseResp baseResp) {
        int result;
        switch (baseResp.errCode) {
            case BaseResp.ErrCode.ERR_OK:
                result = R.string.errcode_success;
                UnityCallApi.unityLogInfo(TAG, "onResp OK");
                break;
            case BaseResp.ErrCode.ERR_USER_CANCEL:
                result = R.string.errcode_cancel;
                UnityCallApi.unityLogInfo(TAG, "onResp ERR_USER_CANCEL ");
                break;
            case BaseResp.ErrCode.ERR_AUTH_DENIED:
                result = R.string.errcode_deny;
                UnityCallApi.unityLogInfo(TAG, "onResp ERR_AUTH_DENIED");
                break;
            case BaseResp.ErrCode.ERR_UNSUPPORT:
                result = R.string.errcode_unsupported;
                UnityCallApi.unityLogInfo(TAG, "onResp ERR_UNSUPPORT " + baseResp.errCode);
                break;
            default:
                result = R.string.errcode_unknown;
                UnityCallApi.unityLogInfo(TAG, "onResp default errCode " + baseResp.errCode);
                break;
        }
        ToastUtils.show(this,getString(result)+ ", type=" + baseResp.getType());

        if (baseResp.getType() == ConstantsAPI.COMMAND_SENDAUTH) {
            // 校验 state
            String state = ((SendAuth.Resp) baseResp).state;
            if (state.equals(WeChat.WXState)) {
                String code = ((SendAuth.Resp) baseResp).code;
                // 返回 code 进行下一步
                mWeChatCode.getResponse(code);
                UnityCallApi.unityLogInfo(TAG, "Get WeChat scope. code:" + code);
            } else {
                String errorLog = "onResp: State not match!" + WeChat.WXState + "/" + state;
                UnityCallApi.unityLogError(TAG, errorLog);
            }
        }
    }

3. 获取 access_token

优先判断本地是否已经存储 access_token,有则进行有效期检测,没有则通过 code 获取最新 access_token。

    public void login(Activity activity) {
        WXEntryActivity.loginWeChat(this.activity, WXAPI, new WXEntryActivity.WeChatCode() {
            @Override
            public void getResponse(String code) {
                // 从手机本地获取存储的授权口令信息,判断是否存在access_token,不存在请求获取,存在就判断是否过期
                String accessToken = (String) ShareUtils.getValue(WeChat.this.activity, WEIXIN_ACCESS_TOKEN_KEY, "none");
                String openid = (String) ShareUtils.getValue(WeChat.this.activity, WEIXIN_OPENID_KEY, "");
                if (!"none".equals(accessToken)) {
                    // 有access_token,判断是否过期有效
                    isExpireAccessToken(accessToken, openid);
                } else {
                    // 没有access_token
                    getAccessToken(code);
                }
            }
        });
    }

getAccessToken 获取最新 access_token

    /**
     * 微信登录获取授权口令
     */
    private void getAccessToken(String code) {
        String url = "https://api.weixin.qq.com/sns/oauth2/access_token?" +
                "appid=" + WX_ID +
                "&secret=" + WX_SECRET +
                "&code=" + code +
                "&grant_type=authorization_code";
        // 网络请求 GET 获取access_toke
        //NetClient是对Okhttp3 的封装,见引用3
        NetClient.getNetClient().callNet(url, new NetClient.MyCallBack() {
            @Override
            public void onFailure(int code) {
                
            }

            @Override
            public void onResponse(String response) {
                // 处理回调
                processGetAccessTokenResult(response);
            }
        });
   }

isExpireAccessToken 校验 access_token,没有通过则刷新 refreshAccessToken

 /**
     * 微信登录判断accesstoken是过期
     *
     * @param accessToken token
     * @param openid      授权用户唯一标识
     */
    private void isExpireAccessToken(final String accessToken, final String openid) {
        String url = "https://api.weixin.qq.com/sns/auth?" +
                "access_token=" + accessToken +
                "&openid=" + openid;
        NetClient.getNetClient().callNet(url, new NetClient.MyCallBack() {
            @Override
            public void onFailure(int code) {
               
            }
            @Override
            public void onResponse(String response) {
                WXErrorInfo info = mGson.fromJson(response, WXErrorInfo.class);
                if (0 == info.getErrcode() && "ok".equals(info.getErrmsg())) {
                    // accessToken没有过期,获取用户信息
                    getUserInfo(accessToken, openid);
                    Toast.makeText(activity.getApplicationContext(), response.toString(), Toast.LENGTH_LONG).show();
                } else {
                    // 过期了,使用refresh_token来刷新accesstoken
                    refreshAccessToken();
                }
            }
        });
    }

    /**
     * 微信登录刷新获取新的access_token
     */
    private void refreshAccessToken() {
        // 从本地获取以存储的refresh_token
        final String refreshToken = (String) ShareUtils.getValue(activity, WEIXIN_REFRESH_TOKEN_KEY, "");
        if (TextUtils.isEmpty(refreshToken)) {
            return;
        }
        // 拼装刷新access_token的url请求地址
        String url = "https://api.weixin.qq.com/sns/oauth2/refresh_token?" +
                "appid=" + WX_ID +
                "&grant_type=refresh_token" +
                "&refresh_token=" + refreshToken;
        // 执行请求
        NetClient.getNetClient().callNet(url, new NetClient.MyCallBack() {
            @Override
            public void onFailure(int code) {
                // 重新请求授权
                login(activity);
            }

            @Override
            public void onResponse(String response) {
                WXAccessTokenInfo info = mGson.fromJson(response, WXAccessTokenInfo.class);
                saveAccessInfoToLocation(info);
                // 判断是否获取成功,成功则去获取用户信息,否则提示失败
                processGetAccessTokenResult(response);
            }
        });

    }

processGetAccessTokenResult 处理请求返回的结果 response

    /**
     * 微信登录处理获取的授权信息结果
     *
     * @param response 授权信息结果
     */
    private void processGetAccessTokenResult(String response) {
        // 验证获取授权口令返回的信息是否成功
        if (validateSuccess(response)) {
            // 使用Gson解析返回的授权口令信息
            WXAccessTokenInfo tokenInfo = mGson.fromJson(response, WXAccessTokenInfo.class);
            // 保存信息到手机本地
            saveAccessInfoToLocation(tokenInfo);
            // 获取用户信息
            getUserInfo(tokenInfo.getAccess_token(), tokenInfo.getOpenid());
        } else {
            // 授权口令获取失败,解析返回错误信息
            WXErrorInfo wxErrorInfo = mGson.fromJson(response, WXErrorInfo.class);
            String result = String.format(Locale.ENGLISH, "processGetAccessTokenResult: Get Access Token Error. Code:%d msg:%s", wxErrorInfo.getErrcode(), wxErrorInfo.getErrmsg());
            UnityCallApi.unityLogError(TAG, result);
        }
    }

WXAccessTokenInfo 是对应正确返回的类

img

WXErrorInfo 是对应错误返回的类

img

四、获取用户信息

在登录后,发送获取用户信息请求。

    /**
     * 微信token验证成功后,联网获取用户信息
     *
     * @param access_token
     * @param openid
     */
    private void getUserInfo(String access_token, String openid) {
        String url = "https://api.weixin.qq.com/sns/userinfo?" +
                "access_token=" + access_token +
                "&openid=" + openid;
        NetClient.getNetClient().callNet(url, new NetClient.MyCallBack() {
            @Override
            public void onFailure(int code) {
                UnityCallApi.unityLogError(TAG, "Get User Info Error.Code:" + code);
                UnityCallApi.sendLoginInfoToUnity(false, "");
            }

            @Override
            public void onResponse(String response) {
                UnityCallApi.unityLogInfo(TAG, "Get User Info Successful.");
                // 发送到 Unity 进行解析
                UnityCallApi.sendLoginInfoToUnity(true, response);
            }
        });
    }

将返回信息传递给 Unity 进行解析。

正确返回 json

img

错误返回 json

img

五、分享

1. 文字

img

        WXTextObject textObj = new WXTextObject();
        textObj.text = text;

        // 多媒体消息对象
        WXMediaMessage msg = new WXMediaMessage();
        msg.mediaObject = textObj;
        // msg.title = "Will be ignored";
        msg.description = text;
        msg.mediaTagName = "我是mediaTagName啊";

        SendMessageToWX.Req req = new SendMessageToWX.Req();
        // type + 时间戳
        req.transaction = buildTransaction("text");
        req.message = msg;
        req.scene = mTargetScene;

        WXAPI.sendReq(req);

2. 图片

img

        WXImageObject imgObj = new WXImageObject(bmp);

        WXMediaMessage msg = new WXMediaMessage();
        msg.mediaObject = imgObj;
        
        // bitmap 缩放到 150*150
        Bitmap thumbBmp = Bitmap.createScaledBitmap(bmp, THUMB_SIZE, THUMB_SIZE, true);
        bmp.recycle();
        // bitmap 转 二进制
        msg.thumbData = ShareUtils.bmpToByteArray(thumbBmp, true);

        SendMessageToWX.Req req = new SendMessageToWX.Req();
        req.transaction = buildTransaction("img");
        req.message = msg;
        req.scene = mTargetScene;
        WXAPI.sendReq(req);

3. 网页

img

    WXWebpageObject webpage = new WXWebpageObject();
    webpage.webpageUrl = "http://www.qq.com";

    WXMediaMessage msg = new WXMediaMessage(webpage);
    msg.title = "叮咚,群助手提醒你~";
    msg.description = "离下班还有最后一个小时了!";

    Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.send_img);
    Bitmap thumbBmp = Bitmap.createScaledBitmap(bmp, THUMB_SIZE, THUMB_SIZE, true);
    bmp.recycle();
    msg.thumbData = Util.bmpToByteArray(thumbBmp, true);
    
    SendMessageToWX.Req req = new SendMessageToWX.Req();
    req.transaction = buildTransaction("webpage");
    req.message = msg;
    req.scene = mTargetScene;
    api.sendReq(req);

六、总结

微信开放平台感觉目前还是缺少API文档, 比如登录的 scope 里到底有哪些作用域,就不是很明确。依靠谷歌,还是找到些线索,根据引用4描述有以下几种。

snsapi_message:帮助你通过该应用向好友发送消息
snsapi_userinfo:获得你的公开信息(昵称,头像等)
snsapi_friend:寻找与你共同使用该应用的好友
snsapi_contact:获得你的好友关系

然而添加后,依旧不能申请到朋友关系,并且也不知道是通过什么接口获取的。当前目测只有和 TX 合作的应用能够申请到相关权限。替代方案为自己手动维护个关系网。分享链接,链接中包含分享用户id。有用户点击,则能判断两人为朋友关系。

img

当前分享应用,用户点击分享跳转应用的操作,推测也需要进行合作。替换方案为分享网页,用户点击后,引导右上角打开默认浏览器,之后就是 Android 通过浏览器起调应用了。

img

七、引用

  1. 资源中心是微信开放平台开发者所需所有相关资源的汇集地,包括: | 微信开放文档
  2. 如何在Unity中使用官方SDK实现微信、QQ、微博帐号登录(Android) -腾讯游戏学院
  3. android 网络请求okhttp解耦逆天封装,使用简单,扩展性强
  4. 作业部落 Cmd Markdown 编辑阅读器