preload-classes的前世今生(1)
preloaded-classes
在Zygote初始化的时候,会调用到ZygoteInit的main方法。在注册了ZygoteSocket的控制通道之后,就调用preload方法去加载一些预加载的数据。
public static void main(String argv[]) {
try {
// Start profiling the zygote initialization.
SamplingProfilerIntegration.start();
registerZygoteSocket();
EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,
SystemClock.uptimeMillis());
preload();
...
preload方法会加载三个部分,preloadClasses,preloadResources和preloadOpenGL。
static void preload() {
preloadClasses();
preloadResources();
preloadOpenGL();
}
preloadClasses的源代码不长,我们简单看一下:
首先,打开PRELOADED_CLASSES文件,读取文件中的内容
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(PRELOADED_CLASSES);
如果不是以'#'开头的,则加载该类:
Class.forName(line);
这个PRELOADED_CLASSES文件位于frameworks/base/preloaded-classes。
我们看看这个文件的前几行:
# Classes which are preloaded by com.android.internal.os.ZygoteInit.
# Automatically generated by frameworks/base/tools/preload/WritePreloadedClassFile.java.
# MIN_LOAD_TIME_MICROS=1250
# MIN_PROCESSES=10
android.R$styleable
android.accounts.Account
android.accounts.Account$1
android.accounts.AccountManager
android.accounts.AccountManager$12
android.accounts.AccountManager$13
android.accounts.AccountManager$6
android.accounts.AccountManager$AmsTask
android.accounts.AccountManager$AmsTask$1
android.accounts.AccountManager$AmsTask$Response
android.accounts.AccountManagerFuture
下面是完整代码,有删节。
private static void preloadClasses() {
final VMRuntime runtime = VMRuntime.getRuntime();
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(PRELOADED_CLASSES);
if (is == null) {
Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
} else {
Log.i(TAG, "Preloading classes...");
long startTime = SystemClock.uptimeMillis();
// Drop root perms while running static initializers.
setEffectiveGroup(UNPRIVILEGED_GID);
setEffectiveUser(UNPRIVILEGED_UID);
// Alter the target heap utilization. With explicit GCs this
// is not likely to have any effect.
float defaultUtilization = runtime.getTargetHeapUtilization();
runtime.setTargetHeapUtilization(0.8f);
// Start with a clean slate.
System.gc();
runtime.runFinalizationSync();
Debug.startAllocCounting();
try {
BufferedReader br
= new BufferedReader(new InputStreamReader(is), 256);
int count = 0;
String line;
while ((line = br.readLine()) != null) {
// Skip comments and blank lines.
line = line.trim();
if (line.startsWith("#") || line.equals("")) {
continue;
}
try {
Class.forName(line);
if (Debug.getGlobalAllocSize() > PRELOAD_GC_THRESHOLD) {
System.gc();
runtime.runFinalizationSync();
Debug.resetGlobalAllocSize();
}
count++;
} catch (ClassNotFoundException e) {
Log.w(TAG, "Class not found for preloading: " + line);
} catch (UnsatisfiedLinkError e) {
Log.w(TAG, "Problem preloading " + line + ": " + e);
} catch (Throwable t) {
Log.e(TAG, "Error preloading " + line + ".", t);
if (t instanceof Error) {
throw (Error) t;
}
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
}
throw new RuntimeException(t);
}
}
Log.i(TAG, "...preloaded " + count + " classes in "
+ (SystemClock.uptimeMillis()-startTime) + "ms.");
} catch (IOException e) {
Log.e(TAG, "Error reading " + PRELOADED_CLASSES + ".", e);
} finally {
IoUtils.closeQuietly(is);
// Restore default.
runtime.setTargetHeapUtilization(defaultUtilization);
// Fill in dex caches with classes, fields, and methods brought in by preloading.
runtime.preloadDexCaches();
Debug.stopAllocCounting();
// Bring back root. We'll need it later.
setEffectiveUser(ROOT_UID);
setEffectiveGroup(ROOT_GID);
}
}
}
WritePreloadedClassFile.java
先看看两个重要参数:MIN_LOAD_TIME_MICROS和MIN_PROCESSES。
- MIN_LOAD_TIME_MICROS:是最小的类加载时间,如果大于这个,才值得被装载进preload-classes
- MIN_PROCESSES:是最少被多少个进程所装载,太少见的就算了
public class WritePreloadedClassFile {
/**
* Preload any class that take longer to load than MIN_LOAD_TIME_MICROS us.
*/
static final int MIN_LOAD_TIME_MICROS = 1250;
/**
* Preload any class that was loaded by at least MIN_PROCESSES processes.
*/
static final int MIN_PROCESSES = 10;
...
我们再往下看,这个工具的用法:
public static void main(String[] args) throws IOException,
ClassNotFoundException {
if (args.length != 1) {
System.err.println("Usage: WritePreloadedClassFile [compiled log]");
System.exit(-1);
}
String rootFile = args[0];
Root root = Root.fromFile(rootFile);
...
这个工具要处理的内容,是一种叫compiled log的东西。
这个compiled log,是经由frameworks/base/tools/preload/Compile.java所编译出来的。
于是我们再看Compile.java的源码,
public class Compile {
public static void main(String[] args) throws IOException {
if (args.length != 2) {
System.err.println("Usage: Compile [log file] [output file]");
System.exit(0);
}
...
这个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,用于记录一个类的加载时间。
LOG(LOG_INFO, "PRELOAD", "%c%d:%d:%d:%s:%d:%s:%lld\n", type, ppid, pid, tid,
get_process_name(), (int) clazz->classLoader, clazz->descriptor,time);
- 第1个参数:type:
- > 定义类开始
- < 定义类结束
- + 初始化开始
- - 初始化结束
- 第2个参数:ppid,父进程的进程号
- 第3个参数:pid,本进程的进程号
- 第4个参数:tid,本线程的线程号
- 第5个参数:process_name,进程名
- 第6个参数:类的classLoader
- 第7个参数:类的描述符
- 第8个参数:时间戳,以纳秒为单位
我们看下源码就一目了然了:
static void logClassLoadWithTime(char type, ClassObject* clazz, u8 time) {
pid_t ppid = getppid();
pid_t pid = getpid();
unsigned int tid = (unsigned int) pthread_self();
LOG(LOG_INFO, "PRELOAD", "%c%d:%d:%d:%s:%d:%s:%lld\n", type, ppid, pid, tid,
get_process_name(), (int) clazz->classLoader, clazz->descriptor,
time);
}
为了计时方便,我们将dvmGetThreadCpuTimeNsec的计时封装在函数内,定义一个包装函数,代码如下:
/*
* Logs information about a class loading.
*/
static void logClassLoad(char type, ClassObject* clazz) {
logClassLoadWithTime(type, clazz, dvmGetThreadCpuTimeNsec());
}
类的定义的点都在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,不过对于打印类加载信息这部分没有什么实质上的变化。
#if LOG_CLASS_LOADING
static void logClassLoadWithTime(char type, ClassObject* clazz, u8 time) {
pid_t ppid = getppid();
pid_t pid = getpid();
unsigned int tid = (unsigned int) pthread_self();
ALOG(LOG_INFO, "PRELOAD", "%c%d:%d:%d:%s:%d:%s:%lld", type, ppid, pid, tid,
get_process_name(), (int) clazz->classLoader, clazz->descriptor,
time);
}
static void logClassLoad(char type, ClassObject* clazz) {
logClassLoadWithTime(type, clazz, dvmGetThreadCpuTimeNsec());
}
#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
h5. log示例
下面我们摘取一段log中抽取出来的一个完整的片段,包含了android.text.format.DateFormat类的定义和初始化一个对象的完整日志。
I/PRELOAD ( 6327): >356:6327:1074467156:unknown:0:Ljava/lang/Object;:206523751
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的源代码:
39 Root root = new Root();
40
41 List<Record> records = new ArrayList<Record>();
42
43 BufferedReader in = new BufferedReader(new InputStreamReader(
44 new FileInputStream(args[0])));
45
46 String line;
47 int lineNumber = 0;
48 while ((line = in.readLine()) != null) {
49 lineNumber++;
50 if (line.startsWith("I/PRELOAD")) {
51 try {
52 String clipped = line.substring(19);
53 records.add(new Record(clipped, lineNumber));
54 } catch (RuntimeException e) {
55 throw new RuntimeException(
56 "Exception while recording line " + lineNumber + ": " + line, e);
57 }
58 }
59 }
说实话,原生这段逻辑的处理方式实在是过于简单了,对log格式的处理实在是不足够现代化。每条期待TAG为PRELOAD,其它的log全部过滤。并且,取到type值的办法用的是最土的直接数出19个字符这样的方法。这样导致,用logcat -vtime抓出来的log是无法识别的。
下面我们来分析Record的生成过程。这里显示出现有设计的一个不足,就是采用":"来做分隔。
代码如下:
125 line = line.substring(1);
126 String[] parts = line.split(":");
这就导致了,如果包名中出现了":"的话,就会split失败。为了解决这个问题,只好打补丁。
Android采用了一个非常土的办法,将":"替换成\u003A,这个过程需要手动去做。
这一段逻辑的完整代码如下:
109 Record(String line, int lineNum) {
110 char typeChar = line.charAt(0);
111 switch (typeChar) {
112 case '>': type = Type.START_LOAD; break;
113 case '<': type = Type.END_LOAD; break;
114 case '+': type = Type.START_INIT; break;
115 case '-': type = Type.END_INIT; break;
116 default: throw new AssertionError("Bad line: " + line);
117 }
118
119 sourceLineNumber = lineNum;
120
121 for (int i = 0; i < REPLACE_CLASSES.length; i+= 2) {
122 line = line.replace(REPLACE_CLASSES[i], REPLACE_CLASSES[i+1]);
123 }
124
125 line = line.substring(1);
126 String[] parts = line.split(":");
127
128 ppid = Integer.parseInt(parts[0]);
129 pid = Integer.parseInt(parts[1]);
130 tid = Integer.parseInt(parts[2]);
131
132 processName = decode(parts[3]).intern();
133
134 classLoader = Integer.parseInt(parts[4]);
135 className = vmTypeToLanguage(decode(parts[5])).intern();
136
137 time = Long.parseLong(parts[6]);
138 }
我们看看这些patch冒号的代码,如果遇到了新的类,我们也需要去添加新的类。
22 /**
23 * The delimiter character we use, {@code :}, conflicts with some other
24 * names. In that case, manually replace the delimiter with something else.
25 */
26 private static final String[] REPLACE_CLASSES = {
27 "com.google.android.apps.maps:FriendService",
28 "com.google.android.apps.maps\\u003AFriendService",
29 "com.google.android.apps.maps:driveabout",
30 "com.google.android.apps.maps\\u003Adriveabout",
31 "com.google.android.apps.maps:GoogleLocationService",
32 "com.google.android.apps.maps\\u003AGoogleLocationService",
33 "com.google.android.apps.maps:LocationFriendService",
34 "com.google.android.apps.maps\\u003ALocationFriendService",
35 "com.google.android.apps.maps:MapsBackgroundService",
36 "com.google.android.apps.maps\\u003AMapsBackgroundService",
37 "com.google.android.apps.maps:NetworkLocationService",
38 "com.google.android.apps.maps\\u003ANetworkLocationService",
...
比如我不幸遇上了虾米音乐:fm.xiami.yunos:ui,就写这样两行将”:“替换成\u003A。
"fm.xiami.yunos:ui",
"fm.xiami.yunos\\u003Aui"
这方法土掉渣了,需要一个一个地探雷。。。
光读log还不行,还要去在线查询内存占用情况。我们来看frameworks/base/tools/preload/MemoryUsage.java中的measure方法。
221 private MemoryUsage measure() {
222 String[] commands = GET_DIRTY_PAGES;
223 if (className != null) {
224 List<String> commandList = new ArrayList<String>(
225 GET_DIRTY_PAGES.length + 1);
226 commandList.addAll(Arrays.asList(commands));
227 commandList.add(className);
228 commands = commandList.toArray(new String[commandList.size()]);
229 }
230
231 try {
232 final Process process = Runtime.getRuntime().exec(commands);
这里面要在adb shell中执行一个GET_DIRTY_PAGES的工具命令,我们看看这是何方神圣:
160 private static final String CLASS_PATH = "-Xbootclasspath"
161 + ":/system/framework/core.jar"
162 + ":/system/framework/ext.jar"
163 + ":/system/framework/framework.jar"
164 + ":/system/framework/framework-tests.jar"
165 + ":/system/framework/services.jar"
166 + ":/system/framework/loadclass.jar";
167
168 private static final String[] GET_DIRTY_PAGES = {
169 "adb", "shell", "dalvikvm", CLASS_PATH, "LoadClass" };
这个是要调用LoadClass类,位于system/framework/loadclass.jar中。那么这个loadclass.jar又是从哪里来的呢?答案是在frameworks/base/tools/preload/loadclass/目录中。
我们看loadclass目录中的Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_MODULE_TAGS := tests
LOCAL_MODULE := loadclass
include $(BUILD_JAVA_LIBRARY)
于是我们make一下这个工具:mmm frameworks/base/tools/preload/loadclass/
然后我们把这个jar push到手机上的/system/framework/中去。