背景
定位現在是很多APP最基本也不可或缺的能力之一,尤其是對打車、外賣之類的應用來說。但對定位的呼叫可不能沒有節制,稍有不慎可能導致裝置耗電過快,最終導致使用者解除安裝應用。
筆者所在專案是一個在後臺執行的APP,且需要時不時在後臺獲取一下當前位置,再加上專案裡會引入很多合作第三方的庫,這些庫內部同樣也會有呼叫定位的行為,因此經常會收到測試的反饋說我們的應用由於定位過於頻繁導致耗電過快。
排查這個問題的時候,筆者首先排除了我們業務邏輯的問題,因為專案中的各個功能模組在定位時呼叫的是統一封裝後的定位模組介面,該模組中由對相應的介面做了一些呼叫頻率的統計和監控並列印了相關的log語句, 而問題log中跟定位相關的log語句列印頻率跟次數都是在非常合理的範圍內。
這時我才意識到頻繁定位的罪魁禍首並不在我們內部,而是第三方庫搞的鬼。
那麼問題來了,引入的第三方庫那麼多,我怎麼知道誰的定位呼叫頻率不合理呢?
雖然我在專案中的公共定位模組中打了log,但問題是第三方庫可調不到我們內部的介面。
那麼我們能不能到更底層的地方去埋點統計呢?
AOPAOP,即面向切面程式設計,已經不是什麼新鮮玩意了。
就我個人的理解,AOP就是把我們的程式碼抽象為層次結構,然後通過非侵入式的方法在某兩個層之間插入一些通用的邏輯,常常被用於統計埋點、日誌輸出、許可權攔截等等,詳情可搜尋相關的文章,這裡不具體展開講AOP了。
要從應用的層級來統計某個方法的呼叫,很顯然AOP非常適合。而AOP在Android的典型應用就是AspectJ了,所以我決定用AspectJ試試,不過哪裡才是最合適的插入點呢?我決定去SDK原始碼裡尋找答案。
策略探索首先我們來看看定位介面一般是怎麼呼叫的:
LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);//單次定位locationManager.requestSingleUpdate(provider, new MyLocationLisenter(), getLooper());//連續定位locationManager.requestSingleUpdate(provider,minTime, minDistance, new MyLocationLisenter());
當然不止這兩個介面,還有好幾個過載介面,但是通過檢視LocationManager的原始碼,我們可以發現最後都會調到這個方法:
//LocationManager.javaprivate 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的工作機制,我們就可以知道為什麼這個方法行不通了:
...在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); }}
以上這個代理的作用就是取代LocationManager的mService成員, 而實際的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@Overridepublic Object getSystemService(String name) { return SystemServiceRegistry.getSystemService(this, name);}
我們再到SystemServiceRegistry一探究竟
//SystemServiceRegistry.javafinal 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_FETCHERS中LocationService對應的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.javapublic 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)
最後對於程式設計師來說,要學習的知識內容、技術有太多太多,要想不被環境淘汰就只有不斷提升自己,從來都是我們去適應環境,而不是環境來適應我們!
相信它會給大家帶來很多收穫:
【Android進階學習視訊】、【全套Android面試祕籍PDF】、【Android開發核心知識點筆記】可以 私信我【面試】免費獲取!
當程式設計師容易,當一個優秀的程式設計師是需要不斷學習的,從初級程式設計師到高階程式設計師,從初級架構師到資深架構師,或者走向管理,從技術經理到技術總監,每個階段都需要掌握不同的能力。早早確定自己的職業方向,才能在工作和能力提升中甩開同齡人。