阿里最新熱修復(fù)框架sophix-3.1.6集成詳解

*本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨家發(fā)布


本文更新于2017年11月20日

前言

關(guān)于sophix集成和使用,網(wǎng)上有了很多前輩寫的博客。讀了很多,感覺都不太詳細和系統(tǒng)。所以自己嘗試寫sophix集成文章,本文包括四部分內(nèi)容:

  • 控制臺開通移動熱修復(fù)
  • 工程代碼快速接入
  • 生成、上傳、調(diào)試補丁
  • 補丁灰度發(fā)布、全量發(fā)布、機型過濾

關(guān)于sophix的原理和與其他熱修復(fù)框架的比較,戳官方文檔

阿里手淘團隊出書了,業(yè)界首部全方位系統(tǒng)介紹熱修復(fù)原理書籍,從阿里Sophix方案開發(fā)過程入手權(quán)威解讀!《深入探索Android熱修復(fù)技術(shù)原理》
這本書建議讀一讀。


話不多說,集成開始:

控制臺開通移動熱修復(fù)

阿里云控制臺的使用有點繞,要注意了,對照著一步一步來

  • 登錄阿里云,開通移動熱修復(fù)

阿里云熱修復(fù)控制臺地址

Ps:

如果自己進了阿里云官網(wǎng)首頁,怎么找熱修復(fù): 鼠標滑到 菜單欄 【產(chǎn)品】,彈出的菜單,找到白色字體類別【移動云】,移動云 的子菜單里找到【移動熱修復(fù)】

image.png

· 右上角登錄,可以使用淘寶賬號直接登錄。注冊一個也行。

· 左邊 點擊 立即開通。

沒開通的,會跳轉(zhuǎn)到一個頁面,告知 【確認開通】。

確認開通后,跳轉(zhuǎn)到控制臺的移動熱修復(fù)頁面,醬紫的

移動熱修復(fù) 控制臺

Ps:

如果讀者自己是通過點官網(wǎng)首頁左上角的【控制臺】,直接進入了【管理控制臺】,那怎么進到移動熱修復(fù)的控制臺頁面呢:看上面的截圖,菜單欄的 【產(chǎn)品與服務(wù)】,是以首字母排列的。找Y類-【移動熱修復(fù)】。點一下,就切換到移動熱修復(fù)的管理了。

截圖中 【創(chuàng)建App】是新開一個標簽頁,跳轉(zhuǎn)到 [移動云] 控制臺(Mobile Hub)去創(chuàng)建的,和當前處在的 [移動熱修復(fù)] 控制臺 不同,不要搞混。

  • 點擊【創(chuàng)建App】,會提示先【創(chuàng)建產(chǎn)品】
    產(chǎn)品下包含著 創(chuàng)建應(yīng)用(App),產(chǎn)品的名字隨便起。
結(jié)果:移動云- 產(chǎn)品列表頁
  • 點擊 藍色字體產(chǎn)品名稱 或 【管理】,進入 產(chǎn)品信息頁。
結(jié)果:移動云 -產(chǎn)品信息頁

Ps:

在本頁的 應(yīng)用列表的App都有 查看信息 選項,這里用不到它,因為沒有我們需要的RSA密鑰。

點擊 【創(chuàng)建應(yīng)用】,填入App名(最好和項目名稱一致),應(yīng)用類型 選 Android,填入packageName。 (bundleId是iOS的標識)

創(chuàng)建成功后,在下方的應(yīng)用列表展示信息。

  • 點擊 移動熱修復(fù),再點擊應(yīng)用列表 對應(yīng)App 的【管理】,查看 AppId、AppSecret、RSA密鑰

進入移動熱修復(fù)有兩種方法:
1.看上圖,可以在當前移動云 產(chǎn)品信息頁 ,點擊 移動熱修復(fù)標簽,
2.可以關(guān)掉當前網(wǎng)頁(還記得在移動熱修復(fù)控制臺【創(chuàng)建App】是新開一個標簽頁嗎)這樣也可以回到移動熱修復(fù)的頁面,再刷新一下。

第1種方法結(jié)果:


第1種方法結(jié)果

第2種方法結(jié)果:


第2種方法,圖一

點擊應(yīng)用列表【管理】,進入圖二
第2種方法,圖二

總之,一定要在創(chuàng)建完產(chǎn)品和應(yīng)用后,到 [移動熱修復(fù)] 標簽頁,才能查看到AppId,AppSecret,RSA密鑰。不要在移動云的產(chǎn)品處查看,那樣你是看不到RSA密鑰的。

關(guān)于 【管理控制臺】 的更多使用詳情, 戳這里


工程代碼快速接入

  • studio添加依賴:

gradle遠程倉庫依賴, 打開項目找到app的build.gradle文件,添加如下配置:
添加maven倉庫地址:

repositories {
   maven {
   url "http://maven.aliyun.com/nexus/content/repositories/releases"
     }
  }

添加gradle坐標版本依賴:

compile 'com.aliyun.ams:alicloud-android-hotfix:3.1.6'
  • 配置AndroidManifest文件

    • 需要用到一下權(quán)限:

      <! -- 網(wǎng)絡(luò)權(quán)限 -->
      <uses-permission android:name="android.permission.INTERNET" />
      <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
      <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
      <! -- 外部存儲讀權(quán)限,調(diào)試工具加載本地補丁需要 -->
      <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
      

      READ_EXTERNAL_STORAGE權(quán)限屬于Dangerous Permissions,僅調(diào)試工具獲取外部補丁需要,不影響線上發(fā)布的補丁加載,調(diào)試時請自行做好android6.0以上的運行時權(quán)限獲取。

    • application節(jié)點下添加如下配置:添加AppId,AppSecret,RSA密鑰

      <meta-data
        android:name="com.taobao.android.hotfix.IDSECRET" android:value="AppId" />
      <meta-data
        android:name="com.taobao.android.hotfix.APPSECRET" android:value="AppSecret" />
      <meta-data
        android:name="com.taobao.android.hotfix.RSASECRET" android:value="RSA密鑰" />
      

      因為AppSecret和RSA密鑰比較敏感,出于安全考慮,可以在代碼中通過setSecretMetaData這個方法進行設(shè)置。這個下面寫Java代碼時再說。

  • 混淆配置

  #基線包使用,生成mapping.txt
  -printmapping mapping.txt
  #生成的mapping.txt在app/buidl/outputs/mapping/release路徑下,移動到/app路徑下
  #修復(fù)后的項目使用,保證混淆結(jié)果一致
  #-applymapping mapping.txt
  #hotfix
  -keep class com.taobao.sophix.**{*;}
  -keep class com.ta.utdid2.device.**{*;}
  #防止inline
  -dontoptimize
  • Java代碼初始化接入

Sophix 3.1.6版本以后引入了新的初始化方式。

原來的初始化方式仍然可以使用,不過新方式將會帶來以下優(yōu)點:初始化與應(yīng)用原先業(yè)務(wù)代碼完全隔離,使得原先真正的Application可以修復(fù),并且減少了補丁預(yù)加載時間。而且,新方式已經(jīng)優(yōu)先支持Android 8.0版本。
本文使用這種新型方式。

1- 導(dǎo)入SophixStubApplication
需要加入這個類:

package com.my.pkg;
import android.app.Application;
import android.content.Context;
import android.support.annotation.Keep;
import android.util.Log;
import com.taobao.sophix.PatchStatus;
import com.taobao.sophix.SophixApplication;
import com.taobao.sophix.SophixEntry;
import com.taobao.sophix.SophixManager;
import com.taobao.sophix.listener.PatchLoadStatusListener;
import com.my.pkg.MyRealApplication;
/**
 * Sophix入口類,專門用于初始化Sophix,不應(yīng)包含任何業(yè)務(wù)邏輯。
 * 此類必須繼承自SophixApplication,onCreate方法不需要實現(xiàn)。
 * AndroidManifest中設(shè)置application為此類,而SophixEntry中設(shè)為原先Application類。
 * 注意原先Application里不需要再重復(fù)初始化Sophix,并且需要避免混淆原先Application類。
 * 如有其它自定義改造,請咨詢官方后妥善處理。
 */
public class SophixStubApplication extends SophixApplication {
    private final String TAG = "SophixStubApplication";
    // 此處SophixEntry應(yīng)指定真正的Application,也就是你的應(yīng)用中原有的主Application,并且保證RealApplicationStub類名不被混淆。
    @Keep
    @SophixEntry(MyRealApplication.class)
    static class RealApplicationStub {}
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //         如果需要使用MultiDex,需要在此處調(diào)用。
        //         MultiDex.install(this);
        initSophix();
    }
    private void initSophix() {
        String appVersion = "0.0.0";
        try {
          appVersion = this.getPackageManager()
                         .getPackageInfo(this.getPackageName(), 0)
                         .versionName;
        } catch (Exception e) {
        }
        final SophixManager instance = SophixManager.getInstance();
    instance.setContext(this)
            .setAppVersion(appVersion)
            .setSecretMetaData(null, null, null) //三個參數(shù)分別對應(yīng)AndroidManifest里面的AppId、AppSecret、RSA密鑰,可以不在AndroidManifest設(shè)置而是用此函數(shù)來設(shè)置Secret。放到代碼里面進行設(shè)置可以自定義混淆代碼,更加安全,此函數(shù)的設(shè)置會覆蓋AndroidManifest里面的設(shè)置,如果對應(yīng)的值設(shè)為null,默認會在使用AndroidManifest里面的。
            .setEnableDebug(true)//默認為false,設(shè)為true即調(diào)試模式下會輸出日志以及不進行補丁簽名校驗. 線下調(diào)試此參數(shù)可以設(shè)置為true, 它會強制不對補丁進行簽名校驗, 所有就算補丁未簽名或者簽名失敗也發(fā)現(xiàn)可以加載成功. 但是正式發(fā)布該參數(shù)必須為false, false會對補丁做簽名校驗, 否則就可能存在安全漏洞風(fēng)險。
            .setEnableFullLog()
            .setPatchLoadStatusStub(new PatchLoadStatusListener() {
                @Override
                public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
                    if (code == PatchStatus.CODE_LOAD_SUCCESS) {
                        Log.i(TAG, "sophix load patch success!");
                    } else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
                        // 如果需要在后臺重啟,建議此處用SharePreference保存狀態(tài)。
                        Log.i(TAG, "sophix preload patch success. restart app to make effect.");
                        /** 不可以直接Process.killProcess(Process.myPid())來殺進程,這樣會擾亂Sophix的內(nèi)部狀態(tài)。
                         * 因此如果需要殺死進程,建議使用這個方法,它在內(nèi)部做一些適當處理后才殺死本進程。*/
                        instance.killProcessSafely();
                    }
                }
            }).initialize();
    }
    @Override
    public void onCreate() {
      super.onCreate();
      // queryAndLoadNewPatch不可放在attachBaseContext 中,否則無網(wǎng)絡(luò)權(quán)限,建議放在后面任意時刻,如onCreate中
      SophixManager.getInstance().queryAndLoadNewPatch();
      /** 補丁在后臺發(fā)布之后, 并不會主動下行推送到客戶端, 客戶端通過調(diào)用queryAndLoadNewPatch方法查詢后臺補丁是否可用*/
    }
}

初始化sophix務(wù)必放在attachBaseContext中,onCreate不需要自行實現(xiàn)。同時自定義的SophixStubApplication需要繼承com.taobao.sophix.SophixApplication。
這其中,關(guān)鍵一點是:

@Keep
@SophixEntry(MyRealApplication.class)
static class RealApplicationStub {}

SophixEntry應(yīng)指定項目中原先真正的Application(原項目里application的android::name指定的),這里用MyRealApplication指代。并且保證RealApplicationStub類名不被混淆。而SophixStubApplication的類名和包名可以自行取名。

這里的Keep是android.support包中的類,目的是為了防止這個內(nèi)部靜態(tài)類的類名被混淆,因為sophix內(nèi)部會反射獲取這個類的SophixEntry。如果項目中沒有依賴android.support的話,就需要在progurad里面手動指定RealApplicationStub不被混淆。

2- 然后,在proguard文件里面需要加上下面內(nèi)容:

-keepclassmembers class com.my.pkg.MyRealApplication {
  public <init>();
}
# 如果不使用android.support.annotation.Keep則需加上此行
# -keep class com.my.pkg.SophixStubApplication$RealApplicationStub

目的是防止真正Application的構(gòu)造方法被proguard混淆。

最后,需要把AndroidManifest里面的application改為這個新增的SophixStubApplication類:

 <application
    android:name="com.my.pkg.SophixStubApplication"
    ... ...>
    ... ...

sample源碼


生成、上傳、調(diào)試補丁

下載打包工具:
patch補丁包生成需要使用到打補丁工具SophixPatchTool, 如還未下載打包工具,請前往下載Android打包工具。

該工具提供了Windows和macOS和Linux版本,Windows下運行SophixPatchTool.exe,macOS下運行SophixPatchTool.app,Linux下(Ubuntu 16.04 64bit最佳)運行SophixPatchTool。并且需要安裝Java環(huán)境且在JDK7或以上才能正常使用。

我是先 生成調(diào)試包,有問題的程序,Build apk,改名字為舊包.apk。然后修復(fù)完,再Build apk,改名字為新包.apk。這樣能看Log。測試成功后,再生成發(fā)布包,再測試一遍。

補丁打包工具主對話框
  • 舊包:<必填> 有問題的APK。
  • 新包:<必填> 修復(fù)過該問題APK。
  • 日志:打開日志輸出窗口。
  • 高級:展開高級選項。
  • 設(shè)置:補丁輸出路徑和簽名文件設(shè)置。
  • GO!:開始生成補丁。

點擊【高級】,彈出 補丁和簽名設(shè)置


image.png
  • 強制冷啟動:勾選的話強制生成補丁包為需要冷啟動才能修復(fù)的格式。默認不選的話,工具會根據(jù)代碼變更情況自動選擇即時熱替換或者冷啟動修復(fù)。
  • 不比較資源:打補丁時不比較資源的變化。
  • 不比較SO庫:打補丁時不比較SO庫的變化。
    所以,高級選項可以不做處理。
    強制冷啟動:勾選的話強制生成補丁包為需要冷啟動才能修復(fù)的格式。默認不選的話,工具會根據(jù)代碼變更情況自動選擇即時熱替換或者冷啟動修復(fù)。
    不比較資源:打補丁時不比較資源的變化。
    不比較SO庫:打補丁時不比較SO庫的變化。

點擊【設(shè)置】


image.png
  • 補丁輸出路徑:<必填> 指定生成補丁之后補丁的存放位置,必須是已存在的目錄。
  • Key Store Path:<選填>本地的簽名文件的路徑,不輸入則不做簽名。
  • Key Store Password:<選填>證書文件的密碼。
  • Key Alias:<選填>Key的別名。
  • Key Passwrod:<選填>Key的密碼。
    下面的一般不做處理:
  • AES Key:<選填>自定義aes秘鑰, 必須是16位數(shù)字或字母的組合。必須與setAesKey中設(shè)置的秘鑰一致。
  • Filter Class File:<選填>本地的白名單類列表文件的路徑,放進去的類不會再計算patch,文件格式: 一行一個類名。

Ps:

mac下的補丁工具若出現(xiàn)一打開就崩潰的情況,請將補丁工具移到“應(yīng)用程序”目錄下即可。

點擊 Go ,生成的補丁如下圖:


補丁

補丁文件名必須為:sophix-patch.jar。不能更改。

上傳補丁

  • 首先進入 移動熱修復(fù) 管理控制臺


    移動熱修復(fù) 管理控制臺
  • 點擊App列表里的操作-【管理】,進入詳情頁


    App詳情頁
  • 點擊 【添加版本】,也就是應(yīng)用的版本號
    這里的版本號一定要和工程里的gradle文件里記錄的一致。我截圖上的一個1.0和1.0.0。搞1.0.0測試了半天,沒結(jié)果,傻不傻。gradle里默認是“1.0”

  • 添加完版本,點擊應(yīng)用版本列表下的 【查看詳情】,進入版本詳情頁


    版本詳情頁

點擊 【上傳補丁】,補丁版本列表更新。

  • 點擊 補丁版本列表 的 【查看詳情】,進入 補丁詳情頁,可以查看補丁屬性和補丁狀態(tài)


    補丁詳情頁

上圖有個二維碼,在正式發(fā)布前,我們用測試工具掃碼測試下。
測試工具是個apk。它是通過掃描補丁二維碼,下載到手機上,然后通過在apk界面上輸入你要測試的應(yīng)用包名,將補丁打到應(yīng)用里。

調(diào)試補丁

調(diào)試工具App地址:http://ams-hotfix-repo.oss-cn-shanghai.aliyuncs.com/hotfix_debug_tool-release.apk

調(diào)試工具App界面

那個截圖上的 【斷開連接應(yīng)用】,最開始是 【連接應(yīng)用】

  • 先把你的有bug的apk安裝到手機上
  • 然后打開該調(diào)試工具App,先輸入 bug應(yīng)用 包名,點 【連接應(yīng)用】
  • 點 【掃描二維碼】,掃 網(wǎng)頁上 補丁詳情頁 的二維碼
    接下來,就不用管了。它會下載補丁,并打到應(yīng)用上。

看到調(diào)試App界面是輸出信息,有以下幾條,就代表成功了。
app connect successful.
patch download success.
please restart app to reload new patch as exist old patch.

打開你的bug應(yīng)用,就可以看到變化了。
來個截圖示例,應(yīng)用源碼就是文章前面給的sample


sophix調(diào)試補丁.gif

SophixTest原來只顯示個 helloworld,經(jīng)過Sophix調(diào)試工具V3的打補丁后,再次打開SophixTest就變成了有福利字樣,并顯示張美女圖片。


補丁灰度發(fā)布、全量發(fā)布、機型過濾

注意事項:

  • 支持多渠道包僅選用某個渠道包的補丁,只需要保證變化相同即可,不過對于不同的apk包最好進行全面的測試。
  • 發(fā)布前請嚴格按照:掃碼內(nèi)測 => 灰度發(fā)布 => 全量發(fā)布的流程進行,以保證補丁包能夠正常在所有Android版本的機型上生效。
應(yīng)用版本詳情頁
  • 補丁狀態(tài):
    • 等待中:補丁上傳成功,等待操作。
    • 已灰度:補丁正在進行灰度發(fā)布。
    • 已發(fā)布:補丁已全量發(fā)布至所有設(shè)備。
    • 已停止:補丁發(fā)布行為已暫停。

灰度發(fā)布

在應(yīng)用版本詳情頁,點擊補丁版本列表里的【查看詳情】,進入 補丁詳情頁。


補丁詳情頁

在剛剛上傳完補丁后,補丁處于 等待中 的狀態(tài),勾選 灰度發(fā)布。

設(shè)置完設(shè)備數(shù),客戶端拉取補丁會消耗該設(shè)備數(shù),達到灰度設(shè)備數(shù)后,灰度補丁自動置為停止狀態(tài)。
設(shè)備數(shù):指設(shè)備請求更新該補丁的次數(shù),并不等于絕對設(shè)備數(shù)。

例如:1個設(shè)備請求了2次更新該補丁,則會消耗掉2的設(shè)備數(shù)。

  • 確認發(fā)布
    點擊【確認發(fā)布】,補丁狀態(tài)為 已灰度 ,進入灰度發(fā)布狀態(tài)。


    灰度發(fā)布狀態(tài)

這時,當用戶打開客戶端,就會拉取線上的補丁,修復(fù)程序。
還記得代碼中的queryAndLoadNewPatch()方法嗎,它的作用去看sample源碼注釋。

  • 成功推送設(shè)備數(shù):每當有設(shè)備發(fā)起一次更新請求,且補丁下載成功,則記為一次成功推送。
  • 累計加載設(shè)備數(shù):每當有設(shè)備成功加載該補丁,則記為一次累計加載。

注:

· 只會下載補丁版本號比當前應(yīng)用存在的補丁版本號高的補丁, 比如當前應(yīng)用已經(jīng)下載了補丁版本號為5的補丁, 那么只有后臺發(fā)布的補丁版本號>5才會重新下載.

· 在上傳新的補丁之后,要調(diào)試時,如果以往的補丁有處于 已灰度已發(fā)布狀態(tài),要停止發(fā)布。 如果不停止,最新的補丁處于等待中,也就是未發(fā)布。那么當你打開客戶端,它會拉取以往發(fā)布的補丁修復(fù)程序,這樣會影響你觀測調(diào)試結(jié)果。

· 后臺數(shù)據(jù)可能有少許延遲。

  • 停止發(fā)布
    點擊【停止發(fā)布】后,用戶選擇停止發(fā)布后,系統(tǒng)將停止該補丁的繼續(xù)發(fā)布,但已加載該補丁的設(shè)備會依然保持安裝該補丁的狀態(tài)。

界面變成:


停止發(fā)布 后
  • 繼續(xù)發(fā)布
    用戶點擊【繼續(xù)發(fā)布】后,將可以重新設(shè)置發(fā)布規(guī)則。

如果當前版本在停止前處于灰度中,繼續(xù)發(fā)布可以:

· 重設(shè)灰度發(fā)布規(guī)則,新的規(guī)則中設(shè)備數(shù)必須大于之前的值。
· 改為全量發(fā)布。

灰度狀態(tài)下繼續(xù)發(fā)布

所以,從灰度發(fā)布到全量發(fā)布的步驟是

· 先在補丁詳情頁勾選灰度發(fā)布,點擊確認發(fā)布
· 推送完所有灰度設(shè)備后,點擊停止發(fā)布
· 再點擊繼續(xù)發(fā)布,彈出框里選擇全量發(fā)布

如果當前版本在停止前處于全量發(fā)布,繼續(xù)發(fā)布可以:

繼續(xù)全量發(fā)布。 --- 對,你沒看錯,就是逗你玩!

  • 選擇回滾
    用戶選擇回滾的目標補丁后,所有該應(yīng)用版本下的設(shè)備都會回滾到目標補丁的版本。

使用回滾功能必需要具備一下幾個條件:

· 當前的版本已停止發(fā)布。
· 該版本之前存在至少一個全量發(fā)布的歷史版本。

全量發(fā)布

選擇全量發(fā)布后,將對所有安裝了當前應(yīng)用版本(即之前創(chuàng)建應(yīng)用時所填寫的應(yīng)用版本號)的設(shè)備推送該補丁。

與灰度發(fā)布類似,在全量發(fā)布會可以根據(jù)自身需要停止本次全量發(fā)布,停止發(fā)布后可以選擇:

· 繼續(xù)全量發(fā)布。
· 回滾版本(如果存在歷史版本)

添加過濾機型

全量發(fā)布后,我們可以添加過濾機型。
不全量發(fā)布是不可以添加機型過濾的

image.png

在App版本詳情頁,點擊【添加過濾機型】
點擊添加過濾機型彈出框

這里對過濾機型的彈出框參數(shù)進行說明:

  • 系統(tǒng)版本
    系統(tǒng)版本是指手機所使用的OS的版本。

在控制臺中,有相應(yīng)的系統(tǒng)版本列表可供選擇。如果列表中沒有需要自定義,請按如下標準獲取系統(tǒng)版本。

android.os.Build.VERSION.RELEASE
例如系統(tǒng)版本結(jié)果是:7.1

  • 手機品牌
    手機品牌是指手機貼牌商標代表的品牌,需要區(qū)別手機制造商,手機制造商可能會生產(chǎn)多個品牌,一個品牌也可能是多個制造商生產(chǎn)。

在控制臺中,我們有相應(yīng)的品牌列表供選擇使用。如果需要自定義,請按如下標準獲取手機品牌,注意實際過濾時不區(qū)分大小寫。

android.os.Build.BRAND
例如手機品牌是:Xiaomi

  • 手機機型
    手機機型是指某個手機品牌下手機具體的型號。

目前由于手機機型龐雜,沒有提供選擇列表供選擇,后續(xù)會支持。填寫手機機型時請按如下標準,不區(qū)分大小寫。

android.os.Build.MODEL
例如手機型號是:OPPO R11

【注意】如果想設(shè)置全部機型,請在自定義機型里面,輸入 :all
(就是 冒號+all)

到這里,sophix集成的全部內(nèi)容就結(jié)束了。阿里熱修復(fù)官方的文檔有點瑣碎,我把重點和注意點都挑出來了。讀完這四篇,相信你會迅速集成sophix到自己的應(yīng)用里。

這再給出官方接入文檔地址,給還想看官方文檔的朋友。官方接入文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容