原文地址: https://blog.csdn.net/Rong_L/article/details/75212472
前言
相信Android 開發中大家或多或少都會集成一些第三方sdk, 而其中難免要會使用到他們的so文件。但有時,你會發現這些so文件過多,對於一些需要經常更新的應用來說,這將會大大浪費用戶的流量。而有些sdk的集成僅僅是只為了一個不是必須的功能,我們完全有充足的理由用一些技術的手段來解決因這部分sdk集成帶來的安裝包大小問題。
so目錄
觀察發現,很多sdk的大小主要集中在so文件上。為了盡可能多的適應不同cup,sdk通常會提供不同二進制文件,這些文件被分門別類地放在armeabi,x86,mips等目錄下。這里我們有必要了解下這些目錄的含義。
目錄 cpu類型
armeabi ARM 通用cpu
armeabi-v7a 支持浮點運算ARM cpu,向下兼容armeabi
arm64-v8a ARM 64位cpu, 向下兼容armeabi-v7a
x86 x86通用型cpu
x86_64 x86 64位cpu
不同cpu在apk應用安裝時,會查找對應的目錄,比如,arm64位機子,會優先查看apk中是否有arm64-v8a目錄,如果有,則采用該目錄下的so文件,如果沒有,則會查找兼容的目錄。一旦確定下目錄之后,其他的目錄便不會再去管了。(日后如果在確定的目錄下沒有找到對應的so文件,也不會去其他目錄中找到)
目前市面上大部分手機都兼容armeabi-v7a,哪怕x86的cpu也會兼容(性能會有損耗)。所以armeabi-v7a目錄建議一定配置,其相比armeabi在性能上有很大的提升。
動態加載so
再次回到前言中的問題,我們有沒有什么辦法能夠減少so的大小,從而減少apk安裝包的大小呢?
1. 如果不太在意性能的損耗,那么我們完全可以只適配armeabi-v7a包和x86包,讓64位機器運行32位的so文件。
2. 單獨出arm版本和x86版本,這樣也可以減少一半的so大小。
可如果你覺得這樣包還是太大,比如我們現在用的crosswalk瀏覽器內核,單個so文件就達到了27M,同時適配x86的話,會達到58M, 這是我們所無法接受的事情!
於是乎開始想有沒有什么辦法能把so文件與apk文件分離開來,在程序運行的時候來把so文件下載下來,並引導程序去加載。從而實現動態加載so文件的目的。
System.load 與 System.loadLibrary
google出的結果直接導向了System.load和System.loadLibrary這兩個方法。
system.load 參數中加載的so的路徑,比如:system.load(“/data/data/com.codemao.android/libs/libcrosswalk.so”)
system.loadLibrary參數中傳入的是so的名稱,比如system.loadLibrary(“crosswalk”), 系統會自動根據名稱與機器的cpu型號,找到對應的so目錄,並加載對應的lib crosswalk.so文件。
(兩者文件都只能在app的私有目錄下)
那這樣子的話,是不是我們從遠程下載完so文件之后,解壓到app私有目錄下,在調用sdk的地方調用system.load主動加載so之后,就可以實現動態加載so文件了呢?
同學,你真是太天真啦!我們回想下自己寫的so文件是如何調用的?是不是在需要使用的類里主動調了system.loadLibrary呢?sdk也一樣,sdk在自己的代碼里主動調用了system.loadLibrary。而這時,我們so文件因為沒有隨着apk安裝到手機上,並不在它的尋找范圍之內,最后的結果是你即使調用了system.load加載了so文件,理論是可以找到相應的native方法了,但是sdk在調用system.loadLibrary時會拋出找不到對應的so文件的錯誤。
插件化如何處理so
這該如何處理sdk內部調用loadLibrary拋出的異常信息呢?apk內的so文件最終被放到了/data/app/com.codemao.android/lib/下面,我們總不能把遠程下載下來的so文件放入這里吧,可/data/app這個目錄下面的文件我們是沒有權限去執行讀寫操作的。
這里我們想到了另一個問題,插件化可以運行另一個apk,而apk里面難免會有so,那宿主程序又是如何處理插件的so文件呢?
查詢之后發現:原文地址
有時候我們在開發插件的時候,可能會調用so文件,一般來說有兩種方案:
一種是在加載插件的時候,先把插件中的so文件釋放到本地目錄,然后在把目錄設置到DexClassLoader類加載器的nativeLib中。
一種在插件初始化的時候,釋放插件中的so文件到本地目錄,然后使用System.load方法去全路徑加載so文件
這兩種方式的區別在於,
第一種方式的代碼邏輯放在了宿主工程中,同時so文件可以放在插件的任意目錄中,然后在解壓插件文件找到這個so文件釋放即可。
第二種方式的代碼邏輯是放在了插件中,同時so文件只能放在插件的assets目錄中,然后通過把插件文件設置到程序的AssetManager中,最后通過訪問assets中的so文件進行釋放。
我們自己apk使用的classloader是pathclassloader, 那我們是不是只要把so所在的目錄加入到pathclassloader的nativeLib之中就好了呢?
讓我們再次來看下system.loadLibrary:
public static void loadLibrary(String libname) { Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname); }
Runtime.java
synchronized void loadLibrary0(ClassLoader loader, String libname) { if (libname.indexOf((int)File.separatorChar) != -1) { throw new UnsatisfiedLinkError( "Directory separator should not appear in library name: " + libname); } String libraryName = libname; //loader這里傳入的是pathclassloader, 不為空 if (loader != null) { //調用findLibrary找到so路徑 String filename = loader.findLibrary(libraryName); if (filename == null) { throw new UnsatisfiedLinkError(loader + " couldn't find \"" + System.mapLibraryName(libraryName) + "\""); } //調用doLoad加載找到的so文件 String error = doLoad(filename, loader); if (error != null) { throw new UnsatisfiedLinkError(error); } return; } ...//以下邏輯我們可以暫且忽略 }
好的,這里我們看到加載的過程主要兩步:
1. 調用pathclassloader.findLibrary,先找到對應的so文件
2. 調用doLoad加入找到的so文件
那我們來看下classloader.findLibrary是如何找到對應so文件的:
@Override public String findLibrary(String name) { return pathList.findLibrary(name); }
pathList 是BaseDexClassLoader 里的DexPathList對象(注意6.0 開始nativeLibraryDirectories放的不在是File, 不過加載邏輯是一樣的, 要注意適配。)
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; }
這里主要做的事:
1. 調用system.mapLibraryName, 補全名稱, 如比libraryName=crosswalk, 補全之后會是lib crosswalk.so
2. 遍歷nativeLibraryDirectories,看下目錄下面有對應的文件嗎
哈哈,到這里,機會來了,我們只要把遠程下載so的目錄通過反射的方式放入nativeLibraryDirectories中就ok啦,真是太激動啦!!!
適配與實現方案
為了盡量減少性能損耗,我們先根據cpu的類型確定自己要下載的so文件,之后再用反射的方式把so的目錄加入到classloader中,這樣便可以解決so過大而引起apk包過大的問題。
但我們前面說過,6.0之后的DexPathList與6.0之前的DexPathList不一樣,這里要注意適配的問題,
6.0之后findLibrary 變為了:
public String findLibrary(String libraryName) { String fileName = System.mapLibraryName(libraryName); for (Element element : nativeLibraryPathElements) { String path = element.findNativeLibrary(fileName); if (path != null) { return path; } } return null; }
6.0和之前的:
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; }
Element 中的代碼如下:
public String findNativeLibrary(String name) { maybeInit(); if (isDirectory) { String path = new File(dir, name).getPath(); if (IoUtils.canOpenReadOnly(path)) { return path; } } else if (zipFile != null) { String entryName = new File(dir, name).getPath(); if (isZipEntryExistsAndStored(zipFile, entryName)) { return zip.getPath() + zipSeparator + entryName; } } return null; }
所以我這里直接給出適配好的關鍵代碼,供大家參考
/** * 將 so所在的目錄放入PathClassLoader里的nativeLibraryDirectories中 * * @param context */ public void installSoDir(Context context) { //安卓4.0以下不維護 if(Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { return ; } File soDirFile = context.getDir(soDir, Context.MODE_PRIVATE); if(!soDirFile.exists()) { soDirFile.mkdirs(); } if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { v23Install(soDirFile, context); } else { v14Install(soDirFile, context); } } private void v14Install(File soDirFile, Context context) { PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader(); Object pathList = getPathList(pathClassLoader); if(pathList != null) { //獲取當前類的屬性 try { Field nativeLibraryDirectoriesField = pathList.getClass().getDeclaredField("nativeLibraryDirectories"); nativeLibraryDirectoriesField.setAccessible(true); Object list = nativeLibraryDirectoriesField.get(pathList); if(list instanceof List) { ((List) list).add(soDirFile); } else if(list instanceof File[]) { File[] newList = new File[((File[]) list).length + 1]; System.arraycopy(list, 0 , newList, 0, ((File[]) list).length); newList[((File[]) list).length] = soDirFile; nativeLibraryDirectoriesField.set(pathList, newList); } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } private void v23Install(File soDirFile, Context context) { PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader(); Object pathList = getPathList(pathClassLoader); if(pathList != null) { //獲取當前類的屬性 try { Field nativeLibraryPathField = pathList.getClass().getDeclaredField("nativeLibraryPathElements"); nativeLibraryPathField.setAccessible(true); Object list = nativeLibraryPathField.get(pathList); Class<?> elementType = nativeLibraryPathField.getType().getComponentType(); Constructor<?> constructor = elementType.getConstructor(File.class, boolean.class, File.class, DexFile.class); constructor.setAccessible(true); Object element = constructor.newInstance(soDirFile, true, null, null); if(list instanceof List) { ((List) list).add(element); } else if(list instanceof Object[]) { Object[] newList = new File[((Object[]) list).length + 1]; System.arraycopy(list, 0 , newList, 0, ((Object[]) list).length); newList[((Object[]) list).length] = element; nativeLibraryPathField.set(pathList, newList); } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } } private Object getPathList(Object classLoader) { Class cls = null; String pathListName = "pathList"; try { cls = Class.forName("dalvik.system.BaseDexClassLoader"); Field declaredField = cls.getDeclaredField(pathListName); declaredField.setAccessible(true); return declaredField.get(classLoader); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return null; }
參考文章
Android中so使用知識和問題總結以及插件開發過程中加載so的方案解析
Android項目針對libs(armeabi,armeabi-v7a,x86)進行平台兼容
Android JNI之System.loadLibrary()流程
