background

Location is now one of the most basic and indispensable capabilities of many apps, especially for apps like taxi hailing and food delivery. However, do not use positioning without restraint. A slight error may cause the device to consume power too fast, and ultimately lead to the uninstallation of the application.

The author’s project is a run in the background of the APP, and from time to time to obtain the current position in the background, combined with cooperation project will introduce many third-party libraries, the libraries inside will also have to invoke the behavior of the positioning, so often will receive a test feedback that our application caused by the positioning too often power consumption too fast.

When investigating this problem, the author first excluded the problem of our business logic, because each functional module in the project calls the unified encapsulation positioning module interface when positioning. The module by the corresponding interface to do some call frequency statistics and monitoring and print the relevant log statement, and the problem of the log statement related to positioning printing frequency and times are very reasonable range.

That’s when I realized that the culprit frequently identified was not inside us, but a third party library. So the problem is, there are so many third-party libraries introduced, how do I know whose location call frequency is not reasonable? Although I logged in the public location module of the project, the problem was that the third-party library could not be tuned to our internal interface. So, can we go down and bury some statistics?

AOP

AOP, or aspect oriented programming, is nothing new. As I understand it, AOP is about abstracting our code into a hierarchy and then inserting some common logic between the two layers in a non-intrusive way, often used for statistical burying points, log output, permission blocking, and so on.

AOP is an obvious fit for counting method calls at the application level. The typical use of AOP on Android is AspectJ, so I decided to try AspectJ, but where is the best insertion point? I decided to go to the SDK source code to find out.

Strategy to explore

First let’s look at how the positioning interface is called in general:

LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
// Single location
locationManager.requestSingleUpdate(provider, new MyLocationLisenter(), getLooper());
// continuous positioning
locationManager.requestSingleUpdate(provider,minTime, minDistance, new MyLocationLisenter());
Copy the code

There are several overloaded interfaces, of course, but looking at the source of the LocationManager, we can see that this method will always end up being called:

//LocationManager.java
private void requestLocationUpdates(LocationRequest request, LocationListener listener, Looper looper, PendingIntent intent) {

    String packageName = mContext.getPackageName();

    // wrap the listener class
    ListenerTransport transport = wrapListener(listener, looper);

    try {
        mService.requestLocationUpdates(request, transport, intent, packageName);
    } catch (RemoteException e) {
        throwe.rethrowFromSystemServer(); }}Copy the code

This seems like a good insertion point, but if you print the log when the method is called using AspectJ annotations (AspectJ usage is not the focus of this article), you will find that the log you want is not printed at all.

By looking at how AspectJ works, we can see why this approach doesn’t work:

. After the class file is generated until the dex file is generated, all pointcuts that match the declaration in the AspectJ file are traversed and matched, and the pre-declared code is woven in before and after the pointcuts

LocationManager is a class in Android.jar that does not participate in compilation (Android.jar resides on the Android device). This declared that AspectJ’s solution was inadequate.

A different approach

Soft can not only come to the hard, I decided to sacrifice reflection + dynamic proxy kill, but also the premise or to find a suitable insertion point.

Reading the LocationManager source code above, you can see that the location operation is ultimately delegated to the requestLocationUpdates method of the mService member object. The mService is a good place to start, so it is now clear to implement a proxy class for the mService and then perform some of your own buried logic (such as logging or uploading to the server) when the method we are interested in (requestLocationUpdates) is called. First implement the proxy class:

public class ILocationManagerProxy implements InvocationHandler {
    private Object mLocationManager;

    public ILocationManagerProxy(Object locationManager) {
        this.mLocationManager = locationManager;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (TextUtils.equals("requestLocationUpdates", method.getName())) {
            // Get the current function call stack
            StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
            if (stackTrace == null || stackTrace.length < 3) {
                return null;
            }
            StackTraceElement log = stackTrace[2];
            String invoker = null;
            boolean foundLocationManager = false;
            for (int i = 0; i < stackTrace.length; i++) {
                StackTraceElement e = stackTrace[i];
                if (TextUtils.equals(e.getClassName(), "android.location.LocationManager")) {
                    foundLocationManager = true;
                    continue;
                }
                // Find the caller outside the LocationManager
                if(foundLocationManager && ! TextUtils.equals(e.getClassName(),"android.location.LocationManager")) {
                    invoker = e.getClassName() + "." + e.getMethodName();
                    // Here you can record the caller information of the locating interface according to your own needs. Here I will print out the calling class, function name, and parameters
                    Log.d("LocationTest"."invoker is " + invoker + "(" + args + ")");
                    break; }}}returnmethod.invoke(mLocationManager, args); }}Copy the code

This agent replaces the mService member of the LocationManager, and the actual ILocationManager is wrapped by this agent. This way I can peg the methods of the actual ILocationManager, for example by logging, or logging the call information to a local disk, etc. It’s worth noting that since I only care about requestLocationUpdates, I’ve filtered this method, but you can make your own filtering rules as needed. With the proxy class implemented, it’s time to start the real hook operation, so we implement the following method:

    public static void hookLocationManager(LocationManager locationManager) {
        try {
            Object iLocationManager = null; Class<? > locationManagerClazsz = Class.forName("android.location.LocationManager");
            // Get the mService member of the LocationManager
            iLocationManager = getField(locationManagerClazsz, locationManager, "mService"); Class<? > iLocationManagerClazz = Class.forName("android.location.ILocationManager");

            // Create the proxy class
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                    newClass<? >[]{iLocationManagerClazz},new ILocationManagerProxy(iLocationManager));

            // Replace the original ILocationManager with a proxy class here
            setField(locationManagerClazsz, locationManager, "mService", proxy);
        } catch(Exception e) { e.printStackTrace(); }}Copy the code

The hook is done in a few lines of code, and the method is as simple as passing the LocationManager instance into the method. Now recall how we got the LocationManager instance:

LocationManager locationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
Copy the code

We usually want to hook the global location interface of the Application. You may want to hook the Application during initialization. That is

public class App extends Application {
    @Override
    public void onCreate(a) {
        LocationManager locationManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
        HookHelper.hookLocationManager(locationManager);
        super.onCreate(); }}Copy the code

But does this really guarantee that the global LocationManager can be hooked? For example, if you get a LocationManager instance from the Activity context, it will not be hooked because it is not the same instance as the LocationManager instance obtained from the Application. If you want to know why, see here,

So if we want to hook all LocationManager instances, we have to look at how the LocationManager is actually created.

//ContextImpl.java
@Override
public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
}
Copy the code

Let’s go to SystemServiceRegistry to find out

//SystemServiceRegistry.java
final class SystemServiceRegistry {
    private static final String TAG = "SystemServiceRegistry"; .static{...// Register ServiceFetcher, which is the factory class used to create LocationManager
	registerService(Context.LOCATION_SERVICE, LocationManager.class,
                new CachedServiceFetcher<LocationManager>() {
            @Override
            public LocationManager createService(ContextImpl ctx) throws ServiceNotFoundException {
                IBinder b = ServiceManager.getServiceOrThrow(Context.LOCATION_SERVICE);
                return newLocationManager(ctx, ILocationManager.Stub.asInterface(b)); }}); . }// All ServiceFetcher mappings to service names
    private static finalHashMap<String, ServiceFetcher<? >> SYSTEM_SERVICE_FETCHERS =newHashMap<String, ServiceFetcher<? > > ();public static Object getSystemService(ContextImpl ctx, String name) { ServiceFetcher<? > fetcher = SYSTEM_SERVICE_FETCHERS.get(name);returnfetcher ! =null ? fetcher.getService(ctx) : null;
    }
	
    static abstract interface ServiceFetcher<T> {
       T getService(ContextImpl ctx); }}Copy the code

Here, we also knew that actually creating LocationManager instance where is in CachedServiceFetcher createService, that the question is simple, I call hookLocationManager where the LocationManager was created, so I can’t get away with it. To do this, however, we need to hook the CachedServiceFetcher corresponding to the LocationService. In the general idea is to SYSTEM_SERVICE_FETCHERS LocationService corresponding CachedServiceFetcher replace for us to achieve the proxy class LMCachedServiceFetcherProxy, Call hookLocationManager in the proxy method. The code is as follows:

public class LMCachedServiceFetcherProxy implements InvocationHandler {

    private Object mLMCachedServiceFetcher;

    public LMCachedServiceFetcherProxy(Object LMCachedServiceFetcher) {
        this.mLMCachedServiceFetcher = LMCachedServiceFetcher;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // Why intercept getService instead of createService?
        if(TextUtils.equals(method.getName(), "getService")){
            Object result = method.invoke(mLMCachedServiceFetcher, args);
            if(result instanceof LocationManager){
                // Hook LocationManager here
                HookHelper.hookLocationManager((LocationManager)result);
            }
            return result;
        }
        returnmethod.invoke(mLMCachedServiceFetcher, args); }}Copy the code
//HookHelper.java
public static void hookSystemServiceRegistry(a){
    try {
        Object systemServiceFetchers  = null; Class<? > locationManagerClazsz = Class.forName("android.app.SystemServiceRegistry");
        // get the SYSTEM_SERVICE_FETCHERS member of SystemServiceRegistry
        systemServiceFetchers = getField(locationManagerClazsz, null."SYSTEM_SERVICE_FETCHERS");
        if(systemServiceFetchers instanceofHashMap){ HashMap fetchersMap = (HashMap) systemServiceFetchers; Object locationServiceFetcher = fetchersMap.get(Context.LOCATION_SERVICE); Class<? > serviceFetcheClazz = Class.forName("android.app.SystemServiceRegistry$ServiceFetcher");
            // Create the proxy class
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                        newClass<? >[] { serviceFetcheClazz },new LMCachedServiceFetcherProxy(locationServiceFetcher));
            // Replace ServiceFetcher with a proxy class
            if(fetchersMap.put(Context.LOCATION_SERVICE, proxy) == locationServiceFetcher){
                Log.d("LocationTest"."hook success! "); }}}catch(Exception e) { e.printStackTrace(); }}Copy the code

Maybe you found, we clearly said above create instances of LocationManager CachedServiceFetcher. Is that in createService, This is because createService is called too early, even before Application initialization, so we have to start with getService. Through the above analysis we know that every time you call context. The getSystemService, CachedServiceFetcher. GetService invocation, but createService doesn’t call every time, The reason is that CachedServiceFetcher implements caching internally, ensuring that only one instance of LocationManager can be created per context. Another problem is that the same LocationManager can be hooked multiple times. This problem is easily solved by logging every instance of LocationManager that is hooked. The final code for HookHelper looks like this:

public class HookHelper {
    public static final String TAG = "LocationHook";

    private static final Set<Object> hooked = new HashSet<>();

    public static void hookSystemServiceRegistry(){ try { Object systemServiceFetchers = null; Class<? > locationManagerClazsz = Class.forName("android.app.SystemServiceRegistry"); SystemServiceFetchers = getField(locationManagerClazsz, null,"SYSTEM_SERVICE_FETCHERS");
            if(systemServiceFetchers instanceof HashMap){ HashMap fetchersMap = (HashMap) systemServiceFetchers; Object locationServiceFetcher = fetchersMap.get(Context.LOCATION_SERVICE); Class<? > serviceFetcheClazz = Class.forName("android.app.SystemServiceRegistry$ServiceFetcher"); Object proxy = proxy.newProxyInstance (thread.currentThread ().getContextClassLoader(), new Class<? >[] { serviceFetcheClazz }, new LMCachedServiceFetcherProxy(locationServiceFetcher)); // Replace ServiceFetcher with a proxy classif(fetchersMap.put(Context.LOCATION_SERVICE, proxy) == locationServiceFetcher){
                    Log.d("LocationTest"."hook success! "); } } } catch (Exception e) { e.printStackTrace(); } } public static void hookLocationManager(LocationManager locationManager) { try { Object iLocationManager = null; Class<? > locationManagerClazsz = Class.forName("android.location.LocationManager"); ILocationManager = getField(LocalManagerClazsz, LocationManager,"mService");
            
            if(hooked.contains(iLocationManager)){
                return; } Class<? > iLocationManagerClazz = Class.forName("android.location.ILocationManager"); Object proxy = proxy.newProxyInstance (thread.currentThread ().getContextClassLoader(), new Class<? >[]{iLocationManagerClazz}, new ILocationManagerProxy(iLocationManager)); // Replace the original ILocationManager with a proxy class heresetField(locationManagerClazsz, locationManager, "mService", proxy); // Record the instance hooked. Add (proxy); } catch (Exception e) { e.printStackTrace(); } } public static Object getField(Class clazz, Object target, String name) throws Exception { Field field = clazz.getDeclaredField(name); field.setAccessible(true);
        return field.get(target);
    }

    public static void setField(Class clazz, Object target, String name, Object value) throws Exception {
        Field field = clazz.getDeclaredField(name);
        field.setAccessible(true); field.set(target, value); }}Copy the code

conclusion

With reflection + dynamic proxy, we create a hook for the LocationManager and then do some buried logic when locating the relevant method execution. The author’s original intention is to be able to monitor and count the positioning requests of each module from the application level. After actual measurement, the above realization can perfectly meet my needs.

The author’s specific monitoring strategies are as follows:

Print the class name, method name, and parameter values passed to requestLocationUpdates each time requestLocationUpdates are called.

Although the author only hook the location service here, this idea may be applicable to other system services, such as AlarmManager, but the actual operation is definitely not the same, the specific details still need to see the source code. If you have good ideas, welcome to exchange learning.

Matters needing attention

  • The implementation of this article is based on Android P source code, other platforms may need to do additional adaptation (the general idea is the same)
  • Since reflection is used, there must be some performance loss, so it should be considered in production environment.
  • As we all know, Android P has started to disable unofficial apis, which are classified as light Greylist, Dark Greylist, and blacklist. When using the above implementation hook LocationManager, you will find that the system prints the following log, indicating that the interface has been in the light grey list, or can still run normally, but the future Android version can not be guaranteed.
W/idqlocationtes: Accessing hidden field Landroid/location/LocationManager; ->mService:Landroid/location/ILocationManager; (light greylist, reflection)Copy the code

Further reading

  • Look at AspectJ’s strong insertion into Android
  • Understand the various contexts in Android in depth