关于 AOP思想 和 AspectJX框架 大家都耳熟能详,AspectJ为开发者提供了实现AOP的基础能力,可以通过它来实现符合各自业务需求的功能。
这里借助AspectJX框架来实现效能提升相关的一些有意思的功能,AspectJX框架的配置和使用在 README 中有详细步骤,也可以参考 官方demo 。
AspectJ中的语法说明详见: github.com/hiphonezhu/… github.com/HujiangTech…
日常开发中,经常会在某个关键方法中打印Log输出一段字符串和参数变量的值来进行分析调试,或者在方法执行前后打印Log来查看方法执行的耗时。
如果需要在业务主流程中的多个关键方法中增加日志,查看方法执行的输入参数和返回结果是否正确,只能繁琐的在每个方法开头添加Log调用打印输出每个参数。若该方法有返回值,则在return前再添加Log打印输出返回值。若该方法中有多个if分支进行return,还得在每个分支return前打印Log。
统计方法耗时需要在方法开头记录时间,在每个return前计算时间并打印Log。不仅繁琐,还容易遗漏。
可以通过给想要打印日志的方法上标记一个注解,在编译时给标记注解的方法织入代码,自动打印这个方法运行时的输入输出信息和耗时信息。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AutoLog { /** logcat筛选tag */ String tag(); /** 打印日志级别(默认VERBOSE) */ LogLevel level() default LogLevel.VERBOSE; } 复制代码
AutoLog.java
自定义一个注解 AutoLog ,用于给想要打印日志的方法做标记。
@Aspect public class LogAspect { /** * 切入点,添加了AutoLog注解的所有方法体内 */ @Pointcut("execution (@com.cdh.aop.toys.annotation.AutoLog * *(..))") public void logMethodExecute() { } // Advice ··· } 复制代码
创建一个日志切面 LogAspect ,在其中定义一个切入点,对所有添加了AutoLog注解的方法进行代码织入。
@Aspect public class LogAspect { // Pointcut ··· /** * 对上面定义的切入点的方法进行织入,Around的作用是替代原方法体内代码 */ @Around("logMethodExecute()") public Object autoLog(ProceedingJoinPoint joinPoint) { try { // 获取被织入方法的签名信息,MethodSignature包含方法的详细信息 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); // 获取方法上添加的AutoLog注解 AutoLog log = methodSignature.getMethod().getAnnotation(AutoLog.class); if (log != null) { // 用于拼接日志详细信息 StringBuilder sb = new StringBuilder(); // 拼接方法名称 String methodName = methodSignature.getMethod().getName(); sb.append(methodName); // 拼接每个参数的值 Object[] args = joinPoint.getArgs(); if (args != null && args.length > 0) { sb.append("("); for (int i=0; i<args.length; i++) { sb.append(args[i]); if (i != args.length-1) { sb.append(","); } } sb.append(")"); } // 记录开始执行时的时间 long beginTime = System.currentTimeMillis(); // 执行原方法代码,并获得返回值 Object result = joinPoint.proceed(); // 计算方法执行耗时 long costTime = System.currentTimeMillis() - beginTime; if (methodSignature.getReturnType() != void.class) { // 若该方法返回类型不是void,则拼接返回值 sb.append(" => ").append(result); } // 拼接耗时 sb.append(" | ").append("cost=").append(costTime); // 拼接方法所在类名和行号 String className = methodSignature.getDeclaringType().getSimpleName(); int srcLine = joinPoint.getSourceLocation().getLine(); sb.append(" | [").append(className).append(":").append(srcLine).append("]"); // 打印日志,使用AutoLog注解设置的tag和级别调用Log类的对应方法 LogUtils.log(log.level(), log.tag(), sb.toString()); return result; } } catch (Throwable t) { t.printStackTrace(); } return null; } } 复制代码
LogAspect.java
使用 Around 可以替换原方法中的逻辑,也可以通过 ProceedingJoinPoint.proceed 继续执行原方法逻辑。这里在执行原方法逻辑之外,还进行了方法参数信息的拼接和耗时计算,最后打印日志输出。
到这里完成了一个基本的日志切面织入功能,接下来在想要自动打印日志的方法上添加注解即可。
随意写几个方法调用,在这几个方法上添加AutoLog注解。
public class AddOpWithLog extends BaseOp { public AddOpWithLog(BaseOp next) { super(next); } @Override @AutoLog(tag=TAG, level=LogLevel.DEBUG) protected int onOperate(int value) { return value + new Random().nextInt(10); } } 复制代码
AddOpWithLog.java
public class SubOpWithLog extends BaseOp { public SubOpWithLog(BaseOp next) { super(next); } @Override @AutoLog(tag=TAG, level=LogLevel.WARN) protected int onOperate(int value) { return value - new Random().nextInt(10); } } 复制代码
SubOpWithLog.java
public class MulOpWithLog extends BaseOp { public MulOpWithLog(BaseOp next) { super(next); } @Override @AutoLog(tag=TAG, level=LogLevel.WARN) protected int onOperate(int value) { return value * new Random().nextInt(10); } } 复制代码
MulOpWithLog.java
public class DivOpWithLog extends BaseOp { public DivOpWithLog(BaseOp next) { super(next); } @Override @AutoLog(tag=TAG, level=LogLevel.DEBUG) protected int onOperate(int value) { return value / (new Random().nextInt(10)+1); } } 复制代码
DivOpWithLog.java
@AutoLog(tag = BaseOp.TAG, level = LogLevel.DEBUG) public void doWithLog(View view) { BaseOp div = new DivOpWithLog(null); BaseOp mul = new MulOpWithLog(div); BaseOp sub = new SubOpWithLog(mul); BaseOp add = new AddOpWithLog(sub); int result = add.operate(100); Toast.makeText(this, result+"", Toast.LENGTH_SHORT).show(); } 复制代码
MainActivity.java
运行doWithLog方法,查看logcat输出日志:
效果如图所示,打印方法名称以及每个入参的值和直接结果返回值(若是void则不打印返回值),还有该方法的执行耗时(单位ms)。
日常开发中经常会涉及线程切换操作,例如网络请求、文件IO和其他耗时操作需要放在自线程中执行,UI操作需要切回主线程执行。
每次切换线程时需要创建Runnable,在它的run方法中执行业务逻辑,或者利用AsyncTask和Executor(切回主线程还需要利用Handler),需要在方法调用处或方法体内部增加这些代码来切换线程运行。
如果能通过给方法加个标记,就能自动让该方法在主或子线程执行,就可以让方法调用过程变得清晰和极大的减少代码量。
同样可以利用注解给方法标记,在编译器织入线程调用的代码,自动进行线程切换。 注意:这里的实现方案较为鸡肋,仅提供一个思路和演示。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AutoThread { /** * 指定方法运行在主/子线程 * 可选枚举值: MAIN(期望运行在主线程) BACKGROUND(期望运行在子线程) */ ThreadScene scene(); /** * 设置是否阻塞等待该方法执行完成才返回(默认true) */ boolean waitUntilDone() default true; } 复制代码
AutoThread.java 自定义注解 AutoThread ,用于标记想要自动切换线程运行的方法。
@Aspect public class ThreadAspect { @Pointcut("execution (@com.cdh.aop.toys.annotation.AutoThread * *(..))") public void threadSceneTransition() { } // Advice ··· } 复制代码
ThreadAspect.java 这里定义了一个切面 ThreadAspect 和切入点 threadSceneTransition 。
@Aspect public class ThreadAspect { // Pointcut ··· @Around("threadSceneTransition()") public Object executeInThread(final ProceedingJoinPoint joinPoint) { // result用于保存原方法执行结果 final Object[] result = {null}; try { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); // 获取我们添加的方法注解AutoThread AutoThread thread = methodSignature.getMethod().getAnnotation(AutoThread.class); if (thread != null) { // 获取注解中设置的ThreadScene值, ThreadScene threadScene = thread.scene(); if (threadScene == ThreadScene.MAIN && !ThreadUtils.isMainThread()) { // 若期望运行在主线程,但当前不在主线程 // 切换到主线程执行 ThreadUtils.runOnMainThread(new Runnable() { @Override public void run() { try { // 执行原方法,并保存结果 result[0] = joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } } }, thread.waitUntilDone()); } else if (threadScene == ThreadScene.BACKGROUND && ThreadUtils.isMainThread()) { // 若期望运行在子线程,但当前在主线程 // 切换到子线程执行 ThreadUtils.run(new Runnable() { @Override public void run() { try { // 执行原方法,并保存结果 result[0] = joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } } }, thread.waitUntilDone()); } else { // 直接在当前线程运行 result[0] = joinPoint.proceed(); } } } catch (Throwable t) { t.printStackTrace(); } // 返回原方法返回值 return result[0]; } } 复制代码
这里使用Around替换原方法逻辑,在执行原方法之前,先进行线程判断,然后切换到对应线程再执行原方法。
上面看到,当需要切换主线程时,调用ThreadUtils.runOnMainThread来执行原方法,看看这个方法的内部实现:
/** * 主线程执行 * * @param runnable 待执行任务 * @param block 是否等待执行完成 */ public static void runOnMainThread(Runnable runnable, boolean block) { if (isMainThread()) { runnable.run(); return; } // 利用CountDownLatch来阻塞当前线程 CountDownLatch latch = null; if (block) { latch = new CountDownLatch(1); } // 利用Pair保存Runnable和CountDownLatch Pair<Runnable, CountDownLatch> pair = new Pair<>(runnable, latch); // 将Pair参数发送到主线程处理 getMainHandler().obtainMessage(WHAT_RUN_ON_MAIN, pair).sendToTarget(); if (block) { try { // 等待CountDownLatch降为0 latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } } private static class MainHandler extends Handler { MainHandler() { super(Looper.getMainLooper()); } @Override public void handleMessage(Message msg) { if (msg.what == WHAT_RUN_ON_MAIN) { // 取出Pair参数 Pair<Runnable, CountDownLatch> pair = (Pair<Runnable, CountDownLatch>) msg.obj; try { // 取出Runnable参数运行 pair.first.run(); } finally { if (pair.second != null) { // 使CountDownLatch降1,这里会降为0,唤醒前面的阻塞等待 pair.second.countDown(); } } } } } 复制代码
ThreadUtils.java 切换到主线程的方式还是利用主线程Handler。若设置等待结果返回,则会创建CountDownLatch,阻塞当前调用线程,等待主线程中执行完任务后才返回。
接下来看看切换子线程执行的方法ThreadUtils.run:
/** * 子线程执行 * * @param runnable 待执行任务 * @param block 是否等待执行完成 */ public static void run(final Runnable runnable, final boolean block) { Future future = getExecutorService().submit(new Runnable() { @Override public void run() { // 通过线程池运行在子线程 runnable.run(); } }); if (block) { try { // 等待执行结果 future.get(); } catch (Exception e) { e.printStackTrace(); } } } 复制代码
切换到子线程就是通过线程池提交任务执行。
同样写几个方法,然后加上AutoThread注解
public class AddOpInThread extends BaseOp { public AddOpInThread(BaseOp next) { super(next); } @Override @AutoThread(scene = ThreadScene.BACKGROUND) protected int onOperate(int value) { // 打印该方法运行时所在线程 Log.w(BaseOp.TAG, "AddOpInThread onOperate: " + java.lang.Thread.currentThread()); return value + new Random().nextInt(10); } } 复制代码
AddOpInThread.java 方法注解指定运行在子线程。
public class SubOpInThread extends BaseOp { public SubOpInThread(BaseOp next) { super(next); } @Override @AutoThread(scene = ThreadScene.MAIN) protected int onOperate(int value) { // 打印该方法运行时所在线程 Log.w(BaseOp.TAG, "SubOpInThread onOperate: " + java.lang.Thread.currentThread()); return value - new Random().nextInt(10); } } 复制代码
SubOpInThread.java 指定运行在主线程。
public class MulOpInThread extends BaseOp { public MulOpInThread(BaseOp next) { super(next); } @Override @AutoThread(scene = ThreadScene.MAIN) protected int onOperate(int value) { // 打印该方法运行时所在线程 Log.w(BaseOp.TAG, "MulOpInThread onOperate: " + java.lang.Thread.currentThread()); return value * new Random().nextInt(10); } } 复制代码
MulOpInThread.java 指定运行在主线程。
public class DivOpInThread extends BaseOp { public DivOpInThread(BaseOp next) { super(next); } @Override @AutoThread(scene = ThreadScene.BACKGROUND) protected int onOperate(int value) { // 打印该方法运行时所在线程 Log.w(BaseOp.TAG, "DivOpInThread onOperate: " + java.lang.Thread.currentThread()); return value / (new Random().nextInt(10)+1); } } 复制代码
DivOpInThread.java 指定运行在子线程。
接下来调用方法:
public void doWithThread(View view) { BaseOp div = new DivOpInThread(null); BaseOp mul = new MulOpInThread(div); BaseOp sub = new SubOpInThread(mul); BaseOp add = new AddOpInThread(sub); int result = add.operate(100); Toast.makeText(this, result+"", Toast.LENGTH_SHORT).show(); } 复制代码
MainActivity.java
运行doWithThread方法,查看logcat输出日志:
可以看到第一个方法已经切换到子线程中运行,第二、三个方法又运行在主线程中,第四个方法又运行在子线程中。
通常我们在创建使用Thread时,需要给它设置一个名称,便于分析和定位该Thread所属业务模块。
开发过程中出现疏漏或者引入的第三方库中不规范使用线程,例如直接创建线程运行,或者匿名线程等。当想要分析线程时,就会看到很多Thread-1、2、3的线程,如果有一个清晰的名称就容易一眼看出该线程所属的业务。
可以通过拦截所有的Thread.start调用时机,在start之前检测线程名称。若是默认名称,则进行警告,并且自动修改线程名称。
这里把线程相关织入操作都放在一个切面ThreadAspect中:
@Aspect public class ThreadAspect { private static final String TAG = "ThreadAspect"; @Before("call (* java.lang.Thread.start(..))") public void callThreadStart(JoinPoint joinPoint) { try { // 获取joinPoint所在对象,即执行start方法的那个Thread实例 Thread thread = (Thread) joinPoint.getTarget(); // 通过正则检测线程名称 if (ThreadUtils.isDefaultThreadName(thread)) { // 打印警告信息(线程对象和该方法调用的位置) LogUtils.e(TAG, "发现启动线程[" + thread + "]未自定义线程名称! [" + joinPoint.getSourceLocation() + "]"); // 设置线程名称,名称拼接该方法调用处上下文this对象 thread.setName(thread.getName() + "-" + joinPoint.getThis()); } } catch (Throwable t) { t.printStackTrace(); } } } 复制代码
ThreadAspect.java
该切入点会在所有调用thread.start的地方前面织入名称检测和设置名称的代码。
若thread未设置名称,则会使用默认名称,可以看Thread的构造方法。
/* For autonumbering anonymous threads. */ private static int threadInitNumber; private static synchronized int nextThreadNum() { return threadInitNumber++; } public Thread() { // 第三个参数即默认名称 init(null, null, "Thread-" + nextThreadNum(), 0); } 复制代码
看ThreadUtils.isDefaultThreadName方法:
public static boolean isDefaultThreadName(Thread thread) { String name = thread.getName(); String pattern = "^Thread-[1-9]//d*$"; return Pattern.matches(pattern, name); } 复制代码
通过正则表达式来判断,若完全匹配则表示当前是默认名称。
创建几个Thread,分别设置名称和不设置名称,然后启动运行。
public void renameThreadName(View view) { // 未设置名称 new Thread(new PrintNameRunnable()).start(); // 设置名称 Thread t = new Thread(new PrintNameRunnable()); t.setName("myname-thread-test"); t.start(); } private static class PrintNameRunnable implements Runnable { @Override public void run() { // 打印线程名称 Log.d(TAG, "thread name: " + Thread.currentThread().getName()); } } 复制代码
运行后查看logcat输出日志:
可以看到检测到一个线程启动时未设置自定义名称,并且打印出该方法调用位置。
当线程启动后,在Runnable中打印当前线程名称,可以看到线程名称已经被设置,并且可以知道thread启动所在上下文。
工信部发文要求APP在用户未同意隐私协议之前,不得收集用户、设备相关信息,例如imei、device id、设备已安装应用列表、通讯录等能够唯一标识用户和用户设备隐私相关的信息。
注意,这里的用户同意隐私协议不同于APP权限申请,是属于业务层面上的隐私协议。若用户未同意隐私协议,即使在系统应用设置中打开该APP的所有权限,业务代码中也不能获取相关信息。
如图,必须用户同意后,业务代码中才能获取需要的信息。
要对代码中所有涉及隐私信息获取的地方做检查,容易疏漏。万一出现遗漏,将面临工信部的下架整改处罚。而且部分三方SDK中没有严格按照工信部要求,会私自进行用户、设备相关信息的获取。
可以在所有调用隐私信息API的地方前面织入检查代码,一举涵盖自身业务代码和三方SDK代码进行拦截。
@Aspect public class PrivacyAspect { // 拦截获取手机安装应用列表信息的调用 private static final String POINT_CUT_GET_INSTALLED_APPLICATION = "call (* android.content.pm.PackageManager.getInstalledApplications(..))"; private static final String POINT_CUT_GET_INSTALLED_PACKAGES = "call (* android.content.pm.PackageManager.getInstalledPackages(..))"; // 拦截获取imei、device id的调用 private static final String POINT_CUT_GET_IMEI = "call (* android.telephony.TelephonyManager.getImei(..))"; private static final String POINT_CUT_GET_DEVICE_ID = "call(* android.telephony.TelephonyManager.getDeviceId(..))"; // 拦截getLine1Number方法的调用 private static final String POINT_CUT_GET_LINE_NUMBER = "call (* android.telephony.TelephonyManager.getLine1Number(..))"; // 拦截定位的调用 private static final String POINT_CUT_GET_LAST_KNOWN_LOCATION = "call (* android.location.LocationManager.getLastKnownLocation(..))"; private static final String POINT_CUT_REQUEST_LOCATION_UPDATES = "call (* android.location.LocationManager.requestLocationUpdates(..))"; private static final String POINT_CUT_REQUEST_LOCATION_SINGLE = "call (* android.location.LocationManager.requestSingleUpdate(..))"; // ··· @Around(POINT_CUT_GET_INSTALLED_APPLICATION) public Object callGetInstalledApplications(ProceedingJoinPoint joinPoint) { return handleProceedingJoinPoint(joinPoint, new ArrayList<ApplicationInfo>()); } @Around(POINT_CUT_GET_INSTALLED_PACKAGES) public Object callGetInstalledPackages(ProceedingJoinPoint joinPoint) { return handleProceedingJoinPoint(joinPoint, new ArrayList<PackageInfo>()); } @Around(POINT_CUT_GET_IMEI) public Object callGetImei(ProceedingJoinPoint joinPoint) { return handleProceedingJoinPoint(joinPoint, ""); } @Around(POINT_CUT_GET_DEVICE_ID) public Object callGetDeviceId(ProceedingJoinPoint joinPoint) { return handleProceedingJoinPoint(joinPoint, ""); } @Around(POINT_CUT_GET_LINE_NUMBER) public Object callGetLine1Number(ProceedingJoinPoint joinPoint) { return handleProceedingJoinPoint(joinPoint, ""); } @Around(POINT_CUT_GET_LAST_KNOWN_LOCATION) public Object callGetLastKnownLocation(ProceedingJoinPoint joinPoint) { return handleProceedingJoinPoint(joinPoint, null); } @Around(POINT_CUT_REQUEST_LOCATION_UPDATES) public void callRequestLocationUpdates(ProceedingJoinPoint joinPoint) { handleProceedingJoinPoint(joinPoint, null); } @Around(POINT_CUT_REQUEST_LOCATION_SINGLE) public void callRequestSingleUpdate(ProceedingJoinPoint joinPoint) { handleProceedingJoinPoint(joinPoint, null); } // ··· } 复制代码
PrivacyAspect.java
定义一个切面 PrivacyAspect ,和需要检查调用的方法的切入点。其中使用Around替换对敏感API的调用的代码,调用handleProceedingJoinPoint处理,第一个参数是连接点ProceedingJoinPoint,第二个参数是默认返回值(若原方法有返回值,则会返回结果)。
接着进入handleProceedingJoinPoint方法:
private Object handleProceedingJoinPoint(ProceedingJoinPoint joinPoint, Object fakeResult) { if (!PrivacyController.isUserAllowed()) { // 若用户未同意 StringBuilder sb = new StringBuilder(); // 打印调用的方法和该调用所在位置 sb.append("用户未同意时执行了").append(joinPoint.getSignature().toShortString()) .append(" [").append(joinPoint.getSourceLocation()).append("]"); LogUtils.e(TAG, sb.toString()); // 返回一个空的默认值 return fakeResult; } try { // 执行原方法,返回原结果 return joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } return fakeResult; } 复制代码
该方法中判断用户是否同意。若未同意,则返回空的返回值。否则放行,调用原方法。
部分三方SDK中会通过反射调用敏感API,并且对方法名称字符串做加密处理,以绕过静态检查,因此也需要对反射调用进行拦截。
@Aspect public class PrivacyAspect { // 拦截反射的调用 private static final String POINT_CUT_METHOD_INVOKE = "call (* java.lang.reflect.Method.invoke(..))"; // 反射方法黑名单 private static final List<String> REFLECT_METHOD_BLACKLIST = Arrays.asList( "getInstalledApplications", "getInstalledPackages", "getImei", "getDeviceId", "getLine1Number", "getLastKnownLocation", "loadClass" ); @Around(POINT_CUT_METHOD_INVOKE) public Object callReflectInvoke(ProceedingJoinPoint joinPoint) { // 获取该连接点调用的方法名称 String methodName = ((Method) joinPoint.getTarget()).getName(); if (REFLECT_METHOD_BLACKLIST.contains(methodName)) { // 若是黑名单中的方法,则进行检查 return handleProceedingJoinPoint(joinPoint, null); } try { // 执行原方法,返回原结果 return joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } return null; } } 复制代码
通过拦截Method.invoke的调用,判断反射调用的方法是不是黑名单中的方法。
@Aspect public class PrivacyAspect { // 拦截加载类的调用 private static final String POINT_CUT_DEX_FIND_CLASS = "call (* java.lang.ClassLoader.loadClass(..))"; @Around(POINT_CUT_DEX_FIND_CLASS) public Object callLoadClass(ProceedingJoinPoint joinPoint) { Object result = null; try { result = joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } // 打印该连接点的相关信息 StringBuilder sb = new StringBuilder(); sb.append(joinPoint.getThis()).append("中动态加载"); Object[] args = joinPoint.getArgs(); if (args != null && args.length > 0) { sb.append("/"").append(args[0]).append("/""); } sb.append("得到").append(result); sb.append(" ").append(joinPoint.getSourceLocation()); LogUtils.w(TAG, sb.toString()); return result; } } 复制代码
拦截到loadClass后,打印日志输出调用处的位置。
public void interceptPrivacy(View view) { Log.d(TAG, "用户同意: " + PrivacyController.isUserAllowed()); // 获取手机安装应用信息 List<ApplicationInfo> applicationInfos = DeviceUtils.getInstalledApplications(this); if (applicationInfos != null && applicationInfos.size() > 5) { applicationInfos = applicationInfos.subList(0, 5); } Log.d(TAG, "getInstalledApplications: " + applicationInfos); // 获取手机安装应用信息 List<PackageInfo> packageInfos = DeviceUtils.getInstalledPackages(this); if (packageInfos != null && packageInfos.size() > 5) { packageInfos = packageInfos.subList(0, 5); } Log.d(TAG, "getInstalledPackages: " + packageInfos); // 获取imei Log.d(TAG, "getImei: " + DeviceUtils.getImeiValue(this)); // 获取电话号码 Log.d(TAG, "getLine1Number: " + DeviceUtils.getLine1Number(this)); // 获取定位信息 Log.d(TAG, "getLastKnownLocation: " + DeviceUtils.getLastKnownLocation(this)); try { // 加载一个类 Log.d(TAG, "loadClass: " + getClassLoader().loadClass("com.cdh.aop.sample.op.BaseOp")); } catch (ClassNotFoundException e) { e.printStackTrace(); } try { // 通过反射获取手机安装应用信息 PackageManager pm = getPackageManager(); Method method = PackageManager.class.getDeclaredMethod("getInstalledApplications", int.class); List<ApplicationInfo> list = (List<ApplicationInfo>) method.invoke(pm, 0); if (list != null && list.size() > 5) { list = list.subList(0, 5); } Log.d(TAG, "reflect getInstalledApplications: " + list); } catch (Exception e) { e.printStackTrace(); } } 复制代码
运行后查看logcat输出日志:
打印了敏感API调用的警告信息和调用处所在位置。
调用方最终获取到的都是空值。
在集成 AspectJX框架 打包apk后可能会遇到ClassNotFoundException,反编译apk发现很多类没有打进去,甚至包括Application。绝大部分原因是因为依赖的三方库中使用了AspectJ框架导致的冲突,或者是自己写的切入点的语法有错误,或织入代码有问题,例如方法返回值没有对应上,或者对同一个切入点定义了有冲突的通知。若发生错误,会在build中显示错误信息。
如果不用AOP思想和AspectJ框架实现上面的需求,会有很多繁琐的工作量。这里通过几个简单场景的应用,可以发现若能深入理解AOP思想和掌握AspectJ使用,会对架构设计和开发效率有很大的提升和帮助。