阅读 5165

第三方登录/分享最佳实践

本文会不定期更新,推荐watch下项目

如果喜欢请star,如果觉得有纰漏请提交issue,如果你有更好的点子可以提交pull request。

本文的示例代码主要是基于ShareLoginLib这个库编写的,若你有其他的技巧和方法可以参与进来一起完善这篇文章。

本文固定连接:github.com/tianzhijiex…


背景

如果你做过早期的app开发,你会发现很少有app支持第三方登录的,分享层面基本都是调用原生的分享接口。但随着微信、微博、手Q的兴起,平台化的第三方登录和分享就变成了app的刚需和优势了,它既大大降低了用户的注册成本,也方便app利用社交工具进行广泛的推广。

其实第三方登录/分享在web时代已经有了一套成熟的体系,这套体系现在对于原生开发来说也成了必备的能力了,所以花五分钟来学习一下是很有必要的。在我苦学后发现第三方的各种SDK都十分难用、各有缺点:

  • 2017年了还在提供jar包
  • 文档万年不更新
  • 文档阅读难度极大,信息分散
  • 必须权限和混淆配置模糊不清
  • 技术支持缓慢,微博SDK虽然在github上但没人处理issue

于是我通过一年的探索,找到了一个更加轻量的集成方案,并将其开源了出来,这就是ShareLoginLib

ShareLoginLib原先是fork自流利说的一个库,后续我自己维护和重构了多个版本,于是就成了现在这个样子。

需求

  • 要能判断是否已安装了第三方客户端
  • 能通过第三方App进行登录和分享
  • 能自动压缩缩略图,不会因为图片过大而分享失败
  • 要支持大图的高清分享,越清晰越好
  • 能支持网页、纯图片和简单文本的分享
  • 对于不支持的分享内容应该有报错提示,方便排错
  • 解决微博点击“保存草稿”无回调的问题
  • 最好帮我干掉微信强制引入的WXEntryActivity
  • 要能自带混淆功能,不让使用者考虑混淆的问题
  • 配置各种第三方key的工作越简单越好
  • 这个库体积和方法数越少越好
  • 登录/分享开始后应该有loading界面,有结果后loading消失
  • 第三方jar升级后应该能快速更新

实现

判断是否安装了第三方客户端

  • 如果你没有安装手Q,直接调用QQ登录时会被引导去下载手Q
  • 微信是根本不引导你下载,直接告诉你没有安装微信,登录失败
  • 微博就比较良心了,可以支持web和客户端的两种登录方式

就展示策略来说,一般的策略是如果没有安装对应的app就在分享或登录的时候提示安装或隐藏对应的按钮,这样会给用户更好的体验。

就程序实现来讲我们是需要判断第三方app的安装情况的,目前可以通过这三个静态方法来判断当前手机上是否安装了对应的app:

ShareLoginSDK.isWeiXinInstalled(this);
ShareLoginSDK.isWeiBoInstalled(this);
ShareLoginSDK.isQQInstalled(this);复制代码

方法虽然简单,我们要了解下原理,先来看下微信SDK自带的判断方法:

public final boolean isWXAppInstalled() {
        if(this.detached) {
            throw new IllegalStateException("isWXAppInstalled fail, WXMsgImpl has been detached");
        } else {
            try {
                PackageInfo var1;
                return (var1 = this.context.getPackageManager().getPackageInfo("com.tencent.mm", 64)) == null?false:WXApiImplComm.validateAppSignature(this.context, var1.signatures, this.checkSignature);
            } catch (NameNotFoundException var2) {
                return false;
            }
        }
    }复制代码

其中

public static final int GET_SIGNATURES = 64;

所以可以看出它是通过手机上微信的签名信息来判断是否已经安装了微信的。

qq没有默认的方法,所以需要我们自己来实现一下:

public static boolean isQQInstalled(@NonNull Context context) {
        PackageManager pm = context.getApplicationContext().getPackageManager();
        if (pm == null) {
            return false;
        }
        List<PackageInfo> packages = pm.getInstalledPackages(0);
        for (PackageInfo info : packages) {
            String name = info.packageName.toLowerCase(Locale.ENGLISH);
            if ("com.tencent.mobileqq".equals(name)) {
                return true;
            }
        }
        return false;
    }复制代码

注意:
因为判断应用是否存在是需要权限的,所以强烈建议测试下各个手机的权限引导,否则遇到魅族这样的奇葩手机就难办了。

通过第三方App进行登录/分享

登录

要做好第三方登录就必须了解下OAuth 2.0。当用户不给你第三方的密码,但他还想要得到第三方的信息的时候,OAuth 2.0的中间代理模式就十分有意义了,下面是一个流程图:

摘自:阮一峰的《理解OAuth 2.0》

微信也是按照oauth2.0进行了实现,算是一个相当标准的实现了:

微博和QQ就比较简单了,省略了通过code+secret来换token的步骤,我猜想是利用了应用签名和包名进行了这步的安全校验。

原理分析完毕后直接看代码的实现:

SsoLoginManager.login(this, SsoLoginType.XXX(QQ,WEIBO,WEIXIN), new LoginListener() {

     /**
     * @param accessToken 第三方给的一次性token,几分钟内会失效
     * @param uId         用户的id
     * @param expiresIn   过期时间
     * @param wholeData   第三方本身返回的全部json数据
     */
      public void onSuccess(String accessToken, String uId, long expiresIn, @Nullable String wholeJsonData) {}

      public void onError(String errorMsg) {}

      public void onCancel() {}
  });复制代码

得到了token后,我们就可以直接获得用户信息了:

SsoUserInfoManager.getUserInfo(context, SsoLoginType.XXX, accessToken, userId,
    new UserInfoListener() {

        public void onSuccess(OAuthUserInfo userInfo) {
            // 可以得到:昵称、性别、头像、用户id
        }

        public void onError(String errorMsg) {
        }
    });复制代码
  1. 通过login()来唤起第三方app
  2. 第三方认证后回调你的app得到token
  3. 你的app通过token来访问第三方的服务器,最终得到用户信息

注意:
对于微信而言,登录流程中是有个code变量的,如果你的服务器做了通过code换token的工作,那么你可以利用login(act,type,loginListener,wxLoginRespListener)来传入一个WXLoginRespListener

public interface WXLoginRespListener {
    void onLoginResp(String respCode, SsoLoginManager.LoginListener listener);
}复制代码

如果你设置了WXLoginRespListener,那么你就可以拿到code,通过你自己的服务器换取token了。

就使用来说,提供一个静态方法进行登录操作,比起之前的在onActivityForResult()中处理回调明显简单了很多!

分享

调用分享的方法也十分简单,一个静态方法搞定:

SsoShareManager.share(MainActivity.this, SsoShareType.XXX
        new ShareContentWebpage("title", "summary", "http://www.kale.com", thumbBmp,largeBmp),
        new ShareStateListener() {

                  public void onSuccess() {}

                  public void onCancel() {}

                  public void onError(String errorMsg) {}
              });复制代码

这个库支持分享网页、文本和图片类型的内容,具体的对象是:

  • ShareContentWebPage
  • ShareContentText
  • ShareContentPic

分享的途径也各有不同,目前支持分享到

  • 微博
  • qq好友
  • qq空间
  • 微信好友
  • 微信朋友圈
  • 微信收藏

对应的SsoShareType就是:QQ_ZONE、QQ_FRIEND、WEIBO_TIME_LINE、WEIXIN_FRIEND、WEIXIN_FRIEND_ZONE、WEIXIN_FAVORITE。

能压缩分享时的缩略图

第三方app会对于分享的内容大小进行限制,一般的限制就是字数和图片的大小,在字数方面可以不用太注意,但是就图片来说缩略图的限制就是32kb(转换为byte[]的长度)之内,这个还是比较严格的。

对于这点,我们应该对传入的缩略图进行压缩处理。我一般的处理方案是:如果长宽超过了250,那么就会进行压缩,这样能保证得到的bitmap都是小于32kb的。(这个250是我做压缩的经验谈,并非一个准确的数字)

@Nullable
    static byte[] getImageThumbByteArr(@Nullable Bitmap src) {
        if (src == null) {
            return null;
        }

        final Bitmap bitmap;
        if (src.getWidth() > 250 || src.getHeight() > 250) {
            bitmap = ThumbnailUtils.extractThumbnail(src, 250, 250);
        } else {
            bitmap = src;
        }

        byte[] thumbData = null;
        ByteArrayOutputStream outputStream = null;
        try {
            outputStream = new ByteArrayOutputStream();
            bitmap.compress(Bitmap.CompressFormat.JPEG, 85, outputStream);
            thumbData = outputStream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.flush();
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return thumbData;
    }复制代码

通过这个内部方法处理后,我们就可以保证传入的缩略图是合法的,不会再因为缩略图过大而分享失败了。

注意:
虽然我们可以做这样的操作,我还是强烈建议大家在传入之前就对bitmap进行压缩,千万不要直接传入一个大图进来。图片越大bitmap越大,占用的内存也就越大,库处理图片的时间也就越长,所以从网络下载的话最好做好服务器压缩,直接拿小图,并且在用完bitmap后可以考虑调用下Bitmap.recycle()

附:微博关于缩略图的文档

支持分享高清大图

public ShareContentPic(@Nullable Bitmap thumbBmp, @Nullable Bitmap largeBmp)复制代码

如果我们分享的Content选择的是ShareContentPic,这就说明你要分享的是图片类型的对象了,我们当然希望分享出去的图片越高清越好。

对于largeBmp我推荐传入1M以内的图片,因为这样图片质量既不会太差而且占用内存也少。对于传入的这个largetBmp,我们肯定不能将其无脑的通过intent传递给第三方app,这个肯定会爆。
我们应该将bitmap存入外部磁盘(不能是内部私密缓存),然后传给第三方一个图片的地址,让第三方的app根据这个地址读取sd卡的图片。

static String saveLargeBitmap(final Bitmap bitmap) {
        if (bitmap == null) {
            return null;
        }

        String path = SlConfig.pathTemp;
        if (!TextUtils.isEmpty(path)) {
            String imagePath = path + "sl_large_pic";
            FileOutputStream fos = null;
            try {
                fos = new FileOutputStream(imagePath);
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } finally {
                if (fos != null) {
                    try {
                        fos.flush();
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return imagePath;
        } else {
            return null;
        }
    }复制代码

这里1M也是经验谈,因为是存在磁盘,所以当然支持大于1M的图了,但为了内存还是希望小一点好

注意:
这里用到了外部存储卡,所以会申请外部存储的权限。对于6.0以上的手机,需要开发者去主动检查一下,如果没有权限,那么图片就分享不出去了。

支持分享网页和简单文本

对于一般来说网页的分享样式都是card样式:

但是对于微博来说,只有合作网站才能有卡片样式的展示,没有合作就是很丑的文字链接。对于这样的情况,我建议在微博分享的时候,不传url,而是让后端将url拼接到文本正文中,这样分享出去的内容就很美观了。

因为之前对于大图了处理,这里的图片点开后也是相当清楚的:

对于不支持的分享内容会报错

一个合理的SDK应该对于自己不支持的内容进行报错,提示开发者进行修复,已知的不合法类型就两个:

  1. 目前不支持分享纯文本信息给QQ好友
  2. 目前不支持分享纯文本/图片到QQ空间

一旦出错,代码会通过log和toast的方式自动提示开发者,方便定位出错的原因。(我知道有些人确实不会注意不崩溃时的log,所以专门做了toast)

解决微博分享会没有回调的BUG

微博有个常年的bug——输入了一些信息后取消分享,如果你点击了报错草稿,那么你就永远接收不到回调了。为了解决这个问题,我在onResume的时候来强行触发一次回调,保证每次分享都尽可能能拿到结果。

@Override
    protected void onResume() {
        super.onResume();
        if (isFirstIn) {
            isFirstIn = false;
        } else {
            if (mIsLogin) {
                // 这里处理通过网页登录无回调的问题
                finish();
            } else {
                // 这里处理保存到草稿箱的逻辑
                BaseResponse response = new SendMessageToWeiboResponse();
                response.errCode = WBConstants.ErrorCode.ERR_CANCEL;
                response.errMsg = "weibo cancel";
                onResponse(response);
            }
        }
    }复制代码

解决微信需要硬写一个WXEntryActivity的问题

微信也有一个常年让我诟病的问题,它的所有回调都必须在一个叫做WXEntryActivity的activity中进行处理,而且这个activity还必须在你自己app的包名下的根目录中!
为了解决这个问题,我利用了targetActivity这个技巧干掉了它,减少了使用者的配置负担。

<activity-alias
            android:name="${applicationId}.wxapi.WXEntryActivity"
            android:exported="true"
            android:screenOrientation="portrait"
            android:targetActivity="com.liulishuo.share.activity.SL_WeiXinHandlerActivity"
            android:theme="@android:style/Theme.Translucent.NoTitleBar"
            />复制代码

让库自带混淆配置

consumerProguardFiles可以允许在库中配置自己的混淆方案,然后它会将混淆配置打包到aar中,这样使用者在使用的时候就无需关心混淆问题,需要的配置方案会自动生效。

我们都知道第三方的登录分享SDK本身就有很多混淆的配置,为了减少使用者的负担,我利用consumerProguardFiles的这个特性大大降低了库的使用复杂度。

lib/build.gradle

defaultConfig {
    minSdkVersion 9
    targetSdkVersion 24
    consumerProguardFiles 'consumer-proguard-rules.pro'
}复制代码

consumer-proguard-rules.pro

# ————————  微信 start    ————————
-keep class com.tencent.mm.opensdk.** {
   *;
}
-keep class com.tencent.wxop.** {
   *;
}
-keep class com.tencent.mm.sdk.** {
   *;
}
# ————————  微信 end    ————————

# ————————  微博 start    ————————   
-keep class com.sina.weibo.sdk.api.* {*;}
# ————————  微微博 end    ————————

# ————————  qq start    ————————
-keep class * extends android.app.Dialog {*;}
-keep class com.tencent.open.TDialog$*
-keep class com.tencent.open.TDialog$* {*;}
-keep class com.tencent.open.PKDialog
-keep class com.tencent.open.PKDialog {*;}
-keep class com.tencent.open.PKDialog$*
-keep class com.tencent.open.PKDialog$* {*;}
# ————————  qq end    ————————复制代码

开启release生成app后,我们发现需要keep的类都已经自动keep了,混淆配置对于使用者完全无感知了。

使用占位符来简化配置

利用applicationId做占位

前面也说到微信的activity必须在app的包名下,所以我利用了applicationId来做占位符,这样保证编译后的activity配置是在包名下的:

利用manifestPlaceholders来赋值

腾讯的key必须定义在manifest中,为了简化配置我定义了一个tencentAuthId的变量来占位,然后在使用的时候通过给tencentAuthId赋值的形式来实现初始化key。

defaultConfig {
        applicationId "com"
        minSdkVersion 18
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
        manifestPlaceholders = '123334443434'] // 使用时的配置
    }复制代码

利用builder来一次性配置各种参数

java代码中我做了builder模式,可以方便的进行各个参数的配置:

SlConfig cfg = new SlConfig.Builder()
        .debug(true)
        .appName("test app")
        .picTempFile(null)
        .qq(QQ_APPID, QQ_SCOPE)
        .weiBo(WEIBO_APPID, WEIBO_REDIRECT_URL, WEIBO_SCOPE)
        .weiXin(WEIXIN_APPID, WEIXIN_SECRET)
        .build();

ShareLoginSDK.init(this, cfg);复制代码

减少代码量,减少库体积

腾讯和微信的SDK提供了精简版的jar包,精简版的jar提供了完整的登录/分享代码,所以完全没必要用全量包。

我是用gradle来配置微信的依赖的(删除了mta):

compile 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:1.0.2'复制代码

第三方的SDK中会带有全面的so库,我们可能用不到那么多,所以你可以利用《App瘦身最佳实践 》中讲到的技巧来瘦身:

defaultConfig {
    versionCode 1
    versionName '1.0.0'

    // http://stackoverflow.com/questions/30794584/exclude-jnilibs-folder-from-production-apk
    ndk {
        abiFilters "armeabi", "armeabi-v7a" ,"x86" // 保留这三个
    }
}复制代码

不要忘了Loading

无论是登录还是分享,我们都会开启别的activity,为了减少突兀感,我在代码里给activity加了启动的动画:

activity.startActivity(intent);
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);复制代码

除此之外,因为整个过程都是异步的,前面说的图片处理和压缩都是耗时操作,你完全可以在开始整个流程的时候做个loading的对话框,然后在回调时让对话框消失,这样会给用户更好的体验。

第三方sdk升级后如何处理

因为第三方的SDK是不断变化的,作为它们的封装库,我们应该要时刻想着保持同样的更新频率,可惜的是第三方sdk是通过jar依赖的,没办法发挥gradle的自动选择最新版的优势,因此对于这个问题基本就只能靠库作者的更新了。我的建议是大家可以fork这个库,然后建立私有仓库,如果发现更新不及时,可以随时改代码,随时应对需求,然后再提交一个pr就行。

每次更新第三方的SDK后可以都跑一次测试用例,这样可以极大强度保证稳定性,这也是“测试用例对于稳定项目有着极高的性价比”的一大立正。

总结

第三方登录分享是一个很重要的功能,里面的坑也相当不少,本文列出的也许仅仅是一些常见的坑和优化点,我希望大家看完本篇后可以对第三方登录/分享的封装有一个全面的思路,减少一些杂乱无章的代码。

developer-kale@foxmail.com

微博:@天之界线2010

参考文章: