AOP概述
AOP,即面向切面编程,是一种编程思想,强调的是在‘某一层面’上编写程序的方式,而这‘某一层面’就被称为切面。
比如打印log,作为调试的一种手段,一般会渗透到项目中的许多地方,那么打印log就可看成是一种切
面,而AOP会指导我们怎样编写打印log会更好。AOP的主要目标是尽可能地对切面代码进行解耦。
Android代码注入
AOP采用代码注入技术来实现高度的代码解耦,而在Android中常见的代码注入主要分为以下几种:
编译时注入:在编译代码阶段直接对class文件进行代码注入,属于静态代码注入,它的特点是对性能的影响较小,而缺陷仅能注入参与编译的代码。AspectJ支持编译时注入,注意在Android中是无法注入系统API类,如android.jar下的类。但可以注入support包。
加载时注入:在Android的虚拟机加载dex文件时进行代码注入,特点是对性能的影响较小,缺陷是门槛较高,比如热更新等。
运行时注入:在程序运行阶段进行注入,相比前面的静态注入,运行时进行注入会更加的灵活,但是对性能的运行较大,比如Java的动态代理。
AspectJ
AspectJ是AOP这种理论来指导实践的一种工具,更切确的说是一种编程语言,它也有自己的语法规则、编译工具等。
对于打印log这一切面进行编程,AspectJ的做法是通过代码注入技术来解耦,此种做法让人耳目一新。通过预先标记目标代码,在编译代码阶段注入切面代码。对于Java项目来说即在编译阶段往字节码注入代码,注入的技术支撑是hook。
AspectJ与Java是两种不同语言,不能直接交流。目前较好的解决方案是:AspectJ通过java注解进行语法规则的映射,并借助gradle插件完成编译工作。
AspectJ核心
Joint point:在AspectJ中,允许注入代码的地方被称为Joint point(连接点)。Joint point主要分为以下几种:
显然Joint point就是程序中关键的地方,如方法的调用与执行、字段的读写、异常和静态代码块等。虽Joint point类型是有限的,但对于一般的应用场景是足够了。重点关注execution与call的区别:- execution:这个Joint point是位于执行的方法体中
- call:这个Joint point是位于该方法被调用的地方
- handler:这个Joint point是位于异常throw的地方
Pointcut:切入点,其作用是挑选出需要注入的目标Joint point。简单理解就是告诉AspectJ要在哪些Joint point执行代码注入。除了上图中包括的直接Joint point类型,Pointcut还提供了以下过滤Joint point的辅助工具:
- within():可以筛选出指定包或类下的Joint point
- withincode():可以筛选出指定构造方法或普通方法的execution类型的Joint point
- cflow():可以筛选出目标call类型Joint point中包括的所有Joint point,包括目标call这个Joint point,注意括号中的参数必须是Pointcut
- cflowbelow():与cflow()唯一区别就是不包括目标call这个Joint point
总之,Pointcut借助于逻辑操作符(&&,||,!)可以组合成一个功能复杂的Pointcut
Advice:作用决定代码注入的时机以及执行代码注入。比如对某个方法进行注入,到底是在方法执行之前或之后,甚至于替换这个方法。分为以下几种:
- before:在目标方法执行之前注入
- around:取得目标方法执行的控制权,可以替换目标方法
- after:在目标方法执行之后注入
Aspect:指的是Pointcut与Advice的组合,其代表的就是往目标切面注入代码。
Weaving:代码织入,对于Android应用需要反编译代码才能看到AspectJ所注入的代码。
小结:上述AspectJ中的涉及的核心概念可总结为如下关系图:
Android AspectJ实践
目前AspectJ还未提供标准的gradle插件,考虑到接入成本,此处选用开源插件gradle_plugin_android_aspectjx。具体配置如下:在工程目录build.gradle的配置aspectjx插件依赖,如下代码
dependencies { classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.10' }
在app目录build.gradle的配置如下代码
apply plugin: 'android-aspectjx' aspectjx { excludeJarFilter '.jar' } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'org.aspectj:aspectjrt:1.8.+' }
编写Pointcut,预先标记目标代码。Pointcut指明在何处注入代码,如下例子
@Aspect public class DemoAspect { private static final Logger logger = LoggerFactory.getLogger(DemoAspect.class); @Pointcut("execution(* com.example.dson.app.ui.activity.MainActivity.onCreate(..))") public void injectCode() { } @Before("injectCode()") public void execInjectCode(JoinPoint joinPoint) { logger.debug("joinPoint:" + joinPoint); logger.debug("getStaticPart:" + joinPoint.getStaticPart().getKind()); logger.debug("getKind:" + joinPoint.getKind()); logger.debug("getTarget:" + joinPoint.getTarget()); logger.debug("getThis:" + joinPoint.getThis()); } }
public class MainActivity extends BaseActivity { private static final String TAG = MainActivity.class.getSimpleName(); private ListView mListView; private final List<ActivityEntry> mEntries = new ArrayList<>(); @Inject ActivityEntryDao mDao; @Override protected void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); PackageManager pm = getPackageManager(); try { PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), PackageManager .GET_ACTIVITIES); ActivityInfo[] activityInfo = packageInfo.activities; int len = activityInfo.length; for (int i = 0; i < len; i++) { ActivityEntry entry = new ActivityEntry(); entry.setActivityClass(Class.forName(activityInfo[i].name)); String name = entry.getActivityClass().getSimpleName(); if (name.equals(TAG)) { continue; } entry.setName(name); entry.setInfo(activityInfo[i]); mEntries.add(entry); } } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } Collections.sort(mEntries); mDao.saveAll(mEntries); mListView = (ListView) findViewById(R.id.activity_list); mListView.setAdapter(new ActivityAdapter()); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Intent intent = new Intent(MainActivity.this, mEntries.get(position) .getActivityClass()); startActivity(intent); } }); } @Override public String getActivityTag() { return TAG; } private class ActivityAdapter extends BaseAdapter { @Override public int getCount() { return mEntries.size(); } @Override public Object getItem(int position) { return mEntries.get(position); } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { Holder holder; if (convertView == null) { holder = new Holder(); convertView = LayoutInflater.from(MainActivity.this).inflate(android.R.layout .simple_list_item_1, null); holder.textView = (TextView) convertView.findViewById(android.R.id.text1); convertView.setTag(holder); } else { holder = (Holder) convertView.getTag(); } ActivityEntry entry = (ActivityEntry) getItem(position); holder.textView.setText(entry.getName()); return convertView; } } private static class Holder { TextView textView; } }
运行结果如下:
aspect编译与代码混淆的关系:结论是aspect编译阶段是在代码混淆之前执行的,因为gradle task是以依赖链的顺序执行的,aspect注入代码生成新包作为混淆的输入文件。比如如下构建过程的log:
#开始执行构建
:app:preBuild UP-TO-DATE
#此处省略的其它log
:app:transformClassesWithAspectTransformForDebug
aspect start..........
excludeJar:::D:\code\MyApplication\app\libs\pinyin4j-2.5.0.jar
excludeJar:::C:\Users\tscyd\.gradle\caches\modules-2\files-2.1\com.github.tony19\logback-android-classic\1.1.1-4\f20b1158fcca0aef6043886ef8605556a09058ca\logback-android-classic-1.1.1-4.jar
excludeJar:::C:\Users\tscyd\.android\build-cache\a88d9c5e67accbd9b1823c970eac763ceaaa5d66\output\jars\classes.jar
excludeJar:::C:\Users\tscyd\.android\build-cache\b192e9d007b5d7be93e942b19d27dbb92e2180b7\output\jars\classes.jar
#此处省略的其它log
directoryInput:::D:\code\MyApplication\app\build\intermediates\classes\debug
aspect do work..........
aspect jar merging..........
aspect done...................
:app:transformClassesWithAspectTransformForDebug spend 1732ms
:app:processDebugJavaRes NO-SOURCE
:app:processDebugJavaRes spend 1ms
:app:transformResourcesWithMergeJavaResForDebug
:app:transformResourcesWithMergeJavaResForDebug spend 52ms
:app:transformClassesAndResourcesWithProguardForDebug
ProGuard, version 5.3.2
Reading input...
Reading program jar [D:\code\MyApplication\app\build\intermediates\transforms\mergeJavaRes\debug\jars\2\1f\main.jar] (filtered)
Reading program jar [D:\code\MyApplication\app\build\intermediates\transforms\AspectTransform\debug\jars\1\10\007a1ac9907af9957c6690d04b022d90eac5914e.jar] (filtered)
Reading program jar [D:\code\MyApplication\app\build\intermediates\transforms\AspectTransform\debug\jars\1\10\063b4994867b45bf033b64ccc6f46e3c5b4a4fde.jar] (filtered)
Reading program jar [D:\code\MyApplication\app\build\intermediates\transforms\AspectTransform\debug\jars\1\10\0c89586b3ffc5096ea8d8623ce3eeef124d37923.jar] (filtered)
Reading program jar [D:\code\MyApplication\app\build\intermediates\transforms\AspectTransform\debug\jars\1\1f\aspected.jar] (filtered)
Reading program jar [D:\code\MyApplication\app\build\intermediates\transforms\AspectTransform\debug\jars\1\2\505132f1bd3fb37ea5a4e2660b961781369b6547.jar] (filtered)
Reading library jar [C:\sdk\platforms\android-25\android.jar]
Reading library jar [C:\sdk\platforms\android-25\optional\org.apache.http.legacy.jar]
#此处省略的其它log
Shrinking...
Printing usage to [D:\code\MyApplication\app\build\outputs\mapping\debug\usage.txt]...
Removing unused program classes and class elements...
Original number of program classes: 3011
Final number of program classes: 2548
Obfuscating...
Printing mapping to [D:\code\MyApplication\app\build\outputs\mapping\debug\mapping.txt]...
Writing output...
Preparing output jar [D:\code\MyApplication\app\build\intermediates\transforms\proguard\debug\jars\3\1f\main.jar]
#此处省略的其它log
:app:transformClassesWithDexForDebug spend 882ms
:app:transformClassesWithShrinkResForDebug
Removed unused resources: Binary resource data reduced from 514KB to 492KB: Removed 4%
:app:transformClassesWithShrinkResForDebug spend 167ms
:app:mergeDebugJniLibFolders
:app:mergeDebugJniLibFolders spend 5ms
:app:transformNativeLibsWithMergeJniLibsForDebug
:app:transformNativeLibsWithMergeJniLibsForDebug spend 9ms
:app:transformNativeLibsWithStripDebugSymbolForDebug
:app:transformNativeLibsWithStripDebugSymbolForDebug spend 3ms
:app:validateSigningDebug
:app:validateSigningDebug spend 0ms
:app:packageDebug
:app:packageDebug spend 330ms
:app:assembleDebug
:app:assembleDebug spend 0ms
BUILD SUCCESSFUL in 11s
43 actionable tasks: 43 executed
Task spend time:
1239ms :app:mergeDebugResources
471ms :app:processDebugResources
2373ms :app:compileDebugJavaWithJavac
1732ms :app:transformClassesWithAspectTransformForDebug
52ms :app:transformResourcesWithMergeJavaResForDebug
3078ms :app:transformClassesAndResourcesWithProguardForDebug
882ms :app:transformClassesWithDexForDebug
167ms :app:transformClassesWithShrinkResForDebug
330ms :app:packageDebug
AspectJ实践总结
- 优点:AOP编程的神兵利器,可极大地提高编程的效率。
- 缺点:语法比较多,相关资料不多;方法数增加;延长编译时间。
- 局限:
- 无法植入系统框架层代码,如view包等,对用户实现代码或依赖库则没有限制。
- 多个工程项目依赖下,源码项目工程植入困难。