第三方登入/分享最佳實踐

天之界線2010發表於2017-03-10

本文會不定期更新,推薦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

參考文章:

相關文章