Sparky


  • 首页

  • 归档

Android开发图书分类

发表于 2019-05-06

Android开发图书分类

  • 入门实战类。初中级开发者适合,主要是讲解Android基础知识,包括Android UI开发,数据存储,网络通信,数据库操作,传感器使用的基础知识,在加上几个实战项目练手和巩固。看完这些书之后基本都能达到中级开发水平,之后这些书就属于工具书的角色,查一查重新温习。
  • 源码分析类。主要针对Android系统开发者,又分为Framework层源码分析类和Native层源码分析类。当然对于普通应用开发者来说如果了解底层的实现原理对个人发展大有裨益。
  • 安全和逆向分析类。主要针对Android安全工程师,市场上有专门的系统安全相关的职位。这类书主要介绍Android平台的软件安全,逆向分析和加解密技术。主要设计Android软件的静态分析,动态调试,破解和反破解等技术以及一些Android系统安全策略。这个方向对Android应用开发者和Android系统开发者都有需要。
  • 系统移植和驱动类。这类图书主要介绍的是Android内核,移植和驱动开发的整个底层的嵌入式开发知识,其实不属于移动端开发,属于传统的嵌入式开发,毕竟Android底层就是Linux系统。

Android混淆踩坑实践

发表于 2019-05-06

Android混淆踩坑实践

一般情况下,网上的大多数模板都是可用的,如果出错通常需要特殊的指定要不去混淆的类或方法等。

两种混淆错误,一种是编译时报错,一种是运行时报错。
编译时错误一般是某些类需要混淆。
运行时错误一般是库调用java代码出错。

1. 耐心看log,一般根据log就可以定位到错误的地方。

跟Android项目中的warning不一样这里的warning也会导致编译不过去

Warning: Exception while processing task java.io.IOException: Please correct the above warnings first.
:fBReader:transformClassesAndResourcesWithProguardForRelease FAILED

FAILURE: Build failed with an exception.


遇到这种情况就滑到上边看log里的最上边的出现的warning

Note: the configuration refers to the unknown class 'android.webkit.webViewClient'
Note: the configuration refers to the unknown class 'android.webkit.webViewClient'
Note: the configuration refers to the unknown class 'android.webkit.webView'
Note: the configuration refers to the unknown class 'jav.lang.String'
      Maybe you meant the fully qualified name 'java.lang.String'?
Note: the configuration refers to the unknown class 'org.greenrobot.greendao.AbstractDao'
Warning: com.xxx.xxx: can't find superclass or interface android.xxx.xxx

说明com.xxx.xxx这个类出现了warning,在proguard.pro文件中添加

    -dontwarn com.xx.xx即可

2. 如果混淆之后打包编译都正常,千万不要以为万事大吉,一定要安装查看运行时基本功能是否正常

混淆之后经常出现运行时错误,也是需要去看应用crash的log,一行一行分析定位,实在不行就去google。

1、ClassNotFoundException,NoSuchMethodError

找不到类和方法的原因很多,需要根据log去定位。

Didn't find class "xxx" on path: DexPathList[[zip file "/data/app/xxx/base.apk"],nativeLibraryDirectories=[/data/app/xxx/lib/arm, /vendor/lib, /system/lib]]

然后java的loadLibrary方法会报错,这个问题乍一看可能以为是so库的问题,你可能回去不混淆so等,但是其实不用,因为so默认就是不混淆的,问题在于jni调用java类时,java类的路径因为混淆而发生变化,所以你需要去不去混淆这个类。

-keep class xxx{*;}

除了这种jni调用java会出现classnotfoundexception之外,本地代码通过反射调用其他的类等也会出现上述问题,只要分析log找到对应的类不混淆即可

2、ExceptionInInitializerError

原因:这是由于类初始化的时候发生了异常。

解决办法:找到具体是哪里的类哪个方法哪个类初始化的时候发生的异常,然后解决问题。

注:遇到这个错误,首先要确认是不是因为第三方的jar包导致的。如果不是的话,就找本地代码,看是不是写的有问题。如果确实是因为第三方jar包的代码导致的,尽量找到源码或者反编译,查看问题到底是什么引起的,然后找到相应的配置在proguard里面配置。

例如:我们项目中碰到过一个问题,就是因为第三方的jar包里面有一个字段初始化的时候报了空指针,然后导致我们的代码报了上面的错。当时很奇怪,为什么第三方的jar包还能报错,最后调查了之后才发现,是因为人家用到了类的注解,而proguard在混淆优化的时候把注解去掉了,所以报了空指针,只需要在proguard里面加上保护注解就可以了-keepattributes *Annotation*

3、 ClassCastException

原因:类强制转换的时候出错。

解决办法:找到代码,看是代码写的问题,还是混淆后的问题。如果没有混淆正常运行的话,一般都是因为混淆后遇到了各种问题才报的错。我们项目中遇到的问题是因为没有让proguard保持泛型,所以强转的时候报错。只需要在proguard文件里面加上泛型即可-keepattributes Signature

4、Resources$NotFoundException(resource not found)

资源没有找到,是因为第三方jar包或者自己的代码是通过反射获得R文件中的资源,所以需要将R文件屏蔽掉

原因:代码进行了混淆,R文件没有了,所以通过反射获取的R文件找不到

解决办法:在proguard文件里设置不混淆R文件    -keep class **.R$* { *; }

Android高级知识点总结

发表于 2019-05-06

1. 触摸事件传递机制

  1. 触摸事件的类型(MotionEvent(ACTION_DOWN,ACTION_MOVE,ACTION_UP等))
  2. 触摸事件的三个阶段(分发-拦截-消费)
  3. view的时间传递机制(activity->view)
  4. viewgroup的时间传递机制 (activity->viewgroup->view)

2. Android View的绘制流程

setContentView方法详解

  1. Android UI管理系统的层级关系(activity-phonewindow-decorview-(titleview,contentview))
  2. 绘制的整个流程(performtraversals->performMeasure->performLayout->performDraw)
  3. MesureSpec(unspecified,at_most(wrap_content),exactly(match_parent))
  4. Measure
  5. layout
  6. draw(drawBackground-saveLayer-onDraw-dispatchDraw-restoreToCount-onDrawScrollBar)

3. Android动画

  1. 帧动画(xml方式/动画方式添加)
  2. 补间动画(alpha/rotate/scale/translate,添加方式)
  3. 属性动画(Evaluator,animatorset,valueanimator,objectanimator)
  4. 过渡动画(transition)(本质是属性动画,但是对属性动画做了封装,方便实现过度动画的效果。需要动画前后两个布局)(定义transition的步骤:1. 定义动画前后两个布局 2. 根据布局文件创建布局前后的scene 3. 创建过度动画transitionset 4. 调用TransitionManager.go(scene, transitionSet))

    • scene(定义页面信息)
    • transition(changebounds,fade,transitionset,autotransition)
    • transitionmanager(切换控制器)

4. Support Annotation Library

  1. 基础概念(support library,support annotion library)
  2. Nullness注解(@NonNull,@Nullable)
  3. 资源文件类型注解(@AnimatorRes,@AnimRes,@AnyRes,@ArrayRes,@AttrRes,@BoolRes,@ColorRes,@DrawableRes,@FractionRes,@IdRes,@IntegerRes,@InterpolatorRes,@LayoutRes,@MenuRes,@PluralsRes,@RawRes,@StringRes,@StyleableRes,@StyleRes,@TransitionRes,@XmlRes)
  4. 权限注解(@RequiresPermission)
  5. 类型定义注解(@IntDef @interface @Retention,定义注解类)
  6. 线程注解(@MainThread,@UiThread, @WorkThread,@BinderThread)
  7. 颜色值注解(@ColorInt)
  8. 值范围注解(@Size(n),@Size(max=m),@Size(min=n),@Size(Multiple=2),@IntRange(from=0,to=255),@FloatRange(from=0.0,to=1.0))
  9. 重写函数注解(@CallSuper)
  10. 返回值注解(@CheckResult)
  11. keep注解(@keep)
  12. visibletest注解(@VisibleTest)
  13. lint的用法(Analyze->Inspect Code->选择范围)

5. Percent Support Library(百分比布局)

  1. PercentRelativeLayout
  2. PercentFramelayout
  3. layout_heightPercent/layout_widthPercent
  4. aspectRatio

6. Design Support Library

  1. 简介,snackbar,navigationview,floatingactionbutton,coordinatorlayout,constraintlayout,collapsingtoolbarlayout
  2. snackbar(setAction)
  3. textinputlayout(用法设置为edittext的父布局)
  4. tablayout
  5. navigationview

    5.1 导航菜单
    5.2 导航头部

  6. FloatingActionButton

    6.1 基本属性(elevation(未按压状态的阴影),pressedTranslationZ(按压状态的阴影))
    6.2 其它选项(backgroundTint(背景颜色),rippleColor(波纹颜色),fabSize(可选尺寸))

  7. CoordinatorLayout(协调布局,可以使不同布局的组件之间直接相互作用,并且协调动画效果)(例如,floatingactionbar和snackbar,点击floatingactionbutton出现snackbar,然后floatingactionbutton自动往上移动,snackbar消失时,floatingactionbutton自动往下移动,这里就需要将coordinatorlayout作为floatingactionbutton的父容器)

  8. CollapsingToolbarLayout(可以实现当屏幕内容滚动时,toolbar收缩的效果,通常和appbar配合使用)
  9. BottomSheetBehavior(1. 布局中添加app:layout_behavior属性,并将这个view作为coordinatorlayout的子view 2. 布局文件中创建bottomsheetbehavior对象,并设置状态变化的监听)

7. NDK开发

  1. ABI(应用程序二进制接口application binary interface)(arm-v5,arm-v7,arm-v8,x86,x86_64,mips,mips6)
  2. 引入预编译的二进制文件
  3. 自己写c/c++文件
  4. 使用.so文件的注意事项

8. Gradle必知必会

  1. 概念(是Android studio标配的构建系统,settings.gradle,project build.gradle,module build.gradle,gradle的文件结构)
  2. 共享变量(1. 在项目路径下创建xxx.gradle文件 2. 在文件中定义变量如project.ext{javaVersion = 1.8} 3. 在module build.gradle中apply from: “${project.rootDir}/xxx.gradle”,然后引用变量名就可以)
  3. 通用配置(在project build.gradle下创建subprojects{}定义配置)
  4. 如何引用aar(1. 指定aar文件的位置,在module build.gradle中的android节点下创建repositories{flatDir{dirs ‘libs’}} 2. 在dependencies节点下添加compile(name: ‘xxx’, ext: ‘aar’))
  5. 签名混淆的配置

9. gradle打包发布

  1. maven central
  2. jcenter
  3. jitpack

10. Builder建造者模式详解

  1. 经典的建造者模式(java设计模式之建造者模式)
  2. 建造者模式的变种
  3. 变种的自动化生成
  4. 使用builder模式的开源例子

11. 注解在Android中的使用

  1. 注解的定义(java语言的特性之一,他是在源代码中插入的标签,在之后的编译和运行中起到某种作用,每个注解都必须通过@interface进行声明,接口的方法对应着注解的元素)
  2. 标准注解(java包中默认给出的注解)

    2.1 编译相关的注解(@override,@deprecated,@suppresswarnings,@safevarargs,@generated,@functuinalinterface)

    2.2 资源相关的注解(javaee中用到)(@PostConstruct,@PreDestroy,@Resource,@resources)

    2.3 元注解(用来定义和实现注解的注解)

    2.3.1 @Target(指定注解所适用的对象的范围)
    
    2.3.2 @Retention(用来指明注解的访问范围,也就是在什么级别保留注解)(源码级注解(@Retention(RetentionPolicy.SOURCE)),编译级注解(@Retention(RetentionPolicy.CLASS)),运行时注解(@Retention(RetentionPolicy.RUNTIME)))
    
    2.3.3 @Documented(表示被修饰的注解应该保存在被注解项的文档中)
    
    2.3.4 @Inherited(表示该注解可以被子类继承)
    
    2.3.5 @Repeatable(表示这个注解可以在注解项上应用多次)
    
  3. 运行时注解(该注解会被保存在.class和.java文件中,在执行时,也保留注解信息。可以通过反射机制读取注解的信息)

  4. 编译时注解

12. ANR产生的原因及定位分析

  1. ANR产生的原因

    只有当UI线程响应超时才会引起ANR,主要两种原因导致超时,1. 事件被阻塞,正在处理其它阻塞事件 2. 自身处理事件耗时太长。
    activity/view 5s, broadcast 10s, Service 20s

  2. 经典场景

    1. 应用程序UI线程中存在耗时操作,例如在UI线程中进行网络操作,数据库操作和文件操作等,导致ui线程阻塞无法响应用户数据等。
    2. 应用程序的ui线程在等待工作线程释放某个锁,从而无法处理事件
    3. 耗时的动画需要大量的计算工作,可能导致CPU负荷过重
  3. 定位和分析

    1. logcat分析可以看到是anr发生的进程及包名和类名,pid,reason,cpu使用情况
    2. 如果还是没有具体定位到anr发生的位置,就需要借助/data/anr/xxx.txt文件。文件包括发生anr的进程名,id和时间,cpu架构,堆内存的使用情况,线程信息,以及anr发生的位置及错误信息。
  4. 避免和检测
    不要在主线程做耗时操作,使用检测工具检测存在违规操作的工具类

13. Android异步处理技术

常见的异步处理技术:Thread,AsyncTask,Handler & Looper, Executors。

Handler异步处理消息机制:

  1. Looper.prepare方法会在本线程中创建一个Looper实例,在实例化过程中会创建一个MessageQueue,这个looper实例会被保存到ThreadLocal对象中(ThreadLocal对像可以再线程中存储变量)
  2. Looper.loop方法会让当前线程进入一个无限循环,不断的从Messagequeue中得到message对象,并且通过msg.target.dispatchMessage将消息发送出去
  3. Handler的构造方法会获取当前线程中保存的looper实例,并与looper对象的messagequeue关联起来
  4. handler.sendmessage中将msg.tar设置为handler对象本身,并将message添加到messagequue中
  5. msg.target.dispatchmessage即是handler.dispatchmessage,他会回调handler.message方法
1. Thread

概念: 执行任务的基本单元

2种创建方式: 继承Thread,实现runnable

3种线程的分类:

  • 主线程(ui线程),随着应用启动而启动,用来运行Android组件,刷新组件元素。通过handle顺序处理其它线程发来的消息。
  • binder线程,Binder线程用于不同进程之间的通信,每个应用在创建之后,就创建了一个线程池,用于处理其它进程的线程发送的消息,经典场景是aidl
  • 后台线程(工作线程)
2. HandlerThread

HandlerTread是Thread的子类,它内部集成了Looper和MessageQueue。内部其实也是handler机制。

3. AsyncQueryHandler

主要用于contentprovider中的异步数据查询,不常用

4. AsyncTask
asynctask task = new asynctask(){
    onpreexecute();
    onpostexecute();
    doinbackground();
    onprogressupdate();
};
task.execute();

构造方法中创建mHandler,mWork,mFuture->execute()->executeOnExecutor() 1. onpreexecute() 2. exec.execute()->exec.execute() exec对象是在execute方法中的参数,是sdefaultexecutor成员变量实际上是一个SerialExecutor类的实例->创建serialexecutor对象会首先生成arraydeque队列,将mfuture对象封装成runnable添加到队列中,判断mactive是否为空,如果为空执行scheduleNext()->这个方法会先去将队列的队尾赋值为mactive,然后执行THREAD_POOL_EXECUTOR.execute(mactive)方法->实际上THREAD_POOL_EXECUTOR是一个线程池,相当于执行线程的run方法->mactive对象实际上是mFuture,FutureTask.run实际上会执行callable.call()->mWork.call() 1. 执行doinbackground 2. 执行postresult->postresult 1. 封装result为message,message 2. 调用message.senttotarget发送。实际上是调用handle处理消息->mhandler对象handlemessage,两种事件 1. PostResult 回调onCancel,onPostExecute 2. PostProgress 回调onProgressUpdate

5. Loader

不常用

14. Android数据序列化

15. WebView

将独立应用作为library module导入项目

发表于 2019-05-06

将独立应用作为library module导入项目

可以视为插件化思想,之后可以作为插件方便调用.

  1. File-New-Import Module
  2. 修改module build.gradle.主要修改: 1. 将apply plugin: ‘com.android.application’改为’com.android.library’ . 2. 将applicationId删除 3. 如果有releaseConfigs 删除之.
  3. 修改Module AndroidManifest.xml. 主要修改: 将application标签中的icon leble等去掉
  4. 代码修改. 1. library module中不能使用switch case,android studio alt+enter 自动修复代码可以自动转换 2. 如果module中使用了butterkenife,可参考butterknife readme中步骤将R替换为R2. 3. 项目中可能使用了系统数据库,需要修改系统数据库path,也可以改为直接使用getapplicationcontext().getdatabasepath(databsename).getabsolutepath();

Android Service回顾

发表于 2019-05-06

Android Service

service知识点

什么是service?

service是一个长时间运行在后台但不提供用户界面的应用组件。当切换到其它应用,Service仍能在后台继续运行。

Service两种启动方式
1. bindservice

通过bindservice方式启动的服务是跟启动他的组件绑定在一起的。如果多个组件绑定了一个服务,当所有组件都销毁时,服务才停止,如果只有一个组件绑定了服务,这个组件销毁,服务就停止。
onCreate()–>onBind()–>onUnBind()–>onDestroy()

服务是如何被绑定的?

  1. 首先添加一个类,继承Binder,在Binder类中添加与其它组件要与服务交互的方法,并在onBind()方法中返回IBinder实例。
  2. 在创建服务的时候需要传递ServiceConnection对象。在接口回调中获取binder对象与服务通信。
  3. 绑定和解绑服务。
    2. startservice
    通过startservice方式启动的服务,一旦启动就会独立于启动他的组件,即使组件销毁了也会继续在后台执行。
    onCreate()–>onCommandStart()–>onDestroy()
3. startservice并且bindservice

当一个service在startservice的同时bindservice,该Service即使所有的与之绑定的组件都销毁了这个组件也不会停止,如果你想要手动去结束掉服务,必须调用stopService或者stopself方法。

组件和服务之间进行通信
  1. BroadcastReceiver

    如果是以startservice方式启动服务,那么他与组件将是相对独立的。

  2. BindService

    组件和服务之间通信的常用方式

  3. AIDL

IntentService

IntentService is a base class for {@link Service}s that handle asynchronous
requests (expressed as {@link Intent}s) on demand. Clients send requests
through {@link android.content.Context#startService(Intent)} calls; the
service is started as needed, handles each Intent in turn using a worker
thread, and stops itself when it runs out of work.

IntentService是一个异步处理的类,你可以通过startservice来提交请求,该Service会在需要的时候创建,请求是在工作线程处理,并且Service会在执行完销毁。

所以IntentService是Service的子类,他是使用工作线程逐一处理启动请求,如果你不要求服务同时处理多个请求,这是最好的选择。

分析IntentService源码:
oncreate(): 创建一个工作线程,获取到这个工作线程的looper对象用于意图处理,创建一个ServiceHandler对象用于处理消息。
onStart()/onCOmmandStart():发送消息给handler
ServiceHandler.handleMessage()中会回调onHandleIntent(),之后调用stopSelf()方法停止service

总结

Service

Service 有2种启动方式,startService 启动服务,服务启动起来后,在后台无限期运行,直到通过stopService 或者 stopSelf 停止服务,服务与组件独立,通信比较困难(但还是有办法的,通过BroadcastReceiver )。另一种方式就是 bindService 即绑定服务,组件和服务绑定在一起,服务的生命后期受组件影响,如果绑定到服务的组件全部被销毁了,那么服务也就会停止了。绑定服务的方式通常用于组件和服务之间 需要相互通信。startService 这种 方式一般用于在后台执行任务,而不需要返回结果给组件。 这两种方式并非完全独立,也就是说,你可以绑定已经通过 startService 启动起来的服务,可以通过在Intent 中添加Action 来标示要执行的动作。比如:通过Intent Action 标记要播放的音乐,调用startService 来启动音乐服务播放音乐,在界面需要显示播放进度的时候,可以通过binderService 来绑定服务,从而获取歌曲信息。这种情况下,Service 需要实现两种方式的生命周期。这种情况下,除非所有客户端都已经取消绑定,否则通过stopService 或者 stopSelf 是不能停止服务的。

Service 是运行在主线程中的,因此不能执行耗时的或者密集型的任务,如果要执行耗时操作或者密集型计算任务,请在服务中开启工作线程,在线程中执行。或者使用下面一节将要讲的IntentService。

Intentservice

IntentService是Service 的子类,默认给我们开启了一个工作线程执行耗时任务,并且执行完任务后自 动停止服务。扩展IntentService比较简单,提供一个构造方法和实现onHandleIntent 方法就可了,不用重写父类的其他方法。但是如果要绑定服务的话,还是要重写onBind 返回一个IBinder 的。使用Service 可以同时执行多个请求,而使用IntentService 只能同时执行一个请求。

参考

Service和IntentService详解

STAR法则

发表于 2019-05-06

star法则
situaction :介绍项目的背景
task:介绍自己的职责
action:介绍自己所做的工作
result:介绍自己的成果

s:Android电子书阅读器
t: launcher界面和阅读器模块
a:

  1. ndk(编译mupdf/koreader等开源项目so,进行深度开发)
  2. 适配(1072*1440与普通手机)(因为是e-ink屏幕所以减少刷新频率,使用一些少刷新的控件)
  3. 自定义view实现阅读器(pageview)及部分ui(搜索view,亮度调节view,开机动画)
  4. 自定义recyclerview(可以横向滑动的recyclerview)
  5. 封装网络接口显示ui数据(没有给出接口的时候,事先封装好databean)
  6. 使用greendao实现本地数据库接口(launcher的书架数据库和阅读器的书签,笔记,书摘数据库)
  7. 其它(仿京东购买详情页,本地图书管理,第三方登录,第三方支付)

r: 成果及收获

  1. 完成所有ui以及大部分功能的开发(mupdf是一个完整的开源项目,并不是只针对Android的开源项目,所以需要自己去编译相关的ndk库,c层的东西是有很多忘记了,所以大多数功能是在java层做的,例如添加菜单,触摸事件控制,书签笔记等,因为demo是很简单的,有的问题例如修改排版和字体需要在c层实现)
  2. 对ndk有了系统实际的开发经验,对make fike及aosp有一定认识(aosp编译失败)
  3. 对阅读器行业有一定认识
  4. 对自身的优缺点有进一步认识(强[ui,自定义view],弱[底层,阅读代码能力,缺乏性能优化实践])
  5. 业余实践搞了一下ffmpeg和基于ijkplayer的视频播放器。看了一些kotlin和python的语法
  6. 从入门到放弃xxxx

AIDL

发表于 2019-05-06

AIDL

aidl全称Android接口定义语言

  • 为什么要定义这个语言
  • aidl语法是什么
  • 如何使用aidl进行进程间通信

    为什么要定义AIDL,以及AIDL的应用场景

    进程间通信,通过aidl来制定一些规则规定数据格式等

AIDL语法

  • 文件类型,aidl文件类型是.aidl而不是.java
  • 数据结构。aidl中除了默认支持的数据类型,其它数据类型都需要导包。比如自定义的bean类需要导包。默认数据类型有java中的八种基本数据类型,String,CharSequence,List,Map
  • 定向tag,in,out,inout
  • 数据分类,一种是数据型,一种是接口型

    使用AIDL的步骤

  1. 使数据实现Parceable接口
  2. 编写aidl文件
  3. 编写service文件

未命名

发表于 2019-05-06
  1. config.gradle
  2. isBuildApp
  3. apply plugin: ‘com.android.application’
  4. applicationId “com.gw.ebookos.ebookos”
  5. manifest
  6. aroute

未命名

发表于 2019-05-06

java annotation基础

java注解分为标准注解和元注解。

标准注解是java为我们提供的预定义的注解,@override,@deprecated,@suppresswarnnings,@safevarargs

元注解是给我们自定义注解用的共有5种,@target,@retention,@documented,@inherited,@repeatable

@Target:用来修饰注解能够修饰的对象的类型,接收一个elementtype类型的数组。

@Remention:用来指定注解的保留策略。可以指定如下值

- SOURCE 注解只保留在源码层面,编译时即被丢弃
- CLASS 注解可以保留在class文件中,但会被jvm丢弃
- RUNTIME 注解也会在jvm运行事件保留,可以通过反射读取注解信息

@Documented:此注解将被包含在javadoc中

@Inherited:此注解可以被继承

@Repeatable:指定的注解可以重复应用到指定的对象上边

标准注解也是用元注解创建的

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override{}

如上所示:

@Target(ElementType.METHOD)表示这个注解能够修饰方法

@Retention(RetentionPolicy.SOURCE)表示这个注解只存在于源码

解析注解的两种方式

1. 运行时注解可以使用反射解析

下边的使用反射解析注解的实例:创建一个类,然后在类中对字段做注解,最后通过反射的方法解析注解的内容

首先需要创建注解类

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    String name();
    int id() default -1;
}

然后定义一个类,使用上边的注解

public class Persion {
    @UseCase(id = 0, name = "nancy")
    int number;

    @UseCase(id = 2, name = "lucky")
    String name;
}

最后通过反射解析注解

try {
            Class clz =Class.forName("com.dou.demo.knife_annotation.Persion");
            Field[] fields = clz.getDeclaredFields();

            for (Field field : fields) {
                UseCase annotation = field.getAnnotation(UseCase.class);

                if (annotation != null) {
                    System.out.println(annotation.name() + annotation.id());
                }
            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

2. 编译时注解可以使用annotationprocessor解析

编译时注解的使用场景很多,包括很多的第三方库例如Butterknife, Retrofit, GreenDao等,下边我们就做一个简单的demo,类似于butterknife的例子来讲解如何使用annotationprocessor。

首先来讲一下butterknife的大致的注入流程

首先我们需要在activity中使用@BindView(R.id.xx)注解对应的控件,然后使用Butterknife.bind(this)方法完成对控件的绑定,实际上底层还是调用findviewbyid方法。大概的思路就是在编译期生成一个类似MainActivity_ViewBinding.java文件,在这个类的构造方法中最终会调用findViewById()方法。而调用Butterknife.bind(this)方法,首先会找到MainActivity_ViewBinding这个类的构造方法对象,然后实例化该构造方法(执行类的构造方法),也就是执行findviewbyid动作

基于annotationprocessor使用注解的步骤

原理为:先在activity中使用注解@BindView或者@OnClick标注控件对象,然后调用knife.bind(this)方法进行绑定,编译注解时生成一个Activity$$Injector类,bind方法主要就是调用这个类的inject方法,这个方法底层调用findviewbyid

因为自己学习时已经定义了@BindView,下边主要定义另外一个注解@OnClick

定义注解类

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
    int[] id() default -1;
}

封装绑定接口

在activity中使用了注解之后,在编译的时候自动生成对应的XXActivity$$Inject类,这个类中有一个inject方法,这个方法提供findviewbyid和setonclicklistener方法。这里的我们首先定义一个Injector接口,在接口中定义一个inject方法,自动生成的xxactivity$$injector类实现了这个方法。这个接口封装绑定接口指的就是Knife.bind(this)的这个过程,我们使用多态的特性调用Injector.inject()方法完成绑定。具体代码如下:

Knife类:

public class Knife {

    static Finder ACTIVITY_FINDER = new ActivityFinder();
    static Finder VIEW_FINDER = new ViewFinder();

    public Knife() {
        throw new AssertionError("not available for instance.");
    }

    public static void bind(Context context){
        bind(context, ACTIVITY_FINDER);
    }

    public static void bind(View view){
        bind(view, VIEW_FINDER);
    }

    public static void bind(Object host, Finder finder){
        bind(host, host, finder);
    }

    public static void bind(Object host, Object source, Finder finder){
        String className = host.getClass().getName();

        try {
            Class findClass = Class.forName(className + "$$Injector");
            Injector injector = null;
            try {
                injector = (Injector) findClass.newInstance();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

            if (host == null) {
                Log.d("xxx", "bind: host");
                return;
            } else if (finder == null) {
                Log.d("xxx", "bind: finder");
                return;
            }

            injector.inject(host, source, finder);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Injector接口:

public interface Injector<T> {
    void inject(T host, Object source, Finder finder);
}

其他的类:

public interface Finder {
    Context getContext();
    View findView(Object source, int resId);
}

public class ActivityFinder implements Finder {

    Context context;

    @Override
    public Context getContext() {
        return context;
    }

    @Override
    public View findView(Object activity, int resId) {
        context = ((Activity) activity);
        return ((Activity) activity).findViewById(resId);
    }
}

public class ViewFinder implements Finder {

    Context context;

    @Override
    public Context getContext() {
        return context;
    }

    @Override
    public View findView(Object view, int resId) {
        context = ((View) view).getContext();
        return ((View) view).findViewById(resId);
    }
}

定义annotationprocessor

这里是对编译时注解进行解析并生成对应类的地方,我们在这里使用annotationprocessor和javapoet。annotationprocessor会在注解编译时期提供回调,我们的主要工作都是在它的process()中进行的。javapoet是square开源的生成java类的开源库。

首先我们需要定义一个类集成AbstractProcessor,他是annotationprocessor的核心类,我们需要实现它的4个方法:

  • init() 初始化代码,一般会去获取Elements,messager,filer
  • process() 处理方法
  • getSupportedAnnotationTypes() 用来指定该处理器适用的注解
  • getSupportedSourceVersion() 用来指定你的编译器的java版本

之后还需要对处理器进行注册,第一种方法是在java同级目录下创建一个resources/META-INF/service文件夹,然后在文件夹中创建名为javax.annotation.processing.Processor的文件,文件内容为 我们的处理器的目录。
另一种方法是使用谷歌的@AutoService注解,你需要添加依赖

compile 'com.google.auto.service:auto-service:1.0-rc2'

然后在自己的处理器上面加上@AutoService(Processor.class)注解即可。

代码的生成过程

首先做一些初始化操作,指定要注解类型,java编译器版本

  @Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    System.out.println("processor: init");
    filer = processingEnvironment.getFiler();
    messager = processingEnvironment.getMessager();
    elements = processingEnvironment.getElementUtils();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
    System.out.println("processor: getSupportedAnnotationTypes");
    Set<String> types = new LinkedHashSet<>();
    types.add(BindView.class.getCanonicalName());
    return types;
}

@Override
public SourceVersion getSupportedSourceVersion() {
    System.out.println("processor: getSupportedSourceVersion");
    return SourceVersion.latestSupported();
}

在process中进行代码生成的工作,从参数roundEnvironment中,我们可以获得注解对应的Element信息。

@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("processor: process");
        processBindview(roundEnvironment);
        processOnClick(roundEnvironment);

        System.out.println("processor: annotationCount = " + annotationClassMap.size());

        for (AnnotationClass annotation : annotationClassMap.values()) {
            try {
                annotation.generateInjector().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

private void processOnClick(RoundEnvironment roundEnvironment) {
        System.out.println("processor: processOnClick");

        for (Element element : roundEnvironment.getElementsAnnotatedWith(OnClick.class)){

            AnnotationClass annotationClass = getAnnotationClass(element);
            OnClickMethod field = new OnClickMethod(element);
            annotationClass.addField(field);
        }
    }

    private void processBindview(RoundEnvironment roundEnvironment) {

        System.out.println("processor: processBindview");

        for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)){
            AnnotationClass annotationClass = getAnnotationClass(element);
            BindViewField bindViewField = new BindViewField(element);
            annotationClass.addField(bindViewField);
        }
    }

    private AnnotationClass getAnnotationClass(Element element){
        String className = ((TypeElement) element.getEnclosingElement()).getQualifiedName().toString();
        AnnotationClass annotationClass = annotationClassMap.get(className);
        if (annotationClass == null) {
            annotationClass = new AnnotationClass((TypeElement) element.getEnclosingElement(), elements);
            annotationClassMap.put(className, annotationClass);
        }
        return annotationClass;
    }

上边的逻辑主要是根据roundEnvironment分别处理@BindView和@OnClick注解,这些注解分别被保存到AnnotaionClass对象的List和List集合中,最后调用annotationClass.generateInjector()来生成java类。

public JavaFile generateInjector(){
        System.out.println("processor: generateInjector");
        // to create method declear
        // @Override
        // public void inject(MainActivity host, Object source, Finder finder) {
        // }
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(typeElement.asType()), "host", Modifier.FINAL)
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(ClassName.get("com.dou.demo.knife_api.finder", "Finder"), "finder");

        // to create method body
        // host.targetview = (TextView) finder.findView)(source, R.id.xx)
        for (BindViewField field : bindViewFields) {
            methodBuilder.addStatement("host.$N=($T)finder.findView(source,$L)",
                    field.getFieldName(),
                    ClassName.get(field.getFieldType()),
                    field.getViewId());
        }

        // to create variable declear
        // OnClickListener listener;
        if (onClickMethods.size() > 0) {
            methodBuilder.addStatement("$T listener",
                    ClassName.get("android.view", "View", "OnClickListener"));
        }

        // to create varible define
        // listener = new OnClickListener(){
        //      @Override
        //      public void onClick(View v ){
        //          host.onClick();
        //      }
        // }
        for (OnClickMethod onClickMethod : onClickMethods) {
            TypeSpec listener = TypeSpec.anonymousClassBuilder("")
                    .addSuperinterface(ClassName.get("android.view", "View", "OnClickListener"))
                    .addMethod(MethodSpec.methodBuilder("onClick")
                            .addAnnotation(Override.class)
                            .addModifiers(Modifier.PUBLIC)
                            .returns(TypeName.VOID)
                            .addParameter(ClassName.get("android.view", "View"), "view")
                            .addStatement("host.$N()", onClickMethod.methodName)
                            .build())
                    .build();

            methodBuilder.addStatement("listener=$L", listener);

            for (int id : onClickMethod.viewIds) {
                methodBuilder.addStatement("finder.findView(source, $L).setOnClickListener(listener)", id);
            }
        }

        String packagename = getPackageName(typeElement);
        String classname = getBinderClassName(packagename, typeElement);

        ClassName binderClassname = ClassName.get(packagename, classname);
        TypeSpec injectorClass = TypeSpec.classBuilder(binderClassname.simpleName() + "$$Injector")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(ClassName.get("com.dou.demo.knife_api", "Injector"), TypeName.get(typeElement.asType())))
                .addMethod(methodBuilder.build())
                .build();

        return JavaFile.builder(packagename, injectorClass).build();
    }

使用方式

implementation project(":knife-annotation")
    implementation project(":knife-api")
    annotationProcessor project(":knife-compiler")


public class MainActivity extends AppCompatActivity {

    @BindView(id = R.id.tv_content)
    TextView tv_content;

    @OnClick(id = R.id.tv_content)
    public void onclick(){
        Toast.makeText(this, "ggggggg", Toast.LENGTH_SHORT).show();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Knife.bind(this);
        tv_content.setText("hello knife");
    }
}

总结

上边可能有一些需要注意的地方:

  1. 定义processor时最好创建java library类型,不然会提示AbstractProcessor类找不到
  2. 可能会出现注解编译的时候没有生成对应的注解的情况,需要配合gradle console的log查看哪里出错了

源码:

douyn/annotation-demo

参考:

Java 注解及其在 Android 中的应用

Shouheng88/Android-references

未命名

发表于 2019-05-06

终极适配

参考https://blog.csdn.net/qeqeqe236/article/details/42968805

1…678…10
Sparky

Sparky

我是谁?我在做什么?我为什么要这么做?我想做什么?我要怎么做?

99 日志
© 2019 Sparky
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4