Android怎么解决APP定位过于频繁问题
本篇内容介绍了“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定位过于频繁问题”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341