背景
定位現在是很多APP最基本也不可或缺的能力之一,尤其是對打車、外賣之類的應用來說。但對定位的調用可不能沒有節制,稍有不慎可能導致設備耗電過快,最終導致用戶卸載應用。
筆者所在項目是一個在後臺運行的APP,且需要時不時在後臺獲取一下當前位置,再加上項目裏會引入很多合作第三方的庫,這些庫內部同樣也會有調用定位的行爲,因此經常會收到測試的反饋說我們的應用由於定位過於頻繁導致耗電過快。
排查這個問題的時候,筆者首先排除了我們業務邏輯的問題,因爲項目中的各個功能模塊在定位時調用的是統一封裝後的定位模塊接口,該模塊中由對相應的接口做了一些調用頻率的統計和監控並打印了相關的log語句, 而問題log中跟定位相關的log語句打印頻率跟次數都是在非常合理的範圍內。
這時我才意識到頻繁定位的罪魁禍首並不在我們內部,而是第三方庫搞的鬼。
那麼問題來了,引入的第三方庫那麼多,我怎麼知道誰的定位調用頻率不合理呢?
雖然我在項目中的公共定位模塊中打了log,但問題是第三方庫可調不到我們內部的接口。
那麼我們能不能到更底層的地方去埋點統計呢?
AOP
AOP,即面向切面編程,已經不是什麼新鮮玩意了。
就我個人的理解,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.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的工作機制,我們就可以知道爲什麼這個方法行不通了:
...在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
@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_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.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)
最後對於程序員來說,要學習的知識內容、技術有太多太多,要想不被環境淘汰就只有不斷提升自己,從來都是我們去適應環境,而不是環境來適應我們!
這裏附上上述的技術體系圖相關的幾十套騰訊、頭條、阿里、美團等公司19年的面試題,把技術點整理成了視頻和PDF(實際上比預期多花了不少精力),包含知識脈絡 + 諸多細節,由於篇幅有限,這裏以圖片的形式給大家展示一部分。
相信它會給大家帶來很多收穫:
**上述【高清技術腦圖】以及【配套的架構技術PDF】
背景
定位現在是很多APP最基本也不可或缺的能力之一,尤其是對打車、外賣之類的應用來說。但對定位的調用可不能沒有節制,稍有不慎可能導致設備耗電過快,最終導致用戶卸載應用。
筆者所在項目是一個在後臺運行的APP,且需要時不時在後臺獲取一下當前位置,再加上項目裏會引入很多合作第三方的庫,這些庫內部同樣也會有調用定位的行爲,因此經常會收到測試的反饋說我們的應用由於定位過於頻繁導致耗電過快。
排查這個問題的時候,筆者首先排除了我們業務邏輯的問題,因爲項目中的各個功能模塊在定位時調用的是統一封裝後的定位模塊接口,該模塊中由對相應的接口做了一些調用頻率的統計和監控並打印了相關的log語句, 而問題log中跟定位相關的log語句打印頻率跟次數都是在非常合理的範圍內。
這時我才意識到頻繁定位的罪魁禍首並不在我們內部,而是第三方庫搞的鬼。
那麼問題來了,引入的第三方庫那麼多,我怎麼知道誰的定位調用頻率不合理呢?
雖然我在項目中的公共定位模塊中打了log,但問題是第三方庫可調不到我們內部的接口。
那麼我們能不能到更底層的地方去埋點統計呢?
AOP
AOP,即面向切面編程,已經不是什麼新鮮玩意了。
就我個人的理解,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.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的工作機制,我們就可以知道爲什麼這個方法行不通了:
...在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
@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_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.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)
最後對於程序員來說,要學習的知識內容、技術有太多太多,要想不被環境淘汰就只有不斷提升自己,從來都是我們去適應環境,而不是環境來適應我們!
這裏附上上述的技術體系圖相關的幾十套騰訊、頭條、阿里、美團等公司19年的面試題,把技術點整理成了視頻和PDF(實際上比預期多花了不少精力),包含知識脈絡 + 諸多細節,由於篇幅有限,這裏以圖片的形式給大家展示一部分。
相信它會給大家帶來很多收穫:
上述【高清技術腦圖】以及【配套的架構技術PDF】可以 加我wx:X1524478394 免費獲取!
當程序員容易,當一個優秀的程序員是需要不斷學習的,從初級程序員到高級程序員,從初級架構師到資深架構師,或者走向管理,從技術經理到技術總監,每個階段都需要掌握不同的能力。早早確定自己的職業方向,才能在工作和能力提升中甩開同齡人。
**