天天看點

Android 源碼分析實戰 - 動态加載修複 so 庫

1. 需求背景

俗話說養兵千日用兵一時,學習源碼分析到底有什麼用呢?我們遇到的所有問題,都能通過分析源碼解決;看似無法實作的功能,都能通過源碼分析找到思路…。這些都是之前無數次給大家洗腦的概念,我們來看一下實際的開發需求,我帶大家來動手實戰幾次。之前還在有信時,我們做的是一個音頻直播的項目,後面由于這一塊業務一直上不去,老闆要我們在裡面做一個 3D 的玩法,也就是采用 Unity + Android 的開發方式。Unity 導出的資源大概有 200M 左右,直接內建到 Android 肯定會增大包體積。而包體積太大,會影響我們的安裝速度和啟動速度等等,這個在之前就分析過源碼原理,不是我們的重點。我們的重點是需求得實作但不能增大包體積,後面我們做出來的效果是包體積隻增大了 50K 。由于實作了 Unity 與 Android 項目互動的從 0 到 1,實作了 Unity 資源的動态加載,我被評為了公司的優秀員工。後來需求搞完部門就合并了,大家走的走散的散,業務起不來我們也沒了激情。再後來在面試履歷上也是簡單的寫上了一筆,迷迷糊糊就進了騰訊,幸福也是來得太突然。其實大公司也很悲催 996 壓力大,隻是一般人我不好意思告訴他。

2. 需求分析

Unity 導出的資源大緻分為三個部分,一個部分是 jar 包 50K 左右,第二部分是 so 庫 30M 左右,第三部分是 assets 資源 100M 左右。首先是 jar 包,我們寫的 Activity 需要繼承自 jar 裡面的 UnityPlayerActivity,而且 jar 包也并不大,是以我們不做動态加載應該沒問題;其次是 assets 資源,這個也是很容易處理的;難就難在 so 的動态加載,當然有的同學肯定會認為這有什麼難的,不就是:

static {
  System.load("下載下傳好的 so 目錄全路徑");
}  
           

問題是 unity 導出的 jar 包中是這樣寫的

static {
  (new k()).a();
  o = false;
  o = loadLibraryStatic("main");
}

protected static boolean loadLibraryStatic(String var0) {
  try {
    System.loadLibrary(var0);
    return true;
  } catch (UnsatisfiedLinkError var1) {
    com.unity3d.player.e.Log(6, "Unable to find " + var0);
    return false;
  } catch (Exception var2) {
    com.unity3d.player.e.Log(6, "Unknown error " + var2);
    return false;
  }
}
           

如果隻是這樣也還好,我們隻要把這個 jar 裡的源碼,修改成 load 這種方案就好了,但比較坑的就是 libmain.so 在 C++ 層,還會動态的加載另外的兩個 so 庫檔案,當時我是一晚上沒睡好呀。晚上實在睡不着,我就開始翻 so 加載流程的源碼。

3. 源碼分析

public static void loadLibrary(String libname) {
  Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}

synchronized void loadLibrary0(ClassLoader loader, String libname) {
  String libraryName = libname;
  if (loader != null) {
    // 通過 libname 從 ClassLoader 去找到 filename
    String filename = loader.findLibrary(libraryName);
    if (filename == null) {
      throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
        System.mapLibraryName(libraryName) + "\"");
    }
    String error = nativeLoad(filename, loader);
    if (error != null) {
      throw new UnsatisfiedLinkError(error);
    }
    return;
  }
  ...
}

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

public String findLibrary(String libraryName) {
  String fileName = System.mapLibraryName(libraryName);
  // 通過 nativeLibraryPathElements 來周遊查找傳回的
  for (Element element : nativeLibraryPathElements) {
    String path = element.findNativeLibrary(fileName);
    if (path != null) {
      return path;
    }
  }
  return null;
}
           

源碼其實還是比較簡單的,最終的 so 檔案是通過 DexPathList 周遊 nativeLibraryPathElements 來查找傳回的,那麼我們是不是可以往 nativeLibraryPathElements 的最前面插入一個 Element 呢?顯然這是可以的,而且我們早在三年前的視訊中就開始用這種套路,來動态加載修複 class 類了,難度系數并不大。

4. 版本适配

封裝寫完代碼後,我開始迫不及待的炫耀了一番,轉手就拿給同僚去內建了:

Android 源碼分析實戰 - 動态加載修複 so 庫

這才意識到自己是新手上路,于是我把 Android 4.0 - 9.0 的源碼統統翻了個遍。

6.0 源碼
private final Element[] nativeLibraryPathElements;

static class Element {
  private final File dir;
  private final boolean isDirectory;
  private final File zip;
  private final DexFile dexFile;
  private ZipFile zipFile;

  public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
    this.dir = dir;
    this.isDirectory = isDirectory;
    this.zip = zip;
    this.dexFile = dexFile;
  }
}

8.0 源碼
private final NativeLibraryElement[] nativeLibraryPathElements;

static class NativeLibraryElement {
  private final String zipDir;
  private boolean initialized;

  public NativeLibraryElement(File dir) {
    this.path = dir;
    this.zipDir = null;
  }

  public NativeLibraryElement(File zip, String zipDir) {
    this.path = zip;
    this.zipDir = zipDir;
    if (zipDir == null) {
      throw new IllegalArgumentException();
    }
  }
}

5.0 的源碼
private final File[] nativeLibraryDirectories;
           

很多同學在開發的過程中,可能下意識的會想到大廠的一些第三方架構,但其實很多功能他們也未必有現成的實作,其次很多東西我們未必用得上,最重要的是自己寫的未必就不行。

Android 源碼分析實戰 - 動态加載修複 so 庫

視訊位址:https://pan.baidu.com/s/1COarOAlmOPCUlKsazho0Cw 

視訊密碼:8lr0