归纳AOP在Android开发中的几种常见用法

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 归纳AOP在Android开发中的几种常见用法

AOP 是什么



在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。


它是一种关注点分离的技术。我们软件开发时经常提一个词叫做“业务逻辑”或者“业务功能”,我们的代码主要就是实现某种特定的业务逻辑。但是我们往往不能专注于业务逻辑,比如我们写业务逻辑代码的同时,还要写事务管理、缓存、日志等等通用化的功能,而且每个业务功能都要和这些业务功能混在一起,非常非常地痛苦。为了将业务功能的关注点和通用化功能的关注点分离开来,就出现了AOP技术。


AOP 和 OOP



面向对象的特点是继承、多态和封装。为了符合单一职责的原则,OOP将功能分散到不同的对象中去。让不同的类设计不同的方法,这样代码就分散到一个个的类中。可以降低代码的复杂程度,提高类的复用性。


但是在分散代码的同时,也增加了代码的重复性。比如说,我们在两个类中,可能都需要在每个方法中做日志。按照OOP的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但是因为OOP的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。然而AOP就是为了解决这类问题而产生的,它是在运行时动态地将代码切入到类的指定方法、指定位置上的编程思想。


如果说,面向过程的编程是一维的,那么面向对象的编程就是二维的。OOP从横向上区分出一个个的类,相比过程式增加了一个维度。而面向切面结合面向对象编程是三维的,相比单单的面向对象编程则又增加了“方面”的维度。从技术上来说,AOP基本上是通过代理机制实现的。


image.png


AOPConcept.JPG


AOP 在 Android 开发中的常见用法



我封装的 library 已经把常用的 Android AOP 用法概况在其中


github地址:https://github.com/fengzhizi715/SAF-AOP


0. 下载和安装


在根目录下的build.gradle中添加

buildscript {
     repositories {
         jcenter()
     }
     dependencies {
         classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'
     }
 }


在app 模块目录下的build.gradle中添加

apply plugin: 'com.hujiang.android-aspectjx'
...
dependencies {
    compile 'com.safframework:saf-aop:1.0.0'
    ...
}


1. 异步执行app中的方法


告别Thread、Handler、BroadCoast等方式更简单的执行异步方法。只需在目标方法上标注@Async

import android.app.Activity;
import android.os.Bundle;
import android.os.Looper;
import android.widget.Toast;
import com.safframework.app.annotation.Async;
import com.safframework.log.L;
/**
 * Created by Tony Shen on 2017/2/7.
 */
public class DemoForAsyncActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initData();
    }
    @Async
    private void initData() {
        StringBuilder sb = new StringBuilder();
        sb.append("current thread=").append(Thread.currentThread().getId())
                .append("\r\n")
                .append("ui thread=")
                .append(Looper.getMainLooper().getThread().getId());
        Toast.makeText(DemoForAsyncActivity.this, sb.toString(), Toast.LENGTH_SHORT).show();
        L.i(sb.toString());
    }
}


可以清晰地看到当前的线程和UI线程是不一样的。


image.png


@Async执行结果.png


@Async 的原理如下, 借助 Rxjava 实现异步方法。

import android.os.Looper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import rx.Observable;
import rx.Subscriber;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
/**
 * Created by Tony Shen on 16/3/23.
 */
@Aspect
public class AsyncAspect {
    @Around("execution(!synthetic * *(..)) && onAsyncMethod()")
    public void doAsyncMethod(final ProceedingJoinPoint joinPoint) throws Throwable {
        asyncMethod(joinPoint);
    }
    @Pointcut("@within(com.safframework.app.annotation.Async)||@annotation(com.safframework.app.annotation.Async)")
    public void onAsyncMethod() {
    }
    private void asyncMethod(final ProceedingJoinPoint joinPoint) throws Throwable {
        Observable.create(new Observable.OnSubscribe<Object>() {
            @Override
            public void call(Subscriber<? super Object> subscriber) {
                Looper.prepare();
                try {
                    joinPoint.proceed();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
                Looper.loop();
            }
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe();
    }
}


2. 将方法返回的结果放于缓存中


我先给公司的后端项目写了一个 CouchBase 的注解,该注解是借助 Spring Cache和 CouchBase 结合的自定义注解,可以把某个方法返回的结果直接放入 CouchBase 中,简化了 CouchBase 的操作。让开发人员更专注于业务代码。


受此启发,我写了一个 Android 版本的注解,来看看该注解是如何使用的。

import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;
import com.safframework.app.annotation.Cacheable;
import com.safframework.app.domain.Address;
import com.safframework.cache.Cache;
import com.safframework.injectview.Injector;
import com.safframework.injectview.annotations.OnClick;
import com.safframework.log.L;
import com.safframwork.tony.common.utils.StringUtils;
/**
 * Created by Tony Shen on 2017/2/7.
 */
public class DemoForCacheableActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo_for_cacheable);
        Injector.injectInto(this);
        initData();
    }
    @Cacheable(key = "address")
    private Address initData() {
        Address address = new Address();
        address.country = "China";
        address.province = "Jiangsu";
        address.city = "Suzhou";
        address.street = "Ren min Road";
        return address;
    }
    @OnClick(id={R.id.text})
    void clickText() {
        Cache cache = Cache.get(this);
        Address address = (Address) cache.getObject("address");
        Toast.makeText(this, StringUtils.printObject(address),Toast.LENGTH_SHORT).show();
        L.json(address);
    }
}


在 initData() 上标注 @Cacheable 注解和缓存的key,点击text按钮之后,就会打印出缓存的数据和 initData() 存入的数据是一样的。


image.png


@Cacheable执行结果.png


目前,该注解 @Cacheable 只适用于 Android 4.0以上。


3. 将方法返回的结果放入SharedPreferences中


该注解 @Prefs 的用法跟上面 @Cacheable 类似,区别是将结果放到SharedPreferences。


同样,该注解 @Prefs 也只适用于 Android 4.0以上


4. App 调试时,将方法的入参和出参都打印出来


在调试时,如果一眼无法看出错误在哪里,那肯定会把一些关键信息打印出来。


在 App 的任何方法上标注 @LogMethod,可以实现刚才的目的。

public class DemoForLogMethodActivity extends Activity{
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initData1();
        initData2("test");
        User u = new User();
        u.name = "tony";
        u.password = "123456";
        initData3(u);
    }
    @LogMethod
    private void initData1() {
    }
    @LogMethod
    private String initData2(String s) {
        return s;
    }
    @LogMethod
    private User initData3(User u) {
        u.password = "abcdefg";
        return u;
    }
}


image.png


@LogMethod执行结果.png


目前,方法的入参和出参只支持基本类型和String,未来我会加上支持任意对象的打印以及优雅地展现出来。


5. 在调用某个方法之前、以及之后进行hook


通常,在 App 的开发过程中会在一些关键的点击事件、按钮、页面上进行埋点,方便数据分析师、产品经理在后台能够查看和分析。


以前在大的电商公司,每次 App 发版之前,都要跟数据分析师一起过一下看看哪些地方需要进行埋点。发版在即,添加代码会非常仓促,还需要安排人手进行测试。而且埋点的代码都很通用,所以产生了 @Hook 这个注解。它可以在调用某个方法之前、以及之后进行hook。可以单独使用也可以跟任何自定义注解配合使用。

@HookMethod(beforeMethod = "method1",afterMethod = "method2")
    private void initData() {
        L.i("initData()");
    }
    private void method1() {
        L.i("method1() is called before initData()");
    }
    private void method2() {
        L.i("method2() is called after initData()");
    }


来看看打印的结果,不出意外先打印method1() is called before initData(),再打印initData(),最后打印method2() is called after initData()。


image.png


@Hook执行的结果.png


@Hook的原理如下, beforeMethod和afterMethod即使找不到或者没有定义也不会影响原先方法的使用。

import com.safframework.app.annotation.HookMethod;
import com.safframework.log.L;
import com.safframwork.tony.common.reflect.Reflect;
import com.safframwork.tony.common.reflect.ReflectException;
import com.safframwork.tony.common.utils.Preconditions;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
/**
 * Created by Tony Shen on 2016/12/7.
 */
@Aspect
public class HookMethodAspect {
    @Around("execution(!synthetic * *(..)) && onHookMethod()")
    public void doHookMethodd(final ProceedingJoinPoint joinPoint) throws Throwable {
        hookMethod(joinPoint);
    }
    @Pointcut("@within(com.safframework.app.annotation.HookMethod)||@annotation(com.safframework.app.annotation.HookMethod)")
    public void onHookMethod() {
    }
    private void hookMethod(final ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        HookMethod hookMethod = method.getAnnotation(HookMethod.class);
        if (hookMethod==null) return;
        String beforeMethod = hookMethod.beforeMethod();
        String afterMethod = hookMethod.afterMethod();
        if (Preconditions.isNotBlank(beforeMethod)) {
            try {
                Reflect.on(joinPoint.getTarget()).call(beforeMethod);
            } catch (ReflectException e) {
                e.printStackTrace();
                L.e("no method "+beforeMethod);
            }
        }
        joinPoint.proceed();
        if (Preconditions.isNotBlank(afterMethod)) {
            try {
                Reflect.on(joinPoint.getTarget()).call(afterMethod);
            } catch (ReflectException e) {
                e.printStackTrace();
                L.e("no method "+afterMethod);
            }
        }
    }
}


6. 安全地执行方法,不用考虑异常情况


一般情况,写下这样的代码肯定会抛出空指针异常,从而导致App Crash。

private void initData() {
        String s = null;
        int length = s.length();
    }


然而,使用 @Safe 可以确保即使遇到异常,也不会导致 App Crash,给 App 带来更好的用户体验。

@Safe
    private void initData() {
        String s = null;
        int length = s.length();
    }


再看一下logcat的日志,App 并没有 Crash 只是把错误的日志信息打印出来。


image.png


logcat的日志.png


我们来看看,@Safe的原理,在遇到异常情况时直接catch Throwable。

import com.safframework.log.L;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
 * Created by Tony Shen on 16/3/23.
 */
@Aspect
public class SafeAspect {
    @Around("execution(!synthetic * *(..)) && onSafe()")
    public Object doSafeMethod(final ProceedingJoinPoint joinPoint) throws Throwable {
        return safeMethod(joinPoint);
    }
    @Pointcut("@within(com.safframework.app.annotation.Safe)||@annotation(com.safframework.app.annotation.Safe)")
    public void onSafe() {
    }
    private Object safeMethod(final ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;
        try {
            result = joinPoint.proceed(joinPoint.getArgs());
        } catch (Throwable e) {
            L.w(getStringFromException(e));
        }
        return result;
    }
    private static String getStringFromException(Throwable ex) {
        StringWriter errors = new StringWriter();
        ex.printStackTrace(new PrintWriter(errors));
        return errors.toString();
    }
}


7. 追踪某个方法花费的时间,用于性能调优


无论是开发 App 还是 Service 端,我们经常会用做一些性能方面的测试,比如查看某些方法的耗时。从而方便开发者能够做一些优化的工作。@Trace 就是为这个目的而产生的。

@Trace
    private void initData() {
        for (int i=0;i<10000;i++) {
            Map map = new HashMap();
            map.put("name","tony");
            map.put("age","18");
            map.put("gender","male");
        }
    }


来看看,这段代码的执行结果,日志记录花费了3ms。


image.png


@Trace执行结果.png


只需一个@Trace注解,就可以实现追踪某个方法的耗时。如果耗时过长那就需要优化代码,优化完了再进行测试。


当然啦,在生产环境中不建议使用这样的注解。


总结



AOP 是 OOP 的有力补充。玩好 AOP 对开发 App 是有很大的帮助的,当然也可以直接使用我的库:),而且新的使用方法我也会不断地更新。由于水平有限,如果有任何地方阐述地不正确,欢迎指出,我好及时修改:)

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
5天前
|
JavaScript 搜索推荐 Android开发
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
27 8
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
|
17天前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
124 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
15天前
|
Dart 前端开发 Android开发
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
38 4
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
30天前
|
缓存 前端开发 Android开发
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
80 12
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
|
1天前
|
安全 Android开发 iOS开发
escrcpy:【技术党必看】Android开发,Escrcpy 让你无线投屏新体验!图形界面掌控 Android,30-120fps 超流畅!🔥
escrcpy 是一款基于 Scrcpy 的开源项目,使用 Electron 构建,提供图形化界面来显示和控制 Android 设备。它支持 USB 和 Wi-Fi 连接,帧率可达 30-120fps,延迟低至 35-70ms,启动迅速且画质清晰。escrcpy 拥有丰富的功能,包括自动化任务、多设备管理、反向网络共享、批量操作等,无需注册账号或广告干扰。适用于游戏直播、办公协作和教育演示等多种场景,是一款轻量级、高性能的 Android 控制工具。
|
1月前
|
Dart 前端开发 Android开发
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
36 1
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
2月前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
78 19
|
2月前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
92 14
|
2月前
|
开发框架 Android开发 iOS开发
安卓与iOS开发中的跨平台策略:一次编码,多平台部署
在移动应用开发的广阔天地中,安卓和iOS两大阵营各占一方。随着技术的发展,跨平台开发框架应运而生,它们承诺着“一次编码,到处运行”的便捷。本文将深入探讨跨平台开发的现状、挑战以及未来趋势,同时通过代码示例揭示跨平台工具的实际运用。
174 3
|
2月前
|
搜索推荐 前端开发 测试技术
打造个性化安卓应用:从设计到开发的全面指南
在这个数字时代,拥有一个定制的移动应用不仅是一种趋势,更是个人或企业品牌的重要延伸。本文将引导你通过一系列简单易懂的步骤,从构思你的应用理念开始,直至实现一个功能齐全的安卓应用。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你提供必要的工具和知识,帮助你将创意转化为现实。

热门文章

最新文章

  • 1
    如何修复 Android 和 Windows 不支持视频编解码器的问题?
  • 2
    Android历史版本与APK文件结构
  • 3
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 4
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
  • 5
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
  • 6
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 7
    【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
  • 8
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
  • 9
    Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
  • 10
    Cellebrite UFED 4PC 7.71 (Windows) - Android 和 iOS 移动设备取证软件