Hooking Android Looper Observer for Message Timing Monitoring and Bypassing Hidden APIs
This article explains how to use reflection and dynamic proxies to hook the hidden Android Looper.Observer API for detailed message‑dispatch timing, demonstrates code examples for installing and removing the hook, discusses workarounds for hidden‑API restrictions and the .dex loading changes introduced in Android 14.
Introduction
When debugging a performance‑critical task on the main thread, the author discovered the need to monitor long‑running messages using Android's Looper and its hidden Observer interface instead of the traditional Printer approach.
Understanding Looper.Observer
Reading the source reveals that Looper contains a static sObserver field of type Observer , which provides three callbacks: messageDispatchStarting , messageDispatched , and dispatchingThrewException . These callbacks give direct access to the Message object and allow precise timing without the overhead of string concatenation.
public final class Looper {
private static Observer sObserver;
...
public static void setObserver(@Nullable Observer observer) { sObserver = observer; }
public static void loop() {
// simplified loop
final Printer logging = me.mLogging;
if (logging != null) { logging.println(...); }
final Observer observer = sObserver;
// dispatch message
if (observer != null) { token = observer.messageDispatchStarting(); }
try { msg.target.dispatchMessage(msg); }
finally { if (observer != null) { observer.messageDispatched(token, msg); } }
}
}Implementing the Hook
The author creates a dynamic proxy that implements the hidden Observer interface. The proxy records the start time in messageDispatchStarting , calculates wait and work times in messageDispatched , and logs the results with different severity levels based on duration.
public class HandlerLoopHookHelper {
public static void hookLooperObserver(@Nullable Context context) {
Class
sObserverClass = Class.forName("android.os.Looper$Observer");
ObserverInvocation invocation = new ObserverInvocation();
Object proxy = Proxy.newProxyInstance(sObserverClass.getClassLoader(), new Class[]{sObserverClass}, invocation);
Field sObserver = Class.forName("android.os.Looper").getDeclaredField("sObserver");
sObserver.setAccessible(true);
sObserver.set(Looper.getMainLooper(), proxy);
Looper.getMainLooper().setMessageLogging(invocation.printer);
}
... // invoke, timing, logging logic
}Bypassing Hidden API Restrictions
To access the hidden Observer on newer Android versions, the article recommends using the FreeReflection library (or similar) to unseal the runtime, allowing reflective calls to private APIs. It also mentions alternative strategies such as creating a dummy project that mimics the system classes.
Android 14 .dex Loading Issue
Starting with Android 14, dynamically loaded .dex files must be marked read‑only; otherwise a security exception is thrown. The author shows how to copy the FreeReflection library, modify its source, and set the appropriate file permissions to avoid the crash.
Correct Hook Usage Example
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; }
}
public static void hookLooperObserver(@Nullable Context context) {
Log.d(TAG, "hookLoopObserver: " + Build.VERSION.SDK_INT);
try {
checkHooked(context);
Class
sObserverClass = Class.forName("android.os.Looper$Observer");
ObserverInvocation invocation = new ObserverInvocation();
Object proxy = Proxy.newProxyInstance(sObserverClass.getClassLoader(), new Class[]{sObserverClass}, invocation);
Field sObserver = Class.forName("android.os.Looper").getDeclaredField("sObserver");
sObserver.setAccessible(true);
sObserver.set(Looper.getMainLooper(), proxy);
Looper.getMainLooper().setMessageLogging(invocation.printer);
} catch (Throwable e) { e.printStackTrace(); }
}
... // unhook and idle‑handler utilities
}Conclusion
The hidden Observer provides richer information than Printer and avoids extra string concatenation.
Various techniques (FreeReflection, dummy projects, compile‑only stubs) can bypass hidden‑API restrictions during development.
Observer was introduced in Android 10; older versions still need the Printer method.
On Android 14, dynamically loaded .dex files must be read‑only to prevent security exceptions.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.