Android動態加載補充 加載SD卡中的SO庫

基本信息

JNI與NDK

Android中JNI的使用其實就包含了動態加載,APP運行時動態加載.so庫並經過JNI調用其封裝好的方法。後者通常是使用NDK工具從C/C++代碼編譯而成,運行在Native層,效率會比執行在虛擬機的Java代碼高不少,因此Android中常常經過動態加載.so庫來完成一些對性能比較有需求的工做(好比T9搜索、或者Bitmap的解碼、圖片高斯模糊處理等)。此外,因爲.so庫是由C++編譯而來的,只能被反編譯成彙編代碼,相比Smali更難被破解,所以.so庫也能夠被用於安全領域。git

與咱們常說的基於ClassLoader的動態加載不一樣,SO庫的加載是使用System類的(因而可知對SO庫的支持也是Android的基礎功能),因此這裏這是做爲補充說明。不過,若是使用ClassLoader加載SD卡里插件APK,而插件APK裏面包含有SO庫,這就涉及到了對插件APK裏的SO庫的加載,因此咱們也要知道如何加載SD卡里面的SO庫。github

通常的SO文件的使用姿式

以一個「圖片高斯模糊」的功能爲例,若是使用Java代碼對圖像Bitmap的每個像素點進行計算,那總體耗時將會很是大,因此能夠考慮使用JNI。(詳細的JNI使用教程網絡上有許多,這裏不贅述)面試

這裏推薦一個開源的高斯模糊項目 Android StackBlur數組

在命令行定位到Android.mk文件所在目錄,運行NDK工具的ndk-build命令就能編譯出咱們須要SO庫
安全

再把SO庫複製到Android Studio項目的jniLibs目錄中

(Android Studio如今也支持直接編譯SO庫,可是有許多坑,這裏我選擇手動編譯)網絡

接着在Java中把SO庫對應的模塊加載進來app

// load so file from internal directory
        try {
            System.loadLibrary("stackblur");
            NativeBlurProcess.isLoadLibraryOk.set(true);
            Log.i("MainActivity", "loadLibrary success!");
        } catch (Throwable throwable) {
            Log.i("MainActivity", "loadLibrary error!" + throwable);
        }

加載成功後就能夠直接使用Native方法了ide

public class NativeBlurProcess {
    public static AtomicBoolean isLoadLibraryOk = new AtomicBoolean(false);
    //native method
    private static native void functionToBlur(Bitmap bitmapOut, int radius, int threadCount, int threadIndex, int round);
    }

因而可知,在Android項目中,SO庫的使用也是一種動態加載,在運行時把可執行文件加載進來。通常狀況下,SO庫都是打包在APK內部的,不容許修改。這種「動態加載」看起來不是咱們熟悉的那種啊,貌似沒什麼卵用。不過,其實SO庫也是能夠存放在外部存儲路徑的。

如何把SO文件存放在外部存儲

注意到上面加載SO庫的時候咱們用到了System類的「loadLibrary」方法,同時咱們也發現System類還有一個「load」方法,看起來差很少啊,看看他們有什麼區別吧!

/**
     * See {@link Runtime#load}.
     */
    public static void load(String pathName) {
        Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());
    }

    /**
     * See {@link Runtime#loadLibrary}.
     */
    public static void loadLibrary(String libName) {
        Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
    }

先看看loadLibrary,這裏調用了Runtime的loadLibrary,進去一看,又是動態加載熟悉的ClassLoader了(這裏也佐證了SO庫的使用就是一種動態加載的說法)

/*
     * Searches for and loads the given shared library using the given ClassLoader.
     */
    void loadLibrary(String libraryName, ClassLoader loader) {
        if (loader != null) {
            String filename = loader.findLibrary(libraryName);
            String error = doLoad(filename, loader);
            return;
        }
        ……
    }

看樣子就像是經過庫名獲取一個文件路徑,再調用「doLoad」方法加載這個文件,先看看「loader.findLibrary(libraryName)」

protected String findLibrary(String libName) {
        return null;
    }

ClassLoader只是一個抽象類,它的大部分工做都在BaseDexClassLoader類中實現,進去看看

public class BaseDexClassLoader extends ClassLoader {
    public String findLibrary(String name) {
        throw new RuntimeException("Stub!");
    }
}

不對啊,這裏只是拋了一個RuntimeException異常,什麼都沒作啊!

其實這裏有一個誤區,也是剛開始開Android SDK源碼的同窗容易搞混的。Android SDK自帶的源碼其實只是給咱們開發者參考的,基本只是一些經常使用的類,Google不會把整個Android系統的源碼都放到這裏來,由於整個項目很是大,ClassLoader類平時咱們接觸得少,因此它的具體實現的源碼並無打包進SDK裏,若是須要,咱們要到官方AOSP項目裏面去看(順便一提,整個AOSP5.1項目大小超過150GB,真的有須要的話推薦用一個移動硬盤存儲)。

這裏爲了方便,咱們能夠直接看在線的代碼 BaseDexClassLoader.java

@Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }

再看進去DexPathList類

/**
     * Finds the named native code library on any of the library
     * directories pointed at by this instance. This will find the
     * one in the earliest listed directory, ignoring any that are not
     * readable regular files.
     *
     * @return the complete path to the library or {@code null} if no
     * library was found
     */
    public String findLibrary(String libraryName) {
        String fileName = System.mapLibraryName(libraryName);
        for (File directory : nativeLibraryDirectories) {
            File file = new File(directory, fileName);
            if (file.exists() && file.isFile() && file.canRead()) {
                return file.getPath();
            }
        }
        return null;
    }

到這裏已經明朗了,根據傳進來的libName,掃描APK內部的nativeLibrary目錄,獲取並返回內部SO庫文件的完整路徑filename。再回到Runtime類,獲取filename後調用了「doLoad」方法,看看

private String doLoad(String name, ClassLoader loader) {
        String ldLibraryPath = null;
        String dexPath = null;
        if (loader == null) {
            ldLibraryPath = System.getProperty("java.library.path");
        } else if (loader instanceof BaseDexClassLoader) {
            BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
            ldLibraryPath = dexClassLoader.getLdLibraryPath();
        }
        synchronized (this) {
            return nativeLoad(name, loader, ldLibraryPath);
        }
    }

到這裏就完全清楚了,調用Native方法「nativeLoad」,經過完整的SO庫路徑filename,把目標SO庫加載進來。

說了半天尚未進入正題呢,不過咱們能夠想到,若是使用loadLibrary方法,到最後仍是要找到目標SO庫的完整路徑,再把SO庫加載進來,那咱們能不能一開始就給出SO庫的完整路徑,而後直接加載進來?咱們猜測load方法就是幹這個的,看看。

void load(String absolutePath, ClassLoader loader) {
        if (absolutePath == null) {
            throw new NullPointerException("absolutePath == null");
        }
        String error = doLoad(absolutePath, loader);
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
    }

我勒個去,一上來就直接來到doLoad方法了,這證實咱們的猜測多是正確的,那麼在實際項目中測試看看吧!

咱們先把SO放在Asset裏,而後再複製到內部存儲,再使用load方法把其加載進來。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        File dir = this.getDir("jniLibs", Activity.MODE_PRIVATE);
        File distFile = new File(dir.getAbsolutePath() + File.separator + "libstackblur.so");

        if (copyFileFromAssets(this, "libstackblur.so", distFile.getAbsolutePath())){
            //使用load方法加載內部儲存的SO庫
            System.load(distFile.getAbsolutePath());
            NativeBlurProcess.isLoadLibraryOk.set(true);
        }
    }

    public void onDoBlur(View view){
        ImageView imageView = (ImageView) findViewById(R.id.iv_app);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), android.R.drawable.sym_def_app_icon);
        Bitmap blur = NativeBlurProcess.blur(bitmap,20,false);
        imageView.setImageBitmap(blur);
    }


    public static boolean copyFileFromAssets(Context context, String fileName, String path) {
        boolean copyIsFinish = false;
        try {
            InputStream is = context.getAssets().open(fileName);
            File file = new File(path);
            file.createNewFile();
            FileOutputStream fos = new FileOutputStream(file);
            byte[] temp = new byte[1024];
            int i = 0;
            while ((i = is.read(temp)) > 0) {
                fos.write(temp, 0, i);
            }
            fos.close();
            is.close();
            copyIsFinish = true;
        } catch (IOException e) {
            e.printStackTrace();
            Log.e("MainActivity", "[copyFileFromAssets] IOException "+e.toString());
        }
        return copyIsFinish;
    }
}

點擊onDoBlur按鈕,果真加載成功了!

那能不能直接加載外部存儲上面的SO庫呢,把SO庫拷貝到SD卡上面試試。

看起來是不能夠的樣子,Permission denied!

java.lang.UnsatisfiedLinkError: dlopen failed: couldn't map "/storage/emulated/0/libstackblur.so" segment 1: Permission denied

看起來像是沒有權限的樣子,看看源碼哪裏拋出的異常吧

/*
     * Loads the given shared library using the given ClassLoader.
     */
    void load(String absolutePath, ClassLoader loader) {
        if (absolutePath == null) {
            throw new NullPointerException("absolutePath == null");
        }
        String error = doLoad(absolutePath, loader);
        if (error != null) {
            // 這裏拋出的異常
            throw new UnsatisfiedLinkError(error);
        }
    }

應該是執行doLoad方法時出現了錯誤,可是上面也看過了,doLoad方法裏調用了Native方法「nativeLoad」,那應該就是Native代碼裏出現的錯誤。平時我不多看到Native裏面,上一次看的時候,是由於須要看看點九圖NinePathDrawable的縮放控制信息chunk數組的具體做用是怎麼樣,費了很久才找到我想要的一小段代碼。因此這裏就暫時不跟進去了,有興趣的同窗能夠告訴我關鍵代碼的位置。

我在一個Google的開發者論壇上找到了一些答案

The SD Card is mounted noexec, so I'm not sure this will work.

Moreover, using the SD Card as a storage location is a really bad idea, since any other application can modify/delete/corrupt it easily.
Try downloading the library to your application's data directory instead, and load it from here.

這也容易理解,SD卡等外部存儲路徑是一種可拆卸的(mounted)不可執行(noexec)的儲存媒介,不能直接用來做爲可執行文件的運行目錄,使用前應該把可執行文件複製到APP內部存儲再運行。

最後,咱們也能夠看看官方的API文檔

看來load方法的用途和咱們理解的一致,文檔裏說的shared library就是指SO庫(shared object),至此,咱們就能夠把SO文件移動到外部存儲了,或者從網絡下載都行。

相關文章
相關標籤/搜索