03月19, 2014

Android下如何获取App启动时间

Android平台上,一个App的启动时间可以说是一个重要的性能指标。如何获取一个App启动时间呢,接下来咱们详细探讨一下。

在查阅Android的文档之后发现,Android的shell命令里面是有这个功能的,打开adb,输入以下命令

am是shell中集成的一个命令,ActivityManager的简写。一共需要提供两个参数-W,-n,其中-W是指启动完成之后,返回启动耗时,是最关键的一个参数。-n后面跟的是需要启动的App的包名和launchActivity。点击确定之后,会发现App被成功启动,且adb中会输入以下结果

其中ThisTime即是本次App启动所花费的时间。

到了这里我们基本完成了一半的工作,但是每次都需要在adb中才能查看启动时间无疑是很麻烦的,那么如何在手机上通过一个App控制其他App的启动,并获取启动时间呢,咱们继续看。

首先通过shell看一下am命令中包含什么,在System/bin目录下面找到“am”命令,并打印出之后是如下的结果

可以看出他跟普通的shell命令不一样,他是通过调用一个/framework/目录下的一个am.jar完成的工作,并最终执行com.android.commands.am包下面的Am.java。那我们接下来要做的工作就是下载Android源码,并找到这个Am.java。

Am.java部分代码如下:

public class Am extends BaseCommand {

    private IActivityManager mAm;
    ...

    public static void main(String[] args) {
        (new Am()).run(args);
    }

    ...

    @Override
    public void onRun() throws Exception {
        if (op.equals("start")) {
            runStart();
        } 
        ...
    }

    private void runStart() throws Exception {
        ...
        IActivityManager.WaitResult result = null;
        int res;
        if (mWaitOption) {
            result = mAm.startActivityAndWait(null, null, intent, mimeType,
                        null, null, 0, mStartFlags, mProfileFile, fd, null, mUserId);
            res = result.result;
        } else {
            res = mAm.startActivityAsUser(null, null, intent, mimeType,
                    null, null, 0, mStartFlags, mProfileFile, fd, null, mUserId);
        }
        ...
    }
}

看到代码之后突然就有了一种豁然开朗的感觉,没错,就是main函数!他就是整个am命令的入口,并调用onRun方法。如果我们输入的参数是“start”的话,会调用runStart()方法。在runStart()方法之前的预处理中,根据我们输入的-W参数,将mWaitOption赋值为true。然后最终就找到了整个命令的精髓所在–startActivityAndWait()!

到这一步之后,我们需要做的工作就是如何在自己的App中调用这个startActivityAndWait方法。

不过很明显这个API是不对开发者开放的,Android系统中有很多隐藏API,Google之所以要将一些API隐藏(指加上@hide标记的public类、方法或常量)是因为Android系统本身还在不断的进化发展中。从1.0、1.1到现在4.4,这些隐藏的API本身可能是不稳定的(方法的参数数量,类型都会变化),所以使用隐藏API,意味着程序更差的兼容性。

但这并不意味着我们就不能使用它,这些隐藏的API在系统中都是真实存在的,只是不对外开放而已。我们可以通过三种方法调用它,分别是:JAVA反射机制、API欺骗、重新编译Android源码。

反射是Java的语言特性之一,在此不进行赘述,需要介绍一下API欺骗:烧制到手机中的android.jar包含了Android所需的各种类与方法;而供开发者使用的android.jar只是其中的一部分。API欺骗是指在应用中去模拟未公开的类和方法让应用编译通过并生成APK,然而在应用实际运行中调用的却仍是烧制到手机中真实的android.jar。

通过查看源码我们可以看到需要模拟的是ActivityManagerNative、IActivityManager和他的startActivityAndWait方法。参照源码,实现以下代码:

package android.app;

public abstract class ActivityManagerNative {

    public static IActivityManager getDefault() {
        return null;
    }
}

package android.app;

import android.content.ComponentName;
import android.os.Parcel;
import android.os.Parcelable;

public abstract interface IActivityManager {

    public WaitResult startActivityAndWait(IApplicationThread caller, String callingPackage,
            Intent intent, String resolvedType, IBinder resultTo, String resultWho,
            int requestCode, int flags, String profileFile,
            ParcelFileDescriptor profileFd, Bundle options, int userId) throws RemoteException;

    public static class WaitResult implements Parcelable {
        public int result;
        public boolean timeout;
        public ComponentName who;
        public long thisTime;
        public long totalTime;
    };
}

分别查阅4.0、4.4的源码之后发现startActivityAndWait的参数个数和顺序居然不一样,那通过API欺骗的方式适配所有的系统版本看来是不现实了。但是我们可以结合Java的反射和API欺骗来完成这个工作。所以改写后的代码如下:

package android.app;

public abstract class ActivityManagerNative {

    public static IActivityManager getDefault() {
        return null;
    }
}

package android.app;

import android.content.ComponentName;
import android.os.Parcel;
import android.os.Parcelable;

public abstract interface IActivityManager {

    public static class WaitResult implements Parcelable {
        public int result;
        public boolean timeout;
        public ComponentName who;
        public long thisTime;
        public long totalTime;
    };
}

然后在使用时,首先通过API欺骗获取到一个IActivityManager的实例,然后通过Java反射找到startActivityAndWait方法,根据不同的系统版本,传递不同的参数,最终完成工作,代码如下:

private void getStartActivityMethod() {
        activityManager = ActivityManagerNative.getDefault();
        Method[] methods = activityManager.getClass().getDeclaredMethods();

        for (int i = 0; i < methods.length; i++) {
            String methodName = methods[i].getName();
            if (methodName.contains("startActivityAndWait")) {
                startActivityMethod = methods[i];
                startActivityMethod.setAccessible(true);
                break;
            }
        }
    }

    // 4.4
    private long startActivityWithFieldsForApi19(Intent intent) {
        Object[] objects = new Object[] { null, null, intent, null, null, null, 0, 0, null, null, null, 0 };
        return startActivityForResult(objects);
    }

    private long startActivityForResult(Object[] objects) {
        try {
            Object object = startActivityMethod.invoke(activityManager, objects);
            WaitResult waitResult = (WaitResult) object;
            return waitResult.thisTime;
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return -1;
    }

最后附上Android源码的下载地址:http://grepcode.com/project/repository.grepcode.com/java/ext/com.google.android/android/

本文链接:http://blogs.360.cn/post/android获取app启动时间.html

-- EOF --

Comments