作者:Android-until
背景
定位现在是很多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