归纳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天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
7天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
9天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
【10月更文挑战第35天】在数字化时代,安卓应用的开发成为了一个热门话题。本文旨在通过浅显易懂的语言,带领初学者了解安卓开发的基础知识,同时为有一定经验的开发者提供进阶技巧。我们将一起探讨如何从零开始构建第一个安卓应用,并逐步深入到性能优化和高级功能的实现。无论你是编程新手还是希望提升技能的开发者,这篇文章都将为你提供有价值的指导和灵感。
|
7天前
|
存储 API 开发工具
探索安卓开发:从基础到进阶
【10月更文挑战第37天】在这篇文章中,我们将一起探索安卓开发的奥秘。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和建议。我们将从安卓开发的基础开始,逐步深入到更复杂的主题,如自定义组件、性能优化等。最后,我们将通过一个代码示例来展示如何实现一个简单的安卓应用。让我们一起开始吧!
|
8天前
|
存储 XML JSON
探索安卓开发:从新手到专家的旅程
【10月更文挑战第36天】在这篇文章中,我们将一起踏上一段激动人心的旅程,从零基础开始,逐步深入安卓开发的奥秘。无论你是编程新手,还是希望扩展技能的老手,这里都有适合你的知识宝藏等待发掘。通过实际的代码示例和深入浅出的解释,我们将解锁安卓开发的关键技能,让你能够构建自己的应用程序,甚至贡献于开源社区。准备好了吗?让我们开始吧!
20 2
|
9天前
|
Android开发
布谷语音软件开发:android端语音软件搭建开发教程
语音软件搭建android端语音软件开发教程!
|
15天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
28 5
|
13天前
|
JSON Java Android开发
探索安卓开发之旅:打造你的第一个天气应用
【10月更文挑战第30天】在这个数字时代,掌握移动应用开发技能无疑是进入IT行业的敲门砖。本文将引导你开启安卓开发的奇妙之旅,通过构建一个简易的天气应用来实践你的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都将成为你宝贵的学习资源。我们将一步步地深入到安卓开发的世界中,从搭建开发环境到实现核心功能,每个环节都充满了发现和创造的乐趣。让我们开始吧,一起在代码的海洋中航行!
|
15天前
|
缓存 数据库 Android开发
安卓开发中的性能优化技巧
【10月更文挑战第29天】在移动应用的海洋中,性能是船只能否破浪前行的关键。本文将深入探讨安卓开发中的性能优化策略,从代码层面到系统层面,揭示如何让应用运行得更快、更流畅。我们将以实际案例和最佳实践为灯塔,引领开发者避开性能瓶颈的暗礁。
33 3
|
11天前
|
安全 Java 测试技术
Java开发必读,谈谈对Spring IOC与AOP的理解
Spring的IOC和AOP机制通过依赖注入和横切关注点的分离,大大提高了代码的模块化和可维护性。IOC使得对象的创建和管理变得灵活可控,降低了对象之间的耦合度;AOP则通过动态代理机制实现了横切关注点的集中管理,减少了重复代码。理解和掌握这两个核心概念,是高效使用Spring框架的关键。希望本文对你深入理解Spring的IOC和AOP有所帮助。
24 0