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

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

时间: 2024-11-02 15:24:48

preload-classes的前世今生(1)的相关文章

阿里十年经验输出,大数据平台“数加”的前世今生

2016 年1月20日,在云栖大会上阿里云发布了一站式大数据平台"数加",该平台集合了阿里巴巴十年的大数据能力以及上万名工程师实战检验,该平台是一站式的解决方案,首批亮相20款产品,覆盖数据采集.计算引擎.数据加工.数据分析.机器学习.数据应用等数据生产全链条. 数加平台由大数据计算服务(MaxCompute).分析型数据库(Analytic DB).流计算(StreamCompute)共同组成了底层强大的计算引擎,速度更快.成本更低.计算引擎之上,"数加"提供了丰

手游开发工具CocoStudio的前世今生

要了解CocoStudio,需要先了解Cocos2d-x,Cocos2d-x是开源的游戏引擎,一个支持多平台的2D手机游戏引擎,使用C++开发,基于OpenGLES,基于Cocos2d-iphone,支持iOS4.1,Android2.1,WindowsPhone7及更高版本. Cocos2D-X引擎的来历 Cocos2D-X游戏引擎并不是最初的版本.从名字读者就能看出最早的版本其实为Cocos2D引擎版本.追溯起来,Cocos2D引擎已经有5 年的历史了.在2008年3月,Ricardo Qu

第十五节--Zend引擎的发展 -- Classes and Objects in PHP5 [15]

object|php5 /*+-------------------------------------------------------------------------------+| = 本文为Haohappy读<<Core PHP Programming>> | = 中Classes and Objects一章的笔记 | = 翻译为主+个人心得 | = 为避免可能发生的不必要的麻烦请勿转载,谢谢 | = 欢迎批评指正,希望和所有PHP爱好者共同进步! +--------

第十二节--类的自动加载 -- Classes and Objects in PHP5 [12]

object|php5|加载 | = 本文为Haohappy读<<Core PHP Programming>> | = 中Classes and Objects一章的笔记 | = 翻译为主+个人心得 | = 为避免可能发生的不必要的麻烦请勿转载,谢谢 | = 欢迎批评指正,希望和所有PHP爱好者共同进步! +-------------------------------------------------------------------------------+*/ 第十二节-

第十一节--重载 -- Classes and Objects in PHP5[11]

object|php5 | = 本文为Haohappy读<<Core PHP Programming>> | = 中Classes and Objects一章的笔记 | = 翻译为主+个人心得 | = 为避免可能发生的不必要的麻烦请勿转载,谢谢 | = 欢迎批评指正,希望和所有PHP爱好者共同进步! +-------------------------------------------------------------------------------+*/ 第十一节--重载

第二节--PHP5 的对象模型 -- Classes and Objects in PHP5 [2]

object|php5|对象 | = 本文为Haohappy读<<Core PHP Programming>> | = 中Classes and Objects一章的笔记 | = 翻译为主+个人心得 | = 为避免可能发生的不必要的麻烦请勿转载,谢谢 | = 欢迎批评指正,希望和所有PHP爱好者共同进步! +-------------------------------------------------------------------------------+*/ 第二节--

第一节--面向对象编程 -- Classes and Objects in PHP5 [1]

object|php5|编程|对象 | = 本文为Haohappy读<<Core PHP Programming>> | = 中Classes and Objects一章的笔记 | = 翻译为主+个人心得 | = 为避免可能发生的不必要的麻烦请勿转载,谢谢 | = 欢迎批评指正,希望和所有PHP爱好者共同进步! +---------------------------------------------------------------------+*/ 第一节--面向对象编程

《Java编程思想》(第二版)第6章:重复运用classes

编程|重复 Java有着众人赞叹的功能,程序代码的重复运用便是其中之一.但是,如果想获得革命性的改变,你得远远超越"复制程序代码,然后改变之"的旧式程序代码的复用途径:组合(Composition)和继承(Inheritance) toString():每个非基本类别的对象都具备toString(),当编译器希望的到一个String,而你手上却只有那些对象的情况下,这个函数便会被唤起. 测试tips:"为每个class提供main(),不管class是不是public&quo

十 Pseudo Classes 伪类

伪类像是指定选择器状态或关联选择器的门闩.它们的形式如:selector:pseudo class { property: value; },在选择器和伪属性之间使用冒号. 一些属性不被所有浏览器支持,但有四个伪类可以安全使用在链接上. link 没有点击过的链接 visited以点击过的链接 active获得焦点时的链接(比如在点击时) hover 鼠标在链接上面 a.snowman:link {color: blue;}a.snowman:visited {color: purple;}a.