通过aspectj对Android数据统计的简单实现

295 阅读4分钟

功能需求

一个项目实现之后,我们并不知道用户对某个部分的使用频率是对少,为了更好的来对项目各个功能的使用统计,我们需要做一些数据埋点的功能,也就是每当用户点击按钮的时候,都对这次点击进行保存处理,然后再之后统一上传到服务器,进行数据分析。

实现思路

条件

假如,当前有两个方法进行数据埋点:登录和注册。 功能表的数据结构如下:功能id、操作次数、操作人id

public class FunctionsTable {

	//功能Id
	private long functionId;
	//操作次数
	private int operateCounts;
	//操作用户Id
	private long operatorId;

	public long getFunctionId() {
		return functionId;
	}

	public void setFunctionId(long functionId) {
		this.functionId = functionId;
	}

	public int getOperateCounts() {
		return operateCounts;
	}

	public void setOperateCounts(int operateCounts) {
		this.operateCounts = operateCounts;
	}

	public long getOperatorId() {
		return operatorId;
	}

	public void setOperatorId(long operatorId) {
		this.operatorId = operatorId;
	}
}

下载我们需要做的就是,每次用户点登陆和注册的时候,把数据库中的operateCount字段每次+1。

分析

首先,最直接的实现方法也就是封装一个保存数据的数据库静态操作方法,然后将每次需要操作的地方传入functionId,来进行操作。
这种方法看似可行,但是实际操作的时候会发现很可能需要修改原来的代码结构,同时,一旦function的数量到达一定量以后,这时候产品告诉我们,需求要改,还需要加入额外的操作,这时候一旦运气不好,比如增加一个方法参数,所有调用方法地方都得修改一遍。这时候就比较麻烦了。
有没有什么更好的方法?肯定是有的。首先分析上述方案,它是一个单一重复的调用过程,唯一的区别就是传入的functionId,这是一个单一却又重复的操作,那很明显,aop思想来解决这种问题是最好的。

具体实现

首先我们选用aop一个常用的类库:aspectj。因此这里我们通过字节码插桩的方式,修改编译之后的class来进行代码的自动生成,这样就不会对我们敲代码的逻辑产生任何影响。

代码原逻辑

非常简单,就是两个点击事件,模拟一下登陆注册的点击。

public class MainActivity extends AppCompatActivity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		Button btnLogin = findViewById(R.id.btn_login);
		Button btnRegister = findViewById(R.id.btn_register);

		btnLogin.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				Log.e("Statistics", "登陆");
			}
		});

		btnRegister.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				Log.e("Statistics", "注册");
			}
		});
	}
}

aspectJ字节码插桩的实现

1.新建一个枚举类来定义需要埋点的功能:登陆、注册

public enum Function {

	LOGIN(1, "登陆"),
	REGISTER(2, "注册");

	int functionId;
	String functionName;

	Function(int functionId, String functionName) {
		this.functionId = functionId;
		this.functionName = functionName;
	}

	public String getFunctionName() {
		return functionName;
	}
}

2.新建一个注解类来标记需要统计点击次数的方法:
使用的时候只需要将需要统计的方法加上注解即可

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Statistics {
    Function function();
}

3.Aspectj的简单实现:

    @Aspect
    public class StatisticsInstrumentation {

	    public static final String TAG = "Statistics";

    	@Around("execution(@com.noob.databurialpoint.Statistics * *(..)) && @annotation(statistics)")
    	public void aroundJoinPoint(ProceedingJoinPoint joinPoint, Statistics statistics) throws Throwable {
    		calculate(statistics);
    		joinPoint.proceed();//执行原方法
    	}

        private void calculate(Statistics statistics){
        	if(statistics != null){
        		Log.e(TAG, "对" + statistics.function().getFunctionName() + "进行统计");
        		// select * from FunctionsTable where operatorId=statistics.getFunctionId()
        		//if(size > 0){
        		// int counts = operateCounts ++
        		// update FunctionsTable set operateCounts = counts
        		// }else {
        		// insert into FunctionsTable values (xxx, statistics.getFunctionId(), 1)
        		// }
        	}
        }
}

代码解释:

@Around("execution(@com.noob.databurialpoint.Statistics * *(..)) && @annotation(statistics)")
  • com.noob.databurialpoint.Statistics是Statistics注解的具体包名
  • @annotation(statistics) 代表执行方法中传入注解参数,才能再aroundJoinPoint方法里获取这个注解对象
  • calculate是点击统计的伪代码

注解使用

 public class MainActivity extends AppCompatActivity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		Button btnLogin = findViewById(R.id.btn_login);
		Button btnRegister = findViewById(R.id.btn_register);

		btnLogin.setOnClickListener(new View.OnClickListener() {
			@Override
			@Statistics(function = Function.LOGIN)
			public void onClick(View v) {
				Log.e("Statistics", "登陆");
			}
		});

		btnRegister.setOnClickListener(new View.OnClickListener() {
			@Override
			@Statistics(function = Function.REGISTER)
			public void onClick(View v) {
				Log.e("Statistics", "注册");
			}
		});
	}
}

测试结果:

相信大家也都看到了,我们根本没有对之前的方法进行修改,唯一的区别就是在调用方法上添加一了一个 @Statistics注解,如果我们需要修改逻辑,也只需要修改一次aspectJ的实现类StatisticsInstrumentation即可,这样就开发的时候就非常方便。
究其原因是因为aspectj修改了MainActivity.class类,修改后编译生成的class代码如下:

不再只是一个简单的log打印,而是回去调用我们额外写的StatisticsInstrumentation中的方法.这就是aspectj的作用。

总结

aspectj是一个很好的aop框架,此处只是aspectj的一个简单使用示例,关于更深入的用法这里就不再介绍,大家可以去网上寻找相关代码。

上述demo地址:github.com/JavaNoober/…