APP定位過於頻繁,我用反射+動態代理揪出元兇

背景

定位如今是不少APP最基本也不可或缺的能力之一,尤爲是對打車、外賣之類的應用來講。但對定位的調用可不能沒有節制,稍有不慎可能致使設備耗電過快,最終致使用戶卸載應用。java

筆者所在項目是一個在後臺運行的APP,且須要時不時在後臺獲取一下當前位置,再加上項目裏會引入不少合做第三方的庫,這些庫內部一樣也會有調用定位的行爲,所以常常會收到測試的反饋說咱們的應用因爲定位過於頻繁致使耗電過快。android

排查這個問題的時候,筆者首先排除了咱們業務邏輯的問題,由於項目中的各個功能模塊在定位時調用的是統一封裝後的定位模塊接口,該模塊中由對相應的接口作了一些調用頻率的統計和監控並打印了相關的log語句, 而問題log中跟定位相關的log語句打印頻率跟次數都是在很是合理的範圍內。編程

這時我才意識到頻繁定位的罪魁禍首並不在咱們內部,而是第三方庫搞的鬼。 那麼問題來了,引入的第三方庫那麼多,我怎麼知道誰的定位調用頻率不合理呢?雖然我在項目中的公共定位模塊中打了log,但問題是第三方庫可調不到咱們內部的接口。那麼咱們能不能到更底層的地方去埋點統計呢?緩存

AOP

AOP,即面向切面編程,已經不是什麼新鮮玩意了。就我我的的理解,AOP就是把咱們的代碼抽象爲層次結構,而後經過非侵入式的方法在某兩個層之間插入一些通用的邏輯,經常被用於統計埋點、日誌輸出、權限攔截等等,詳情可搜索相關的文章,這裏不具體展開講AOP了。bash

要從應用的層級來統計某個方法的調用,很顯然AOP很是適合。而AOP在Android的典型應用就是AspectJ了,因此我決定用AspectJ試試,不過哪裏纔是最合適的插入點呢?我決定去SDK源碼裏尋找答案。服務器

策略探索

首先咱們來看看定位接口通常是怎麼調用的:app

LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
//單次定位
locationManager.requestSingleUpdate(provider, new MyLocationLisenter(), getLooper());
//連續定位
locationManager.requestSingleUpdate(provider,minTime, minDistance, new MyLocationLisenter());
複製代碼

固然不止這兩個接口,還有好幾個重載接口,可是經過查看LocationManager的源碼,咱們能夠發現最後都會調到這個方法:ide

//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();
    }
}
複製代碼

看起來這裏是一個比較合適的插入點,可是若是你經過AspectJ的註解在這個方法被調用的時候打印log(AspectJ的具體用法不是本文重點,這裏不講解), 編譯運行下來後會發現根本沒有打出你要的log。函數

經過了解AspectJ的工做機制,咱們就能夠知道爲何這個方法行不通了:oop

...在class文件生成後至dex文件生成前,遍歷並匹配全部符合AspectJ文件中聲明的切點,而後將事先聲明好的代碼在切點先後織入

LocationManager是android.jar裏的類,並不參與編譯(android.jar位於android設備內)。這也宣告AspectJ的方案沒法知足需求。

另闢蹊徑

軟的不行只能來硬的了,我決定祭出反射+動態代理殺招,不過還前提仍是要找到一個合適的插入點。

經過閱讀上面LocationManager的源碼能夠發現定位的操做最後是委託給了mService這個成員對象的的requestLocationUpdates方法執行的。這個mService是個不錯的切入點,那麼如今思路就很清晰了,首先實現一個mService的代理類,而後在咱們感興趣的方法(requestLocationUpdates)被調用時,執行本身的一些埋點邏輯(例如打log或者上傳到服務器等)。 首先實現代理類:

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())) {
            //獲取當前函數調用棧
            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;
                }
                //找到LocationManager外層的調用者 
                if (foundLocationManager && !TextUtils.equals(e.getClassName(), "android.location.LocationManager")) {
                    invoker = e.getClassName() + "." + e.getMethodName();
                    //此處可將定位接口的調用者信息根據本身的需求進行記錄,這裏我將調用類、函數名、以及參數打印出來
                    Log.d("LocationTest", "invoker is " + invoker + "(" + args + ")");
                    break;
                }
            }
        }
        return method.invoke(mLocationManager, args);
    }
}
複製代碼

以上這個代理的做用就是取代LocationManagermService成員, 而實際的ILocationManager將被這個代理包裝。這樣我就能對實際ILocationManager的方法進行插樁,好比能夠打log,或將調用信息記錄在本地磁盤等。值得一提的是, 因爲我只關心requestLocationUpdates, 因此對這個方法進行了過濾,固然你也能夠根據須要制定本身的過濾規則。 代理類實現好了以後,接下來咱們就要開始真正的hook操做了,所以咱們實現以下方法:

public static void hookLocationManager(LocationManager locationManager) {
        try {
            Object iLocationManager = null;
            Class<?> locationManagerClazsz = Class.forName("android.location.LocationManager");
            //獲取LocationManager的mService成員
            iLocationManager = getField(locationManagerClazsz, locationManager, "mService");
            Class<?> iLocationManagerClazz = Class.forName("android.location.ILocationManager");

            //建立代理類
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                    new Class<?>[]{iLocationManagerClazz}, new ILocationManagerProxy(iLocationManager));

            //在這裏移花接木,用代理類替換掉原始的ILocationManager
            setField(locationManagerClazsz, locationManager, "mService", proxy);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製代碼

簡單幾行代碼就能夠完成hook操做了,使用方法也很簡單,只須要將LocationManager實例傳進這個方法就能夠了。如今回想一下咱們是怎麼獲取LocationManager實例的:

LocationManager locationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
複製代碼

我們通常固然是想hook應用全局的定位接口調用了,聰明的你也許想到了在Application初始化的時候去執行hook操做。也就是

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

但是這樣真的能保證全局的LocationManager都能被hook到嗎? 實測後你會發現仍是有漏網之魚的,例如若是你經過Activity的context獲取到的LocationManager實例就不會被hook到,由於他跟Application中獲取到的LocationManager徹底不是同一個實例,想知道具體緣由的話可參閱這裏

因此若是要hook到全部的LocationManager實例的話,咱們還得去看看LocationManager究竟是怎麼被建立的。

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

咱們再到SystemServiceRegistry一探究竟

//SystemServiceRegistry.java
final class SystemServiceRegistry {
    private static final String TAG = "SystemServiceRegistry";
    ...
    static {
	...
	//註冊ServiceFetcher, ServiceFetcher就是用於建立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));
            }});
	...
    }
    
    //全部ServiceFetcher與服務名稱的映射
    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);
    }
	
}
複製代碼

到這裏,咱們也就知道真正建立LocationManager實例的地方是在CachedServiceFetcher.createService,那問題就簡單了,我在LocationManager被建立的地方調用hookLocationManager,這下不就沒有漏網之魚了。可是要達到這個目的,咱們得把LocationService對應的CachedServiceFetcher也hook了。大致思路是將SYSTEM_SERVICE_FETCHERSLocationService對應的CachedServiceFetcher替換爲咱們實現的代理類LMCachedServiceFetcherProxy,在代理方法中調用hookLocationManager。代碼以下:

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 {
        //爲何攔截getService,而不是createService?
        if(TextUtils.equals(method.getName(), "getService")){
            Object result = method.invoke(mLMCachedServiceFetcher, args);
            if(result instanceof LocationManager){
                //在這裏hook LocationManager
                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");
        //獲取SystemServiceRegistry的SYSTEM_SERVICE_FETCHERS成員
        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));
            //用代理類替換掉原來的ServiceFetcher
            if(fetchersMap.put(Context.LOCATION_SERVICE, proxy) == locationServiceFetcher){
                Log.d("LocationTest", "hook success! ");
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
複製代碼

也許你發現了,上面咱們明明說的建立LocationManager實例的地方是在CachedServiceFetcher.createService,但是這裏我在getService調用時纔去hook LocationManager,這是由於createService的調用時機太早,甚至比Application的初始化還早,因此咱們只能從getService下手。通過上面的分析咱們知道每次你調用context.getSystemService的時候,CachedServiceFetcher.getService都會調用,可是createService並不會每次都調用,緣由是CachedServiceFetcher內部實現了緩存機制,確保了每一個context只能建立一個LocationManager實例。那這又衍生另外一個問題,即同一個LocationManager可能會被hook屢次。 這個問題也好解決,咱們記錄每一個被hook過的LocationManager實例就好了,HookHelper的最終代碼以下:

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");
            //獲取SystemServiceRegistry的SYSTEM_SERVICE_FETCHERS成員
            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));
                //用代理類替換掉原來的ServiceFetcher
                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");
            //獲取LocationManager的mService成員
            iLocationManager = getField(locationManagerClazsz, locationManager, "mService");
            
            if(hooked.contains(iLocationManager)){
                return;//這個實例已經hook過啦
            }
            
            Class<?> iLocationManagerClazz = Class.forName("android.location.ILocationManager");

            //建立代理類
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                    new Class<?>[]{iLocationManagerClazz}, new ILocationManagerProxy(iLocationManager));

            //在這裏移花接木,用代理類替換掉原始的ILocationManager
            setField(locationManagerClazsz, locationManager, "mService", proxy);
            //記錄已經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);
    }
}
複製代碼

總結

經過反射+動態代理,咱們建立了一個LocationManager的鉤子,而後在定位相關的方法執行時作一些埋點邏輯。筆者的初衷是可以從應用的層面,監測和統計各個模塊對定位的請求狀況,通過實測,以上實現可以完美得達到個人需求。

筆者具體的監測策略以下:

每次requestLocationUpdates被調用時打印出調用方的類名,方法名,以及傳入requestLocationUpdates的參數值(參數中比較重要的信息有這次定位採用的Provider,連續定位的時間間隔、距離)

這裏筆者雖然只是hook了定位服務,但這種思路也許能夠適用於其餘的系統服務,好比AlarmManager等,但實際操做起來確定不太同樣了,具體的細節仍是須要去看源碼了。若是你們有不錯的想法,歡迎交流學習。

注意事項

  • 本文的實現基於Android P源碼, 其餘平臺可能須要作額外的適配(整體思路是同樣的)
  • 既然用了反射, 確定是有必定性能上的損耗了, 因此應用到生產環境上的話得好好斟酌一下。
  • 衆所周知,Android P開始禁用非官方API,受影響的API被分爲淺灰名單(light greylist)、深黑名單(dark greylist)、黑名單 (blacklist)。當使用以上實現hook LocationManager時,會發現系統打印如下log,說明這個接口已經在淺灰名單了,仍是能正常運行,不過將來的Android版本可不敢保證了。
W/idqlocationtes: Accessing hidden field Landroid/location/LocationManager;->mService:Landroid/location/ILocationManager; (light greylist, reflection)
複製代碼

擴展閱讀

相關文章
相關標籤/搜索