動態加載so庫

今天在集成虹軟的人臉demo的時候,發現so庫太大也就導致apk體積大。於是用動態加載,理論上是從服務器下載,然後放到指定位置進行加載,這裏先在本地進行拷貝。一共兩個文件:libarcsoft_face.so和libarcsoft_face_engine.so。

1.將so文件放到sd卡根目錄的arcFace下

String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "arcFace";

然後需要用到一個工具類SoFile

package com.arcsoft.arcfacedemo.common;

import android.content.Context;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class SoFile {

    /**
     * 加載 so 文件
     * @param context
     * @param fromPath 下載到得sdcard目錄
     */
    public static void loadSoFile(Context context, String fromPath) {
        File dirs = context.getDir("libs", Context.MODE_PRIVATE);
        if (!isLoadSoFiles("libarcsoft_face.so", dirs) || !isLoadSoFiles("libarcsoft_face_engine.so", dirs)) {
            copyFile(fromPath, dirs.getAbsolutePath());
        }
    }

    /**
     * 判斷immqy so 文件是否存在
     * @param dir
     * @param name "libimmqy" so庫
     * @return boolean
     */
    public static boolean isLoadSoFiles(String name,File dirs) {
        boolean getSoLib = false;
        File[] currentFiles;
        currentFiles = dirs.listFiles();
        if (currentFiles == null) {
            return false;
        }
        for (int i = 0; i < currentFiles.length; i++) {
            if (currentFiles[i].getName().contains(name)) {
                getSoLib = true;
            }
        }
        return getSoLib;
    }


    /**
     * 要複製的目錄下的所有非子目錄(文件夾)文件拷貝
     * @param fromFile 指定的下載目錄
     * @param toFile 應用的包路徑
     * @return int
     */
    public static int copySdcardFile(String fromFiles, String toFile) {
        Log.d("要複製到", toFile);
        try {
            FileInputStream fileInput = new FileInputStream(fromFiles);
            FileOutputStream fileOutput = new FileOutputStream(toFile);
            ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024*1];
            int len = -1;
            while ((len = fileInput.read(buffer)) != -1) {
                byteOut.write(buffer, 0, len);
            }
            // 從內存到寫入到具體文件
            fileOutput.write(byteOut.toByteArray());
            // 關閉文件流
            byteOut.close();
            fileOutput.close();
            fileInput.close();
            return 0;
        } catch (Exception ex) {
            return -1;
        }
    }



    /**
     *
     * @param fromFiles 指定的下載目錄
     * @param toFile 應用的包路徑
     * @return int
     */
    public static int copyFile(String fromFiles, String toFile) {
        //要複製的文件目錄
        File[] currentFiles;
        File root = new File(fromFiles);
        //如同判斷SD卡是否存在或者文件是否存在,如果不存在則 return出去
        if (!root.exists()) {
            return -1;
        }
        //如果存在則獲取當前目錄下的全部文件 填充數組
        currentFiles = root.listFiles();
        if (currentFiles == null) {
            Log.d("soFile---","未獲取到文件");
            return -1;
        }
        //目標目錄
        File targetDir = new File(toFile);
        //創建目錄
        if (!targetDir.exists()) {
            targetDir.mkdirs();
        }
        //遍歷要複製該目錄下的全部文件
        for (int i = 0; i < currentFiles.length; i++) {
            if (currentFiles[i].isDirectory()) {
                Log.d("當前項爲子目錄 進行遞歸", "copyFile: ");
                //如果當前項爲子目錄 進行遞歸
                copyFile(currentFiles[i].getPath() + "/", toFile + currentFiles[i].getName() + "/");
            } else {
                //如果當前項爲文件則進行文件拷貝
                if (currentFiles[i].getName().contains(".so")) {
                    Log.d("當前項爲文件則進行文件拷貝", "copyFile: ");
                    int id = copySdcardFile(currentFiles[i].getPath(), toFile + File.separator + currentFiles[i].getName());
                }
            }
        }
        return 0;
    }
}

2.再調用下面這個方法完成拷貝

SoFile.loadSoFile(getApplicationContext(), path);

3.加載so文件

File dir = getApplicationContext().getDir("libs", Context.MODE_PRIVATE);
File[] currFiles = null;
currFiles = dir.listFiles();
Log.d("listFiles", String.valueOf(currFiles.length));
for (int i = 0; i < currFiles.length; i++) {
    System.load(currFiles[i].getAbsolutePath());
}

然後,沒什麼問題就能正常使用了。

這裏遇到幾個問題:

一、在此之前要進行文件讀寫權限的申請,否則可能出現找不到你存放的文件(listFiles返回值爲null)。

二、"xxx.so" is 64-bit instead of 32-bit ,這個問題是由於在64位的android機上,會有32位的虛擬機和64位的虛擬機,在啓動apk的時候,虛擬機會根據apk中的so的位數啓動對應的虛擬機。這個時候最簡單的解決辦法就是將任意一個32位的so文件放到指定目錄下,我的就是本來有兩個so文件(如下圖),當我只把libarcsoft_face.so單獨拿出來動態加載就可以,但是把兩個一起拿出來動態加載就會出現這個問題,所以應該可以判斷libarcsoft_face_engine.so文件是32位的。

來自:加載包含so文件的插件報錯XXXX.so" is 32-bit instead of 64-bit

三、java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader *** couldn't find "libarcsoft_face.so"

這裏報錯的原因是,在虹軟提供的jar包裏面,在FaceEngine這個類中有一個靜態代碼塊,會從系統目錄加載so庫,也就是仍然調用了“System.loadLibrary("arcsoft_face.so")”,因爲App無法把so文件複製到系統目錄,所以導致System.loadLibrary方法找不到arcsoft_face.so。我這裏解決的辦法是,把jar包中的類複製出來,刪掉jar包,在複製的類中刪掉這個靜態代碼塊。這樣就能走我們的System.load()方法動態加載了。

參考資料:動態加載so庫的實現方法與問題處理

四、dlopen failed: library "libarcsoft_face.so" not found

還有其他類型的報錯,但是都是提示找不到這個so文件。查閱大量資料後得知,問題可能出在so文件的加載順序不對,我把加載的順序打印出來,發現先加載的是libarcsoft_face_engine.so這個文件,我把順序改變後,這個錯誤就沒有了,因此結合自己的猜想,應該是在libarcsoft_face_engine有對libarcsoft_face.so的調用,但是libarcsoft_face.so此時還未加載,所以報錯說找不到。

後來我想着這樣畢竟不太好,就把listFiles獲取的兩個so文件按大小排序,但是又發現,華爲nova 2s手機上排序正常,我的一加3T手機排序後還是那樣,於是放棄,就這麼寫吧。

參考資料:Android——JNI加載so兩種方式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章