转载

AOP应用场景实战-基于AspectJX开发效能提升工具

关于 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注解的方法进行代码织入。

切入点中的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;
    }
}
复制代码

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输出日志:

AOP应用场景实战-基于AspectJX开发效能提升工具

效果如图所示,打印方法名称以及每个入参的值和直接结果返回值(若是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();
}
复制代码

MainActivity.java

运行doWithThread方法,查看logcat输出日志:

AOP应用场景实战-基于AspectJX开发效能提升工具

可以看到第一个方法已经切换到子线程中运行,第二、三个方法又运行在主线程中,第四个方法又运行在子线程中。

线程名称检测

通常我们在创建使用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

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输出日志:

AOP应用场景实战-基于AspectJX开发效能提升工具

可以看到检测到一个线程启动时未设置自定义名称,并且打印出该方法调用位置。

AOP应用场景实战-基于AspectJX开发效能提升工具

当线程启动后,在Runnable中打印当前线程名称,可以看到线程名称已经被设置,并且可以知道thread启动所在上下文。

工信部检查

工信部发文要求APP在用户未同意隐私协议之前,不得收集用户、设备相关信息,例如imei、device id、设备已安装应用列表、通讯录等能够唯一标识用户和用户设备隐私相关的信息。

注意,这里的用户同意隐私协议不同于APP权限申请,是属于业务层面上的隐私协议。若用户未同意隐私协议,即使在系统应用设置中打开该APP的所有权限,业务代码中也不能获取相关信息。

AOP应用场景实战-基于AspectJX开发效能提升工具

如图,必须用户同意后,业务代码中才能获取需要的信息。

痛点

要对代码中所有涉及隐私信息获取的地方做检查,容易疏漏。万一出现遗漏,将面临工信部的下架整改处罚。而且部分三方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.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;
}
复制代码

该方法中判断用户是否同意。若未同意,则返回空的返回值。否则放行,调用原方法。

拦截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输出日志:

AOP应用场景实战-基于AspectJX开发效能提升工具

打印了敏感API调用的警告信息和调用处所在位置。

AOP应用场景实战-基于AspectJX开发效能提升工具

调用方最终获取到的都是空值。

尾声

在集成 AspectJX框架 打包apk后可能会遇到ClassNotFoundException,反编译apk发现很多类没有打进去,甚至包括Application。绝大部分原因是因为依赖的三方库中使用了AspectJ框架导致的冲突,或者是自己写的切入点的语法有错误,或织入代码有问题,例如方法返回值没有对应上,或者对同一个切入点定义了有冲突的通知。若发生错误,会在build中显示错误信息。

如果不用AOP思想和AspectJ框架实现上面的需求,会有很多繁琐的工作量。这里通过几个简单场景的应用,可以发现若能深入理解AOP思想和掌握AspectJ使用,会对架构设计和开发效率有很大的提升和帮助。

文中示例完整源码见 Efficiency-Toys

原文  https://juejin.im/post/5eedb170f265da02f31df329
正文到此结束
Loading...