1. 程式人生 > >ANDROID動態載入 使用SO庫時要注意的一些問題

ANDROID動態載入 使用SO庫時要注意的一些問題

基本資訊

Android專案裡的SO庫

正好動態載入系列文章談到了載入SO庫的地方,我覺得這裡可以順便談談使用SO庫時需要注意的一些問題。或許這些問題對於經常和SO庫開發打交道的同學來說已經是老生長談,但是既然要討論一整個動態載入系列,我想還是有必要說說使用SO庫時的一些問題。

在專案裡使用SO庫非常簡單,在 載入SD卡中的SO庫 中也有談到,只需要把需要用到的SO庫拷貝進 jniLibs(或者Eclipse專案裡面的libs) 中,然後在JAVA程式碼中呼叫 System.loadLibrary("xxx") 載入對應的SO庫,就可以使用JNI語句呼叫SO庫裡面的Native方法了。

但是有同學注意到了,SO庫檔案可以隨便改檔名,卻不能任意修改資料夾路徑,而是“armeabi”、“armeabi-v7a”、“x86”等資料夾名有著嚴格的要求,這些資料夾名有什麼意義麼?

SO庫型別和CPU架構型別

原因很簡單,不同CPU架構的裝置需要用不同型別SO庫(從檔名也可以猜出來個大概嘛 ╮( ̄▽ ̄")╭)。

記得還在學校的時候,提及ARM處理器時,老師說以後移動裝置的CPU基本就是ARM型別的了。老師不曾欺我,早期的Android系統幾乎只支援ARM的CPU架構,不過現在至少支援以下七種不同的CPU架構:ARMv5,ARMv7,x86,MIPS,ARMv8,MIPS64和x86_64。每一種CPU型別都對應一種ABI(Application Binary Interface),“armeabi-v7a”資料夾前面的“armeabi”指的就是ARM這種型別的ABI,後面的“v7a”指的是ARMv7。這7種CPU型別對應的SO庫的資料夾名是:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。

不同型別的移動裝置在執行APP時,需要載入自己支援的型別的SO庫,不然就GG了。通過 Build.SUPPORTED_ABIS 我們可以判斷當前裝置支援的ABI,不過一般情況下,不需要開發者自己去判斷ABI,Android系統在安裝APK的時候,不會安裝APK裡面全部的SO庫檔案,而是會根據當前CPU型別支援的ABI,從APK裡面拷貝最合適的SO庫,並儲存在APP的內部儲存路徑的 libs 下面。(這裡說一般情況,是因為有例外的情況存在,比如我們動態載入外部的SO庫的時候,就需要自己判斷ABI型別了。)

一種CPU架構 = 一種對應的ABI引數 =  一種對應型別的SO庫

到這裡,我們發現使用SO庫的邏輯還是比較簡單的,但是Android系統載入SO庫的邏輯還是給我們留下了一些坑。

使用SO庫時要注意的一些問題

1. 別把SO庫放錯地方

SO庫其實都是APP執行時載入的,也就是說APP只有在執行的時候才知道SO庫檔案的存在,這就無法通過靜態程式碼檢查或者在編譯APP時檢查SO庫檔案是否正常。所以,Android開發對SO庫的存放路徑有嚴格的要求。

使用SO庫的時候,除了“armeabi-v7a”等資料夾名需要嚴格按照規定的來自外,SO庫要放在專案的哪個資料夾下也要按照套路來,以下是一些總結:

  • Android Studio 工程放在 jniLibs/xxxabi 目錄中(當然也可以通過在build.gradle檔案中的設定jniLibs.srcDir屬性自己指定);

  • Eclipse 工程放在 libs/xxxabi 目錄中(這也是使用ndk-build命令生成SO庫的預設目錄);

  • aar 依賴包中位於 jni/ABI 目錄中(SO庫會自動包含到引用AAR壓縮包到APK中);

  • 最終構建出來的APK檔案中,SO庫存在 lib/xxxabi 目錄中(也就是說無論你用什麼方式構建,只要保證APK包裡SO庫的這個路徑沒錯就沒問題);

  • 通過 PackageManager 安裝後,在小於 Android 5.0 的系統中,SO庫位於 APP 的 nativeLibraryPath 目錄中;在大於等於 Android 5.0 的系統中,SO庫位於 APP 的 nativeLibraryRootDir/CPU_ARCH 目錄中;

既然扯到了這裡,順便說一下,我在使用 Android Studio 1.5 構建APK的時候,發現 Gradle 外掛只會預設打包application型別的module的jniLibs下面的SO庫檔案,而不會打包aar依賴包的SO庫,所以會導致最終構建出來的APK裡的SO庫檔案缺失。暫時的解決方案是把所有的SO庫都放在application模組中(這顯然不是很好的解決方案),不知道這是不是Studio的BUG,同事的解決方案是通過修改Gradle外掛來增加對aar依賴包的SO庫的打包支援(GitHub有開源的第三方Gradle外掛專案,使用Java和Groovy語言開發)。

2. 儘可能提供CPU支援的最優SO庫

當一個應用安裝在裝置上,只有該裝置支援的CPU架構對應的SO庫會被安裝。但是,有時候,裝置支援的SO庫型別不止一種,比如大多的X86裝置除了支援X86型別的SO庫,還相容ARM型別的SO庫(目前應用市場上大部分的APP只適配了ARM型別的SO庫,X86型別的裝置如果不能相容ARM型別的SO庫的話,大概要嗝屁了吧)。

所以如果你的APK只適配了ARM型別的SO庫的話,還是能以相容的模式在X86型別的裝置上執行(比如華碩的平板),但是這不意味著你就不用適配X86型別的SO庫了,因為X86的CPU使用相容模式執行ARM型別的SO庫會異常卡頓(試著回想幾年前你開始學習Android開發的時候,在PC上使用AVD模擬器的那種感覺)。

3. 注意SO庫的編譯版本

除了要注意使用了正確CPU型別的SO庫,也要注意SO庫的編譯版本的問題。雖然現在的Android Studio支援在專案中直接編譯SO庫,但是更多的時候我們還是選擇使用事先編譯好的SO庫,這時就要注意了,編譯APK的時候,我們總是希望使用最新版本的build-tools來編譯,因為Android SDK最新版本會幫我們做出最優的向下相容工作。

但是這對於編譯SO庫來說就不一樣了,因為NDK平臺不是向下相容的,而是向上相容的。應該使用app的minSdkVersion對應的版本的NDK標本來編譯SO庫檔案,如果使用了太高版本的NDK,可能會導致APP效能低下,或者引發一些SO庫相關的執行時異常,比如“UnsatisfiedLinkError”,“dlopen: failed”以及其他型別的Crash。

一般情況下,我們都是使用編譯好的SO庫檔案,所以當你引入一個預編譯好的SO庫時,你需要檢查它被編譯所用的平臺版本。

4. 儘可能為每種CPU型別都提供對應的SO庫

比如有時候,因為業務的需求,我們的APP不需要支援AMR64的裝置,但這不意味著我們就不用編譯ARM64對應的SO庫。舉個例子,我們的APP只支援armeabi-v7a和x86架構,然後我們的APP使用了一個第三方的Library,而這個Library提供了AMR64等更多型別CPU架構的支援,構建APK的時候,這些ARM64的SO庫依然會被打包進APK裡面,也就是說我們自己的SO庫沒有對應的ARM64的SO庫,而第三方的Library卻有。這時候,某些ARM64的裝置安裝該APK的時候,發現我們的APK裡帶有ARM64的SO庫,會誤以為我們的APP已經做好了AMR64的適配工作,所以只會選擇安裝APK裡面ARM64型別的SO庫,這樣會導致我們自己專案的SO庫沒有被正確安裝(雖然armeabi-v7a和x86型別的SO庫確實存在APK包裡面)。

這時正確的做法是,給我們自己的SO庫也提供AMR64支援,或者不打包第三方Library專案的ARM64的SO庫。使用第二種方案時,可以把APK裡面不需要支援的ABI資料夾給刪除,然後重新打包,而在Android Studio下,則可以通過以下的構建方式指定需要型別的SO庫。

productFlavors {
    flavor1 {
        ndk {
            abiFilters "armeabi-v7a"
            abiFilters "x86"
            abiFilters "armeabi"
        }
    }
    flavor2 {
        ndk {
            abiFilters "armeabi-v7a"
            abiFilters "x86"
            abiFilters "armeabi"
            abiFilters "arm64-v8a"
            abiFilters "x86_64"
        }
    }
}

需要說明的是,如果我們的專案是SDK專案,我們最好提供全平臺型別的SO庫支援,因為APP能支援的裝置CPU型別的數量,就是專案中所有SO庫支援的最少CPU型別的數量(使用我們SDK的APP能支援的CPU型別只能少於等於我們SDK支援的型別)。

5. 不要通過“減少其他CPU型別支援的SO庫”來減少APK的體積

確實,所有的x86/x86_64/armeabi-v7a/arm64-v8a裝置都支援armeabi架構的SO庫,因此似乎移除其他ABIs的SO庫是一個減少APK大小的好辦法。但事實上並不是,這不只影響到函式庫的效能和相容性。

X86裝置能夠很好的執行ARM型別函式庫,但並不保證100%不發生crash,特別是對舊裝置,相容只是一種保底方案。64位裝置(arm64-v8a, x86_64, mips64)能夠執行32位的函式庫,但是以32位模式執行,在64位平臺上執行32位版本的ART和Android元件,將丟失專為64位優化過的效能(ART,webview,media等等)。

過減少其他CPU型別支援的SO庫來減少APK的體積不是很明智的做法,如果真的需要通過減少SO庫來做APK瘦身,我們也有其他辦法。

減少SO庫體積的正確姿勢

1. 構建特定ABI支援的APK

我們可以構建一個APK,它支援所有的CPU型別。但是反過來,我們可以為每個CPU型別都單獨構建一個APK,然後不同CPU型別的裝置安裝對應的APK即可,當然前提是應用市場得提供使用者裝置CPU型別設別的支援,就目前來說,至少PLAY市場是支援的。

Gradle可以通過以下配置生成不同ABI支援的APK(引用自別的文章,沒實際使用過):

android {
    ...
    splits {
        abi {
            enable true
            reset()
            include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
            universalApk true //generate an additional APK that contains all the ABIs
        }
    }

    // map for the version code
    project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9]

    android.applicationVariants.all { variant ->
        // assign different version code for each output
        variant.outputs.each { output ->
            output.versionCodeOverride =
                    project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + android.defaultConfig.versionCode
        }
    }
 }

2. 從網路下載當前裝置支援的SO庫

說到這裡,總算回到動態載入的主題了。⊙﹏⊙

使用Android的動態載入技術,可以載入外部的SO庫,所以我們可以從網路下載SO庫檔案並載入了。我們可以下載所有型別的SO庫檔案,然後載入對應型別的SO庫,也可以下載對應型別的SO庫然後載入,不過無論哪種方式,我們最好都在載入SO庫前,對SO庫檔案的型別做一下判斷。

我個人的方案是,儲存在伺服器的SO庫依然按照APK包的壓縮方式打包,也就是,SO庫存放在APK包的 libs/xxxabi 路徑下面,下載完帶有SO庫的APK包後,我們可以遍歷libs路徑下的所有SO庫,選擇載入對應型別的SO庫。

具體實現程式碼看上去像是:

/**
 * 將一個SO庫複製到指定路徑,會先檢查改SO庫是否與當前CPU相容
 *
 * @param sourceDir     SO庫所在目錄
 * @param so            SO庫名字
 * @param destDir       目標根目錄
 * @param nativeLibName 目標SO庫目錄名
 * @return
 */
public static boolean copySoLib(File sourceDir, String so, String destDir, String nativeLibName) throws IOException {

    boolean isSuccess = false;
    try {
        LogUtil.d(TAG, "[copySo] 開始處理so檔案");

        if (Build.VERSION.SDK_INT >= 21) {
            String[] abis = Build.SUPPORTED_ABIS;
            if (abis != null) {
                for (String abi : abis) {
                    LogUtil.d(TAG, "[copySo] try supported abi:" + abi);
                    String name = "lib" + File.separator + abi + File.separator + so;
                    File sourceFile = new File(sourceDir, name);
                    if (sourceFile.exists()) {
                        LogUtil.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
                        isSuccess = FileUtil.copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + nativeLibName + File.separator + so);
                        //api21 64位系統的目錄可能有些不同
                        //copyFile(sourceFile.getAbsolutePath(), destDir + File.separator +  name);
                        break;
                    }
                }
            } else {
                LogUtil.e(TAG, "[copySo] get abis == null");
            }
        } else {
            LogUtil.d(TAG, "[copySo] supported api:" + Build.CPU_ABI + " " + Build.CPU_ABI2);

            String name = "lib" + File.separator + Build.CPU_ABI + File.separator + so;
            File sourceFile = new File(sourceDir, name);

            if (!sourceFile.exists() && Build.CPU_ABI2 != null) {
                name = "lib" + File.separator + Build.CPU_ABI2 + File.separator + so;
                sourceFile = new File(sourceDir, name);

                if (!sourceFile.exists()) {
                    name = "lib" + File.separator + "armeabi" + File.separator + so;
                    sourceFile = new File(sourceDir, name);
                }
            }
            if (sourceFile.exists()) {
                LogUtil.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
                isSuccess = FileUtil.copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + nativeLibName + File.separator + so);
            }
        }

        if (!isSuccess) {
            LogUtil.e(TAG, "[copySo] 安裝 " + so + " 失敗 : NO_MATCHING_ABIS");
            throw new IOException("install " + so + " fail : NO_MATCHING_ABIS");
        }

    } catch (IOException e) {
        e.printStackTrace();
        throw e;
    }

    return true;
}

總結

  1. 一種CPU架構 = 一種ABI = 一種對應的SO庫;

  2. 載入SO庫時,需要載入對應型別的SO庫;

  3. 儘量提供全平臺CPU型別的SO庫支援;

題外話,SO庫的使用本身就是一種最純粹的動態載入技術,SO庫本身不參與APK的編譯過程,使用JNI呼叫SO庫裡的Native方法的方式看上去也像是一種“硬程式設計”,Native方法看上去與一般的Java靜態方法沒什麼區別,但是它的具體實現卻是可以隨時動態更換的(更換SO庫就好),這也可以用來實現熱修復的方案,與Java方法一旦載入進記憶體就無法再次更換不同,Native方法不需要重啟APP就可以隨意更換。

出於安全和生態控制的原因,Google Play市場不允許APP有載入外部可執行檔案的行為,一旦你的APK裡被檢查出有額外的可執行檔案時就不好玩了,所以現在許多APP都偷偷把用於動態載入的可執行檔案的字尾名換成“.so”,這樣被發現的機率就降低了,因為載入SO庫看上去就是官方合法版本的動態載入啊(不然SO庫怎麼工作),雖然這麼做看起來有點掩耳盜鈴。

參考文章

相關推薦

ANDROID動態載入 使用SO注意一些問題

基本資訊 Android專案裡的SO庫 正好動態載入系列文章談到了載入SO庫的地方,我覺得這裡可以順便談談使用SO庫時需要注意的一些問題。或許這些問題對於經常和SO庫開發打交道的同學來說已經是老生長談,但是既然要討論一整個動態載入系列,我想還是有必要說說使用SO庫時的一些問題。 在專案裡使用SO庫非常簡

Android動態載入so檔案

簡介 前幾天做一個視訊播放的功能,用到了bilibili開源ijkplayer播放器的(整合ijkplayer),功能確實強大,但就是用到的ffmpeg解碼庫太大,不得已只能只能將so檔案拿出來,通過動態的方式來載入。 什麼是動態載入? 就是講so檔案

android開發 載入so的解析和出現的各種錯誤分析

一.android目前有幾種cpu架構? 早期的Android系統幾乎只支援ARMv5的CPU架構,你知道現在它支援多少種嗎?7種! Android系統目前支援以下七種不同的CPU架構:ARMv5,ARMv7 (從2010年起),x86 (從2011年起),MIP

Android Studio 載入 .so出現couldn't find "*.so"

java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com*.*.*-2/base.apk"],nativeLibraryDirecto

Android 動態載入sd卡里面so

有些so檔案太大,可以從手機記憶體或者sd卡里面拷貝到執行的應用程式裡面。介面都是之前打包在裡面了。還可以做so更新,就是把之前拷貝進行刪除,然後進行不重新打包apk,進行重新拷貝進去。 1,封裝好的類 package com.rtcmdemo.until; impor

Location許可權,因系統版本不同,6.0許可權對話方塊沒有,7.0,8.0正常,開發注意

1、一個獲取Location許可權引發的刺激,就這三個Location許可權 <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission an

Android動態許可權申請

第一次封裝庫,有問題歡迎提出來,哈哈哈^_^ Android6.0之後就要求敏感許可權動態申請,網上也有很多大佬封裝的庫,本人也沒研究。本著自娛自樂的精神,寫了一個。若有不完善需要改進的地方,歡迎大家提出,我也希望借鑑下不同的思想。本人菜鳥一枚,請包涵!!! 先上效果圖: 這裡

android應用上架應用市場需要注意的問題

最近公司剛剛開發完兩個app,需要到各個平臺去釋出,我們公司暫時只發布360應用市場和騰訊應用市場,由於之前準備工作不足導致釋出過程中遇到一些問題,今天在這裡整理一下。 1.首先最重要的是軟著登記證書,現在平臺審查嚴格,凡應用釋出必須有各個應用的軟著,360平臺上有兩個上傳

使用ajax動態載入html元素,onclick事件失效

原因:ajax動態載入之前js就載入完了,事件沒有繫結到動態生成的dom元素上。 問題:使用on事件,$('#btn').click(function(){})繫結無效 解決方案:使用on繫結事件。$(document).on('click','#btn',functio

Android動態載入基礎 ClassLoader工作機制

基本資訊 類載入器ClassLoader 早期使用過Eclipse等Java編寫的軟體的同學可能比較熟悉,Eclipse可以載入許多第三方的外掛(或者叫擴充套件),這就是動態載入。這些外掛大多是一些Jar包,而使用外掛其實就是動態載入Jar包裡的Class進行工作。這其實

簡豪全鋁家居在選擇品牌注意哪些問題

在完成了家庭裝修以後,需要選擇合適的傢俱產品,才可以在居住時,覺得舒適程度非常的高。各種型別的家居產品越來越多,在挑選的時候是需要注意很多方面的問題。越來越多的使用者對簡豪全鋁家居會非常的喜歡,環保級別很高,有很多的款式可以提供給使用者來進行選擇。市場當中也有很

Android webview 載入https:// 網站不展示 圖片資源

可能原因是:該圖片資源不是https的; 解決辦法: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { webSettings.setMixedContentMode(WebSetti

Android動態載入輪播圖BannerView

輪播圖在每個app中扮演著一個點綴的角色,在獨立做了三款app後都有這個需求,所以我決定把它單獨抽出來。以後只需copy,然後再根據需求改一下即可。 /** * 載入網路輪播圖 *@author jiangrongtao * *csdn

Android動態載入Activity原理

activity的啟動流程 載入一個Activity肯定不會像載入一般的類那樣,因為activity作為系統的元件有自己的生命週期,有系統的很多回調控制,所以自定義一個DexClassLoader類載入器來載入外掛中的Activity肯定是不可以的。 首先不得不瞭解一下ac

Android Studio製作.so實踐

前言 因為工作需要可能要用到JNI開發,本篇文章就分享一下我在這方面的實踐,以前我們使用Eclipse做NDK開發,非常麻煩,需要配cygwin的編譯環境,後面NDK功能完善才逐漸簡單點,如果想了解Eclipse如何配置NDK編譯環境可以參考我以前發表的舊文:

JAVA使用並行流(ParallelStream)注意一些問題

List<String> words = new ArrayList<String>(); words.add("your"); words.add("name"); public static Stream<Character> character

Android系統編譯so提示error undefined reference to '__android_log_print問題的解決

在系統原始碼的hardware/qcom下增加psam資料夾,編譯原始碼要生成libpsam.so庫,Android.mk內容 LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS)   LOCAL_MODULE &n

Android動態載入入坑指南

private static Element[] makeDexElements(List<File> files, File optimizedDirectory, List<IOException&g

關於修改Oracle伺服器IP及埠注意的問題

關於修改 Oracle 伺服器 IP 及埠時要注意的問題 (原創: flexitime ,轉載請註明出處,謝謝) 一個安裝好的 Oracle 伺服器,如果修改伺服器的 IP 地址或埠號,可能會使得這個伺服器無法再連線上。所以安裝時要注意一下以下問題。 1. 安裝 O

Android 動態許可權 第三方總結

前言  今天是2017年6月23日,到目前為止,Android6.0已經發布了兩年的時間,隨著時間的推移,Android6.0肯定會越來越普及,而6.0版本的一個重大改動就是增加了執行時許可權(動態許可權):一些危險的許可權不單止要在AndroidMainifest檔案宣告,還要在執行的時候使用程式碼來申請,