前言
关于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,用于给想要打印日志的方法做标记。
定义切面和切入点
@Aspect
public class LogAspect {
/**
* 切入点,添加了AutoLog注解的所有方法体内
*/
@Pointcut("execution (@com.cdh.aop.toys.annotation.AutoLog * *(..))")
public void logMethodExecute() {
}
// Advice ···
}
创建一个日志切面LogAspect,在其中定义一个切入点,对所有添加了AutoLog注解的方法进行代码织入。
切入点中的execution表示在该方法体内进行代码织入,@com.cdh.aop.toys.annotation.AutoLog表示添加了该注解的方法,第一个星表示不限return type,第二个星表示匹配任意方法名称,(..)表示不限方法入参。
@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;
}
}
使用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);
}
}
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);
}
}
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);
}
}
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);
}
}
@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();
}
运行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。
切入点中的execution表示在该方法体内进行代码织入,@com.cdh.aop.toys.annotation.AutoThread表示添加了该注解的方法,第一个星表示不限return type,第二个星表示匹配任意方法名称,(..)表示不限方法入参。
@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();
}
运行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();
}
}
}
Before表示在切入点前织入,call表示在该方法的调用处,第一个星表示不限return type,java.lang.Thread.start表示完全匹配Thread类的start方法,(..)表示不限方法参数。
该切入点会在所有调用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);
}
Thread在创建时会设置一个默认名称,Thread-数字递增,所以可以通过匹配这个名称来判断Thread是否设置了自定义名称。
看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输出日志:


工信部检查
工信部发文要求APP在用户未同意隐私协议之前,不得收集用户、设备相关信息,例如imei、device id、设备已安装应用列表、通讯录等能够唯一标识用户和用户设备隐私相关的信息。
注意,这里的用户同意隐私协议不同于APP权限申请,是属于业务层面上的隐私协议。若用户未同意隐私协议,即使在系统应用设置中打开该APP的所有权限,业务代码中也不能获取相关信息。

如图,必须用户同意后,业务代码中才能获取需要的信息。
痛点
要对代码中所有涉及隐私信息获取的地方做检查,容易疏漏。万一出现遗漏,将面临工信部的下架整改处罚。而且部分三方SDK中没有严格按照工信部要求,会私自进行用户、设备相关信息的获取。
解决
可以在所有调用隐私信息API的地方前面织入检查代码,一举涵盖自身业务代码和三方SDK代码进行拦截。
注意,通过动态加载的代码中的调用行为和native层中的行为无法彻底拦截。
拦截API直接调用
@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,和需要检查调用的方法的切入点。其中使用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;
}
该方法中判断用户是否同意。若未同意,则返回空的返回值。否则放行,调用原方法。
拦截API反射调用
部分三方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输出日志:


尾声
在集成AspectJX框架打包apk后可能会遇到ClassNotFoundException,反编译apk发现很多类没有打进去,甚至包括Application。绝大部分原因是因为依赖的三方库中使用了AspectJ框架导致的冲突,或者是自己写的切入点的语法有错误,或织入代码有问题,例如方法返回值没有对应上,或者对同一个切入点定义了有冲突的通知。若发生错误,会在build中显示错误信息。
如果不用AOP思想和AspectJ框架实现上面的需求,会有很多繁琐的工作量。这里通过几个简单场景的应用,可以发现若能深入理解AOP思想和掌握AspectJ使用,会对架构设计和开发效率有很大的提升和帮助。
文中示例完整源码见Efficiency-Toys