Mobile development: APP positioning is too frequent, I use this way to "find out the culprit"!

Keywords: Android Java Programming SDK

background

Positioning is now one of the most basic and indispensable capabilities of many apps, especially for applications such as taxi and takeout. However, the call to location can not be unrestrained. A little carelessness may cause the device to consume too much power, and eventually lead to the user uninstalling the application.

My project is an APP running in the background, and I need to get the current location in the background from time to time. In addition, many third-party libraries will be introduced into the project, and there will also be the behavior of calling location in these libraries. Therefore, we often receive test feedback that our application consumes too much power due to too frequent location.

When troubleshooting this problem, the author first ruled out the problem of our business logic, because each functional module in the project calls the uniformly encapsulated location module interface when positioning. In this module, some call frequency statistics and monitoring are made for the corresponding interface, and relevant log statements are printed, while the frequency and times of log statements related to positioning are printed in the problem log Numbers are in a very reasonable range.

At this time, I realized that the culprit of frequent positioning is not our internal, but the third-party library.

So the problem is, there are so many third-party libraries introduced, how can I know whose location call frequency is unreasonable?

Although I log in the public location module of the project, the problem is that the third-party library can not be adjusted to our internal interface.

So can we go to the lower level to bury statistics?

AOP

AOP, or aspect oriented programming, is nothing new.

In my personal understanding, AOP is to abstract our code into a hierarchy, and then insert some general logic between two layers through a non-invasive method, which is often used for statistics of buried points, log output, permission interception and so on. For details, you can search relevant articles, and AOP will not be specifically discussed here.

To count the calls of a method from the application level, AOP is obviously very suitable. The typical application of AOP in Android is AspectJ, so I decided to try it with AspectJ, but where is the most suitable insertion point? I decided to go to the SDK source code to find out.

Strategy exploration

First of all, let's see how the location interface is generally called:

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());

Of course, there are not only these two interfaces, but also several overloaded interfaces. However, by looking at the source code of LocationManager, we can find that this method will be called in the end:

//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) {
        throw e.rethrowFromSystemServer();
    }
}

It seems that this is a suitable insertion point, but if you print the log when this method is called through the AspectJ annotation (the specific usage of AspectJ is not the focus of this article, which will not be explained here), after compiling and running, you will find that the log you want is not printed at all.

By understanding the working mechanism of AspectJ, we can know why this method does not work:

... after class file generation and before dex file generation, traverse and match all tangent points declared in AspectJ file, and then weave the pre declared code before and after the tangent point

LocationManager is a class in android.jar and does not participate in compilation (android.jar is located in the Android device). This also declares that AspectJ's solution cannot meet the requirements.

To open or find a new path or snap course

I decided to sacrifice reflection + dynamic agent killing, but I still had to find a suitable insertion point.

By reading the source code of LocationManager, we can find that the location operation is finally delegated to the requestLocationUpdates method of the member object mService.

This mService is a good entry point, so now the idea is very clear. First, implement a proxy class of mService, and then when the method we are interested in (requestLocationUpdates) is called, execute some of our own embedded logic (such as logging or uploading to the server, etc.).

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 for the location manager outer layer 
                if (foundLocationManager && !TextUtils.equals(e.getClassName(), "android.location.LocationManager")) {
                    invoker = e.getClassName() + "." + e.getMethodName();
                    //Here you can record the caller information of the location interface according to your own needs. Here I will print the calling class, function name, and parameters
                    Log.d("LocationTest", "invoker is " + invoker + "(" + args + ")");
                    break;
                }
            }
        }
        return method.invoke(mLocationManager, args);
    }
}

The role of the above agent is to replace the mService member of LocationManager, and the actual ILocationManager will be wrapped by the agent.

In this way, I can plug in the actual methods of ILocationManager, such as log ging or recording the call information on the local disk. It is worth mentioning that since I only care about requestLocationUpdates, this method is filtered. Of course, you can also make your own filtering rules according to your needs.

After the proxy class is implemented, we will start the real hook operation. Therefore, we implement the following methods:

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

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

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

The hook operation can be completed in a few lines of code, and the use method is very simple. Just pass the LocationManager instance into this method. Now think about how we got the LocationManager instance:

LocationManager locationManager = 
    (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);

We usually want to call the global location interface of hook Application. You may be smart enough to perform hook operation during Application initialization.

that is

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

But does this really guarantee that the global location manager can be reached by hook?

After the actual measurement, you will find that there is still a catch. For example, if you get the LocationManager instance through the Activity context, it will not be hook, because it is not the same instance as the LocationManager obtained in the Application. If you want to know the specific reason, please refer to Here.

So if we want to hook to all LocationManager instances, we have to see how LocationManager is created.

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

Let's go to the system service registry to find out

//SystemServiceRegistry.java
final class SystemServiceRegistry {
    private static final String TAG = "SystemServiceRegistry";
    ...
    static {
    ...
    //Register servicecatcher, 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 new LocationManager(ctx, ILocationManager.Stub.asInterface(b));
            }});
    ...
    }

    //Mapping of all servicefetchers to service names
    private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
            new HashMap<String, ServiceFetcher<?>>();

    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }

    static abstract interface ServiceFetcher<T> {
       T getService(ContextImpl ctx);
    }

}

So far, we know that the real place to create the LocationManager instance is CachedServiceFetcher.createService. That's easy. I call hookLocationManager at the place where LocationManager is created, so there's no missing fish.

But to achieve this goal, we need to hook the CachedServiceFetcher corresponding to LocationService.

The general idea is to replace the LocationService corresponding CachedServiceFetcher in SYSTEM_SERVICE_FETCHERS with the proxy class LMCachedServiceFetcherProxy implemented by us, calling 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){
                //Look locationmanager here
                HookHelper.hookLocationManager((LocationManager)result);
            }
            return result;
        }
        return method.invoke(mLMCachedServiceFetcher, args);
    }
}
//HookHelper.java
public static void hookSystemServiceRegistry(){
    try {
        Object systemServiceFetchers  = null;
        Class<?> locationManagerClazsz = Class.forName("android.app.SystemServiceRegistry");
        //Get the system service references member of the 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");
            //Create proxy class
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                        new Class<?>[] { serviceFetcheClazz }, new LMCachedServiceFetcherProxy(locationServiceFetcher));
            //Replace the original ServiceFetcher with a proxy class
            if(fetchersMap.put(Context.LOCATION_SERVICE, proxy) == locationServiceFetcher){
                Log.d("LocationTest", "hook success! ");
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

You may find that the place we explicitly mentioned above to create the LocationManager instance is CachedServiceFetcher.createService, but here I go to hook LocationManager when I call getService, because the call time of createService is too early, even earlier than the initialization of Application, so we can only start from getService.

After the above analysis, we know that every time you call context.getSystemService, CachedServiceFetcher.getService will be called, but createService will not be called every time, because CachedServiceFetcher implements the caching mechanism internally, which ensures that only one LocationManager instance can be created for each context.

Another problem is that the same location manager may be hook multiple times. This problem can also be solved. Let's record each location manager instance that has been hooked. The final code of HookHelper is as follows:

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");
            //Get the system service references member of the 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");
                //Create proxy class
                Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                            new Class<?>[] { serviceFetcheClazz }, new LMCachedServiceFetcherProxy(locationServiceFetcher));
                //Replace the original ServiceFetcher with a proxy class
                if(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");
            //Get mService member of LocationManager
            iLocationManager = getField(locationManagerClazsz, locationManager, "mService");

            if(hooked.contains(iLocationManager)){
                return;//This instance has been hook
            }

            Class<?> iLocationManagerClazz = Class.forName("android.location.ILocationManager");

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

            //Replace the original ILocationManager with a proxy class
            setField(locationManagerClazsz, locationManager, "mService", proxy);
            //Record instances that have been hook
            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);
    }
}

summary

Through reflection + dynamic agent, we create a hook of location manager, and then do some embedding logic when locating the execution of related methods. The original intention of the author is to be able to monitor and count the location requests of each module from the application level. After actual measurement, the above implementation can perfectly meet my needs.

The specific monitoring strategies are as follows:

Each time the requestLocationUpdates is called, the caller's class name, method name and the parameter value of the incoming requestLocationUpdates will be printed out (the more important information in the parameter includes the Provider used for this location, the time interval and distance of continuous location)

Although I just hook the location-based service here, this idea may be applicable to other system services, such as AlarmManager, etc., but the actual operation is definitely different, and the specific details still need to see the source code. If you have good ideas, welcome to exchange and study.

Matters needing attention

  1. The implementation of this article is based on Android P source code, and other platforms may need to do additional adaptation (the overall idea is the same)

  2. Since reflection is used, there must be a certain loss of performance, so it should be considered carefully when applied to the production environment.

  3. As we all know, Android P began to disable unofficial APIs, and the affected APIs were divided into light graylist, dark graylist and blacklist. When using the above implementation of hook LocationManager, you will find that the system prints the following log s, which indicates that the interface is already in the light gray list and still works normally, but the future Android version is not guaranteed.
    W/idqlocationtes: Accessing hidden field Landroid/location/LocationManager;->mService:Landroid/location/ILocationManager;
    (light greylist, reflection)

Finally, for programmers, there are too many knowledge contents and technologies to learn. If they want not to be eliminated by the environment, they have to constantly improve themselves. It is always us to adapt to the environment, not the environment to adapt to us!

Attached here are dozens of interview questions of Tencent, headlines, Alibaba, meituan and other companies related to the above technical system chart for 19 years. The technical points are organized into videos and PDF s (actually more effort than expected), including knowledge context + many details. Due to the limited space, here is a part of the picture to show you.

I believe it will bring you many benefits:

**The above [high-definition technology brain map] and [supporting architecture technology PDF]

background

Positioning is now one of the most basic and indispensable capabilities of many apps, especially for applications such as taxi and takeout. However, the call to location can not be unrestrained. A little carelessness may cause the device to consume too much power, and eventually lead to the user uninstalling the application.

My project is an APP running in the background, and I need to get the current location in the background from time to time. In addition, many third-party libraries will be introduced into the project, and there will also be the behavior of calling location in these libraries. Therefore, we often receive test feedback that our application consumes too much power due to too frequent location.

When troubleshooting this problem, the author first ruled out the problem of our business logic, because each functional module in the project calls the uniformly encapsulated location module interface when positioning. In this module, some call frequency statistics and monitoring are made for the corresponding interface, and relevant log statements are printed, while the frequency and times of log statements related to positioning are printed in the problem log Numbers are in a very reasonable range.

At this time, I realized that the culprit of frequent positioning is not our internal, but the third-party library.

So the problem is, there are so many third-party libraries introduced, how can I know whose location call frequency is unreasonable?

Although I log in the public location module of the project, the problem is that the third-party library can not be adjusted to our internal interface.

So can we go to the lower level to bury statistics?

AOP

AOP, or aspect oriented programming, is nothing new.

In my personal understanding, AOP is to abstract our code into a hierarchy, and then insert some general logic between two layers through a non-invasive method, which is often used for statistics of buried points, log output, permission interception and so on. For details, you can search relevant articles, and AOP will not be specifically discussed here.

To count the calls of a method from the application level, AOP is obviously very suitable. The typical application of AOP in Android is AspectJ, so I decided to try it with AspectJ, but where is the most suitable insertion point? I decided to go to the SDK source code to find out.

Strategy exploration

First of all, let's see how the location interface is generally called:

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());

Of course, there are not only these two interfaces, but also several overloaded interfaces. However, by looking at the source code of LocationManager, we can find that this method will be called in the end:

//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) {
        throw e.rethrowFromSystemServer();
    }
}

It seems that this is a suitable insertion point, but if you print the log when this method is called through the AspectJ annotation (the specific usage of AspectJ is not the focus of this article, which will not be explained here), after compiling and running, you will find that the log you want is not printed at all.

By understanding the working mechanism of AspectJ, we can know why this method does not work:

... after class file generation and before dex file generation, traverse and match all tangent points declared in AspectJ file, and then weave the pre declared code before and after the tangent point

LocationManager is a class in android.jar and does not participate in compilation (android.jar is located in the Android device). This also declares that AspectJ's solution cannot meet the requirements.

To open or find a new path or snap course

I decided to sacrifice reflection + dynamic agent killing, but I still had to find a suitable insertion point.

By reading the source code of LocationManager, we can find that the location operation is finally delegated to the requestLocationUpdates method of the member object mService.

This mService is a good entry point, so now the idea is very clear. First, implement a proxy class of mService, and then when the method we are interested in (requestLocationUpdates) is called, execute some of our own embedded logic (such as logging or uploading to the server, etc.).

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 for the location manager outer layer 
                if (foundLocationManager && !TextUtils.equals(e.getClassName(), "android.location.LocationManager")) {
                    invoker = e.getClassName() + "." + e.getMethodName();
                    //Here you can record the caller information of the location interface according to your own needs. Here I will print the calling class, function name, and parameters
                    Log.d("LocationTest", "invoker is " + invoker + "(" + args + ")");
                    break;
                }
            }
        }
        return method.invoke(mLocationManager, args);
    }
}

The role of the above agent is to replace the mService member of LocationManager, and the actual ILocationManager will be wrapped by the agent.

In this way, I can plug in the actual methods of ILocationManager, such as log ging or recording the call information on the local disk. It is worth mentioning that since I only care about requestLocationUpdates, this method is filtered. Of course, you can also make your own filtering rules according to your needs.

After the proxy class is implemented, we will start the real hook operation. Therefore, we implement the following methods:

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

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

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

The hook operation can be completed in a few lines of code, and the use method is very simple. Just pass the LocationManager instance into this method. Now think about how we got the LocationManager instance:

LocationManager locationManager = 
    (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);

We usually want to call the global location interface of hook Application. You may be smart enough to perform hook operation during Application initialization.

that is

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

But does this really guarantee that the global location manager can be reached by hook?

After the actual measurement, you will find that there is still a catch. For example, if you get the LocationManager instance through the Activity context, it will not be hook, because it is not the same instance as the LocationManager obtained in the Application. If you want to know the specific reason, please refer to Here.

So if we want to hook to all LocationManager instances, we have to see how LocationManager is created.

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

Let's go to the system service registry to find out

//SystemServiceRegistry.java
final class SystemServiceRegistry {
    private static final String TAG = "SystemServiceRegistry";
    ...
    static {
    ...
    //Register servicecatcher, 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 new LocationManager(ctx, ILocationManager.Stub.asInterface(b));
            }});
    ...
    }

    //Mapping of all servicefetchers to service names
    private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
            new HashMap<String, ServiceFetcher<?>>();

    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }

    static abstract interface ServiceFetcher<T> {
       T getService(ContextImpl ctx);
    }

}

So far, we know that the real place to create the LocationManager instance is CachedServiceFetcher.createService. That's easy. I call hookLocationManager at the place where LocationManager is created, so there's no missing fish.

But to achieve this goal, we need to hook the CachedServiceFetcher corresponding to LocationService.

The general idea is to replace the LocationService corresponding CachedServiceFetcher in SYSTEM_SERVICE_FETCHERS with the proxy class LMCachedServiceFetcherProxy implemented by us, calling 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){
                //Look locationmanager here
                HookHelper.hookLocationManager((LocationManager)result);
            }
            return result;
        }
        return method.invoke(mLMCachedServiceFetcher, args);
    }
}
//HookHelper.java
public static void hookSystemServiceRegistry(){
    try {
        Object systemServiceFetchers  = null;
        Class<?> locationManagerClazsz = Class.forName("android.app.SystemServiceRegistry");
        //Get the system service references member of the 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");
            //Create proxy class
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                        new Class<?>[] { serviceFetcheClazz }, new LMCachedServiceFetcherProxy(locationServiceFetcher));
            //Replace the original ServiceFetcher with a proxy class
            if(fetchersMap.put(Context.LOCATION_SERVICE, proxy) == locationServiceFetcher){
                Log.d("LocationTest", "hook success! ");
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

You may find that the place we explicitly mentioned above to create the LocationManager instance is CachedServiceFetcher.createService, but here I go to hook LocationManager when I call getService, because the call time of createService is too early, even earlier than the initialization of Application, so we can only start from getService.

After the above analysis, we know that every time you call context.getSystemService, CachedServiceFetcher.getService will be called, but createService will not be called every time, because CachedServiceFetcher implements the caching mechanism internally, which ensures that only one LocationManager instance can be created for each context.

Another problem is that the same location manager may be hook multiple times. This problem can also be solved. Let's record each location manager instance that has been hooked. The final code of HookHelper is as follows:

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");
            //Get the system service references member of the 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");
                //Create proxy class
                Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                            new Class<?>[] { serviceFetcheClazz }, new LMCachedServiceFetcherProxy(locationServiceFetcher));
                //Replace the original ServiceFetcher with a proxy class
                if(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");
            //Get mService member of LocationManager
            iLocationManager = getField(locationManagerClazsz, locationManager, "mService");

            if(hooked.contains(iLocationManager)){
                return;//This instance has been hook
            }

            Class<?> iLocationManagerClazz = Class.forName("android.location.ILocationManager");

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

            //Replace the original ILocationManager with a proxy class
            setField(locationManagerClazsz, locationManager, "mService", proxy);
            //Record instances that have been hook
            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);
    }
}

summary

Through reflection + dynamic agent, we create a hook of location manager, and then do some embedding logic when locating the execution of related methods. The original intention of the author is to be able to monitor and count the location requests of each module from the application level. After actual measurement, the above implementation can perfectly meet my needs.

The specific monitoring strategies are as follows:

Each time the requestLocationUpdates is called, the caller's class name, method name and the parameter value of the incoming requestLocationUpdates will be printed out (the more important information in the parameter includes the Provider used for this location, the time interval and distance of continuous location)

Although I just hook the location-based service here, this idea may be applicable to other system services, such as AlarmManager, etc., but the actual operation is definitely different, and the specific details still need to see the source code. If you have good ideas, welcome to exchange and study.

Matters needing attention

  1. The implementation of this article is based on Android P source code, and other platforms may need to do additional adaptation (the overall idea is the same)

  2. Since reflection is used, there must be a certain loss of performance, so it should be considered carefully when applied to the production environment.

  3. As we all know, Android P began to disable unofficial APIs, and the affected APIs were divided into light graylist, dark graylist and blacklist. When using the above implementation of hook LocationManager, you will find that the system prints the following log s, which indicates that the interface is already in the light gray list and still works normally, but the future Android version is not guaranteed.
    W/idqlocationtes: Accessing hidden field Landroid/location/LocationManager;->mService:Landroid/location/ILocationManager;
    (light greylist, reflection)

Finally, for programmers, there are too many knowledge contents and technologies to learn. If they want not to be eliminated by the environment, they have to constantly improve themselves. It is always us to adapt to the environment, not the environment to adapt to us!

Attached here are dozens of interview questions of Tencent, headlines, Alibaba, meituan and other companies related to the above technical system chart for 19 years. The technical points are organized into videos and PDF s (actually more effort than expected), including knowledge context + many details. Due to the limited space, here is a part of the picture to show you.

I believe it will bring you many benefits:

The above [HD technology brain map] and [supporting architecture technology PDF] can be obtained free of charge with wx: X1524478394!

It's easy to be a programmer. To be an excellent programmer, you need to keep learning. From junior programmer to senior programmer, from junior architect to senior architect, or to management, from technical manager to technical director, you need to master different abilities at each stage. Early determination of their career direction, in order to work and improve ability to get rid of peers.
**

Posted by pskZero7 on Fri, 27 Dec 2019 05:20:01 -0800