Android 開發中如何動態載入 so 庫檔案

澀郎發表於2017-03-31

我想對於靜態載入 so 庫檔案,大家都已經很熟悉了,這裡就不多說了。在 Android 開發中呼叫動態庫檔案(*.so)都是通過 jni 的方式,而靜態載入往往是在 apk 或 jar 包中呼叫so檔案時,都要將對應 so 檔案打包進 apk 或 jar 包。

動態載入的優點

靜態載入,不靈活,apk 包有可能大。所以採用動態載入 so 庫檔案,有以下幾點好處:

  1. 靈活,so 檔案可以動態載入,不是繫結死的,修改方便,so 庫有問題,我們可以動態更新。
  2. so 庫檔案很大的話,採用動態載入可以減少 apk 的包,變小。
  3. 其實我們常用第三方 so 庫,單個可能沒問題,如果多個第三方 so 庫檔案,同時載入可能會出現衝突,而動態載入就能夠解決這一問題。

注意路徑陷阱

動態載入 so 庫檔案,並不是說可以把檔案隨便存放到某個 sdcard 檔案目錄下,這樣做既不安全,系統也載入不了。

我們在 Android 中載入 so 檔案,提供的 API 如下:

//第一種,pathName 庫檔案的絕對路徑
void System.load(String pathName);
//第二種,引數為庫檔名,不包含庫檔案的副檔名,必須是在JVM屬性Java.library.path所指向的路徑中,路徑可以通過System.getProperty('java.library.path') 獲得
void loadLibrary(String libname)複製程式碼

注意:而這裡載入的檔案路徑只能載入兩個目錄下的 so 檔案。那就是:

  1. /system/lib
  2. 應用程式安裝包的路徑,即:/data/data/packagename/…

所以,so 檔案動態載入的檔案目錄不能隨便放。這是需要注意的一點。

實現思路

既然使用動態載入的好處和陷阱我們都大致瞭解了,那就可以在實現的時候,注意陷阱就可以了。那基本思路如下:

  1. 網路下載 so 檔案到指定目錄
  2. 從指定下載的目錄複製 copy so檔案到可動態載入的檔案目錄下,比如:/data/data/packagename/…
  3. 配置 gradle ,指定 cpu 架構
  4. load 載入

第一步,我們這裡可以簡單忽略,假設我們把 so 檔案下載到了 /mnt/sdcard/armeabi 目錄下。

複製目錄到包路徑下

那我們就應該把 /mnt/sdcard/armeabi 目錄下的 so 檔案,複製到 應用的包路徑下。

/**
 * Created by loonggg on 2017/3/29.
 */

public class SoFile {
    /**
     * 載入 so 檔案
     * @param context
     * @param fromPath 下載到得sdcard目錄
     */
    public static void loadSoFile(Context context, String fromPath) {
        File dir = context.getDir("libs", Context.MODE_PRIVATE);
        if (!isLoadSoFile(dir)) {
            copy(fromPath, dir.getAbsolutePath());
        }
    }

    /**
     * 判斷 so 檔案是否存在
     * @param dir
     * @return
     */
    public static boolean isLoadSoFile(File dir) {
        File[] currentFiles;
        currentFiles = dir.listFiles();
        boolean hasSoLib = false;
        if (currentFiles == null) {
            return false;
        }
        for (int i = 0; i < currentFiles.length; i++) {
            if (currentFiles[i].getName().contains("libwedsa23")) {
                hasSoLib = true;
            }
        }
        return hasSoLib;
    }

    /**
     *
     * @param fromFile 指定的下載目錄
     * @param toFile 應用的包路徑
     * @return
     */
    public static int copy(String fromFile, String toFile) {
        //要複製的檔案目錄
        File[] currentFiles;
        File root = new File(fromFile);
        //如同判斷SD卡是否存在或者檔案是否存在,如果不存在則 return出去
        if (!root.exists()) {
            return -1;
        }
        //如果存在則獲取當前目錄下的全部檔案 填充陣列
        currentFiles = root.listFiles();

        //目標目錄
        File targetDir = new File(toFile);
        //建立目錄
        if (!targetDir.exists()) {
            targetDir.mkdirs();
        }
        //遍歷要複製該目錄下的全部檔案
        for (int i = 0; i < currentFiles.length; i++) {
            if (currentFiles[i].isDirectory()) {
                //如果當前項為子目錄 進行遞迴
                copy(currentFiles[i].getPath() + "/", toFile + currentFiles[i].getName() + "/");
            } else {
                //如果當前項為檔案則進行檔案拷貝
                if (currentFiles[i].getName().contains(".so")) {
                    int id = copySdcardFile(currentFiles[i].getPath(), toFile + File.separator + currentFiles[i].getName());
                }
            }
        }
        return 0;
    }


    //檔案拷貝
    //要複製的目錄下的所有非子目錄(資料夾)檔案拷貝
    public static int copySdcardFile(String fromFile, String toFile) {
        try {
            FileInputStream fosfrom = new FileInputStream(fromFile);
            FileOutputStream fosto = new FileOutputStream(toFile);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = -1;
            while ((len = fosfrom.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            // 從記憶體到寫入到具體檔案
            fosto.write(baos.toByteArray());
            // 關閉檔案流
            baos.close();
            fosto.close();
            fosfrom.close();
            return 0;
        } catch (Exception ex) {
            return -1;
        }
    }
}複製程式碼

配置 grade 指定 cpu 架構

我們都知道,在使用 so 檔案的時候,so 庫型別和 CPU 架構型別,要一致,否則是會報錯的。原因很簡單,不同 CPU 架構的裝置需要用不同型別 so 庫。CPU架構有如下幾種型別:ARMv5,ARMv7,x86,MIPS,ARMv8,MIPS64 和 x86_64。如果要適配很多手機,就需要在不同的型別下,放置對應的 so 檔案。
配置方法如下:

defaultConfig {
        applicationId "xxxx"
        minSdkVersion 17
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"

        ndk {
            abiFilters "armeabi","armeabi-v7a","x86"
        }
    }複製程式碼

load 載入 so 檔案

複製到可載入使用的包路徑下後,配置完 gradle 之後,就可以使用 load API 呼叫了。

File dir =  getApplicationContext().getDir("libs", Context.MODE_PRIVATE);
File[] currentFiles;
currentFiles = dir.listFiles();
for (int i = 0; i < currentFiles.length; i++) {
   System.load(currentFiles[i].getAbsolutePath());
}複製程式碼

這樣,我們就實現了動態載入 so 檔案。

歡迎大家關注我的技術分享公眾號:非著名程式設計師(smart_android)。技術文章均先首發於我的技術分享的微信公眾號。

Android 開發中如何動態載入 so 庫檔案

相關文章