通过在 Android 中归档 ffmpeg/ffprobe 库来减小应用程序大小

Reducing app size by archiving ffmpeg/ffprobe library in Android

我在互联网上看到很多 post 关于如何减少 Android 中的 FFmpeg/FFprobe 库大小的文章。我也对这个话题进行了大约几个月的调查。所以我决定分享我的结论。我不是在谈论编译时设置和 disable/enabling 功能。当然,您可以这样做以获得更好的结果,但 post 与此无关。它是关于归档FFmpeg/FFprobe 编译文件,这将将您的库大小减少 85%

基于FFMpeg/FFprobe compiled for Android

1. 将资产文件压缩 (ffmpeg && ffprobe) 为任何存档格式,并替换为当前文件。

2. 使用一些额外的代码,使用 asynctask

复制 app 目录中的存档文件

3. 使用 this 库提取存档(也支持 rar 存档)。

结果:79.8MB ==> 13.2MB

提取时间:1 秒(荣耀 8 Lite API 25/26。)。




无压缩


有压缩


我正在使用此设置进行压缩:

Windows 10 64 位,

7-Zip申请,

.7z 存档格式,

Ultra 压缩级别,

LZMA2压缩方式.

您可以尝试其他压缩方法和压缩格式来测试不同的结果。





copy/extract存档到应用程序目录所需的代码:


将此添加到您想要初始化复制过程的任何位置(我更喜欢在 Application class 的 onCreate 内)。

FFmpegArchiveUtil.initFFmpegBinary(this, new FFmpegArchiveUtil.FFmpegSupportCallback() {
    @Override
    public void isFFmpegSupported(boolean isSupported) {
        Log.d("isSupported: " , String.valueOf(isSupported));
    }
});

以上方法将处理以下功能:

  • 将存档从资产复制到应用程序目录。

  • 检查 CPU 拱形模型。

  • 根据 CPU 模型提取存档。

  • 提取后删除存档。

  • 使文件可执行。

  • return true/false 设备是否支持 ffmpeg。

完整代码:

添加到 build.gradle :

android {
    defaultConfig {
        ndk {
            abiFilters 'x86' , 'arm64-v8a' , 'armeabi-v7a'
        }
    }
}

dependencies {
    implementation 'com.hzy:libp7zip:1.6.0'
    implementation 'commons-io:commons-io:2.6'
}

FFmpegArchiveUtil.java

import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;

import com.hzy.libp7zip.P7ZipApi;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;


@SuppressWarnings({"unused", "WeakerAccess"})
public class FFmpegArchiveUtil {


    public static final int VERSION = 17; // up this version when you add a new ffmpeg build
    public static final String KEY_PREF_VERSION = "ffmpeg_version";
    public static final String FFMPEG_ARCHIVE = "ffmpeg_arch.7z";
    public static final String FFMPEG_FILE_NAME = "ffmpeg";
    public static final String FFPROBE_FILE_NAME = "ffprobe";

    public static File getFFmpegArchive(Context context) {
        return new File(context.getFilesDir(), FFMPEG_ARCHIVE);
    }

    public static File getFFmpegFile(Context context) {
        return new File(context.getFilesDir(), FFMPEG_FILE_NAME);
    }

    public static File getFFprobe(Context context) {
        return new File(context.getFilesDir(), FFPROBE_FILE_NAME);
    }


    public interface FFmpegSupportCallback {
        void isFFmpegSupported(boolean isSupported);
    }


    private static class FFmpegArchiveCopyTask extends AsyncTask<Void, Void, Boolean> {
        InputStream stream;
        File ffmpegArchive;
        FFmpegSupportCallback callback;

        FFmpegArchiveCopyTask(InputStream stream, File file, FFmpegSupportCallback callback) {
            this.ffmpegArchive = file;
            this.stream = stream;
            this.callback = callback;
        }

        @Override
        protected Boolean doInBackground(Void... params) {
            File ffmpegFile = new File(FilenameUtils.getFullPath(ffmpegArchive.getAbsolutePath()) + FFMPEG_FILE_NAME);
            if (ffmpegFile.exists()) {
                return true;
            } else {
                if (ffmpegArchive.exists()) {
                    return true;
                } else {
                    try {
                        FileUtils.copyToFile(stream, ffmpegArchive);
                        return true;
                    } catch (Exception e) {
                        return false;
                    }
                }
            }
        }

        @Override
        protected void onPostExecute(Boolean result) {
            try {
                FFmpegExtractorAsyncTask fFmpegExtractorAsyncTask = new FFmpegExtractorAsyncTask(stream, ffmpegArchive, callback);
                fFmpegExtractorAsyncTask.execute();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


    @SuppressWarnings("DanglingJavadoc")
    public static class FFmpegExtractorAsyncTask extends AsyncTask<Void, Void, Boolean> {


        InputStream stream;

        /**
         String archiveFormat =  {@link FFMPEG_ARCHIVE}
         */


        /**
         * The outPut path for copying archiveFormat
         * /data/user/0/com.symphonyrecords.mediacomp/files/
         */
        String bb;


        /**
         * FFmpeg archive file
         * /data/user/0/com.symphonyrecords.mediacomp/files/archiveFormat
         */
        File ffmpegArchive;


        /**
         * The main ffmpeg file
         * /data/user/0/com.symphonyrecords.mediacomp/files/ffmpeg
         */
        File ffmpegFile;

        FFmpegSupportCallback callback;

        FFmpegExtractorAsyncTask(InputStream stream, File file, FFmpegSupportCallback callback) {
            this.ffmpegArchive = file;
            this.stream = stream;
            this.callback = callback;
            bb = FilenameUtils.getFullPath(ffmpegArchive.getAbsolutePath());
            ffmpegFile = new File(bb + FFMPEG_FILE_NAME);
        }

        @Override
        protected Boolean doInBackground(Void... params) {
            if (ffmpegFile.exists()) {
                return true;
            } else {
                if (ffmpegArchive.exists()) {
                    try {
                        String cmd = extractCmd(ffmpegArchive.getAbsolutePath(), bb);
                        P7ZipApi.executeCommand(cmd);
                        return true;
                    } catch (Throwable e) {
                        e.printStackTrace();
                        return false;
                    }
                } else {
                    try {
                        FileUtils.copyToFile(stream, ffmpegArchive);
                    } catch (Exception ignored) {
                    }
                }
            }
            return false;
        }

        @Override
        protected void onPostExecute(Boolean isSuccess) {
            super.onPostExecute(isSuccess);
            if (isSuccess) {
                if (ffmpegFile.exists()) {
                    if (ffmpegArchive.exists()) {
                        deleteFile(ffmpegArchive.getAbsolutePath());
                    }
                    Log.d("onPostExecute", "ffmpegFile.exists()");
                    if (makeFileExecutable(ffmpegFile)) {
                        callback.isFFmpegSupported(true);
                        Log.d("onPostExecute", "makeFileExecutable Successful");
                    } else {
                        callback.isFFmpegSupported(false);
                        Log.d("onPostExecute", "makeFileExecutable Failed Again");
                    }
                } else {
                    callback.isFFmpegSupported(false);
                    Log.d("onPostExecute", "!ffmpegFile.exists()");
                }
                Log.d("onPostExecuteResult", "Successful");
            } else {
                Log.d("onPostExecute", "NotSuccessful");
                callback.isFFmpegSupported(false);
            }
        }
    }


    /**
     * Copying FFMPEG binary to application directory////////////
     */
    public static void initFFmpegBinary(Context context, FFmpegSupportCallback callback) {
        try {
            File f = getFFmpegFile(context);
            if (f.exists() && f.canExecute()) {
                callback.isFFmpegSupported(true);
            } else {
                extractFFMPEG(context, callback);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private static void extractFFMPEG(Context context, FFmpegSupportCallback callback) {
        if (CpuArchHelper.cpuNotSupported()) {
            callback.isFFmpegSupported(false);
            return;
        }
        // Copy Archive To App Dir
        SharedPreferences settings = context.getSharedPreferences("ffmpeg_prefs", Context.MODE_PRIVATE);
        int version = settings.getInt(KEY_PREF_VERSION, 0);

        // check if ffmpeg file exists
        if (getFFmpegFile(context).exists() && version >= VERSION) {
            callback.isFFmpegSupported(true);
        } else {
            try {
                Log.d("extractFFMPEG", "FFmpeg Binary does not exist, initializing copy process...");
                InputStream stream = context.getAssets().open(FFMPEG_ARCHIVE);
                FFmpegArchiveCopyTask fFmpegArchiveCopyTask = new FFmpegArchiveCopyTask(stream, getFFmpegArchive(context), callback);
                fFmpegArchiveCopyTask.execute();
                settings.edit().putInt(KEY_PREF_VERSION, VERSION).apply();
            } catch (Exception e) {
                Log.e("extractFFMPEG", "error while opening assets", e);
                callback.isFFmpegSupported(false);
            }
        }
    }

    public static boolean makeFileExecutable(File file) {
        if (!file.canExecute()) {
            // try to make executable
            try {
                try {
                    Runtime.getRuntime().exec("chmod -R 777 " + file.getAbsolutePath()).waitFor();
                } catch (InterruptedException e) {
                    Log.e("makeFileExecutable", "interrupted exception", e);
                    return false;
                } catch (IOException e) {
                    Log.e("makeFileExecutable", "io exception", e);
                    return false;
                }
                if (!file.canExecute()) {
                    if (!file.setExecutable(true)) {
                        Log.e("makeFileExecutable", "unable to make executable");
                        return false;
                    }
                }
            } catch (SecurityException e) {
                Log.e("makeFileExecutable", "security exception", e);
                return false;
            }
        }
        return file.canExecute();
    }

    private static void deleteFile(String fileName) {
        try {
            File file = new File(fileName);
            if (FileUtils.deleteQuietly(file))
                System.out.println(file.getName() + " is deleted!");
            else {
                if (file.delete()) {
                    System.out.println(file.getName() + " is deleted!");
                } else {
                    try {
                        FileUtils.forceDelete(file);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void createPath(File path) {
        try {
            FileUtils.forceMkdir(path);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private static final String P7Z = "7z";
    /**
     * Command  Description
     * a    Add
     * b    Benchmark
     * d    Delete
     * e    Extract
     * h    Hash
     * i    Show information about supported formats
     * l    List
     * rn   Rename
     * t    Test
     * u    Update
     * x    eXtract with full paths
     */
    private static final String CMD_ADD = "a";
    private static final String CMD_BENCHMARK = "b";
    private static final String CMD_DELETE = "d";
    private static final String CMD_EXTRACT = "e";
    private static final String CMD_HASH = "h";
    private static final String CMD_INFO = "i";
    private static final String CMD_LIST = "l";
    private static final String CMD_RENAME = "rn";
    private static final String CMD_TEST = "t";
    private static final String CMD_UPDATE = "u";
    private static final String CMD_EXTRACT1 = "x";

    /**
     * Switch   Description
     * -i   Include filenames
     * -m   Set Compression Method
     * -o   Set Output directory
     * -p   Set Password
     * -t   Type of archive
     * -u   Update options
     * -x   Exclude filenames
     * -y   Assume Yes on all queries
     */
    private static final String SWH_INCLUDE = "-i";
    private static final String SWH_METHOD = "-m";
    private static final String SWH_OUTPUT = "-o";
    private static final String SWH_PASSWORD = "-p";
    private static final String SWH_TYPE = "-t";
    private static final String SWH_UPDATE = "-u";
    private static final String SWH_EXCLUDE = "-x";
    private static final String SWH_YES = "-y";

    public static String compressCmd(String filePath, String outPath, String type) {
        return String.format("7z a -t%s '%s' '%s'", type, outPath, filePath);
    }

    public static String extractCmd(String archivePath, String outPath) {
        return String.format("%s %s '%s' '%s%s' '%s' -aoa", P7Z, CMD_EXTRACT, archivePath, SWH_OUTPUT, outPath, CpuArchHelper.getCpuArchiveFolder());
    }
    //    public static String extractCmd(String archivePath, String outPath) {
    //        return String.format("%s %s '%s' '%s%s' -aoa", P7Z, CMD_EXTRACT1,archivePath, SWH_OUTPUT, outPath);
    //    }


    public static class CpuArchHelper {
        //// ---------  x86 Cpu ABI ----------- ////
        private static final String X86_CPU = "x86";
        private static final String X86_64_CPU = "x86_64";

        //// ---------  ARM Cpu ABI ----------- ////
        private static final String ARM_ABI = "armeabi";
        private static final String ARM_V7_CPU = "armeabi-v7a";
        private static final String ARM_64_CPU = "arm64-v8a";

        //// ---------  MIPS Cpu ABI ----------- ////
        private static final String MIPS_ABI = "mips";
        private static final String MIPS64_ABI = "mips64";


        public enum CpuArch {
            ARMv7, x86, NONE
        }


        public static CpuArch getCpuArch() {
            String cpu = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? Build.SUPPORTED_ABIS[0] : Build.CPU_ABI;
            Log.d("Device_Cpu: ", cpu);
            switch (cpu) {
                case X86_CPU:
                case X86_64_CPU:
                    return CpuArch.x86;

                case ARM_ABI:
                case ARM_V7_CPU:
                case ARM_64_CPU:
                    return CpuArch.ARMv7;

                case MIPS_ABI:
                case MIPS64_ABI:
                    return CpuArch.NONE;

                default:
                    return CpuArch.NONE;
            }
        }

        public static boolean cpuNotSupported() {
            return getCpuArch() == CpuArch.NONE;
        }

        public static String getCpuArchiveFolder() {
            switch (getCpuArch()) {
                case ARMv7:
                    return "arm";
                case x86:
                    return "x86";
                case NONE:
                    return "arm";
                default:
                    return "arm";
            }
        }

    }


}

在您的 Gradle 文件中添加代码

android { ... aaptOptions { ignoreAssetsPattern "!x86" } }

Refer this link

可帮满