Android_動態載入.so檔案,解決apk安裝包過大的問題.
阿新 • • 發佈:2019-02-06
最近專案需要接入音視訊SDK,功能還沒開發,打出來的apk大了30多M…
曲線救國.
依賴的so都是音視訊的必要的核心SDK,不能刪除,但是又不想上傳一個超級大的apk到市場,
那麼解決的方案就是浪費使用者流量,畢竟現在網速那麼快,流量也幾乎等於不要錢了…
這叫從技術角度偽減小apk,其實在用到的時候還是要下載這些apk,但是上傳到市場小了就行.
so檔案不打包進apk,在安裝完應用開啟app的時候通過後臺下載so庫,將下載下來的so檔案再寫入到app裡面。
Android載入so檔案的方式有兩種:
System.loadLibrary();
System.load();
它們都可以用來裝載庫檔案,但是System.load引數必須為庫檔案的絕對路徑,可以是任意路徑;System.loadLibrary引數為庫檔名,不包含庫檔案的副檔名,必須是在JVM屬性java.library.path所指向的路徑中,路徑可以通過System.getProperty(‘java.library.path’) 獲得。所有動態載入的時候我們不能用System.loadLibrary,只能用System.load來載入。
我們知道,如果不做任何修改的話,我們靜態載入的so,他的路徑是App應用的system.lib路徑
由於我們是要動態下載so,並且要把下載的so 拷貝到一個目標路徑,去載入
最理想的方案就是直接指定packagename/system/lib,
既然是最理想的,那麼這方案肯定是不行的,因為這個目錄,只有讀的許可權,沒有寫的許可權.
拿root的 手機可以測試,我們手動把so.拷貝到這個目錄是可以的.
那麼我們只能在系統載入so的路徑中,額外新增一個系統認可的載入路徑即可,
最終的結果如下圖:我們把我們自定義的路徑加入到系統能認識的dexPathList裡面
Field pathListField = ShareReflectUtil.findField(classLoader, "pathList"); Object dexPathList = pathListField.get(classLoader); Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories"); List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList); libDirs.add(0, folder); Field systemNativeLibraryDirectories = // 就是這個路徑,把我們下載的路徑新增到這裡 ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories"); List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList); Method makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class); libDirs.addAll(systemLibDirs); Object[] elements = (Object[]) makePathElements. invoke(dexPathList, libDirs); Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements"); nativeLibraryPathElements.setAccessible(true); nativeLibraryPathElements.set(dexPathList, elements);
這裡我以接入某度的地圖SDK為例,首先我們在gradle 中排除某度的so.就是打包apk的時候不打進去
packagingOptions {
exclude 'lib/armeabi/libindoor.so'
exclude 'lib/armeabi/libBaiduMapSDK_base_v5_2_0.so'
exclude 'lib/armeabi/liblocSDK7b.so'
}
在application中呼叫動態載入so的方法,這裡不提供下載方法,我們直接copy ,so檔案到sd卡目錄
private void dynamicSo() { String dir = FileUtils.getAppRecordDir(sApplication).toString(); SoFile.loadSoFile(sApplication, dir); }
關鍵的來了.我們這邊用借用下騰訊tinker的動態載入部分程式碼
public class TinkerLoadLibrary {
private static final String TAG = "Tinker.LoadLibrary";
public static synchronized void installNativeLibraryPath(ClassLoader classLoader, File folder)
throws Throwable {
if (folder == null || !folder.exists()) {
Logger.e(TAG, "installNativeLibraryPath, folder %s is illegal", folder);
return;
}
// android o sdk_int 26
// for android o preview sdk_int 25
if ((Build.VERSION.SDK_INT == 25 && getPreviousSdkInt() != 0)
|| Build.VERSION.SDK_INT > 25) {
try {
V25.install(classLoader, folder);
return;
} catch (Throwable throwable) {
// install fail, try to treat it as v23
// some preview N version may go here
Logger.e(TAG, "installNativeLibraryPath, v25 fail, sdk: %d, error: %s, try to fallback to V23",
Build.VERSION.SDK_INT, throwable.getMessage());
V23.install(classLoader, folder);
}
} else if (Build.VERSION.SDK_INT >= 23) {
try {
V23.install(classLoader, folder);
} catch (Throwable throwable) {
// install fail, try to treat it as v14
Logger.e(TAG, "installNativeLibraryPath, v23 fail, sdk: %d, error: %s, try to fallback to V14",
Build.VERSION.SDK_INT, throwable.getMessage());
V14.install(classLoader, folder);
}
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(classLoader, folder);
}
}
/**
* fuck部分機型刪了該成員屬性,相容
*
* @return 被廠家刪了返回1,否則正常讀取
*/
@TargetApi(Build.VERSION_CODES.M)
private static int getPreviousSdkInt() {
try {
return Build.VERSION.PREVIEW_SDK_INT;
} catch (Throwable ignore) {
}
return 1;
}
private static final class V14 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
Object dexPathList = pathListField.get(classLoader);
ShareReflectUtil.expandFieldArray(dexPathList, "nativeLibraryDirectories", new File[]{folder});
}
}
private static final class V23 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
Object dexPathList = pathListField.get(classLoader);
Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
libDirs.add(0, folder);
Field systemNativeLibraryDirectories =
ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
Method makePathElements =
ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class, List.class);
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
libDirs.addAll(systemLibDirs);
Object[] elements = (Object[]) makePathElements.
invoke(dexPathList, libDirs, null, suppressedExceptions);
Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.setAccessible(true);
nativeLibraryPathElements.set(dexPathList, elements);
}
}
private static final class V25 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
Object dexPathList = pathListField.get(classLoader);
Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
libDirs.add(0, folder);
Field systemNativeLibraryDirectories =
ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
Method makePathElements =
ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class);
libDirs.addAll(systemLibDirs);
Object[] elements = (Object[]) makePathElements.
invoke(dexPathList, libDirs);
Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.setAccessible(true);
nativeLibraryPathElements.set(dexPathList, elements);
}
}
}
// 獲取路徑的類
public class ShareReflectUtil {
/**
* Locates a given field anywhere in the class inheritance hierarchy.
*
* @param instance an object to search the field into.
* @param name field name
* @return a field object
* @throws NoSuchFieldException if the field cannot be located
*/
public static Field findField(Object instance, String name) throws NoSuchFieldException {
for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
try {
Field field = clazz.getDeclaredField(name);
if (!field.isAccessible()) {
field.setAccessible(true);
}
return field;
} catch (NoSuchFieldException e) {
// ignore and search next
}
}
throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
}
public static Field findField(Class<?> originClazz, String name) throws NoSuchFieldException {
for (Class<?> clazz = originClazz; clazz != null; clazz = clazz.getSuperclass()) {
try {
Field field = clazz.getDeclaredField(name);
if (!field.isAccessible()) {
field.setAccessible(true);
}
return field;
} catch (NoSuchFieldException e) {
// ignore and search next
}
}
throw new NoSuchFieldException("Field " + name + " not found in " + originClazz);
}
/**
* Locates a given method anywhere in the class inheritance hierarchy.
*
* @param instance an object to search the method into.
* @param name method name
* @param parameterTypes method parameter types
* @return a method object
* @throws NoSuchMethodException if the method cannot be located
*/
public static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
throws NoSuchMethodException {
for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
try {
Method method = clazz.getDeclaredMethod(name, parameterTypes);
if (!method.isAccessible()) {
method.setAccessible(true);
}
return method;
} catch (NoSuchMethodException e) {
// ignore and search next
}
}
throw new NoSuchMethodException("Method "
+ name
+ " with parameters "
+ Arrays.asList(parameterTypes)
+ " not found in " + instance.getClass());
}
/**
* Locates a given method anywhere in the class inheritance hierarchy.
*
* @param clazz a class to search the method into.
* @param name method name
* @param parameterTypes method parameter types
* @return a method object
* @throws NoSuchMethodException if the method cannot be located
*/
public static Method findMethod(Class<?> clazz, String name, Class<?>... parameterTypes)
throws NoSuchMethodException {
for (; clazz != null; clazz = clazz.getSuperclass()) {
try {
Method method = clazz.getDeclaredMethod(name, parameterTypes);
if (!method.isAccessible()) {
method.setAccessible(true);
}
return method;
} catch (NoSuchMethodException e) {
// ignore and search next
}
}
throw new NoSuchMethodException("Method "
+ name
+ " with parameters "
+ Arrays.asList(parameterTypes)
+ " not found in " + clazz);
}
/**
* Locates a given constructor anywhere in the class inheritance hierarchy.
*
* @param instance an object to search the constructor into.
* @param parameterTypes constructor parameter types
* @return a constructor object
* @throws NoSuchMethodException if the constructor cannot be located
*/
public static Constructor<?> findConstructor(Object instance, Class<?>... parameterTypes)
throws NoSuchMethodException {
for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
try {
Constructor<?> ctor = clazz.getDeclaredConstructor(parameterTypes);
if (!ctor.isAccessible()) {
ctor.setAccessible(true);
}
return ctor;
} catch (NoSuchMethodException e) {
// ignore and search next
}
}
throw new NoSuchMethodException("Constructor"
+ " with parameters "
+ Arrays.asList(parameterTypes)
+ " not found in " + instance.getClass());
}
/**
* Replace the value of a field containing a non null array, by a new array containing the
* elements of the original array plus the elements of extraElements.
*
* @param instance the instance whose field is to be modified.
* @param fieldName the field to modify.
* @param extraElements elements to append at the end of the array.
*/
public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) jlrField.get(instance);
Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length);
// NOTE: changed to copy extraElements first, for patch load first
System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
System.arraycopy(original, 0, combined, extraElements.length, original.length);
jlrField.set(instance, combined);
}
/**
* Replace the value of a field containing a non null array, by a new array containing the
* elements of the original array plus the elements of extraElements.
*
* @param instance the instance whose field is to be modified.
* @param fieldName the field to modify.
*/
public static void reduceFieldArray(Object instance, String fieldName, int reduceSize)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
if (reduceSize <= 0) {
return;
}
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) jlrField.get(instance);
int finalLength = original.length - reduceSize;
if (finalLength <= 0) {
return;
}
Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), finalLength);
System.arraycopy(original, reduceSize, combined, 0, finalLength);
jlrField.set(instance, combined);
}
public static Object getActivityThread(Context context,
Class<?> activityThread) {
try {
if (activityThread == null) {
activityThread = Class.forName("android.app.ActivityThread");
}
Method m = activityThread.getMethod("currentActivityThread");
m.setAccessible(true);
Object currentActivityThread = m.invoke(null);
if (currentActivityThread == null && context != null) {
// In older versions of Android (prior to frameworks/base 66a017b63461a22842)
// the currentActivityThread was built on thread locals, so we'll need to try
// even harder
Field mLoadedApk = context.getClass().getField("mLoadedApk");
mLoadedApk.setAccessible(true);
Object apk = mLoadedApk.get(context);
Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread");
mActivityThreadField.setAccessible(true);
currentActivityThread = mActivityThreadField.get(apk);
}
return currentActivityThread;
} catch (Throwable ignore) {
return null;
}
}
/**
* Handy method for fetching hidden integer constant value in system classes.
*
* @param clazz
* @param fieldName
* @return
*/
public static int getValueOfStaticIntField(Class<?> clazz, String fieldName, int defVal) {
try {
final Field field = findField(clazz, fieldName);
return field.getInt(null);
} catch (Throwable thr) {
return defVal;
}
}
}
/**
* 檔案描述:提供一個檔案讀寫和載入指定路徑下so的類
*
* @author :feilong on 2018/11/6
*/
public class SoFile {
/**
* 載入 so 檔案(直接指定你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());
}
try {
TinkerLoadLibrary.installNativeLibraryPath(VchatApplication.getMyApplication().getBaseContext().getClassLoader(), dir);
} catch (Throwable throwable) {
Logger.e(throwable.getMessage());
}
}
/**
* 判斷 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 root = new File(fromFile);
//如同判斷SD卡是否存在或者檔案是否存在,如果不存在則 return出去
if (!root.exists()) {
return -1;
}
//如果存在則獲取當前目錄下的全部檔案 填充陣列
File[] currentFiles = root.listFiles();
//目標目錄
File targetDir = new File(toFile);
//建立目錄
if (!targetDir.exists()) {
targetDir.mkdirs();
}
if (currentFiles != null && currentFiles.length > 0) {
//遍歷要複製該目錄下的全部檔案
for (File currentFile : currentFiles) {
if (currentFile.isDirectory()) {
//如果當前項為子目錄 進行遞迴
copy(currentFile.getPath() + "/", toFile + currentFile.getName() + "/");
} else {
//如果當前項為檔案則進行檔案拷貝
if (currentFile.getName().contains(".so")) {
int id = copySdcardFile(currentFile.getPath(), toFile + File.separator + currentFile.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;
}
}
}
由於功力有限,原始碼以及底層的東西也不懂,都是谷歌的資料,和一步步除錯,之前也搜尋了很多之類的動態載入,但是都沒人把完整的程式碼放出來,都是投放了幾個方法,這裡把完整程式碼,和方法都投放
(因為我也不會寫原理,再不上程式碼,還TM寫個毛啊…)