定位如今是不少APP最基本也不可或缺的能力之一,尤为是对打车、外卖之类的应用来讲。但对定位的调用可不能没有节制,稍有不慎可能致使设备耗电过快,最终致使用户卸载应用。java
笔者所在项目是一个在后台运行的APP,且须要时不时在后台获取一下当前位置,再加上项目里会引入不少合做第三方的库,这些库内部一样也会有调用定位的行为,所以常常会收到测试的反馈说咱们的应用因为定位过于频繁致使耗电过快。android
排查这个问题的时候,笔者首先排除了咱们业务逻辑的问题,由于项目中的各个功能模块在定位时调用的是统一封装后的定位模块接口,该模块中由对相应的接口作了一些调用频率的统计和监控并打印了相关的log语句, 而问题log中跟定位相关的log语句打印频率跟次数都是在很是合理的范围内。编程
这时我才意识到频繁定位的罪魁祸首并不在咱们内部,而是第三方库搞的鬼。 那么问题来了,引入的第三方库那么多,我怎么知道谁的定位调用频率不合理呢?虽然我在项目中的公共定位模块中打了log,但问题是第三方库可调不到咱们内部的接口。那么咱们能不能到更底层的地方去埋点统计呢?缓存
AOP,即面向切面编程,已经不是什么新鲜玩意了。就我我的的理解,AOP就是把咱们的代码抽象为层次结构,而后经过非侵入式
的方法在某两个层之间插入一些通用的逻辑,经常被用于统计埋点、日志输出、权限拦截等等,详情可搜索相关的文章,这里不具体展开讲AOP了。bash
要从应用的层级来统计某个方法的调用,很显然AOP很是适合。而AOP在Android的典型应用就是AspectJ
了,因此我决定用AspectJ
试试,不过哪里才是最合适的插入点呢?我决定去SDK源码里寻找答案。服务器
首先咱们来看看定位接口通常是怎么调用的:app
LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
//单次定位
locationManager.requestSingleUpdate(provider, new MyLocationLisenter(), getLooper());
//连续定位
locationManager.requestSingleUpdate(provider,minTime, minDistance, new MyLocationLisenter());
复制代码
固然不止这两个接口,还有好几个重载接口,可是经过查看LocationManager的源码,咱们能够发现最后都会调到这个方法:ide
//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的工做机制,咱们就能够知道为何这个方法行不通了:oop
...在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等,但实际操做起来确定不太同样了,具体的细节仍是须要去看源码了。若是你们有不错的想法,欢迎交流学习。
W/idqlocationtes: Accessing hidden field Landroid/location/LocationManager;->mService:Landroid/location/ILocationManager; (light greylist, reflection)
复制代码