您好,登錄后才能下訂單哦!
本篇內容介紹了“Android怎么解決APP定位過于頻繁問題”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
1. 背景
定位現在是很多 APP 最基本也不可或缺的能力之一,尤其是對打車、外賣之類的應用來說。但對定位的調用可不能沒有節制,稍有不慎可能導致設備耗電過快,最終導致用戶卸載應用。
筆者所在項目是一個在后臺運行的 APP,且需要時不時在后臺獲取一下當前位置,再加上項目里會引入很多合作第三方的庫,這些庫內部同樣也會有調用定位的行為,因此經常會收到測試的反饋說我們的應用由于定位過于頻繁導致耗電過快。
排查這個問題的時候,筆者首先排除了我們業務邏輯的問題,因為項目中的各個功能模塊在定位時調用的是統一封裝后的定位模塊接口,該模塊中由對相應的接口做了一些調用頻率的統計和監控并打印了相關的 log 語句,而問題 log 中跟定位相關的 log 語句打印頻率跟次數都是在非常合理的范圍內。
這時我才意識到頻繁定位的罪魁禍首并不在我們內部,而是第三方庫搞的鬼。那么問題來了,引入的第三方庫那么多,我怎么知道誰的定位調用頻率不合理呢?雖然我在項目中的公共定位模塊中打了 log,但問題是第三方庫可調不到我們內部的接口。那么我們能不能到更底層的地方去埋點統計呢?
2. AOP
AOP,即面向切面編程,已經不是什么新鮮玩意了。就我個人的理解,AOP 就是把我們的代碼抽象為層次結構,然后通過非侵入式的方法在某兩個層之間插入一些通用的邏輯,常常被用于統計埋點、日志輸出、權限攔截等等,詳情可搜索相關的文章,這里不具體展開講 AOP 了。
要從應用的層級來統計某個方法的調用,很顯然 AOP 非常適合。而 AOP 在 Android 的典型應用就是 AspectJ 了,所以我決定用 AspectJ 試試,不過哪里才是最合適的插入點呢?我決定去 SDK 源碼里尋找答案。
3. 策略探
首先我們來看看定位接口一般是怎么調用的:
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 的方案無法滿足需求。
4. 另辟蹊徑
軟的不行只能來硬的了,我決定祭出反射+動態代理殺招,不過還前提還是要找到一個合適的插入點。
通過閱讀上面 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); } }
“Android怎么解決APP定位過于頻繁問題”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。