preload-classes的前世今生(1)

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

  1. 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/中去。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值