导言
忙忙碌碌又一天,最近学习了一个新项目,每天都是吭哧吭哧的去看项目中的各种源码,都来自于各路大神的奉献。每看到一块地方的时分都感觉触到了自己的知识鸿沟,然后又得吭哧吭哧的去查找。不知道大家的查找途径是什么,我的查找途径从以前的 Google
-> ->
简书
-> Other(Baidu)
变成了 ChatGPT
-> Google
-> 其他
。
最近在排查主线程耗时的一个任务,既然在主线程了,那还不好办,直接上 Trace
分析主线程中的耗时任务都有哪些不就完了。完了分析完一波后又无从下手啦。接下来考虑从 MainLooper
下手吧,看看哪些是长音讯,履行任务又比较耗时。
Handler
Looper
那么怎么做长音讯的耗时监控呐?这又遇到了一个问题,在以往许多都是经过 Looper
的 Printer
进行控制长音讯的检查,但是 Printer
也是有缺点的,便是无法知道它的 callback
来自于哪里,及时统计出来了时刻,也不满足需求。
一切的源头都应该从源码进行着手,经过阅览源码发现,在 Looper
中发现了 Observer
,它是什么鬼?
public final class Looper {
private static Observer sObserver;
/**
* Set the transaction observer for all Loopers in this process.
*
* @hide
*/
public static void setObserver(@Nullable Observer observer) {
sObserver = observer;
}
public static void loop() {
// ...此处省掉无关紧要的 code
for (;;) {
// ...此处省掉无关紧要的 code
// 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);
}
// Make sure the observer won't change while processing a transaction.
final Observer observer = sObserver;
// ...此处省掉无关紧要的 code
Object token = null;
// 在履行音讯履行音讯告诉
if (observer != null) {
token = observer.messageDispatchStarting();
}
long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
try {
msg.target.dispatchMessage(msg);
// 在音讯履行结束后又履行了音讯告诉
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
// 音讯履行的进程中产生了异常的回调
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
}
}
}
依据阅览源码来看,我觉得能够从这个地方下手搞工作。
/** {@hide} */
public interface Observer {
/**
* Called right before a message is dispatched.
*
* <p> The token type is not specified to allow the implementation to specify its own type.
*
* @return a token used for collecting telemetry when dispatching a single message.
* The token token must be passed back exactly once to either
* {@link Observer#messageDispatched} or {@link Observer#dispatchingThrewException}
* and must not be reused again.
*
*/
Object messageDispatchStarting();
/**
* Called when a message was processed by a Handler.
*
* @param token Token obtained by previously calling
* {@link Observer#messageDispatchStarting} on the same Observer instance.
* @param msg The message that was dispatched.
*/
void messageDispatched(Object token, Message msg);
/**
* Called when an exception was thrown while processing a message.
*
* @param token Token obtained by previously calling
* {@link Observer#messageDispatchStarting} on the same Observer instance.
* @param msg The message that was dispatched and caused an exception.
* @param exception The exception that was thrown.
*/
void dispatchingThrewException(Object token, Message msg, Exception exception);
}
Observer
是 Looper 的一个内部接口类,用来做事件的回调处理的。首要包含了三个函数 messageDispatchStarting
、messageDispatched
、dispatchingThrewException
,别离会在某条音讯调度前、调度处理后、调度处理进程中产生异常时回调。需求注意的是 messageDispatchStarting
要求回来一个 Object
对象,对类型不做任何约束,每个音讯类型都对应一个 token
,并且理应为独立且仅有。
于是乎,我兴高采烈的开端码起来了。
开搞开搞
先来介绍一会儿思路,咱们是不是能够直接经过反射调用 setObserver
的方法,是不是直接就能够设置Observer
,既然都到了反射,为啥不直接操作 sObserver
,提到这就开干。这儿运用的动态署理进行 hook Observer
。
public class HandlerLoopHookHelper {
/**
* Hook Looper 的 sObserver 调查音讯耗时
*
* @param context 上下文
*/
public static void hookLooperObserver(@Nullable Context context) {
Log.d(TAG, "hookLoopObserver: " + Build.VERSION.SDK_INT);
try {
@SuppressLint("PrivateApi") final Class<?> sObserverClass = Class.forName("android.os.Looper$Observer");
final ObserverInvocation invocation = new ObserverInvocation();
final Object o = sObserverClass.cast(Proxy.newProxyInstance(sObserverClass.getClassLoader(), new Class[]{sObserverClass}, invocation));
final Class<?> looperClass = Class.forName("android.os.Looper");
@SuppressLint("BlockedPrivateApi") final Field sObserver = looperClass.getDeclaredField("sObserver");
sObserver.setAccessible(true);
sObserver.set(getMainLooper(), o);
getMainLooper().setMessageLogging(invocation.printer);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
public class ObserverInvocation implements InvocationHandler {
private long dispatchStart = 0;
private long dispatchEnd = 0;
private static final long MESSAGE_WORK_TIME_300 = 300;
private static final long MESSAGE_WORK_TIME_100 = 100;
private static final long MESSAGE_WORK_TIME_50 = 50;
private static final long MESSAGE_WORK_TIME_10 = 10;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Looper.getMainLooper().isCurrentThread()) {
if ("messageDispatchStarting".equals(method.getName())) {
return messageDispatchStarting();
} else if ("messageDispatched".equals(method.getName())) {
messageDispatched((long) args[0], (Message) args[1]);
} else if ("dispatchingThrewException".equals(method.getName())) {
dispatchingThrewException((long) args[0], (Message) args[1], (Exception) args[2]);
}
}
return null;
}
public Object messageDispatchStarting() {
dispatchStart = SystemClock.uptimeMillis();
final long token = atomicLong.getAndIncrement();
return token;
}
public void messageDispatched(Object token, Message msg) {
dispatchEnd = SystemClock.uptimeMillis();
getTime(msg, (long) token);
}
public void dispatchingThrewException(Object token, Message msg, Exception exception) {
}
/**
* 计时,可依据自己的需求进行具体的计
*/
private void getTime(Message message, long token) {
// message 指定开端时刻,基于开机时刻
final long when = message.getWhen();
// 等候的时长:开端履行 - 指定开端时刻
final long wait = dispatchStart - when;
// 履行的时长:履行结束时刻 - 履行开端时刻
final long work = dispatchEnd - dispatchStart;
printRecord(message, token, wait, work);
}
private void printRecord(Message message, long token, long wait, long work) {
final StringBuilder stringBuilder = new StringBuilder();
if (work <= MESSAGE_WORK_TIME_10) {
stringBuilder.append("(可忽略)").append(dispatchStart).append("-").append(dispatchEnd)
.append(">>> token: ").append(token).append(message.toString())
.append(" wait: ").append(wait).append("ms")
.append(" work: ").append(work).append("ms");
Log.v(TAG, stringBuilder.toString());
} else if (work <= MESSAGE_WORK_TIME_50) {
stringBuilder.append("(看看就好)").append(dispatchStart).append("-").append(dispatchEnd)
.append(">>> token: ").append(token).append(message.toString())
.append(" wait: ").append(wait).append("ms")
.append(" work: ").append(work).append("ms");
Log.i(TAG, stringBuilder.toString());
} else if (work <= MESSAGE_WORK_TIME_100) {
stringBuilder.append("(需求重视一下)").append(dispatchStart).append("-").append(dispatchEnd)
.append(">>> token: ").append(token).append(message.toString())
.append(" wait: ").append(wait).append("ms")
.append(" work: ").append(work).append("ms");
Log.w(TAG, stringBuilder.toString());
} else if (work <= MESSAGE_WORK_TIME_300) {
stringBuilder.append("(需求处理啦~)").append(dispatchStart).append("-").append(dispatchEnd)
.append(">>> token: ").append(token).append(message.toString())
.append(" wait: ").append(wait).append("ms")
.append(" work: ").append(work).append("ms");
Log.d(TAG, stringBuilder.toString());
} else {
stringBuilder.append("(这个就超级严重啦~)").append(dispatchStart).append("-").append(dispatchEnd)
.append(">>> token: ").append(token).append(message.toString())
.append(" wait: ").append(wait).append("ms")
.append(" work: ").append(work).append("ms");
Log.e(TAG, stringBuilder.toString());
}
}
}
Coding 结束啦,这儿面的输出监控建议不要搞到线上,或许会比较的耗时,依据实际情况来确认怎么运用,定制监控的策略,能够考虑超越多长时刻来进行报警,或许上报等操作。废话不多说,run 一会儿吧,结果gg。
日志的大致意思是,这个类是被 @hide
的,对开发者是屏蔽调用及运用的。查阅相关代码得知,基于 Android 10 版别增加的 Observer
,在规划之初就只是为了调查并统计体系服务的Looper音讯调度功用(能够查阅LooperStats
与LooperStatsService
),所以这个API只给自己内部运用。
只要是代码总会有处理的方案,能不能绕过去躲藏 API 呐?能,下来就看看看一下怎么如绕开躲藏API 。
绕开 Observer 躲藏 API
处理 Hidden API
的方法有许多种,或许某些在 Android 高版别体系重被官方封堵。不过因为 Android 体系是开源的,所以无论怎么封堵,仍是能够经过其他的方法绕过,毕竟体系是不会约束用户修正自身进程的内存。
在这儿我运用的方案是 FreeReflection,大致的思路是将自己伪装成体系类,然后就能够调用这些私有 API 了。具体的内容能够参阅作者博客里面的介绍。
Android 14 无法加载.dex
文件
在这儿提个醒,在Android 14及以上版别,关于动态代码的加载产生了一些更改,否则会产生如下内容过错。
经过阅览 DexClassLoader 与 更安全的动态代码加载 发现 Android 14 关于动态加载 .dex
文件做了安全策略的约束,Android 14 关于动态加载(DCL)功用,必须将所有动态加载的文件标记为只读。否则,体系将会抛出异常。
正确操作姿态,便是将 FreeReflection 三方库 Copy
下来,然后修正源码,完美处理在 Android 14 上面不能动态加载 .dex
问题。
然后再跑一会儿。完美处理。
正确的打开姿态
public class HandlerLoopHookHelper {
private static volatile boolean isHooked = false;
private static final String TAG = "HandlerLoopHookHelper";
private static void checkHooked(Context context) {
if (!isHooked) {
Reflection.unseal(context);
isHooked = true;
}
}
/**
* Hook Looper 的 sObserver 调查音讯耗时
*
* @param context 上下文
*/
public static void hookLooperObserver(@Nullable Context context) {
Log.d(TAG, "hookLoopObserver: " + Build.VERSION.SDK_INT);
try {
checkHooked(context);
@SuppressLint("PrivateApi") final Class<?> sObserverClass = Class.forName("android.os.Looper$Observer");
final ObserverInvocation invocation = new ObserverInvocation();
final Object o = sObserverClass.cast(Proxy.newProxyInstance(sObserverClass.getClassLoader(), new Class[]{sObserverClass}, invocation));
final Class<?> looperClass = Class.forName("android.os.Looper");
@SuppressLint("BlockedPrivateApi") final Field sObserver = looperClass.getDeclaredField("sObserver");
sObserver.setAccessible(true);
sObserver.set(getMainLooper(), o);
getMainLooper().setMessageLogging(invocation.printer);
} catch (Throwable e) {
e.printStackTrace();
}
}
/**
* 钩针活套调查者
*
* @param context 上下文
*/
public static void unHookLooperObserver(@Nullable Context context) {
Log.d(TAG, "unHookLooperObserver: " + Build.VERSION.SDK_INT);
try {
checkHooked(context);
@SuppressLint("PrivateApi") final Class<?> sObserverClass = Class.forName("android.os.Looper$Observer");
final ObserverInvocation invocation = new ObserverInvocation();
final Object o = sObserverClass.cast(Proxy.newProxyInstance(sObserverClass.getClassLoader(), new Class[]{sObserverClass}, invocation));
final Class<?> looperClass = Class.forName("android.os.Looper");
@SuppressLint("BlockedPrivateApi") final Field sObserver = looperClass.getDeclaredField("sObserver");
sObserver.setAccessible(true);
sObserver.set(getMainLooper(), null);
getMainLooper().setMessageLogging(null);
} catch (Throwable e) {
e.printStackTrace();
}
}
/**
* 挂钩主活套音讯空闲处理程序
*
* @param context 上下文
*/
public static void hookMainLooperMessageIdleHandlers(@Nullable Context context) {
Log.d(TAG, "hookMainLooperMessageIdleHandlers: " + Build.VERSION.SDK_INT);
try {
checkHooked(context);
final Class<?> looperClass = Class.forName("android.os.Looper");
final Field mQueueF = looperClass.getDeclaredField("mQueue");
mQueueF.setAccessible(true);
final Object mainMessageQueue = mQueueF.get(getMainLooper());
final Class<?> mQueueClass = Class.forName("android.os.MessageQueue");
final Field mainIdleHandlerF = mQueueClass.getDeclaredField("mIdleHandlers");
mainIdleHandlerF.setAccessible(true);
final Object o = mainIdleHandlerF.get(mainMessageQueue);
final ArrayList<MessageQueue.IdleHandler> mIdleHandlers = (ArrayList<MessageQueue.IdleHandler>) o;
Log.d(TAG, "hookMainLooperMessageIdleHandlers size: " + mIdleHandlers.size());
for (MessageQueue.IdleHandler mIdleHandler : mIdleHandlers) {
Log.d(TAG, "hookMainLooperMessageIdleHandlers content is: " + mIdleHandler.toString());
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
小结
-
Observer
相比于Printer
能够直接拿到Message
对象,并且不需求设置Printer
能够防止每个音讯调度时额定拼接字符串的成本。 - 处理开发阶段
Hidden API
访问约束,能够经过许多种方法绕过,也能够考虑经过CompileOnly
一个假工程
来完成,假工程
里面模仿相应的体系源码,从而完成Observer
类的访问。不过我试过这种方法,同样也需求搞定体系的hidden api
形似也是绕不过去的,如果运用这个库 FreeReflection 的话,那么假工程
的方法也是能够完成,在这儿不做赘述,感兴趣的同学能够自己尝试。 -
Observer
机制是在 Anroid 10 开端增加的,因此低版别仍是需求用Printer
的方法进行监听 - Android 14 版别动态加载
.dex
文件需求设置file
为只读模式,否则将会抛出安全异常。