当前位置:   article > 正文

preload-classes的前世今生(1)

zygote preloadclasses

preload-classes的前世今生(1)

preloaded-classes

在Zygote初始化的时候,会调用到ZygoteInit的main方法。在注册了ZygoteSocket的控制通道之后,就调用preload方法去加载一些预加载的数据。

  1. public static void main(String argv[]) {
  2. try {
  3. // Start profiling the zygote initialization.
  4. SamplingProfilerIntegration.start();
  5. registerZygoteSocket();
  6. EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,
  7. SystemClock.uptimeMillis());
  8. preload();
  9. ...

preload方法会加载三个部分,preloadClasses,preloadResources和preloadOpenGL。

  1. static void preload() {
  2. preloadClasses();
  3. preloadResources();
  4. preloadOpenGL();
  5. }

preloadClasses的源代码不长,我们简单看一下:
首先,打开PRELOADED_CLASSES文件,读取文件中的内容

InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(PRELOADED_CLASSES);

如果不是以'#'开头的,则加载该类:

Class.forName(line);

这个PRELOADED_CLASSES文件位于frameworks/base/preloaded-classes。
我们看看这个文件的前几行:

  1. # Classes which are preloaded by com.android.internal.os.ZygoteInit.
  2. # Automatically generated by frameworks/base/tools/preload/WritePreloadedClassFile.java.
  3. # MIN_LOAD_TIME_MICROS=1250
  4. # MIN_PROCESSES=10
  5. android.R$styleable
  6. android.accounts.Account
  7. android.accounts.Account$1
  8. android.accounts.AccountManager
  9. android.accounts.AccountManager$12
  10. android.accounts.AccountManager$13
  11. android.accounts.AccountManager$6
  12. android.accounts.AccountManager$AmsTask
  13. android.accounts.AccountManager$AmsTask$1
  14. android.accounts.AccountManager$AmsTask$Response
  15. android.accounts.AccountManagerFuture

下面是完整代码,有删节。

  1. private static void preloadClasses() {
  2. final VMRuntime runtime = VMRuntime.getRuntime();
  3. InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(PRELOADED_CLASSES);
  4. if (is == null) {
  5. Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
  6. } else {
  7. Log.i(TAG, "Preloading classes...");
  8. long startTime = SystemClock.uptimeMillis();
  9. // Drop root perms while running static initializers.
  10. setEffectiveGroup(UNPRIVILEGED_GID);
  11. setEffectiveUser(UNPRIVILEGED_UID);
  12. // Alter the target heap utilization. With explicit GCs this
  13. // is not likely to have any effect.
  14. float defaultUtilization = runtime.getTargetHeapUtilization();
  15. runtime.setTargetHeapUtilization(0.8f);
  16. // Start with a clean slate.
  17. System.gc();
  18. runtime.runFinalizationSync();
  19. Debug.startAllocCounting();
  20. try {
  21. BufferedReader br
  22. = new BufferedReader(new InputStreamReader(is), 256);
  23. int count = 0;
  24. String line;
  25. while ((line = br.readLine()) != null) {
  26. // Skip comments and blank lines.
  27. line = line.trim();
  28. if (line.startsWith("#") || line.equals("")) {
  29. continue;
  30. }
  31. try {
  32. Class.forName(line);
  33. if (Debug.getGlobalAllocSize() > PRELOAD_GC_THRESHOLD) {
  34. System.gc();
  35. runtime.runFinalizationSync();
  36. Debug.resetGlobalAllocSize();
  37. }
  38. count++;
  39. } catch (ClassNotFoundException e) {
  40. Log.w(TAG, "Class not found for preloading: " + line);
  41. } catch (UnsatisfiedLinkError e) {
  42. Log.w(TAG, "Problem preloading " + line + ": " + e);
  43. } catch (Throwable t) {
  44. Log.e(TAG, "Error preloading " + line + ".", t);
  45. if (t instanceof Error) {
  46. throw (Error) t;
  47. }
  48. if (t instanceof RuntimeException) {
  49. throw (RuntimeException) t;
  50. }
  51. throw new RuntimeException(t);
  52. }
  53. }
  54. Log.i(TAG, "...preloaded " + count + " classes in "
  55. + (SystemClock.uptimeMillis()-startTime) + "ms.");
  56. } catch (IOException e) {
  57. Log.e(TAG, "Error reading " + PRELOADED_CLASSES + ".", e);
  58. } finally {
  59. IoUtils.closeQuietly(is);
  60. // Restore default.
  61. runtime.setTargetHeapUtilization(defaultUtilization);
  62. // Fill in dex caches with classes, fields, and methods brought in by preloading.
  63. runtime.preloadDexCaches();
  64. Debug.stopAllocCounting();
  65. // Bring back root. We'll need it later.
  66. setEffectiveUser(ROOT_UID);
  67. setEffectiveGroup(ROOT_GID);
  68. }
  69. }
  70. }

WritePreloadedClassFile.java

先看看两个重要参数:MIN_LOAD_TIME_MICROS和MIN_PROCESSES。

  • MIN_LOAD_TIME_MICROS:是最小的类加载时间,如果大于这个,才值得被装载进preload-classes
  • MIN_PROCESSES:是最少被多少个进程所装载,太少见的就算了
  1. public class WritePreloadedClassFile {
  2. /**
  3. * Preload any class that take longer to load than MIN_LOAD_TIME_MICROS us.
  4. */
  5. static final int MIN_LOAD_TIME_MICROS = 1250;
  6. /**
  7. * Preload any class that was loaded by at least MIN_PROCESSES processes.
  8. */
  9. static final int MIN_PROCESSES = 10;
  10. ...

我们再往下看,这个工具的用法:

  1. public static void main(String[] args) throws IOException,
  2. ClassNotFoundException {
  3. if (args.length != 1) {
  4. System.err.println("Usage: WritePreloadedClassFile [compiled log]");
  5. System.exit(-1);
  6. }
  7. String rootFile = args[0];
  8. Root root = Root.fromFile(rootFile);
  9. ...

这个工具要处理的内容,是一种叫compiled log的东西。

这个compiled log,是经由frameworks/base/tools/preload/Compile.java所编译出来的。
于是我们再看Compile.java的源码,

  1. public class Compile {
  2. public static void main(String[] args) throws IOException {
  3. if (args.length != 2) {
  4. System.err.println("Usage: Compile [log file] [output file]");
  5. System.exit(0);
  6. }
  7. ...

这个log是谁打出来的呢?靠跟踪是查不下去了,我们需要看另一头,虚拟机是如何打印出这个log的。

虚拟机的log

Android 2.3的dalvik/vm/oo/Class.c

我们先从Android 2.3说起,在这个版本上,有一个宏LOG_CLASS_LOADING,打开这个宏,就可以让虚拟机将类的信息输出到log出。
[http://androidxref.com/2.3.7/xref/dalvik/vm/oo/Class.c#22]
虚拟机会输出一个这样格式的Log,用于记录一个类的加载时间。

  1. LOG(LOG_INFO, "PRELOAD", "%c%d:%d:%d:%s:%d:%s:%lld\n", type, ppid, pid, tid,
  2. get_process_name(), (int) clazz->classLoader, clazz->descriptor,time);
  • 第1个参数:type:

    • 定义类开始

    • < 定义类结束
      • 初始化开始
      • 初始化结束
  • 第2个参数:ppid,父进程的进程号
  • 第3个参数:pid,本进程的进程号
  • 第4个参数:tid,本线程的线程号
  • 第5个参数:process_name,进程名
  • 第6个参数:类的classLoader
  • 第7个参数:类的描述符
  • 第8个参数:时间戳,以纳秒为单位

我们看下源码就一目了然了:

  1. static void logClassLoadWithTime(char type, ClassObject* clazz, u8 time) {
  2. pid_t ppid = getppid();
  3. pid_t pid = getpid();
  4. unsigned int tid = (unsigned int) pthread_self();
  5. LOG(LOG_INFO, "PRELOAD", "%c%d:%d:%d:%s:%d:%s:%lld\n", type, ppid, pid, tid,
  6. get_process_name(), (int) clazz->classLoader, clazz->descriptor,
  7. time);
  8. }

为了计时方便,我们将dvmGetThreadCpuTimeNsec的计时封装在函数内,定义一个包装函数,代码如下:

  1. /*
  2. * Logs information about a class loading.
  3. */
  4. static void logClassLoad(char type, ClassObject* clazz) {
  5. logClassLoadWithTime(type, clazz, dvmGetThreadCpuTimeNsec());
  6. }

类的定义的点都在findClassNoInit方法中,代码见:http://androidxref.com/2.3.7/xref/dalvik/vm/oo/Class.c#1411
类的初始化的打点在dvmInitClass中,代码见:http://androidxref.com/2.3.7/xref/dalvik/vm/oo/Class.c#4228

Android 4.x的dalvik/vm/oo/Class.cpp

4.x之后,Class.c变成了Class.cpp,不过对于打印类加载信息这部分没有什么实质上的变化。

  1. #if LOG_CLASS_LOADING
  2. static void logClassLoadWithTime(char type, ClassObject* clazz, u8 time) {
  3. pid_t ppid = getppid();
  4. pid_t pid = getpid();
  5. unsigned int tid = (unsigned int) pthread_self();
  6. ALOG(LOG_INFO, "PRELOAD", "%c%d:%d:%d:%s:%d:%s:%lld", type, ppid, pid, tid,
  7. get_process_name(), (int) clazz->classLoader, clazz->descriptor,
  8. time);
  9. }
  10. static void logClassLoad(char type, ClassObject* clazz) {
  11. logClassLoadWithTime(type, clazz, dvmGetThreadCpuTimeNsec());
  12. }
  13. #endif

定义类还是在findClassNoInit,http://androidxref.com/4.4.4_r1/xref/dalvik/vm/oo/Class.cpp#1473
初始化类还是在dvmInitClass中,http://androidxref.com/4.4.4_r1/xref/dalvik/vm/oo/Class.cpp#4241

  1. log示例

下面我们摘取一段log中抽取出来的一个完整的片段,包含了android.text.format.DateFormat类的定义和初始化一个对象的完整日志。

  1. I/PRELOAD ( 6327): >356:6327:1074467156:unknown:0:Ljava/lang/Object;:206523751
  2. I/PRELOAD ( 6327): <356:6327:1074467156:unknown:0:Ljava/lang/Object;:206921667

log终于大功告成了,下面我们回到frameworks/base/tools/preload/Compile.java,继续讨论如何将log转化为compiled log.

log的编译过程

frameworks/base/tools/preload/Compile.java是一个host的工具,运行在PC上。好在是java写的,还算跨平台,在Linux下编译出来的jar在Windows下也可以用。
按照新冬说的办法: mmm frameworks/base/tools/preload/
然后从out/host/linux-x86/framework中将preload.jar复制到可以用adb连接手机的PC上。

我们继续看Compile.java的源代码:

  1. 39 Root root = new Root();
  2. 40
  3. 41 List<Record> records = new ArrayList<Record>();
  4. 42
  5. 43 BufferedReader in = new BufferedReader(new InputStreamReader(
  6. 44 new FileInputStream(args[0])));
  7. 45
  8. 46 String line;
  9. 47 int lineNumber = 0;
  10. 48 while ((line = in.readLine()) != null) {
  11. 49 lineNumber++;
  12. 50 if (line.startsWith("I/PRELOAD")) {
  13. 51 try {
  14. 52 String clipped = line.substring(19);
  15. 53 records.add(new Record(clipped, lineNumber));
  16. 54 } catch (RuntimeException e) {
  17. 55 throw new RuntimeException(
  18. 56 "Exception while recording line " + lineNumber + ": " + line, e);
  19. 57 }
  20. 58 }
  21. 59 }

说实话,原生这段逻辑的处理方式实在是过于简单了,对log格式的处理实在是不足够现代化。每条期待TAG为PRELOAD,其它的log全部过滤。并且,取到type值的办法用的是最土的直接数出19个字符这样的方法。这样导致,用logcat -vtime抓出来的log是无法识别的。

下面我们来分析Record的生成过程。这里显示出现有设计的一个不足,就是采用":"来做分隔。
代码如下:

  1. 125 line = line.substring(1);
  2. 126 String[] parts = line.split(":");

这就导致了,如果包名中出现了":"的话,就会split失败。为了解决这个问题,只好打补丁。
Android采用了一个非常土的办法,将":"替换成u003A,这个过程需要手动去做。

这一段逻辑的完整代码如下:

  1. 109 Record(String line, int lineNum) {
  2. 110 char typeChar = line.charAt(0);
  3. 111 switch (typeChar) {
  4. 112 case '>': type = Type.START_LOAD; break;
  5. 113 case '<': type = Type.END_LOAD; break;
  6. 114 case '+': type = Type.START_INIT; break;
  7. 115 case '-': type = Type.END_INIT; break;
  8. 116 default: throw new AssertionError("Bad line: " + line);
  9. 117 }
  10. 118
  11. 119 sourceLineNumber = lineNum;
  12. 120
  13. 121 for (int i = 0; i < REPLACE_CLASSES.length; i+= 2) {
  14. 122 line = line.replace(REPLACE_CLASSES[i], REPLACE_CLASSES[i+1]);
  15. 123 }
  16. 124
  17. 125 line = line.substring(1);
  18. 126 String[] parts = line.split(":");
  19. 127
  20. 128 ppid = Integer.parseInt(parts[0]);
  21. 129 pid = Integer.parseInt(parts[1]);
  22. 130 tid = Integer.parseInt(parts[2]);
  23. 131
  24. 132 processName = decode(parts[3]).intern();
  25. 133
  26. 134 classLoader = Integer.parseInt(parts[4]);
  27. 135 className = vmTypeToLanguage(decode(parts[5])).intern();
  28. 136
  29. 137 time = Long.parseLong(parts[6]);
  30. 138 }

我们看看这些patch冒号的代码,如果遇到了新的类,我们也需要去添加新的类。

  1. 22 /**
  2. 23 * The delimiter character we use, {@code :}, conflicts with some other
  3. 24 * names. In that case, manually replace the delimiter with something else.
  4. 25 */
  5. 26 private static final String[] REPLACE_CLASSES = {
  6. 27 "com.google.android.apps.maps:FriendService",
  7. 28 "com.google.android.apps.maps\\u003AFriendService",
  8. 29 "com.google.android.apps.maps:driveabout",
  9. 30 "com.google.android.apps.maps\\u003Adriveabout",
  10. 31 "com.google.android.apps.maps:GoogleLocationService",
  11. 32 "com.google.android.apps.maps\\u003AGoogleLocationService",
  12. 33 "com.google.android.apps.maps:LocationFriendService",
  13. 34 "com.google.android.apps.maps\\u003ALocationFriendService",
  14. 35 "com.google.android.apps.maps:MapsBackgroundService",
  15. 36 "com.google.android.apps.maps\\u003AMapsBackgroundService",
  16. 37 "com.google.android.apps.maps:NetworkLocationService",
  17. 38 "com.google.android.apps.maps\\u003ANetworkLocationService",
  18. ...

比如我不幸遇上了虾米音乐:fm.xiami.yunos:ui,就写这样两行将”:“替换成u003A。

  1. "fm.xiami.yunos:ui",
  2. "fm.xiami.yunos\\u003Aui"

这方法土掉渣了,需要一个一个地探雷。。。

光读log还不行,还要去在线查询内存占用情况。我们来看frameworks/base/tools/preload/MemoryUsage.java中的measure方法。

  1. 221 private MemoryUsage measure() {
  2. 222 String[] commands = GET_DIRTY_PAGES;
  3. 223 if (className != null) {
  4. 224 List<String> commandList = new ArrayList<String>(
  5. 225 GET_DIRTY_PAGES.length + 1);
  6. 226 commandList.addAll(Arrays.asList(commands));
  7. 227 commandList.add(className);
  8. 228 commands = commandList.toArray(new String[commandList.size()]);
  9. 229 }
  10. 230
  11. 231 try {
  12. 232 final Process process = Runtime.getRuntime().exec(commands);

这里面要在adb shell中执行一个GET_DIRTY_PAGES的工具命令,我们看看这是何方神圣:

  1. 160 private static final String CLASS_PATH = "-Xbootclasspath"
  2. 161 + ":/system/framework/core.jar"
  3. 162 + ":/system/framework/ext.jar"
  4. 163 + ":/system/framework/framework.jar"
  5. 164 + ":/system/framework/framework-tests.jar"
  6. 165 + ":/system/framework/services.jar"
  7. 166 + ":/system/framework/loadclass.jar";
  8. 167
  9. 168 private static final String[] GET_DIRTY_PAGES = {
  10. 169 "adb", "shell", "dalvikvm", CLASS_PATH, "LoadClass" };

这个是要调用LoadClass类,位于system/framework/loadclass.jar中。那么这个loadclass.jar又是从哪里来的呢?答案是在frameworks/base/tools/preload/loadclass/目录中。
我们看loadclass目录中的Android.mk:

  1. LOCAL_PATH := $(call my-dir)
  2. include $(CLEAR_VARS)
  3. LOCAL_SRC_FILES := $(call all-subdir-java-files)
  4. LOCAL_MODULE_TAGS := tests
  5. LOCAL_MODULE := loadclass
  6. include $(BUILD_JAVA_LIBRARY)

于是我们make一下这个工具:mmm frameworks/base/tools/preload/loadclass/
然后我们把这个jar push到手机上的/system/framework/中去。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/227974
推荐阅读
相关标签
  

闽ICP备14008679号