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

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

5. 總結

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

筆者具體的監測策略如下:每次 requestLocationUpdates 被調用時打印出調用方的類名,方法名,以及傳入 requestLocationUpdates 的參數值 (參數中比較重要的信息有此次定位采用的 Provider, 連續定位的時間間隔、距離)

這里筆者雖然只是 hook 了定位服務,但這種思路也許可以適用于其他的系統服務,比如 AlarmManager 等,但實際操作起來肯定不太一樣了,具體的細節還是需要去看源碼了。如果大家有不錯的想法,歡迎交流學習。

6. 注意事項

本文的實現基于 Android P 源碼,其他平臺可能需要做額外的適配(總體思路是一樣的)

既然用了反射,肯定是有一定性能上的損耗了,所以應用到生產環境上的話得好好斟酌一下。

眾所周知,Android P 開始禁用非官方 API,受影響的 API 被分為淺灰名單(light greylist)、深黑名單(dark greylist)、黑名單 (blacklist)。當使用以上實現 hook LocationManager 時,會發現系統打印以下 log,說明這個接口已經在淺灰名單了,還是能正常運行,不過未來的 Android 版本可不敢保證了。

原文 

http://developer.51cto.com/art/201911/605967.htm

本站部分文章源于互聯網,本著傳播知識、有益學習和研究的目的進行的轉載,為網友免費提供。如有著作權人或出版方提出異議,本站將立即刪除。如果您對文章轉載有任何疑問請告之我們,以便我們及時糾正。

PS:推薦一個微信公眾號: askHarries 或者qq群:474807195,里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

轉載請注明原文出處:Harries Blog? » APP 定位過于頻繁,我用反射 + 動態代理揪出元兇

贊 (0)
分享到:更多 ()

評論 0

  • 昵稱 (必填)
  • 郵箱 (必填)
  • 網址
2013平特肖公式