Android視頻直播、點播播放器哪家強?

最近在項目中要加入視頻直播和點播功能,那麼問題來了,我須要一個播放器來播放視頻流,那該如何選擇呢?除了原生的VideoView(VideoView表示臣妾作不到啊),還有一些播放器如Vitamio,B站開源的IjkPlayer等,固然各大直播雲服務商也提供了本身的播放器。花兩天時間調研了幾家,順便記錄下來分享給你們,看一看到底哪家強。html

太長不看版

這裏選擇了開源播放器IjkPlayer和直播雲廠商播放器PLDroidPlayer做爲測試樣本。java

數據統計

軟硬編碼
IjkPlayer
PLDroidPlayer
首開(ms) 內存 min,avg,max(MB) CPU min,avg,max(%) 首開(ms) 內存 min,avg,max(MB) CPU min,avg,max(%)
軟編碼 1559 64.49,110.19,114.92 5.00,30.69,80.72 198 32.34,87.41,93.47 3.11,30.25,67.18
硬編碼 2280 45.37,48.81,52.34 1.36,10.10,17.37 174 30.98,81.67,85.87 2.00,28.00,69.23

包體

對比點
IjkPlayer
PLDroidPlayer
版本 0.8.4 2.0.3
jar/aar包 66KB(java) + 1342KB(armv7a)=1408KB 80KB
so庫(armeabi-v7a,不帶Https功能) 2.58MB 2.27MB
總計 3.96MB 2.35MB

注:linux

  1. IjkPlayer經過gradle下載下來爲aar包,存放在目錄C:\Users\(用戶名)\.gradle\caches\modules-2\files-2.1\tv.danmaku.ijk.media。PLDroidPlayer爲jar包。
  2. IjkPlayer至少須要用到兩個包,分別是java包和armv7a包。
  3. IjkPlayer和PLDroid都可以支持Https,IjkPlayer須要單獨編譯,PLDroid只需添加libqcOpenSSL.so庫便可。這裏對比的是不帶Https功能的。

功能點

這裏只是對主要功能點進行對比,更多PLDroidPlayer功能點介紹能夠查看github.com/pili-engine…,而ijkPlayer並無對其功能進行介紹:github.com/Bilibili/ij…android

功能
IjkPlayer
PLDroidPlayer
版本 0.8.4 2.0.3
RTMP 支持 支持
HLS 支持 支持
HTTP-FLV 支持 支持
HTTPS 支持(須要單獨編譯) 支持
硬解碼 支持 支持
是否須要編譯 須要 不須要
播放控件 不提供 提供
UI定製 能夠 能夠
文檔 不完善 完善
是否開源 開源 不開源
集成難度 略麻煩 容易
技術支持

測試樣本選擇

先說下在測試樣本選擇上我是如何考慮的:git

  1. 開源播放器大V選一家,直播雲廠商選一家。
    ijk無可厚非是開源播放器中的首選,關於如何玩轉ijk的文章也不少,口碑也比較好。而云服務廠商好歹是靠賣直播、點播產品賺錢的,播放器做爲直播點播服務中重要的一環也是人家吃飯的工具,並且能夠說被不少直播大客戶驗證過的,最重要的這些播放器SDK居然都是免費的,不拿來用太浪費了。
  2. 視頻雲廠商播放器評測的選擇。
    由於我司原生app自己體積比較大了,因此對我來講包體大小是第一要考慮的點。而全部在雲廠商裏,我選了包體最小的七牛做爲測試樣本。你們能夠根據本身業務需求再選擇測試樣本。

固然,這裏只是作了幾回數據採樣,須要結果更具說服力,可能還須要更多的測試條件和測試數據,不過咱們能夠從當前獲取到的數據推斷:github

  1. 不論是軟解碼和硬解碼,PLDroidPlayer的首開速度都要遠快於IjkPlayer;
  2. 在軟解碼條件下,PLDroidPlayer的Cpu和內存消耗都要略低於IjkPlayer;
  3. 在硬解碼條件下,PLDroidPlayer的Cpu和內存消耗都要高於IjkPlayer。

基礎

在進行對比以前,咱們須要對直播相關的基礎概念作一些簡單介紹,若是對這一塊比較熟悉的同窗能夠跳過。web

視頻直播

視頻直播就是視頻數據從採集端(攝像頭)經過網絡實時推送到播放端(手機,電腦,電視等),咱們最先接觸到的視頻直播可能就是電視直播了,但隨着智能手機發展,移動直播興起,它的視頻採集端是手機,播放端一般也是手機。android-studio

視頻點播

視頻點播就是一段已經錄製好的視頻數據,用戶能夠點擊播放。因爲是已經錄製完成的視頻數據,因此還能夠控制播放進度。瀏覽器

直播協議

直播協議常見的有三種:RTMP、Http-FLV和HLS。bash

  • RTMP: 基於TCP協議,由Adobe設計,將音視頻數據切割成小的數據包在互聯網上傳輸,延時3s之內,但拆包組包複雜,在海量併發狀況下不穩定。因爲不是基於Http協議,存在被防火牆牆掉的可能性。
  • Http-FLV:基於Http協議,由Adobe設計,在大塊音視頻數據頭部添加標記信息,延時3s之內,海量併發穩定,手機瀏覽器支持不足。
  • HLS:基於Http協議,由Apple設計,將視頻數據切分紅片斷(10s之內),由m3u8索引文件進行管理,高延時(10s到30s),手機瀏覽器支持較好,可經過網頁轉發直播連接。

軟編碼和硬編碼

音視頻數據在互聯網上傳輸以前,因爲存在冗餘數據,須要進行壓縮編碼,編碼存在兩種方式,一種是軟編碼一種是硬編碼。

  • 軟編碼:使用CPU進行編碼
  • 硬編碼:使用非CPU進行編碼,如GPU等

軟解碼和硬解碼

數據進行編碼以後傳輸到播放端,就要進行解碼,那麼解碼也有兩種方式,一種是軟解碼一種是硬解碼。

  • 軟解碼:使用CPU進行解碼
  • 硬解碼:使用非CPU進行解碼

IjkPlayer

IjkPlayer是B站開源播放器,地址爲:github.com/Bilibili/ij…,基於音視頻編解碼庫FFmpeg,支持經常使用的直播協議。IjkPlayer只提供播放器引擎庫,不提供UI界面,因此使用IjkPlayer時還須要對UI界面進行二次封裝,不過Github上有一些基於ijkplayer二次開發的播放器,他們對UI界面作了比較好的封裝。

經過git命令:

git clone https://github.com/Bilibili/ijkplayer.git複製代碼

下載ijkplayer完整項目,而後使用Android Studio打開目錄:ijkplayer\android\ijkplayer,這個是Android的Demo項目,運行以後,效果以下:

可是,咱們暫時仍是沒法播放示例視頻列表當中的視頻,還須要編譯so庫。在編譯so庫的過程當中,躺坑躺到懷疑人生,跟你們分享一下,避免跟我同樣踩坑。

Windows下編譯IjkPlayer

首先你們最好不要在Windows環境下編譯,由於我使用的是Windows系統,因此沒有多想,下載代碼後安裝Cygwin,而後在Cygwin中安裝make,yasm,裝完以後覺得大功告成,開始在Cygwin的命令行中執行編譯腳本,但執行時報錯,由於sh腳本還須要轉換成unix版本,因而在Cygwin中又裝了個dos2unix,將ijkplayer中的全部的sh腳本所有轉換了一遍,而後再執行腳本,在讀取一個配置文件configure又出了問題,仍是文件格式問題,因此能夠預見即便解決了這個文件,後續可能還有一大堆的文件存在這樣的問題,細思極恐,果斷棄坑。

Ubuntu下編譯IjkPlayer

棄坑Windows後又在電腦的虛擬機上安裝了Ubuntu系統,但是等在Ubuntu上面搭建完Android開發環境後,發現硬盤空間不足了,不要問我爲何空間不足,反正就是不足了,我能怎麼辦,我只能選擇原諒本身咯。後來想到我原來的一臺筆記本里面裝了Ubuntu 16.04,因而擦擦上面的灰,開機啓動。

搭建Android開發環境

1.安裝JDK

sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java8-installer
sudo apt-get install oracle-java8-set-default

//在命令行中使用java -version,java, javac命令不報命令找不到,就算安裝成功複製代碼

2.安裝Android Studio

去官網或者國內站點下載Linux版本的Android Studio,這裏,我使用的是Android Studio3.0版本。下載完成後,將Android Studio解壓到/opt目錄,而後使用命令行執行Andorid Studio中的bin/studio.sh啓動Android Studio,進入嚮導界面,嚮導界面最後確認去下載SDK。下載完成後SDK路徑爲/home/xxx(用戶名)/Android/Sdk。

3.下載NDK

使用Android Studio下載完SDK後,不用建立項目,直接打開SDK Manager,在裏面去下載NDK,下載完成以後存放在sdk目錄下的ndk-bundle目錄,但這裏下載的NDK版本是比較新的版本16,而ijkplayer編譯也是不支持的,由於在ijkplayer\android\contrib\tools中有個sh腳本會檢查編譯環境,其中有一段代碼會檢查NDK版本:

case "$IJK_NDK_REL" in
    10e*)
        ........
        echo "IJK_NDK_REL=$IJK_NDK_REL"
        case "$IJK_NDK_REL" in
            11*|12*|13*|14*)
                if test -d ${ANDROID_NDK}/toolchains/arm-linux-androideabi-4.9
                then
                    echo "NDKr$IJK_NDK_REL detected"
                else
                    echo "You need the NDKr10e or later"
                    exit 1
                fi
            ;;
            *)
                echo "You need the NDKr10e or later"
                exit 1
            ;;
        esac
    ;;
esac複製代碼

能夠看出來這裏只支持10e,11,12,13,14,因此ndk版本低了不行,高了也不行,沒辦法,咱們得去從新去官網下載低一點的版本,如r14b。

4.配置SDK和NDK路徑

找到/home/(用戶名)/目錄,使用快捷鍵Ctrl + H顯示隱藏文件,找到.bashrc文件打開,配置本身的SDK和NDK路徑,例如:

export ANDROID_NDK=/home/leon/Android/andriod-ndk-r14b
export ANDROID_SDK=/home/leon/Android/Sdk
export PATH=$ANDROID_NDK:$ANDROID_SDK:$PATH複製代碼

配置完成後,重啓命令行,輸入ndk-build命令,若是不報命令行找不到,說明NDK環境變量配置成功。

編譯IjkPlayer

Android環境搭建好後,就能夠參考官方文檔着手編譯ijkplayer了。

sudo apt-get update
sudo apt-get install git //安裝git
sudo apt-get install yasm //安裝yasm

sudo dpkg-reconfigure dash //在彈出提示框選擇「否」來使用bash

//下載ijkplayer到ijkplayer-android目錄
git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-android
cd ijkplayer-android 

//使用默認配置
cd config
rm module.sh
ln -s module-lite.sh module.sh

cd ..
cd android/contrib
./compile-ffmpeg.sh clean //清理

cd ~/ijkplayer-android //返回源碼根目錄
./init-android.sh //主要是去下載ffmpeg

cd android/contrib
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all //編譯ffmpeg,all是所有編譯,須要等待一段時間

cd .. //回到ijkplayer-android/android
./compile-ijk.sh all //編譯ijkplayer複製代碼

編譯完成後,在android/ijkplayer目錄下各個庫模塊當中找到生成的so庫:


既然so庫已經生成,就可使用Andorid Studio再次打開ijkplayer中的安卓示例項目(android/ijkplayer),運行後就能夠播放示例視頻了。這個帶有so庫的示例項目我已上傳到Github,地址爲 github.com/uncleleonfa…,歡迎下載。

PLDroidPlayer

PLDroidPlayer 是七牛推出的一款適用於 Android 平臺的播放器 SDK,採用全自研的跨平臺播放內核,擁有豐富的功能和優異的性能,可高度定製化和二次開發。示例項目地址爲:github.com/pili-engine…
PLDroidPlayer的集成要比ijkPlayer簡單不少,不用本身編譯so庫,不用本身建立SurfaceView和TextureView來播放視頻。可參考官方開發指南集成便可。

測試開發

爲了保證測試的變量只是播放器引擎自己(這裏暫時將播放器引擎簡單的理解爲各個播放器的MediaPlayer),咱們定義一個公共的UI界面即VideoView來播放視頻流,而後經過代理模式去代理不一樣的播放器引擎。這樣VideoView在播放視頻時,能夠經過代理使用不一樣的播放引擎(MediaPlayer)來播放。咱們這裏主要測試播放器播放視頻首開的時間,播放器播放視頻過程當中Cpu,內存的佔用狀況。測試項目地址爲:github.com/uncleleonfa…,測試項目運行效果:

IMediaPlayer

定義統一的MediaPlayer接口。

public interface IMediaPlayer {

    void prepareAsync() throws IllegalStateException;

    void start() throws IllegalStateException;

    void stop() throws IllegalStateException;

    void pause() throws IllegalStateException;

    void release();

    void reset();

    .......
}複製代碼

IMeidaPlayerProxy

定義MeidaPlayer的代理接口,全部MediaPlayer的代理必須實現newInstance接口建立MediaPlayer。

interface IMediaPlayerProxy {
    IMediaPlayer newInstance();
}複製代碼

IjkPlayer的MediaPlayer的代理

在使用IjkPlayer以前須要添加依賴,而且將編譯好的so庫添加到項目中的jniLibs下。

//添加ijkplayer依賴
dependencies {
    compile 'tv.danmaku.ijk.media:ijkplayer-java:0.8.4'
    compile 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.4'
    compile 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.4'

}

//IjkMediaPlayer代理,實現IMediaPlayer接口
public class IjkMediaPlayerProxy implements IMediaPlayerProxy, IMediaPlayer {

    //聲明一個IjkMediaPlayer對象
    private IjkMediaPlayer mIjkMediaPlayer;

    @Override
    public IMediaPlayer newInstance() {
        //建立IjkMeidaPlayer對象
        mIjkMediaPlayer = new IjkMediaPlayer();
        return this;
    }

    @Override
    public void prepareAsync() throws IllegalStateException {
        mIjkMediaPlayer.prepareAsync();
    }

    @Override
    public void start() throws IllegalStateException {
        mIjkMediaPlayer.start();
    }

    @Override
    public void stop() throws IllegalStateException {
        mIjkMediaPlayer.stop();
    }
    ......
}複製代碼

PLDroidPlayer的MediaPlayer代理

在使用PLMediaPlayer以前參考官方文檔集成PLDroidPlayer

//PLMediaPlayer代理,實現IMediaPlayer接口
public class PLMediaPlayerProxy implements IMediaPlayerProxy, IMediaPlayer {

    //定義PLMediaPlayer對象
    private PLMediaPlayer mMediaPlayer;
    //AVOptions爲MediaPlayer的選項配置,例如能夠配置開啓硬解碼
    private AVOptions mAvOptions;


    @Override
    public IMediaPlayer newInstance() {
        //建立PLDroidPlayer的PLMediaPlayer對象
        mMediaPlayer = new PLMediaPlayer(mContext, mAvOptions);
        return this;
    }


    @Override
    public void prepareAsync() throws IllegalStateException {
        mMediaPlayer.prepareAsync();
    }

    @Override
    public void start() throws IllegalStateException {
        mMediaPlayer.start();
    }

    @Override
    public void stop() throws IllegalStateException {
        mMediaPlayer.stop();
    }

    ........
}複製代碼

VideoView

VideoView仿照原生VideoView的實現,這裏主要修改的是MediaPlayer的邏輯,方便配置使用不一樣播放器的MediaPlayer。

public class VideoView extends SurfaceView implements IMediaPlayer.OnPreparedListener,
        IMediaPlayer.OnErrorListener,
        IMediaPlayer.OnCompletionListener,
        IMediaPlayer.OnInfoListener,
        IMediaPlayer.OnVideoSizeChangeListener{

    //定義MediaPlayer代理
    private IMediaPlayerProxy mMediaPlayerProxy;

    //定義VideoView使用的MediaPlayer
    private IMediaPlayer mMediaPlayer;


    //設置MediaPlayer的代理
    public void setMediaPlayerProxy(IMediaPlayerProxy mediaPlayerProxy) {
        mMediaPlayerProxy = mediaPlayerProxy;
    }

    //打開視頻
    private void openVideo() {
        if (mVideoPath == null) {
            return;
        }
        release();
        //使用代理建立對應的MediaPlayer對象
        mMediaPlayer = mMediaPlayerProxy.newInstance();
        mMediaPlayer.setScreenOnWhilePlaying(true);
        mMediaPlayer.setDisplay(mSurfaceHolder);
        mMediaPlayer.setLogEnabled(BuildConfig.DEBUG);
        mMediaPlayer.setOnPreparedListener(this);
        mMediaPlayer.setOnInfoListener(this);
        mMediaPlayer.setOnCompletionListener(this);
        mMediaPlayer.setOnErrorListener(this);
        mMediaPlayer.setOnVideoSizeChangeListener(this);
        try {
            mMediaPlayer.setDataSource(mVideoPath);
            ......
            mMediaPlayer.prepareAsync();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onPrepared(IMediaPlayer iMediaPlayer) {
        iMediaPlayer.start();//開始播放
    }
}複製代碼

LogUtils

LogUtils用於採樣cpu和內存數據,裏面使用ScheduledThreadPoolExecutor每隔1s採樣一次數據。

//開始採樣
public void start() {
    scheduler.scheduleWithFixedDelay(new SampleTask(), 0L, 1000L, TimeUnit.MILLISECONDS);
}
//中止採樣
public void stop() {
    scheduler.shutdown();
}

//採樣任務
private class SampleTask implements Runnable {

    @Override
    public void run() {
        float cpu = sampleCPU(); //採樣CPU使用
        float mem = sampleMemory(); //採樣內存使用
    }
}複製代碼

LogView

LogView是打印Log的自定義控件,它由一個TextView和ScrollView組成,TextView在ScrollView內部,用來顯示log,ScrollView用來滾動。

public class LogView extends RelativeLayout {

    public LogView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.view_log, this);

        final TextView textView = findViewById(R.id.tv);
        final ScrollView scrollView = findViewById(R.id.scroll_view);
        final StringBuilder stringBuilder = new StringBuilder();

        //監聽LogUtils的log
        LogUtils.getInstance().setOnUpdateLogListener(new LogUtils.OnUpdateLogListener() {
            @Override
            public void onUpdate(final long timestamp, final String msg) {
                //在主線程刷新界面
                post(new Runnable() {
                    @Override
                    public void run() {
                        String dateString = mSimpleDateFormat.format(new Date(timestamp));
                        String log = dateString + ": " + msg + "\n";
                        //添加一行log
                        stringBuilder.append(log);
                        //設置log顯示
                        textView.setText(stringBuilder.toString());
                        //滾動ScrollView到底部
                        scrollView.fullScroll(View.FOCUS_DOWN);
                    }
                });

            }
        });
    }

}複製代碼

測試

測試視頻流是:

//點播MP4視頻
String path = "http://hc.yinyuetai.com/uploads/videos/common/2B40015FD4683805AAD2D7D35A80F606.mp4?sc=364e86c8a7f42de3&br=783&rd=Android";
//HLS直播流
String path = "http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8";複製代碼

在VieoView中,MediaPlayer開始準備播放以前,初始化LogUtils,埋點記錄MediaPlayer的準備時間。

try {
    //設置視頻源
    mMediaPlayer.setDataSource(mVideoPath);
    //初始化LogUtils
    LogUtils.getInstance().init(getContext());
    //記錄開始準備時間
    LogUtils.getInstance().onStartPrepare();
    mMediaPlayer.prepareAsync();
} catch (IOException e) {
    e.printStackTrace();
}複製代碼

當MediaPlayer準備好後,會回調onPrepared,再次記錄準備結束時間,這樣,準備結束時間減去準備開始時間就是MediaPlayer準備耗時,即咱們的首開時間。

//準備好後的回調
@Override
public void onPrepared(IMediaPlayer iMediaPlayer) {
    //記錄準備結束時間
    LogUtils.getInstance().onEndPrepare();
    //開始播放
    iMediaPlayer.start();
    //開始每隔1s採樣,播放結束後中止採樣,主要用於點播採樣
    LogUtils.getInstance().start();

    //開始每隔1s採樣,採樣5min,5min以後,自行中止,主要用於直播採樣
    //LogUtils.getInstance().startForDuration(5);
}

//播放結束
@Override
public void onCompletion(IMediaPlayer iMediaPlayer) {
    //播放結束,中止採樣
    LogUtils.getInstance().stop();
}複製代碼

測試IjkPlayer

建立一個IjkPlayerActivity使用IjkMediaPlayer來播放視頻。

public class IjkPlayerActivity extends AppCompatActivity{

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ijkplayer);


        //初始化IjkPlayer
        IjkMediaPlayer.loadLibrariesOnce(null);
        IjkMediaPlayer.native_profileBegin("libijkplayer.so");

        VideoView videoView = findViewById(R.id.video_view);
        //設置IjkMediaPlayer代理
        videoView.setMediaPlayerProxy(new IjkMediaPlayerProxy());
        String path = "視頻url"
        //設置視頻url
        videoView.setVideoPath(path);
    }


    @Override
    protected void onStop() {
        super.onStop();
        //通知IjkMediaPlayer結束
        IjkMediaPlayer.native_profileEnd();
    }

}複製代碼

另外,在VideoView初始化MediaPlayer時,能夠調用enableMediaCodec()來開啓IjkPlayer的硬解碼:

private void openVideo() {
        .......
        mMediaPlayer.enableMediaCodec();
 }複製代碼

測試PLDroidPlayer

建立一個PLDroidPlayerActivity使用PLMediaPlayer來播放視頻。

public class PLDroidPlayerActivity extends AppCompatActivity{

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pldroid);

        VideoView mVideoView = findViewById(R.id.video_view);

        //配置AVoptions來開啓硬編碼,默認爲軟編碼
        AVOptions avOptions = new AVOptions();
        avOptions.setInteger(AVOptions.KEY_MEDIACODEC, AVOptions.MEDIA_CODEC_HW_DECODE);

        mVideoView.setMediaPlayerProxy(new PLMediaPlayerProxy(this, avOptions));
        String path = "視頻url";
        mVideoView.setVideoPath(path);
    }

}複製代碼

測試結果

Round 1

  • 機型:Moto G (2代)
  • 系統版本:5.1
  • 數據源:String path = "ivi.bupt.edu.cn/hls/cctv1hd…";
  • 軟硬編碼:軟解碼
  • 採樣時長:5min
  • IjkPlayer版本:0.8.4
  • PLDroid版本:2.0.3

IjkPlayer結果

  • 首開時間:1559ms
  • CPU佔比最小值:5.00%
  • CPU佔比最大值:80.72%
  • 平均CPU佔比:30.69%
  • 內存使用最小值:63.49MB
  • 內存使用最大值:114.92MB
  • 平均內存:110.19MB

PLDroidPlayer結果

  • 首開時間:198ms
  • CPU佔比最小值:3.11%
  • CPU佔比最大值:67.18%
  • 平均CPU佔比:30.25%
  • 內存使用最小值:32.34MB
  • 內存使用最大值:93.47MB
  • 平均內存:87.41MB

Round 2

  • 機型:Moto G (2代)
  • 系統版本:5.1
  • 數據源:String path = "ivi.bupt.edu.cn/hls/cctv1hd…";
  • 軟硬編碼:硬解碼
  • 採樣時長:5min
  • IjkPlayer版本:0.8.4
  • PLDroid版本:2.0.3

IjkPlayer結果

  • 首開時間:2280ms
  • CPU佔比最小值:1.36%
  • CPU佔比最大值:17.37%
  • 平均CPU佔比:10.10%
  • 內存使用最小值:45.37MB
  • 內存使用最大值:52.34MB
  • 平均內存:48.81MB

PLDroidPlayer結果

  • 首開時間:174ms
  • CPU佔比最小值:2.00%
  • CPU佔比最大值:69.23%
  • 平均CPU佔比:28.00%
  • 內存使用最小值:30.98MB
  • 內存使用最大值:85.87MB
  • 平均內存:81.67MB

相關文章
相關標籤/搜索