在应用程序Crash的时候, 我们通常想收集一些信息来帮助分析本次Crash。下面列举了一些在应用程序Crash的时候,应该收集的一些基本信息。 注意:此次在应用程序Crash时收集的信息,只用于帮助分析OutOfMemoryError异常,对其它异常只做参考。
Java虚拟机内存区域划分:
- 程序计数器:当前线程所执行的字节码的型号指示器。
- Java虚拟机栈:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
- 本地方法栈:于Java虚拟机一样,本地方法栈区域只是为Native方法服务。
- Java堆:对象实例和数组。
- 方法区:用于存储已被虚拟机家在的类信息、常量、静态变量、即时编译器后的代码等数据。
- 运行常量池:用于存放编译器生成的各种字面量和符号引用。
Java虚拟机会发生的OutOfMemoryError的内存区域:
- Java虚拟机栈:如果虚拟机可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError。
- 本地方法栈:与Java虚拟机栈一样。
- Java堆:如果在堆中没有内存完成实例分配,并且也无法再扩展时,就会抛出OutOfMemoryError异常。
- 方法区:当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
- 运行常量池:当常量池无法再申请到内存会抛出OutOfMemoryError异常。
通过接口Thread.UncaughtExceptionHandler可以知道应用程序Crash时机,在接口回调方法uncaughtException中获取异常信息,Thread.UncaughtExceptionHandler的作用如下:
- 当线程因未捕获的异常而突然终止时调用的处理程序接口。
- 当线程由于未捕获的异常而即将终止时,Java虚拟机将使用{@link #getUncaughtExceptionHandler}向线程查询其UncaughtExceptionHandler,并将调用处理程序的uncaughtException方法,将线程和异常作为参数传递。
- 如果某个线程没有显式设置其UncaughtExceptionHandler,则其ThreadGroup对象将充当其UncaughtExceptionHandler。 如果ThreadGroup对象没有处理异常的特殊要求,它可以将调用转发到{@linkplain #getDefaultUncaughtExceptionHandler默认的未捕获异常处理程序}。
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* 当给定线程由于给定的未捕获异常而终止时调用的方法。
* <p>
* Java虚拟机将忽略此方法抛出的任何异常。
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
在应用程序中实现Thread.UncaughtExceptionHandler接口:
object CrashHandler : Thread.UncaughtExceptionHandler {
private var mApplication: Application? = null
override fun uncaughtException(t: Thread?, e: Throwable?) {
CrashInfo.getAll(mApplication) //收集信息
Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, e)
}
fun init(application: Application) {
mApplication = application
Thread.setDefaultUncaughtExceptionHandler(this)//当线程由于未捕获的异常而即将终止时,由当前类来处理
CrashInfo.track(application) //收集信息
}
}
在Application的onCreate方法中添加Crash处理:
public class YourApplication extents Application
@Override
public void onCreate() {
super.onCreate();
CrashHandler.INSTANCE.init(this);
}
}
在应用程序crash时,除了基本的异常栈信息外,可能还需要一些更加详细的设备基本信息,应用程序内存信息,系统内存信息,应用程序Activity栈信息等来帮助分析Crash。
设备基本信息包括:手机品牌,手机品牌类型,手机制造商,手机系统版本,手机SDK版本,手机屏幕分辨率,手机屏幕密度。
/**
* 设备基本信息
*
* @param context 上下文对象
* @return JSON字符串
*/
public static String getDeviceInfo(Context context) {
DisplayMetrics dm = context.getResources().getDisplayMetrics();
JSONObject dmJSON = new JSONObject();
try {
dmJSON.put("brand", Build.BRAND); //手机品牌
dmJSON.put("release", Build.VERSION.RELEASE);//手机系统版本
dmJSON.put("model", Build.MODEL); //手机品牌类型
dmJSON.put("manufacturer", Build.MANUFACTURER); //手机制造商
dmJSON.put("sdk", Build.VERSION.SDK); //手机SDK版本
dmJSON.put("width*height", new StringBuilder().append(dm.widthPixels).append('*').append(dm.heightPixels).toString()); //手机屏幕分辨率
dmJSON.put("densityDpi", String.valueOf(dm.densityDpi)); //手机屏幕密度
WindowManager windowManager =
(WindowManager) context.getSystemService(Context.
WINDOW_SERVICE);
if (windowManager != null) {//手机是否含有虚拟键盘
final Display display = windowManager.getDefaultDisplay();
Point outPoint = new Point();
if (Build.VERSION.SDK_INT >= 19) {
// 可能有虚拟按键的情况
display.getRealSize(outPoint);
} else {
// 没有虚拟按键
display.getSize(outPoint);
}
dmJSON.put("realWidth*realHeight", new StringBuilder().append(outPoint.x).append('*').append(outPoint.y).toString());
}
} catch (Exception e) {
e.printStackTrace();
}
return dmJSON.toString();
}
应用程序内存信息包括:系统给应用程序分配的最大内存,Java虚拟机将尝试使用的最大内存量,当前可用于当前和未来对象的内存总量等。
- ActivityManager#getMemoryClass:系统能给应用程序分配的最大内存。
- ActivityManager#getLargeMemoryClass:在LargeHeap下,系统能给应用程序分配的最大内存。
- Runtime#maxMemory:返回Java虚拟机将尝试使用的最大内存量。 如果没有固有限制,则返回值{@link java.lang.Long #MAX_VALUE}。
- Runtime#totalMemory:返回Java虚拟机中的内存总量。 此方法返回的值可能会随时间而变化,具体取决于主机环境。请注意,保存任何给定类型的对象所需的内存量可能与实现有关。当前可用于当前和未来对象的内存总量,以字节为单位。
- Runtime#freeMemory:返回Java虚拟机中的可用内存量。 调用gc方法可能会导致freeMemory返回的值增加。当前可用于未来分配对象的内存总量的近似值,以字节为单位。
- Runtime#totalMemory - Runtime#freeMemory:当前已使用内存。
/**
* 程序运行时内存信息
*
* @param context 上下文对象
* @return
*/
public static String getAppMemory(Context context) {
JSONObject appMemoryJSON = new JSONObject();
try {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am != null) {
appMemoryJSON.put("memoryClass", BytesUtil.formatFileSizeByMB(context, am.getMemoryClass()));//系统能给应用程序分配的最大内存
appMemoryJSON.put("largeMemoryClass", BytesUtil.formatFileSizeByMB(context, am.getLargeMemoryClass()));//在LargeHeap下,系统能给应用程序分配的最大内存
if (DEBUG) {
Log.d(TAG, "getAppMemory-->getMemoryClass=" + am.getMemoryClass());
Log.d(TAG, "getAppMemory-->getLargeMemoryClass=" + am.getLargeMemoryClass());
}
}
Runtime r = Runtime.getRuntime();
appMemoryJSON.put("maxMemory", BytesUtil.formatFileSizeByBytes(context, r.maxMemory()));//返回Java虚拟机将尝试使用的最大内存量。 如果没有固有限制,则返回值{@link java.lang.Long #MAX_VALUE}。
appMemoryJSON.put("totalMemory", BytesUtil.formatFileSizeByBytes(context, r.totalMemory()));//当前可用内存
appMemoryJSON.put("freeMemory", BytesUtil.formatFileSizeByBytes(context, r.freeMemory()));//当前空闲内存
appMemoryJSON.put("usedMemory", BytesUtil.formatFileSizeByBytes(context, r.totalMemory() - r.freeMemory()));//当前已使用内存
} catch (Exception e) {
e.printStackTrace();
}
return appMemoryJSON.toString();
}
系统内存信息包括:系统总内存,系统剩余内存,系统是否处于低内存等。
- ActivityManager.MemoryInfo.totalMem:内核可访问的总内存。 这基本上是设备的RAM大小,不包括DMA缓冲区之类的内核固定分配,基带CPU的RAM等。
- ActivityManager.MemoryInfo.availMem:系统上的可用内存。 这个数字不应该被认为是绝对的:由于内核的性质,这个内存的很大一部分实际上在使用,并且需要整个系统运行良好。
- ActivityManager.MemoryInfo.threshold:表示低内存阈值,低于这个阈值,就表示内存很低并开始查杀后台服务和其他非外部进程。
- ActivityManager.MemoryInfo.lowMemory:如果系统认为自己当前处于低内存状态,则设置为true。
/**
* 系统内存信息
*
* @param context 上下文对象
* @return
*/
public static String getDeviceSystemMemory(Context context) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
JSONObject deviceMemoryJSON = new JSONObject();
if (am != null) {
am.getMemoryInfo(memoryInfo);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
deviceMemoryJSON.put("totalMem", BytesUtil.formatFileSizeByBytes(context, memoryInfo.totalMem));//系统总内存
}
deviceMemoryJSON.put("availMem", BytesUtil.formatFileSizeByBytes(context, memoryInfo.availMem));//系统剩余内存
deviceMemoryJSON.put("threshold", BytesUtil.formatFileSizeByBytes(context, memoryInfo.threshold));//当系统剩余内存低于"+threshold+"时就看成低内存运行
deviceMemoryJSON.put("lowMemory", memoryInfo.lowMemory);//系统是否处于低内存运行
} catch (Exception e) {
e.printStackTrace();
}
}
return deviceMemoryJSON.toString();
}
应用程序运行时的虚拟机信息包括:java-heap,native-heap,code,stack,graphics,private-other,system,total-pss,total-swap。
关于PSS定义:按比例分配占用内存 (PSS)
这表示您的应用的 RAM 使用情况,考虑了在各进程之间共享 RAM 页的情况。您的进程独有的任何 RAM 页会直接影响其 PSS 值,而与其他进程共享的 RAM 页仅影响与共享量成比例的 PSS 值。例如,两个进程之间共享的 RAM 页会将其一半的大小贡献给每个进程的 PSS。
PSS 结果一个比较好的特性是,您可以将所有进程的 PSS 相加来确定所有进程正在使用的实际内存。这意味着 PSS 适合测定进程的实际 RAM 比重和比较其他进程的 RAM 使用情况与可用总 RAM。
- java-heap:从 Java 或 Kotlin 代码分配的对象内存。
- native-heap:从 C 或 C++ 代码分配的对象内存。(即使您的应用中不使用 C++,您也可能会看到此处使用的一些原生内存,因为 Android 框架使用原生内存代表您处理各种任务,如处理图像资源和其他图形时,即使您编写的代码采用 Java 或 Kotlin 语言。)
- code:您的应用用于处理代码和资源(如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体)的内存。
- stack: 您的应用中的原生堆栈和 Java 堆栈使用的内存。 这通常与您的应用运行多少线程有关。
- graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。 (请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
- total-pss: 包括所有 Zygote 分配(如上述 PSS 定义所述,通过进程之间的共享内存量来衡量)
利用 Debug.MemoryInfo#getMemoryStats方法获取:
/**
* 调试下程序运行时内存信息
*
* @param context 上下文对象
* @return
*/
@TargetApi(value = Build.VERSION_CODES.M)
public static String getDebugAppMemory(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Debug.MemoryInfo debugMemoryInfo = new Debug.MemoryInfo();
Debug.getMemoryInfo(debugMemoryInfo);
Map<String, String> map = debugMemoryInfo.getMemoryStats();
Set<Map.Entry<String, String>> set = map.entrySet();
Iterator<Map.Entry<String, String>> iterator = set.iterator();
JSONObject jsonObject = new JSONObject();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
String value = BytesUtil.formatFileSizeByKB(context, Long.parseLong(entry.getValue()));
if (DEBUG)
Log.d(TAG, "getDebugAppMemory-->key=" + entry.getKey() + ";getValue=" + entry.getValue() + ";value=" + value);
try {
jsonObject.put(entry.getKey(), value);
} catch (JSONException e) {
e.printStackTrace();
}
}
return jsonObject.toString();
}
return null;
}
设备SD卡内存信息包括:SDCard总内存,SDCard剩余内存。
- Environment.getExternalStorageDirectory().getTotalSpace():SDCard总内存。
- Environment.getExternalStorageDirectory().getUsableSpace():SDCard剩余内存。
/**
* 设备SD卡内存
*
* @param context 上下文对象
* @return
*/
public static String getDeviceSDCardMemory(Context context) {
JSONObject deviceMemoryJSON = new JSONObject();
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
try {
deviceMemoryJSON.put("sdCardTotalSpace", BytesUtil.formatFileSizeByBytes(context, Environment.getExternalStorageDirectory().getTotalSpace()));//sd card总内存
// deviceMemoryJSON.put("sdCardFreeSpace", Formatter.formatFileSize(context, Environment.getExternalStorageDirectory().getFreeSpace()));//sd card剩余内存
deviceMemoryJSON.put("sdCardUsableSpace", BytesUtil.formatFileSizeByBytes(context, Environment.getExternalStorageDirectory().getUsableSpace()));//sd card可用内存
} catch (Exception e) {
e.printStackTrace();
}
}
return deviceMemoryJSON.toString();
}
应用程序运行时,将内存快照dump到.hprof文件中。
- Debug.dumpHprofData:将“hprof”数据转储到指定的文件。 这可能会导致GC。
/**
* 获取内存快照
*
* @param pathname 存储文件路径
*/
public static void dumpToFile(String pathname) {
if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
return;
}
try {
File file = new File(pathname);
if (!file.exists()) {
file.mkdirs();
}
Calendar calendar = SimpleDateFormat.getDateTimeInstance().getCalendar();
calendar.setTime(new Date(System.currentTimeMillis()));
calendar.get(Calendar.YEAR);
StringBuilder sb = new StringBuilder();
int[] fields = {Calendar.YEAR, Calendar.MONTH, Calendar.DAY_OF_MONTH, Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND};
char splitChar = '-';
for (int field : fields) {
sb.append(calendar.get(field)).append(splitChar);
}
if (sb.length() > 1) {
sb.deleteCharAt(sb.length() - 1).append(".hprof");
}
String name = sb.toString();
if (DEBUG)
Log.d(TAG, "dumpToFile-->filename=" + name);
Debug.dumpHprofData(new File(file, name).getPath());
} catch (Exception e) {
e.printStackTrace();
if (DEBUG)
Log.e(TAG, Log.getStackTraceString(new Throwable()));
}
}
文件以YYYY-MM-DD-HH-MM-SS.hprof形式存储,如:2018-12-10-10-23-12.hprof。
通过反射获取应用程序的Activity栈信息(低版本得到的顺序可能不是Acitivity栈顺序,高版本得到的是Acitivity栈顺序,具体情况根据系统版本而定):
/**
* 通过反射获取应用程序的Activity栈信息
*
* @param application 应用程序对象
* @return Activity栈列表
*/
public static List<Activity> getActivitiesByApplication(Application application) {
List<Activity> list = new ArrayList<>();
try {
Class<Application> applicationClass = Application.class;
Field mLoadedApkField = applicationClass.getDeclaredField("mLoadedApk");
mLoadedApkField.setAccessible(true);
Object mLoadedApk = mLoadedApkField.get(application);
Class<?> mLoadedApkClass = mLoadedApk.getClass();
Field mActivityThreadField = mLoadedApkClass.getDeclaredField("mActivityThread");
mActivityThreadField.setAccessible(true);
Object mActivityThread = mActivityThreadField.get(mLoadedApk);
Class<?> mActivityThreadClass = mActivityThread.getClass();
Field mActivitiesField = mActivityThreadClass.getDeclaredField("mActivities");
mActivitiesField.setAccessible(true);
Object mActivities = mActivitiesField.get(mActivityThread);
// 注意这里一定写成Map,低版本这里用的是HashMap,高版本用的是ArrayMap
if (mActivities instanceof Map) {
@SuppressWarnings("unchecked")
Map<Object, Object> arrayMap = (Map<Object, Object>) mActivities;
for (Map.Entry<Object, Object> entry : arrayMap.entrySet()) {
Object value = entry.getValue();
Class<?> activityClientRecordClass = value.getClass();
Field activityField = activityClientRecordClass.getDeclaredField("activity");
activityField.setAccessible(true);
Object o = activityField.get(value);
list.add((Activity) o);
}
}
} catch (Exception e) {
e.printStackTrace();
list = null;
if (DEBUG)
Log.e(TAG, Log.getStackTraceString(new Throwable()));
}
if (DEBUG && list != null) {
Iterator<Activity> iterator = list.iterator();
while (iterator.hasNext()) {
Log.d(TAG, "getActivitiesByApplication-->value=" + iterator.next());
}
}
return list;
}
除了获取到Activity栈信息,还可以获取Activity中的Fragment信息:
/**
* Activity栈信息
*
* @param application 应用程序对象
* @param includeFragment true包括Fragment,否则不包括
* @return
*/
public static String getActivities(Application application, boolean includeFragment) {
List<Activity> list = getActivitiesByApplication(application);
Log.d(TAG, "getActivities-->list=" + list + ";includeFragment=" + includeFragment);
if (list == null || list.isEmpty()) return null;
Map<String, List<String>> stackMap = null;
List<String> stackList = null;
if (includeFragment) {
stackMap = new LinkedHashMap<>(list.size());
} else {
stackList = new ArrayList<>(list.size());
}
Iterator<Activity> iterator = list.iterator();
while (iterator.hasNext()) {
Activity activity = iterator.next();
if (DEBUG) {
Log.d(TAG, "getActivities-->activity-->name=" + activity.getClass().getName() + ";includeFragment=" + includeFragment);
}
if (includeFragment) {
Map<String, List<String>> temp = null;
if (activity instanceof FragmentActivity) {
temp = getSupportFragmentOfActivity(activity.getClass().getName(), ((FragmentActivity) activity).getSupportFragmentManager());//是support包中的Fragment
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
temp = getFragmentOfActivity(activity.getClass().getName(), activity.getFragmentManager());//是app包中的Fragment
}
if (temp != null && !temp.isEmpty()) stackMap.putAll(temp);
} else {
stackList.add(activity.getClass().getName());
}
}
if (includeFragment) {
return new JSONObject(stackMap).toString();
}
return new JSONArray(stackList).toString();
}
//Fragment嵌套是一个树形结构,所以相当于按顺序层级遍历树
@TargetApi(Build.VERSION_CODES.O)
private static Map<String, List<String>> getFragmentOfActivity(String
name, android.app.FragmentManager fragmentManager) {
Map<String, android.app.FragmentManager> fragmentManagers = new LinkedHashMap<>();
fragmentManagers.put(name, fragmentManager);//存储根节点
Map<String, android.app.FragmentManager> childFragmentManagers = null;
Map<String, List<String>> result = new LinkedHashMap<>();
int size = 4;
while (!fragmentManagers.isEmpty()) { //开始遍历每一层的节点
childFragmentManagers = new LinkedHashMap<>(size);
Set<Map.Entry<String, android.app.FragmentManager>> entries = fragmentManagers.entrySet();
Iterator<Map.Entry<String, android.app.FragmentManager>> iterator = entries.iterator();
while (iterator.hasNext()) {
Map.Entry<String, android.app.FragmentManager> entry = iterator.next();
android.app.FragmentManager fm = entry.getValue();
List<android.app.Fragment> fragments = fm.getFragments();
List<String> values = new ArrayList<>(fragments.size());
int index = 0;
for (android.app.Fragment fragment : fragments) {
String fragmentName = fragment.getClass().getName();
if (DEBUG) {
Log.d(TAG, "getFragmentOfActivity-->key=" + entry.getKey() + ";value[" + index + "]=" + fragmentName);
}
if (!fragment.getChildFragmentManager().getFragments().isEmpty()) {
childFragmentManagers.put(values.contains(fragmentName) ? fragmentName + index : fragmentName, fragment.getChildFragmentManager()); //存储下一层的节点
}
values.add(fragmentName);
index++;
}
result.put(entry.getKey(), values);
}
fragmentManagers.clear();
if (!childFragmentManagers.isEmpty())
fragmentManagers.putAll(childFragmentManagers);//下一层节点
}
return result;
}
//和上面方法一样,只不过是support包中的Fragment
private static Map<String, List<String>> getSupportFragmentOfActivity(String
name, FragmentManager fragmentManager) {
Map<String, FragmentManager> fragmentManagers = new LinkedHashMap<>();
fragmentManagers.put(name, fragmentManager);
Map<String, List<String>> result = new LinkedHashMap<>();
Map<String, FragmentManager> childFragmentManagers = null;
int size = 4;
while (!fragmentManagers.isEmpty()) {
childFragmentManagers = new LinkedHashMap<>(size);
Set<Map.Entry<String, FragmentManager>> entries = fragmentManagers.entrySet();
Iterator<Map.Entry<String, FragmentManager>> iterator = entries.iterator();
while (iterator.hasNext()) {
Map.Entry<String, FragmentManager> entry = iterator.next();
FragmentManager fm = entry.getValue();
List<Fragment> fragments = fm.getFragments();
List<String> values = new ArrayList<>(fragments.size());
int index = 0;
for (Fragment fragment : fragments) {
String fragmentName = fragment.getClass().getName();
if (DEBUG) {
Log.d(TAG, "getSupportFragmentOfActivity-->key=" + entry.getKey() + ";value[" + index + "]=" + fragmentName);
}
if (!fragment.getChildFragmentManager().getFragments().isEmpty()) {
childFragmentManagers.put(values.contains(fragmentName) ? fragmentName + index : fragmentName, fragment.getChildFragmentManager());
}
values.add(fragmentName);
index++;
}
result.put(entry.getKey(), values);
}
fragmentManagers.clear();
if (!childFragmentManagers.isEmpty())
fragmentManagers.putAll(childFragmentManagers);
}
if (DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(TAG, "getSupportFragmentOfActivity-->json=" + JSONObject.wrap(result));
}
return result;
}
在Application中可以注册Activity生命周期的监听器。应用程序初始化时,ActivityCount(Activity的个数)为-1,当有Activity调用onStart时,如果ActivityCount为-1,则将ActivityCount置为0,并马上开始记录启动Activity的个数(ActivityCount++);在Activity调用onStop时,ActivityCount减一(ActivityCount--);因此当应用程序退至后台时,ActivityCount为0(所有的Activity都会至少处于onStop状态),当再次启动应用程序时,如果ActivityCount为0,则说明此次就是热启动了。
/**
* 检测应用程序是冷启动还是热启动
*/
class AppColdStart {
private constructor()
companion object {
private var mActivityCount: Int = -1//记录启动的Activity
private var mIsColdStart: Boolean = true //是否是冷启动
/**
* 检测是冷启动还是热启动
*/
fun detectColdStart(application: Application) {
application.registerActivityLifecycleCallbacks(ActivityLifecycleCallbacksAdapterImpl())
}
/**
* 是否是冷启动
*/
fun isColdStart(): Boolean {
return mIsColdStart
}
}
class ActivityLifecycleCallbacksAdapterImpl : ActivityLifecycleCallbacksAdapter() {
override fun onActivityStarted(activity: Activity?) {
super.onActivityStarted(activity)
if (mActivityCount == 0) {
mIsColdStart = false //表示已经启动过
} else if (mActivityCount == -1) {
mActivityCount = 0 //表示初次启动
}
mActivityCount++
}
override fun onActivityStopped(activity: Activity?) {
super.onActivityStopped(activity)
mActivityCount--
}
}
}
除了上面的信息外,还可以统计用户使用应用程序的时间,在各个Activity停留的时间信息等,具体可查看AppCrash中的TrackActivity类。