主頁 > 企業開發 > Deferred Components-實作Flutter運行時動態下發Dart代碼 | 京東云技術團隊

Deferred Components-實作Flutter運行時動態下發Dart代碼 | 京東云技術團隊

2023-05-23 12:25:14 企業開發

導讀

Deferred Components,官方實作的Flutter代碼動態下發的方案,本文主要介紹官方方案的實作細節,探索在國內環境下使用Deferred Components,并且實作了最小驗證demo,讀罷本文,你就可以實作Dart檔案級別代碼的動態下發,

一、引言

Deferred Components是Flutter2.2推出的功能,依賴于Dart2.13新增的對Split AOT編譯支持,將可以在運行時每一個可單獨下載的Dart庫、assets資源包稱之為延遲加載組件,即Deferred Components,Flutter代碼編譯后,所有的業務邏輯都會打包在libapp.so一個檔案里,但如果使用了延遲加載,便可以分拆為多個so檔案,甚至一個Dart檔案也可以編譯成一個單獨的so檔案,

這樣帶來的好處是顯而易見的,可以將一些不常用功能放到單獨的so檔案中,當用戶使用時再去下載,可以大大降低安裝包的大小,提高應用的下載轉換率,另外,因為Flutter具備了運行時動態下發的能力,這讓大家看到了實作Flutter熱修復的另一種可能,截止目前來講,官方的實作方案必須依賴Google Play,雖然也針對中國的開發者給出了不依賴Google Play的自定義方案,但是并沒有給出實作細節,市面上也沒有自定義實作的文章,本文會先簡單介紹官方實作方案,并探究其細節,尋找自定義實作的思路,最侄訓實作一個最小Demo供大家參考,

二、官方實作方案探究

2.1 基本步驟

2.1.1.引入play core依賴,

dependencies {
  implementation "com.google.android.play:core:1.8.0"
}

2.1.2.修改Application類的onCreate方法和attachBaseContext方法,

@Override
protected void onCreate(){
 super.onCreate()
// 負責deferred components的下載與安裝
 PlayStoreDeferredComponentManager deferredComponentManager = new
  PlayStoreDeferredComponentManager(this, null);
FlutterInjector.setInstance(new FlutterInjector.Builder()
    .setDeferredComponentManager(deferredComponentManager).build());
}


@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    // Emulates installation of future on demand modules using SplitCompat.
    SplitCompat.install(this);
}

2.1.3.修改pubspec.yaml檔案,

flutter:
    deferred-components:

2.1.4.在flutter工程里新增box.dart和some_widgets.dart兩個檔案,DeferredBox就是要延遲加載的控制元件,本例中box.dart被稱為一個加載單元,即loading_unit,每一個loading_unit對應唯一的id,一個deferred component可以包含多個加載單元,記得這個概念,后續會用到,

// box.dart


import 'package:flutter/widgets.dart';


/// A simple blue 30x30 box.
class DeferredBox extends StatelessWidget {
  DeferredBox() {}


  @override
  Widget build(BuildContext context) {
    return Container(
      height: 30,
      width: 30,
      color: Colors.blue,
    );
  }
}
import 'box.dart' deferred as box;


class SomeWidget extends StatefulWidget {
  @override
  _SomeWidgetState createState() => _SomeWidgetState();
}


class _SomeWidgetState extends State<SomeWidget> {
  Future<void> _libraryFuture;


  @override
  void initState() {
 //只有呼叫了loadLibrary方法,才會去真正下載并安裝deferred components.
    _libraryFuture = box.loadLibrary();
    super.initState();
  }


  @override
  Widget build(BuildContext context) {
    return FutureBuilder<void>(
      future: _libraryFuture,
      builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          if (snapshot.hasError) {
            return Text('Error: ${snapshot.error}');
          }
          return box.DeferredBox();
        }
        return CircularProgressIndicator();
      },
    );
  }
}

2.1.5.然后在main.dart里面新增一個跳轉到SomeWidget頁面的按鈕,

 Navigator.push(context, MaterialPageRoute(
      builder: (context) {
        return const SomeWidget();
      },
    ));

2.1.6.terminal里運行 flutter build appbundle 命令,此時,gen_snapshot不會立即去編譯app,而是先運行一個驗證程式,目的是驗證此工程是否符合動態下發dart代碼的格式,第一次構建時肯定不會成功,你只需要按照編譯提示去修改即可,當全部修改完畢后,會得到最終的.aab型別的安裝包,

以上便是官方實作方案的基本步驟,更多細節可以參考官方檔案
https://docs.flutter.dev/perf/deferred-components

2.2 本地驗證

在將生成的aab安裝包上傳到Google Play上之前,最好先本地驗證一下,

首先你需要下載bundletool,然后依次運行下列命令就可以將aab安裝包裝在手機上進行最終的驗證了,

java -jar bundletool.jar build-apks --bundle=<your_app_project_dir>/build/app/outputs/bundle/release/app-release.aab --output=<your_temp_dir>/app.apks --local-testing


java -jar bundletool.jar install-apks --apks=<your_temp_dir>/app.apks

2.3 loadLibrary()方法呼叫的生命周期

圖1 官方實作方案介紹圖

(來源:https://github.com/flutter/flutter/wiki/Deferred-Components)

從官方的實作方案中可以知道,只有呼叫了loadLibrary方法后,才會去真正執行deferred components的下載與安裝作業,現在著重看下此方法的生命周期,

呼叫完loadLibrary方法后,dart會在內部查詢此加載單元的id,并將其一直向下傳遞,當到達jni層時,jni負責將此加載單元對應的deferred component的名字以及此加載單元id一塊傳遞給
PlayStoreDynamicFeatureManager,此類負責從Google Play Store服務器下載對應的Deferred Components并負責安裝,安裝完成后會逐層通知,最終告訴dart層,在下一幀渲染時展示動態下發的控制元件,

三、自定義實作

3.1 思路

梳理了loadLibrary方法呼叫的生命周期后,只需要自己實作一個類來代替
PlayStoreDynamicFeatureManager的功能即可,在官方方案中具體負責完成PlayStoreDynamicFeatureManager功能的物體類是io.flutter.embedding.engine.deferredcomponents.PlayStoreDeferredComponentManager,其繼承自DeferredComponentManager,分析原始碼得知,它最重要的兩個方法是installDeferredComponent和loadDartLibrary,

  • installDeferredComponent:這個方法主要負責component的下載與安裝,下載安裝完成后會呼叫loadLibrary方法,如果是asset-only component,那么也需要呼叫DeferredComponentChannel.completeInstallSuccess或者DeferredComponentChannel.completeInstallError方法,
  • loadDartLibrary:主要是負責找到so檔案的位置,并呼叫FlutterJNI dlopen命令打開so檔案,你可以直接傳入apk的位置,flutterJNI會直接去apk里加載so,避免處理解壓apk的邏輯,

那基本思路就有了,自己實作一個物體類,繼承DeferredComponentManager,實作這兩個方法即可,

3.2 代碼實作

本例只是最小demo實作,cpu架構采用arm64,且暫不考慮asset-only型別的component,

3.2.1.新增
CustomDeferredComponentsManager類,繼承DeferredComponentManager,

3.2.2.實作installDeferredComponent方法,將so檔案放到外部SdCard存盤里,代碼負責將其拷貝到應用的私有存盤中,以此來模擬網路下載程序,代碼如下:

@Override
public void installDeferredComponent(int loadingUnitId, String componentName) {
    String resolvedComponentName = componentName != null ? componentName : loadingUnitIdToComponentNames.get(loadingUnitId);
    if (resolvedComponentName == null) {
         Log.e(TAG, "Deferred component name was null and could not be resolved from loading unit id.");
         return;
     }
     // Handle a loading unit that is included in the base module that does not need download.
     if (resolvedComponentName.equals("") && loadingUnitId > 0) {
     // No need to load assets as base assets are already loaded.
         loadDartLibrary(loadingUnitId, resolvedComponentName);
         return;
     }
     //耗時操作,模擬網路請求去下載android module
     new Thread(
         () -> {
//將so檔案從外部存盤移動到內部私有存盤中
              boolean result = moveSoToPrivateDir();
              if (result) {
                 //模擬網路下載,添加2秒網路延遲
                 new Handler(Looper.getMainLooper()).postDelayed(
                                () -> {
                                    loadAssets(loadingUnitId, resolvedComponentName);
                                    loadDartLibrary(loadingUnitId, resolvedComponentName);
                                    if (channel != null) {
                                        channel.completeInstallSuccess(resolvedComponentName);
                                    }
                                }
                                , 2000);
                 } else {
                        new Handler(Looper.getMainLooper()).post(
                                () -> {
                                    Toast.makeText(context, "未在sd卡中找到so檔案", Toast.LENGTH_LONG).show();


                                    if (channel != null) {
                                        channel.completeInstallError(resolvedComponentName, "未在sd卡中找到so檔案");
                                    }


                                    if (flutterJNI != null) {
                                        flutterJNI.deferredComponentInstallFailure(loadingUnitId, "未在sd卡中找到so檔案", true);
                                    }
                                }
                        );
                  }
              }
        ).start();
    }

3.2.3.實作loadDartLibrary方法,可以直接拷貝
PlayStoreDeferredComponentManager類中的此方法,注釋已加,其主要作用就是在內部私有存盤中找到so檔案,并呼叫FlutterJNI dlopen命令打開so檔案,

  @Override
    public void loadDartLibrary(int loadingUnitId, String componentName) {
        if (!verifyJNI()) {
            return;
        }
        // Loading unit must be specified and valid to load a dart library.
        //asset-only的component的unit id為-1,不需要加載so檔案
        if (loadingUnitId < 0) {
            return;
        }


        //拿到so的檔案名字
        String aotSharedLibraryName = loadingUnitIdToSharedLibraryNames.get(loadingUnitId);
        if (aotSharedLibraryName == null) {
            // If the filename is not specified, we use dart's loading unit naming convention.
            aotSharedLibraryName = flutterApplicationInfo.aotSharedLibraryName + "-" + loadingUnitId + ".part.so";
        }


        //拿到支持的abi格式--arm64_v8a
        // Possible values: armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, mips, mips64
        String abi;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            abi = Build.SUPPORTED_ABIS[0];
        } else {
            abi = Build.CPU_ABI;
        }
        String pathAbi = abi.replace("-", "_"); // abis are represented with underscores in paths.


        // TODO(garyq): Optimize this apk/file discovery process to use less i/o and be more
        // performant and robust.


        // Search directly in APKs first
        List<String> apkPaths = new ArrayList<>();
        // If not found in APKs, we check in extracted native libs for the lib directly.
        List<String> soPaths = new ArrayList<>();


        Queue<File> searchFiles = new LinkedList<>();
        // Downloaded modules are stored here--下載的 modules 存盤位置
        searchFiles.add(context.getFilesDir());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            //第一次通過appbundle形式安裝的split apks位置
            // The initial installed apks are provided by `sourceDirs` in ApplicationInfo.
            // The jniLibs we want are in the splits not the baseDir. These
            // APKs are only searched as a fallback, as base libs generally do not need
            // to be fully path referenced.
            for (String path : context.getApplicationInfo().splitSourceDirs) {
                searchFiles.add(new File(path));
            }
        }


        //查找apk和so檔案
        while (!searchFiles.isEmpty()) {
            File file = searchFiles.remove();
            if (file != null && file.isDirectory() && file.listFiles() != null) {
                for (File f : file.listFiles()) {
                    searchFiles.add(f);
                }
                continue;
            }
            String name = file.getName();
            // Special case for "split_config" since android base module non-master apks are
            // initially installed with the "split_config" prefix/name.
            if (name.endsWith(".apk")
                    && (name.startsWith(componentName) || name.startsWith("split_config"))
                    && name.contains(pathAbi)) {
                apkPaths.add(file.getAbsolutePath());
                continue;
            }
            if (name.equals(aotSharedLibraryName)) {
                soPaths.add(file.getAbsolutePath());
            }
        }


        List<String> searchPaths = new ArrayList<>();


        // Add the bare filename as the first search path. In some devices, the so
        // file can be dlopen-ed with just the file name.
        searchPaths.add(aotSharedLibraryName);


        for (String path : apkPaths) {
            searchPaths.add(path + "!lib/" + abi + "/" + aotSharedLibraryName);
        }
        for (String path : soPaths) {
            searchPaths.add(path);
        }
//打開so檔案
        flutterJNI.loadDartDeferredLibrary(loadingUnitId, searchPaths.toArray(new String[searchPaths.size()]));
    }

3.2.4.修改Application的代碼并洗掉
com.google.android.play:core的依賴,

override fun onCreate() {
        super.onCreate()
        val deferredComponentManager = CustomDeferredComponentsManager(this, null)
        val injector = FlutterInjector.Builder().setDeferredComponentManager(deferredComponentManager).build()
        FlutterInjector.setInstance(injector)

至此,核心代碼全部實作完畢,其他細節代碼可以見
https://coding.jd.com/jd_logistic/deferred_component_demo/,需要加權限的聯系shenmingliang1即可,

3.3 本地驗證

  • 運行 flutter build appbundle --release --target-platform android-arm64 命令生成app-release.aab檔案,
  • .運行下列命令將app-release.aab決議出本地可以安裝的apks檔案:java -jar bundletool.jar build-apks --bundle=app-release.aab --output=app.apks --local-testing
  • 解壓上一步生成的app.apks檔案,在加壓后的app檔案夾下找到splits/scoreComponent-arm64_v8a_2.apk,繼續解壓此apk檔案,在生成的scoreComponent-arm64_v8a_2檔案夾里找到lib/arm64-v8a/libapp.so-2.part.so 檔案,
  • 執行 java -jar bundletool.jar install-apks --apks=app.apks命令安裝app.apks,此時打開安裝后的app,點擊首頁右下角的按鈕跳轉到DeferredPage頁面,此時頁面不會成功加載,并且會提示你“未在sd卡中找到so檔案”,
  • 將第3步找到的lipase.so-2.part.so push到指定檔案夾下,命令如下 adb push libapp.so-2.part.so /storage/emulated/0/Android/data/com.example.deferred_official_demo/files,重啟app行程,并重新打開DeferredPage界面即可,

四、 總結

官方實作方案對國內的使用來講,最大的限制無疑是Google Play,本文實作了一個脫離Google Play限制的最小demo,驗證了deferred components在國內使用的可行性,

參考:

  1. https://docs.flutter.dev/perf/deferred-components
  2. https://github.com/flutter/flutter/wiki/Deferred-Components

作者:京東物流 沈明亮

內容來源:京東云開發者社區

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/553178.html

標籤:其他

上一篇:JS中的undefined 與 null

下一篇:返回列表

標籤雲
其他(159540) Python(38162) JavaScript(25444) Java(18101) C(15231) 區塊鏈(8267) C#(7972) AI(7469) 爪哇(7425) MySQL(7207) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5871) 数组(5741) R(5409) Linux(5340) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4575) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2433) ASP.NET(2403) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1976) 功能(1967) Web開發(1951) HtmlCss(1941) C++(1920) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1878) .NETCore(1861) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • Deferred Components-實作Flutter運行時動態下發Dart代碼 | 京東

    Deferred Components,官方實作的Flutter代碼動態下發的方案。本文主要介紹官方方案的實作細節,探索在國內環境下使用Deferred Components,并且實作了最小驗證demo。讀罷本文,你就可以實作Dart檔案級別代碼的動態下發。 ......

    uj5u.com 2023-05-23 12:25:14 more
  • JS中的undefined 與 null

    在 JavaScript 中, undefined 和 null 是兩個特殊的值,用于表示缺失或空值。 undefined 是一個表示未定義或未賦值的原始值。它在以下情況下使用: 1. 變數宣告了但未初始化時,默認為 undefined 。 let x; console.log(x); // und ......

    uj5u.com 2023-05-23 12:25:09 more
  • JS中的undefined 與 null

    在 JavaScript 中, undefined 和 null 是兩個特殊的值,用于表示缺失或空值。 undefined 是一個表示未定義或未賦值的原始值。它在以下情況下使用: 1. 變數宣告了但未初始化時,默認為 undefined 。 let x; console.log(x); // und ......

    uj5u.com 2023-05-23 12:24:47 more
  • Deferred Components-實作Flutter運行時動態下發Dart代碼 | 京東

    Deferred Components,官方實作的Flutter代碼動態下發的方案。本文主要介紹官方方案的實作細節,探索在國內環境下使用Deferred Components,并且實作了最小驗證demo。讀罷本文,你就可以實作Dart檔案級別代碼的動態下發。 ......

    uj5u.com 2023-05-23 12:23:53 more
  • React筆記-Hooks(九)

    # React筆記-Hooks(九) ## Hooks ### 概念 >React Hooks 的意思是 組件盡量寫成純函式 如果需要外部功能和副作用 就用鉤子把外部代碼"鉤"進來 ### 函陣列件和類組件區別 >- 函陣列件沒有狀態(state) 類組件有 >- 函陣列件沒有生命周期 類組件有(掛 ......

    uj5u.com 2023-05-23 09:12:25 more
  • 記錄--九個超級好用的 Javascript 技巧

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 在實際的開發作業程序中,積累了一些常見又超級好用的 Javascript 技巧和代碼片段,包括整理的其他大神的 JS 使用技巧,今天篩選了 9 個,以供大家參考。 1、動態加載 JS 檔案 在一些特殊的場景下,特別是一些庫和框架的開 ......

    uj5u.com 2023-05-23 09:12:15 more
  • HTTP1.0、HTTP1.1、HTTP2.0 協議的區別

    HTTP 1.1相比HTTP 1.0具有以下優點: 1. 持久連接 :HTTP 1.1引入了持久連接機制,允許多個請求和回應復用同一個TCP連接。這樣可以減少建立和關閉連接的開銷,提高性能和效率。2. 流水線處理 :HTTP 1.1支持流水線處理,即可以同時發送多個請求,不需要等待前一個請求的回應。 ......

    uj5u.com 2023-05-23 09:12:09 more
  • 使用 React Three Fiber 和 GSAP 實作 WebGL 輪播影片

    參考:[Building a WebGL Carousel with React Three Fiber and GSAP](https://tympanus.net/codrops/2023/04/27/building-a-webgl-carousel-with-react-three-fibe ......

    uj5u.com 2023-05-23 09:12:04 more
  • 記錄--九個超級好用的 Javascript 技巧

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 在實際的開發作業程序中,積累了一些常見又超級好用的 Javascript 技巧和代碼片段,包括整理的其他大神的 JS 使用技巧,今天篩選了 9 個,以供大家參考。 1、動態加載 JS 檔案 在一些特殊的場景下,特別是一些庫和框架的開 ......

    uj5u.com 2023-05-23 09:11:49 more
  • HTTP1.0、HTTP1.1、HTTP2.0 協議的區別

    HTTP 1.1相比HTTP 1.0具有以下優點: 1. 持久連接 :HTTP 1.1引入了持久連接機制,允許多個請求和回應復用同一個TCP連接。這樣可以減少建立和關閉連接的開銷,提高性能和效率。2. 流水線處理 :HTTP 1.1支持流水線處理,即可以同時發送多個請求,不需要等待前一個請求的回應。 ......

    uj5u.com 2023-05-23 09:11:43 more