1.multidex.install优化
multidex.install优化,无法解决启动速度的问题,是解决主线程ANR的问题
为了提高效率首先要判断是否需要调用:
1.是否是覆盖安装(包括同一个版本号的二次安装和新版本的升级安装)根据applicationInfo.sourceDir
对应的apk是否发生变化,校验依据是apk文件的crc完整性校验码和lastmodifiedTime最后修改时间
2.将LoadingActivity放入multiDexKeepFile file('maindexlist.txt’)中,就是把它放在主class.dex中。
1.1 Mutex.install做了两件事:
(1)解析apk获取里面所有的非class.dex文件并调用dexopt进行优化,这个处理过程比较耗时。
(2)将所有的非主dex文件加载到pathClassLoader对应的DexPathList变量中的Element[]数组中,便于类加载器找到要加载的类,也比较耗时。
1.2优化处理步骤
1.判断是否需要调用mudex.install,在5.0之后使用的是art虚拟机,在app安装的时候它会首先优化dex到可执行的二进制文件
2.如果需要执行mudex.install,即使用的是5.0之前的davik虚拟机
3.在app的application中的onAttachContext初始化中创建一个临时文件,然后开启一个子进程的loadingActivity用来加载初始画面,在loadingActivity中开启一个子线程用来执行mutex.install耗时方法,执行结束删除临时文件,并且杀死该进程。
4.在app主进程中开启while(!file.exit)循环监听临时文件是否存在,如果不存在则证明子进程调用完成
5.在app的onAttachContext再次调用Mudex.install.
2.JpAssist使用
JpAssist 是一个轻量级的 Android 平台字节码编辑插件,基于 Javassist 对字节码操作,根据 xml 配置处理 class 文件,以达到对 class 文件进行动态修改的效果。和其他 AOP 方案不同,JpAssist 提供了一种更加轻量,简单易用,无侵入,可配置化的字节码操作方式,不需要有 Java 字节码的相关知识,只需要在 Xml 插件配置中添加简单的 Java 代码即可实现类似 AOP 的功能,同时不需要引入其他额外的依赖。
2.1实现原理:
定义Gradle transform插件,收集配置xml替换信息,根据配置文件扫描所有类,匹配查找到的类进行代码替换。
JpAssist 实现了日志输出替换,系统 SharedPreferences 替换,SharedPreferences commit 替换 apply,startActivity崩溃 保护,主线程卡顿监控,Activity 生命周期耗时统计,APP启动耗时统计等功能。
由于安卓系统 SharedPreferences 自身机制的问题,当使用 SharedPreferences.apply() 方法过于频繁时,出现大量由 QueuedWork.waitToFinish() 造成的卡顿和 ANR,主要原因是系统在 Activity 的 onPause、onStop,以及 Service 的 start 和 stop 生命周期时会执行等待,google本意是在用户离开组件之前确保操作完成写入,保证持久化成功率。
对系统 SharedPreferences 进行改造,实现自己的 SharedPreferences。大概的思路:不往 QueuedWork 里添加 finisher,这样的 QueuedWork 的 finisher 队列为空,就不会在 QueuedWork.waitToFinish() 时产生阻塞,根据这个思路,我们实现了自己的 SharedPreferences 完美兼容系统 SharedPreferences.
3.ARouter原理分析
Arouter:编译时注解用于模块化间解耦
3.1 运行原理:
1.自定义注解route,注解中包含路由地址path和路由分组group信息
2.自定义注解处理器ArouterProcessor ,继承AbstractProcessor,在编译阶段扫描route注解并使用javapoet生成java类。
伪代码:
Java类:分组加载类:Arouter$${moduleName}$$Root implements IRoot{
public void load(Map<String,Class<? extends IRootGroup> groupMap ){
group.put(“main”,Arouter$${moduleName}$$Group.Class)
}
路由加载类:Arouter$${moduleName}$$Group implements IRootGroup{
Public void load(Map<String,RouteMeta> routemap){
routemap.put(“/main/service2", RouteMeta.build(RouteMeta.Type.ISERVICE,TestServiceImpl2.class, "/main/service2", "main"));
routemap.put(“/main/test", RouteMeta.build(RouteMeta.Type.ACTIVITY,SecondActivity.class, "/main/test", "main"));
}
3.加载工程中所有的分组加载类到内存中,并保存在WareHouse中的静态变量static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>()中。
具体实现是:
在application初始化的时候调用Aroute.getInstance().init()方法,init方法内部会收集applicationInfo.sourceDir以及applicationInfo.sourceSpliteSourceDir中的apk,因为是耗时操作所以开启线程池使用DexFile加载apk获取里面以指定package开头的类名。收集所有的分组映射表以后使用反射创建具体的分组映射表实例对象,并调用对象的load方法加载到WareHouse中的静态变量中。
try {
//加载 apk中的dex 并遍历 获得所有包名为 {packageName} 的类,包名是自定义的固定常量比如:com.router.routes
dexfile = new DexFile(path);
Enumeration<String> dexEntries = dexfile.entries();
while (dexEntries.hasMoreElements()) {
String className = dexEntries.nextElement();
if (className.startsWith(packageName)) {
classNames.add(className);
}
}
tips:开启线程池获取apk中的指定类这是一个异步操作,主线程需要等待线程池执行完毕才能继续执行,这里使用了CountDownLaunch来实现线程间通信。如果耗时过长会阻塞主线程,使得app启动时间过长,这是该路由方案的一个弊端。使用分组加载的目的在于节约内存,不必一次性将所有的路由对象一次加载进去。
4.当根据路由地址跳转的时候,首先根据路由地址中祖名获取WareHouse中groupsIndex映射表中对应的Class字节码,然后利用反射生成路由表映射对象,调用路由表映射对象的load方法,将保存的路由表信息保存到WareHouse中的静态变量routes
public class Warehouse {
// root 映射表 保存分组信息
static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
// group 映射表 保存组中的所有数据
static Map<String, RouteMeta> routes = new HashMap<>();
// group 映射表 保存组中的所有数据
static Map<Class, IService> services = new HashMap<>();
}
5.根据路由路径取出路由表中的RouteMeta对象,里面封装了跳转的目标class,以及跳转类型等信息,然后再封装Intent即可实现跳转。
protected Object navigation(Context context, final Postcard postcard, final int requestCode,
final NavigationCallback callback) {
try {
prepareCard(postcard);
} catch (NoRouteFoundException e) {
e.printStackTrace();
//没找到
if (null != callback) {
callback.onLost(postcard);
}
return null;
}
if (null != callback) {
callback.onFound(postcard);
}
switch (postcard.getType()) {
case ACTIVITY:
final Context currentContext = null == context ? mContext : context;
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
int flags = postcard.getFlags();
if (-1 != flags) {
intent.setFlags(flags);
} else if (!(currentContext instanceof Activity)) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
//可能需要返回码
if (requestCode > 0) {
ActivityCompat.startActivityForResult((Activity) currentContext, intent,
requestCode, postcard.getOptionsBundle());
} else {
ActivityCompat.startActivity(currentContext, intent, postcard
.getOptionsBundle());
}
if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) &&
currentContext instanceof Activity) {
//老版本
((Activity) currentContext).overridePendingTransition(postcard
.getEnterAnim()
, postcard.getExitAnim());
}
//跳转完成
if (null != callback) {
callback.onArrival(postcard);
}
}
});
break;
case ISERVICE:
return postcard.getService();
default:
break;
}
return null;
}
4.crash异常捕获
4.1 java异常捕获
(1)自定义CrashManager implements Thread.UnCaughtExceptionHandler
(2)设置Thread.setDefaultUncaughtExceptionHandler(this);
(3)重写uncaughtException(final Thread t, final Throwable e) 方法捕获异常保存在本地或者开启子线程上传到服务器。
4.2 native层异常捕获
`native异常发生时,cpu会通过异常中断的方式,触发异常处理函数。函数运行在用户态,当遇到系统调用出现异常情况时,程序会进入内核态,信号涉及到了用户态和内核态状态之间的转变。所以在native异常时可以通过注册信号处理函数的方式来捕捉native异常。
(1)注册信号处理函数进行监听异常
(2)如果出现了异常就fork出一个子进程的子线程dump函数堆栈调用的异常信息,并通过 jni的方式回调java层,在java层进行捕捉异常信息。
5 SharePreference优化
sp存在内存中缓存,并不是每次都去文件中读写,有一个以sharedPreference的名称为key(通过名称缓存一个file,以这个file为key),对应这个sharedPerference的内容为value的静态的map来缓存整个应用中的sp。如果存储数据过多会增大内存开销
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// If somebody else (some other process) changed the prefs
// file behind our back, we reload it. This has been the
// historical (if undocumented) behavior.
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
在调用Context.getSharePreference时会构造SharedPreferencesImpl对象,SharedPreferencesImpl(继承自SharedPreferences接口),对象内部会调用startLoadFromDisk开启一个子线程去读取xml文件保存在内存中。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();//
}
//初始化的时候会开一个线程去读取xml文件。
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
5.1 sp的读取:每次读取都会加锁,等待xml文件加载内存完成,只有加载完成后才能获取数据,这里会阻塞主线程操作,有可能会造成卡顿。
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
//awaitLoadedLocked()就是等待sp的构造方法中开启的线程去load本地文件,阻塞等待他load完成。
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
5.2 sp的写操作:使用editor对sp去进行写操作,即使不读只写也会阻塞等待读取xml文件完成。
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
synchronized (this) {
awaitLoadedLocked();//等待xml文件加载到内存中。
}
return new EditorImpl();
}
editor里用一个map将改动的东西存起来,当提交的时候他会把他先提交到内存,然后再形成一个异步的提交。
editor里可以暂时存放多个key的改动,然后形成一次提交,如果我们可以将多个提交合并成一次提交,尽量合并,因为每一次调用apply或者commit都会形成一个新的提交,创建各种锁。
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
//阻塞调用者,谁调用,阻塞谁
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);//这里会添加到任务队列中
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}
QueueWork.waitToFinish
/**
* Internal utility class to keep track of process-global work that's
* outstanding and hasn't been finished yet.
*
* This was created for writing SharedPreference edits out
* asynchronously so we'd have a mechanism to wait for the writes in
* Activity.onPause and similar places, but we may use this mechanism
* for other things in the future.
*
* @hide
*/
// The set of Runnables that will finish or wait on any async
// activities started by the application.
private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
new ConcurrentLinkedQueue<Runnable>();
/**
* Add a runnable to finish (or wait for) a deferred operation
* started in this context earlier. Typically finished by e.g.
* an Activity#onPause. Used by SharedPreferences$Editor#startCommit().
*
* Note that this doesn't actually start it running. This is just
* a scratch set for callers doing async work to keep updated with
* what's in-flight. In the common case, caller code
* (e.g. SharedPreferences) will pretty quickly call remove()
* after an add(). The only time these Runnables are run is from
* waitToFinish(), below.
*/
public static void add(Runnable finisher) {
sPendingWorkFinishers.add(finisher);
}
/**
* Finishes or waits for async operations to complete.
* (e.g. SharedPreferences$Editor#startCommit writes)
*
* Is called from the Activity base class's onPause(), after
* BroadcastReceiver's onReceive, after Service command handling,
* etc. (so async work is never lost)
*/
public static void waitToFinish() {
Runnable toFinish;
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run();
}
}
使用了apply方式异步写sp的时候每次apply()调用都会形成一次提交,每次有系统消息发生的时候(handleStopActivity, handlePauseActivity)都会去检查已经提交的apply写操作是否完成,如果没有完成则阻塞主线程
private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {
ActivityClientRecord r = mActivities.get(token);
if (!checkAndUpdateLifecycleSeq(seq, r, "stopActivity")) {
return;
}
r.activity.mConfigChangeFlags |= configChanges;
StopInfo info = new StopInfo();
performStopActivityInner(r, info, show, true, "handleStopActivity");
if (localLOGV) Slog.v(
TAG, "Finishing stop of " + r + ": show=" + show
+ " win=" + r.window);
updateVisibility(r, show);
// Make sure any pending writes are now committed.
if (!r.isPreHoneycomb()) {
QueuedWork.waitToFinish();//在这里调用
}
// Schedule the call to tell the activity manager we have
// stopped. We don't do this immediately, because we want to
// have a chance for any other pending work (in particular memory
// trim requests) to complete before you tell the activity
// manager to proceed and allow us to go fully into the background.
info.activity = r;
info.state = r.state;
info.persistentState = r.persistentState;
mH.post(info);
mSomeActivitiesChanged = true;
}
5.3 针对sp卡顿优化措施:
5.3.1.禁止存储存储超大的value:
sp在创建的时候会把整个文件全部加载进内存,如果sp文件比较大,会带来严重问题:
- 第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿。
- 解析sp的时候会产生大量的临时对象,导致频繁GC,引起界面卡顿。
- 这些key和value会永远存在于内存之中,占用大量内存。
5.3.2 禁止存储JSON/XML等特殊符号的数据,因为需要转义,这样会带来很多&这种特殊符号,sp在解析碰到这个特殊符号的时候会进行特殊的处理,引发额外的字符串拼接以及函数调用开销。
5.3.3 禁止多次edit多次apply
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
sp.edit().putString("test1", "sss").apply();
sp.edit().putString("test2", "sss").apply();
sp.edit().putString("test3", "sss").apply();
sp.edit().putString("test4", "sss").apply();
5.3.4 在application启动的时候可以通过线程池提前初始化sp,提前加载到内存中
5.3.5 改写sp的apply方法不再调用QueueWork.add添加到任务队列
6.FastenApk原理分析:
6.1 应用解密逻辑:
1.自定义FastenApplication继承自Application,它是应用的入口程序,通过applicationInfo.sourceDir获取当前加密后的apk文件,解析apk获取内部所有的非主dex文件使用对称加密算法解密dex文件,并将解密后的dex文件保存在app私有目录中缓存,使用pathClassLoader替换DexPathList成员变量中的dexElements数组中加载。
2.获取清单文件中的metaData属性中对应的应用的Application配置(应用自己的application全限类名和 版本号),使用反射创建应用真正的application实例。
3.绑定应用application的生命周期:将FastenApplication的上下文环境Context赋值给应用的上下文环境。具体做法是:
1.反射调用application.attach()方法,将FastenApplication的Context传入
2.反射修改应用的mOutContext属性为FastenApplication的Context
3.反射修改ActivityThread中的mInitialApplication属性为应用的application
4.反射修改ActivityThread中的mAllApplications集合中的元素,将应用的application添加进去,并且删除加固的FastenApplication对象。
5.修改LoadedApk中的mApplication属性为应用的application
最终完成FastenApplication到应用真正的application的无缝切换,降低代码侵入。
6.2应用加密
1.自定义Gradle插件FastenPlugin,根据build.gradle中的pluginInfo配置信息找到Fasten-core aar文件
2.解压aar文件获取dex,使用dx命令生成class.dex文件
3.解压apk文件,获取所有的dex文件使用对称加密密钥加密,并且将class.dex文件复制到apk解压后的目录
4.将解压后包含class.dex的目录压缩成apk文件,最终会生成未签名的apk文件
5.获取build.gradle中pluginInfo中的密钥文件,调用jarsinger命令对生成的apk文件进行签名
6.调用zipalign命令对签名后的apk文件压缩对齐优化操作,最后完成一个加密apk文件的生成。
7. app卡顿检测实现原理
借鉴了Looper.loop()方法循环中的下面代码逻辑:
Looper.loop
loop(){
...
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
....
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " +
msg.callback);
}
}
实现步骤:
1.自定义Motior继承Printer类,重写print方法,拦截android.app.ActivityThread$H发来的消息:
@Override
public void println(String x) {
if(!x.contains("android.app.ActivityThread$H")) return;
if (!mPrintingStarted) {
Log.e("stormzsl","startDump:"+x);
mStartTime = System.currentTimeMillis();
mStartThreadTime = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
mStackSampler.startDump();
} else {
final long endTime = System.currentTimeMillis();
long endThreadTime = SystemClock.currentThreadTimeMillis();
mPrintingStarted = false;
Log.e("stormzsl","stopDump>>>>>>:"+x);
if (isBlock(endTime)) {
final ArrayList<String> entries = mStackSampler.getThreadStackEntries(mStartTime, endTime);
if (entries.size() > 0) {
final BlockInfo blockInfo = BlockInfo.newInstance()
.setMainThreadTimeCost(mStartTime, endTime, mStartThreadTime, endThreadTime)
.setThreadStackEntries(entries)
.flushString();
}
}
mStackSampler.stopDump();//当超过200ms时输出当前线程对应的堆栈信息
}
}
2.设置 Looper.getMainLooper().setMessageLogging(mMonitor)对象即可;
8. 检测应用帧率:
Choreographer主要作用是协调动画,输入和绘制的时间,它可以接受垂直同步Vsync信号,然后安排渲染下一个frame的工作。 FrameCallback是和Choreographer交互,在下一个frame被渲染时触发的接口类,开发者可以设置自己的FrameCallback。我们就从自定义FrameCallback作为切入口进行帧率监控。
8.1 调用
Choreographer.getInstance().postFrameCallback(mRateRunnable);
/**
* 读取fps的线程
*/
private class FrameRateRunnable implements Runnable, Choreographer.FrameCallback {
private int totalFramesPerSecond;
@Override
public void run() {
mLastFrameRate = totalFramesPerSecond;
if (mLastFrameRate > MAX_FRAME_RATE) {
mLastFrameRate = MAX_FRAME_RATE;
}
//保存fps数据
if (AppUtils.isAppForeground()) {
writeFpsDataIntoFile();
}
totalFramesPerSecond = 0;
//1s中统计一次
mMainHandler.postDelayed(this, FPS_SAMPLING_TIME);
}
//
@Override
public void doFrame(long frameTimeNanos) {
totalFramesPerSecond++;//计算帧率
Choreographer.getInstance().postFrameCallback(this);
}
}
VSYNC信息一般由硬件中断产生,SurfaceFlinger处理。
scheduleVsync方法用于请求VSNYC信号,
Native方法接收到VSYNC信息处理后会调用java层dispatchVsync方法,
最终调用到FrameDisplayEventReceiver的onVsync方法
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final long starTime=System.nanoTime();
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
Log.e(TAG,"starTime="+starTime+", frameTimeNanos="+frameTimeNanos+", frameDueTime="+(frameTimeNanos-starTime)/1000000);
}
});
}
});
9. Activity渲染耗时统计
1. 反射修改ActivityThread中的mH成员属性的handlerCallback函数,优先会走自定义的callBack处理。
2. 将自定义callback注入到activityThread的mH对象中 后期回调会走ProxyHandlerCallback
Field handlerCallbackField = Handler.class.getDeclaredField("mCallback");
acc = handlerCallbackField.isAccessible();
if (!acc) {
handlerCallbackField.setAccessible(true);
}
Handler.Callback oldCallbackObj = (Handler.Callback) handlerCallbackField.get(handlerObj);
//自定义handlerCallback
ProxyHandlerCallback proxyMHCallback = new ProxyHandlerCallback(oldCallbackObj, handlerObj);
//将自定义callback注入到activityThread的mH对象中 后期回调会走ProxyHandlerCallback
handlerCallbackField.set(handlerObj, proxyMHCallback);
3.拦截handleMessage方法进行耗时处理:
class ProxyHandlerCallback implements Handler.Callback {
private static final String TAG = "ProxyHandlerCallback";
/**
* Android 28开始 变量从110开始
*/
private static final int LAUNCH_ACTIVITY = 100;
/**
* Android 28开始 变量从110开始
*/
private static final int PAUSE_ACTIVITY = 101;
private static final int EXECUTE_TRANSACTION = 159;
private static final String LAUNCH_ITEM_CLASS = "android.app.servertransaction.ResumeActivityItem";
private static final String PAUSE_ITEM_CLASS = "android.app.servertransaction.PauseActivityItem";
private final Handler.Callback mOldCallback;
public final Handler mHandler;
ProxyHandlerCallback(Handler.Callback oldCallback, Handler handler) {
mOldCallback = oldCallback;
mHandler = handler;
}
@Override
public boolean handleMessage(Message msg) {
int msgType = preDispatch(msg);
if (mOldCallback != null && mOldCallback.handleMessage(msg)) {
postDispatch(msgType);
return true;
}
mHandler.handleMessage(msg);
postDispatch(msgType);
return true;
}
private int preDispatch(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
TimeCounterManager.get().onActivityLaunch();
break;
case PAUSE_ACTIVITY:
TimeCounterManager.get().onActivityPause();
break;
//兼容 Android SDK 28及以上
case EXECUTE_TRANSACTION:
return handlerActivity(msg);
default:
break;
}
return msg.what;
}
private int handlerActivity(Message msg) {
Object obj = msg.obj;
Object activityCallback = Reflector.QuietReflector.with(obj).method("getLifecycleStateRequest").call();
if (activityCallback != null) {
String transactionName = activityCallback.getClass().getCanonicalName();
if (TextUtils.equals(transactionName, LAUNCH_ITEM_CLASS)) {
TimeCounterManager.get().onActivityLaunch();
return LAUNCH_ACTIVITY;
} else if (TextUtils.equals(transactionName, PAUSE_ITEM_CLASS)) {
TimeCounterManager.get().onActivityPause();
return PAUSE_ACTIVITY;
}
}
return msg.what;
}
private void postDispatch(int msgType) {
switch (msgType) {
case LAUNCH_ACTIVITY:
TimeCounterManager.get().onActivityLaunched();
break;
case PAUSE_ACTIVITY:
TimeCounterManager.get().onActivityPaused();
break;
default:
break;
}
}
}
网友评论