在 Android 开发中,注解(Annotation)是提升效率、保障代码质量的核心工具 ------ 从系统的@Override到框架的@Room Entity,再到自定义的权限检查注解,注解贯穿开发全流程。本文将从基础原理出发,覆盖注解分类、常用框架注解、自定义注解实战(运行时 + 编译时) ,所有代码块均完整可运行,帮你系统掌握注解的设计与落地。
一、注解基础:本质与核心概念
1.1 什么是注解?
注解是Java/Android 的元数据(Metadata) ,它不直接影响代码执行逻辑,但能为编译器、IDE、框架提供额外信息 ------ 相当于给代码 "打标签"。例如:
java
// @Override 是系统注解:标记方法重写父类方法,编译器会校验方法签名
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
1.2 注解的生命周期(核心分类)
根据@Retention元注解配置,注解分为三类,决定了其有效阶段:
|-------|-------------------------|---------------------|---------------|-----------------------------|
| 类型 | @Retention 取值 | 有效阶段 | 核心场景 | 示例 |
| 源码注解 | RetentionPolicy.SOURCE | 仅.java 源码,编译后消失 | 编译时校验、IDE 提示 | @Override、@SuppressWarnings |
| 字节码注解 | RetentionPolicy.CLASS | 存在于.class 字节码,运行时消失 | 字节码增强(混淆控制) | @Keep(避免混淆) |
| 运行时注解 | RetentionPolicy.RUNTIME | 贯穿源码→字节码→运行时,可反射读取 | 运行时动态处理(权限检查) | @Deprecated |
关键结论 :现代框架(Room、Dagger)优先用编译时注解(SOURCE/CLASS) ,避免运行时反射开销;简单场景(配置标记)用运行时注解。
1.3 元注解:定义 "注解的规则"
元注解是修饰注解的注解,规定自定义注解的行为边界,常用 4 个:
|-------------|---------------------------------|--------------------------------|
| 元注解 | 作用说明 | 示例取值 |
| @Retention | 规定注解生命周期 | SOURCE/CLASS/RUNTIME |
| @Target | 规定注解可修饰的代码元素(类 / 方法 / 属性等) | ElementType.TYPE(类)、METHOD(方法) |
| @Repeatable | 允许注解在同一元素重复使用(如多次标记废弃说明) | 无(仅需标记该注解) |
| @Documented | 生成 JavaDoc 时包含该注解(Android 开发少用) | 无(仅需标记该注解) |
示例:自定义基础注解
定义一个标记 "废弃方法" 的注解:
java
package com.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 生命周期:运行时可反射读取
@Retention(RetentionPolicy.RUNTIME)
// 仅可修饰方法
@Target(ElementType.METHOD)
public @interface MyDeprecated {
// 注解属性:带默认值,使用时可省略
String version() default "1.0"; // 废弃版本
String replaceMethod() default ""; // 替代方法
}
使用示例:
java
public class Utils {
@MyDeprecated(version = "2.0", replaceMethod = "newCalculate")
public int oldCalculate(int a, int b) {
return a + b;
}
public int newCalculate(int a, int b) {
return a * b + 10; // 优化逻辑
}
}
二、Android 常用框架注解:无需重复造轮子
2.1 系统 / AndroidX 核心注解(保障代码质量)
这类注解用于编译时校验和语义说明,需依赖androidx.annotation:
java
// 依赖配置(Module级build.gradle)
implementation "androidx.annotation:annotation:1.6.0"
|---------------|------------------------------------|-------------------------------------------|
| 注解名称 | 作用说明 | 使用示例 |
| @NonNull | 标记参数 / 返回值不可为 null,传 null 时 IDE 报错 | public void setName(@NonNull String name) |
| @Nullable | 标记参数 / 返回值可为 null,提醒调用者判空 | @Nullable public String getAddress() |
| @Keep | 避免被 ProGuard 混淆(字节码注解) | @Keep public class ApiService |
| @UiThread | 标记方法必须在 UI 线程调用,子线程调用 Lint 报错 | @UiThread public void updateTextView() |
| @WorkerThread | 标记方法必须在子线程调用,UI 线程调用 Lint 报错 | @WorkerThread public void loadData() |
| @IntRange | 限制 int 参数范围,超出范围 IDE 提示 | @IntRange(from=0, to=100) int progress |
实战价值:配合 Kotlin 空安全
Java 方法用@NonNull标记后,Kotlin 调用时自动识别为非空类型,减少空判断:
java
// Java类
public class User {
@NonNull public String getName() { return "Android"; }
@Nullable public String getAddress() { return null; }
}
// Kotlin调用
val user = User()
val name = user.name // 非空,直接使用
val address = user.address ?: "未知地址" // 必须判空,否则编译报错
2.2 Jetpack 框架注解(简化开发流程)
(1)Room 数据库注解
通过注解自动生成 SQL 操作代码,避免手写 SQLite:
java
// 1. 依赖配置
implementation "androidx.room:room-runtime:2.5.2"
kapt "androidx.room:room-compiler:2.5.2"
// 2. 实体类(对应数据库表)
@Entity(tableName = "user") // 标记为数据库表
data class User(
@PrimaryKey(autoGenerate = true) val id: Int = 0, // 主键自增
@ColumnInfo(name = "user_name") val name: String, // 自定义列名
@ColumnInfo(name = "user_age") val age: Int
)
// 3. DAO接口(数据访问)
@Dao // 标记为数据访问接口
interface UserDao {
@Query("SELECT * FROM user WHERE age > :minAge") // 自定义查询SQL
fun getUsersOlderThan(minAge: Int): List<User>
@Insert(onConflict = OnConflictStrategy.REPLACE) // 自动生成插入SQL
fun insertUser(user: User)
}
// 4. 数据库类
@Database(entities = [User::class], version = 1) // 关联表和版本
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao // Room自动生成实现类
}
(2)Hilt 依赖注入注解
简化 Dagger 配置,自动注入依赖(如 Repository、ApiService):
java
// 1. 依赖配置
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-android-compiler:2.44"
// 2. 提供依赖的Module
@Module
@InstallIn(SingletonComponent::class) // 作用域:全局单例
object NetworkModule {
@Provides
@Singleton // 单例模式
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideApiService(client: OkHttpClient): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}
// 3. 注入依赖的Repository
class UserRepository @Inject constructor(
private val apiService: ApiService // Hilt自动注入ApiService
) {
suspend fun getUsers() = apiService.getUsers()
}
// 4. Activity中使用依赖
@AndroidEntryPoint // 标记Activity支持Hilt注入
class MainActivity : AppCompatActivity() {
@Inject lateinit var userRepository: UserRepository // 自动注入
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val users = userRepository.getUsers() // 直接使用
}
}
}
三、自定义注解实战 1:运行时注解(权限检查)
3.1 场景需求
方法执行前自动检查指定权限(如WRITE_EXTERNAL_STORAGE),缺失则申请,通过后执行方法,拒绝则提示。
3.2 步骤 1:定义运行时注解 @CheckPermission
java
package com.example.annotation.runtime;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 运行时权限检查注解:标记方法需要特定权限才能执行
*/
@Retention(RetentionPolicy.RUNTIME) // 运行时可反射读取
@Target(ElementType.METHOD) // 仅修饰方法
public @interface CheckPermission {
String[] value(); // 需要检查的权限数组(如{"android.permission.WRITE_EXTERNAL_STORAGE"})
String deniedTip() default "需要该权限才能执行操作,请开启"; // 权限拒绝提示
}
3.3 步骤 2:实现注解解析器(权限逻辑)
通过反射扫描注解,处理权限申请与方法调用:
java
package com.example.annotation.util;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class PermissionChecker {
private static final int PERMISSION_REQUEST_CODE = 10086;
private static Method sPendingMethod; // 待执行方法
private static Activity sPendingActivity; // 待执行方法所在Activity
/**
* 执行带@CheckPermission的方法
* @param activity 方法所在Activity
* @param methodName 方法名(无重载简化版)
*/
public static void invokeWithPermissionCheck(Activity activity, String methodName) {
try {
// 1. 获取目标方法(无参方法,可扩展支持带参)
Method targetMethod = activity.getClass().getDeclaredMethod(methodName);
CheckPermission annotation = targetMethod.getAnnotation(CheckPermission.class);
if (annotation == null) {
targetMethod.invoke(activity); // 无注解,直接执行
return;
}
// 2. 检查权限
String[] permissions = annotation.value();
boolean allGranted = true;
for (String perm : permissions) {
if (ContextCompat.checkSelfPermission(activity, perm) != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
// 3. 权限通过,执行方法(突破private)
targetMethod.setAccessible(true);
targetMethod.invoke(activity);
} else {
// 4. 权限缺失,存储待执行方法并申请权限
sPendingMethod = targetMethod;
sPendingActivity = activity;
ActivityCompat.requestPermissions(activity, permissions, PERMISSION_REQUEST_CODE);
Toast.makeText(activity, annotation.deniedTip(), Toast.LENGTH_SHORT).show();
}
} catch (NoSuchMethodException e) {
Toast.makeText(activity, "方法不存在:" + methodName, Toast.LENGTH_SHORT).show();
} catch (IllegalAccessException | InvocationTargetException e) {
Toast.makeText(activity, "方法执行失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
/**
* 权限申请结果回调(需在Activity中调用)
*/
public static void onPermissionResult(int requestCode, int[] grantResults) {
if (requestCode != PERMISSION_REQUEST_CODE || sPendingMethod == null) return;
// 检查权限是否通过
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
// 权限通过,执行待执行方法
try {
sPendingMethod.setAccessible(true);
sPendingMethod.invoke(sPendingActivity);
} catch (IllegalAccessException | InvocationTargetException e) {
Toast.makeText(sPendingActivity, "方法执行失败", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(sPendingActivity, "权限被拒绝,无法执行", Toast.LENGTH_SHORT).show();
}
// 重置,避免内存泄漏
sPendingMethod = null;
sPendingActivity = null;
}
}
3.4 步骤 3:在 Activity 中使用
java
package com.example.app;
import android.os.Bundle;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
import com.example.annotation.runtime.CheckPermission;
import com.example.annotation.util.PermissionChecker;
class PermissionDemoActivity : AppCompatActivity() {
private lateinit var btnSave: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_permission_demo)
btnSave = findViewById(R.id.btn_save)
// 按钮点击:调用带权限检查的方法
btnSave.setOnClickListener {
PermissionChecker.invokeWithPermissionCheck(this, "saveFileToStorage")
}
}
/**
* 带@CheckPermission的方法:需要存储权限
*/
@CheckPermission(
value = [android.Manifest.permission.WRITE_EXTERNAL_STORAGE],
deniedTip = "需要存储权限才能保存文件,请允许"
)
private fun saveFileToStorage() {
Toast.makeText(this, "文件保存成功!", Toast.LENGTH_SHORT).show()
}
/**
* 权限申请结果回调
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
PermissionChecker.onPermissionResult(requestCode, grantResults)
}
}
布局文件(activity_permission_demo.xml):
java
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="保存文件(需存储权限)"/>
</LinearLayout>
3.5 注意事项
- 适用场景:非高频调用(如按钮点击),反射开销可接受;
- 内存安全:执行后重置sPendingMethod和sPendingActivity,避免 Activity 泄漏;
- 权限声明:需在AndroidManifest.xml中声明权限:
java
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
四、自定义注解实战 2:编译时注解(视图 + 事件绑定)
4.1 场景需求
实现类似 ButterKnife 的功能:
- @MyBindView:自动绑定 View,替代findViewById;
- @OnClick:自动绑定点击事件,替代setOnClickListener;
- 编译时生成代码,无反射开销。
4.2 项目结构准备
需创建 3 个模块(解耦设计):
|------------|--------------|--------------------------|
| 模块名称 | 类型 | 作用 |
| annotation | Java Library | 定义@MyBindView和@OnClick注解 |
| processor | Java Library | 实现注解处理器(扫描注解 + 生成代码) |
| app | Android App | 依赖前两个模块,使用自定义注解 |
4.3 步骤 1:定义编译时注解(annotation 模块)
(1)@MyBindView(视图绑定)
java
package com.example.annotation.compiler;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 编译时视图绑定注解:标记View属性,自动生成findViewById代码
*/
@Retention(RetentionPolicy.SOURCE) // 仅编译时有效
@Target(ElementType.FIELD) // 修饰属性
public @interface MyBindView {
int value(); // View的ID(如R.id.tv_title)
}
(2)@OnClick(事件绑定)
java
package com.example.annotation.compiler;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 编译时点击事件注解:标记方法,自动生成setOnClickListener代码
*/
@Retention(RetentionPolicy.SOURCE) // 仅编译时有效
@Target(ElementType.METHOD) // 修饰方法
public @interface OnClick {
int[] value(); // 绑定的View ID数组(支持多个View绑定同一方法)
}
4.4 步骤 2:实现注解处理器(processor 模块)
需依赖auto-service(自动注册处理器)和javapoet(生成 Java 代码):
(1)processor 模块 build.gradle
java
plugins {
id 'java-library'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
// 1. 依赖自定义注解模块
implementation project(':annotation')
// 2. auto-service:自动注册注解处理器
implementation 'com.google.auto.service:auto-service:1.1.1'
annotationProcessor 'com.google.auto.service:auto-service:1.1.1'
// 3. javapoet:简化Java代码生成
implementation 'com.squareup:javapoet:1.13.0'
}
(2)处理器实现(ViewBinderProcessor.java)
java
package com.example.processor;
import com.example.annotation.compiler.MyBindView;
import com.example.annotation.compiler.OnClick;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 编译时注解处理器:处理@MyBindView和@OnClick,生成绑定代码
*/
@AutoService(Processor.class) // 自动注册为注解处理器
public class ViewBinderProcessor extends AbstractProcessor {
// 存储每个Activity的绑定信息:Activity全类名 → 绑定数据
private Map<String, ActivityBindingData> bindingDataMap = new HashMap<>();
// 支持的注解类型:@MyBindView和@OnClick
@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of(MyBindView.class.getName(), OnClick.class.getName());
}
// 支持的Java版本
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_8;
}
// 核心处理逻辑:扫描注解→生成代码
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 1. 清空上一轮数据(避免重复生成)
bindingDataMap.clear();
// 2. 处理@MyBindView注解(收集视图绑定信息)
processMyBindView(roundEnv);
// 3. 处理@OnClick注解(收集事件绑定信息)
processOnClick(roundEnv);
// 4. 生成每个Activity的绑定类(如MainActivity_ViewBinder)
generateBinderClasses();
return true; // 注解已处理完成
}
/**
* 处理@MyBindView:收集View属性与ID的映射
*/
private void processMyBindView(RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(MyBindView.class);
for (Element element : elements) {
VariableElement viewField = (VariableElement) element; // 被注解的View属性
TypeElement activityClass = (TypeElement) viewField.getEnclosingElement(); // 所属Activity
String activityFullName = activityClass.getQualifiedName().toString(); // Activity全类名
// 获取当前Activity的绑定数据(不存在则创建)
ActivityBindingData bindingData = bindingDataMap.computeIfAbsent(
activityFullName, k -> new ActivityBindingData(activityClass)
);
// 解析注解属性:View ID、属性名、属性类型
MyBindView myBindView = viewField.getAnnotation(MyBindView.class);
int viewId = myBindView.value();
String fieldName = viewField.getSimpleName().toString();
String fieldType = viewField.asType().toString(); // 如android.widget.TextView
// 添加视图绑定信息
bindingData.addViewBinding(new ViewBindingInfo(viewId, fieldName, fieldType));
}
}
/**
* 处理@OnClick:收集方法与View ID的映射
*/
private void processOnClick(RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(OnClick.class);
for (Element element : elements) {
ExecutableElement methodElement = (ExecutableElement) element; // 被注解的方法
TypeElement activityClass = (TypeElement) methodElement.getEnclosingElement(); // 所属Activity
String activityFullName = activityClass.getQualifiedName().toString();
// 获取当前Activity的绑定数据
ActivityBindingData bindingData = bindingDataMap.computeIfAbsent(
activityFullName, k -> new ActivityBindingData(activityClass)
);
// 解析注解属性:View ID数组
OnClick onClick = methodElement.getAnnotation(OnClick.class);
int[] viewIds = onClick.value();
// 校验方法签名(必须void返回值,参数可为空或单个View)
validateOnClickMethod(methodElement);
// 解析方法信息:方法名、是否带View参数
String methodName = methodElement.getSimpleName().toString();
boolean hasViewParam = methodElement.getParameters().size() == 1;
// 为每个View ID添加事件绑定
for (int viewId : viewIds) {
bindingData.addOnClickBinding(new OnClickBindingInfo(viewId, methodName, hasViewParam));
}
}
}
/**
* 校验@OnClick标记的方法签名是否合法
*/
private void validateOnClickMethod(ExecutableElement methodElement) {
// 1. 方法必须是void返回值
if (methodElement.getReturnType().getKind() != TypeKind.VOID) {
processingEnv.getMessager().printMessage(
javax.tools.Diagnostic.Kind.ERROR,
"@OnClick方法必须是void返回值:" + methodElement.getSimpleName()
);
}
// 2. 方法参数只能是0个或1个(且为View类型)
int paramCount = methodElement.getParameters().size();
if (paramCount > 1) {
processingEnv.getMessager().printMessage(
javax.tools.Diagnostic.Kind.ERROR,
"@OnClick方法最多1个参数:" + methodElement.getSimpleName()
);
} else if (paramCount == 1) {
VariableElement paramElement = methodElement.getParameters().get(0);
TypeMirror paramType = paramElement.asType();
// 校验参数是否为View或其子类
TypeMirror viewType = processingEnv.getElementUtils()
.getTypeElement("android.view.View")
.asType();
if (!processingEnv.getTypeUtils().isAssignable(paramType, viewType)) {
processingEnv.getMessager().printMessage(
javax.tools.Diagnostic.Kind.ERROR,
"@OnClick方法参数必须是View类型:" + methodElement.getSimpleName()
);
}
}
}
/**
* 生成绑定类(如MainActivity_ViewBinder.java)
*/
private void generateBinderClasses() {
for (Map.Entry<String, ActivityBindingData> entry : bindingDataMap.entrySet()) {
ActivityBindingData bindingData = entry.getValue();
TypeElement activityClass = bindingData.activityClass;
// 1. 获取Activity信息:类名、包名
String activityClassName = activityClass.getSimpleName().toString();
String packageName = processingEnv.getElementUtils()
.getPackageOf(activityClass)
.getQualifiedName()
.toString();
// 2. 构建bind方法(入口方法,完成视图+事件绑定)
MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(ClassName.get(activityClass), "activity"); // 参数:当前Activity
// 3. 添加视图绑定代码(activity.tvTitle = (TextView) activity.findViewById(R.id.tv_title))
for (ViewBindingInfo viewBinding : bindingData.viewBindings) {
bindMethodBuilder.addStatement(
"activity.$L = ($L) activity.findViewById($L)",
viewBinding.fieldName,
viewBinding.fieldType,
viewBinding.viewId
);
}
// 4. 添加事件绑定代码(setOnClickListener)
for (OnClickBindingInfo onClickBinding : bindingData.onClickBindings) {
// 生成匿名ClickListener:new View.OnClickListener() { ... }
CodeBlock clickListener = CodeBlock.builder()
.beginControlFlow("new android.view.View.OnClickListener()")
.addStatement("@Override")
.beginControlFlow("public void onClick(android.view.View v)")
// 根据方法是否带参数,生成不同调用代码
.addStatement(
onClickBinding.hasViewParam ? "activity.$L(v)" : "activity.$L()",
onClickBinding.methodName,
onClickBinding.methodName
)
.endControlFlow()
.endControlFlow()
.build();
// 添加代码:findViewById + setOnClickListener
bindMethodBuilder.addStatement(
"((android.view.View) activity.findViewById($L)).setOnClickListener($L)",
onClickBinding.viewId,
clickListener
);
}
// 5. 构建绑定类(类名:Activity名 + _ViewBinder)
TypeSpec binderClass = TypeSpec.classBuilder(activityClassName + "_ViewBinder")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(bindMethodBuilder.build())
.build();
// 6. 生成Java文件(写入app模块的build/generated目录)
try {
JavaFile.builder(packageName, binderClass)
.build()
.writeTo(processingEnv.getFiler());
} catch (Exception e) {
e.printStackTrace();
}
}
}
// ------------------------------ 数据类:存储绑定信息 ------------------------------
/**
* 单个Activity的所有绑定信息(视图+事件)
*/
private static class ActivityBindingData {
TypeElement activityClass; // Activity的TypeElement
java.util.List<ViewBindingInfo> viewBindings = new java.util.ArrayList<>(); // 视图绑定列表
java.util.List<OnClickBindingInfo> onClickBindings = new java.util.ArrayList<>(); // 事件绑定列表
ActivityBindingData(TypeElement activityClass) {
this.activityClass = activityClass;
}
void addViewBinding(ViewBindingInfo info) {
viewBindings.add(info);
}
void addOnClickBinding(OnClickBindingInfo info) {
onClickBindings.add(info);
}
}
/**
* 单个视图的绑定信息(ID+属性名+属性类型)
*/
private static class ViewBindingInfo {
int viewId;
String fieldName;
String fieldType;
ViewBindingInfo(int viewId, String fieldName, String fieldType) {
this.viewId = viewId;
this.fieldName = fieldName;
this.fieldType = fieldType;
}
}
/**
* 单个点击事件的绑定信息(ID+方法名+是否带参数)
*/
private static class OnClickBindingInfo {
int viewId;
String methodName;
boolean hasViewParam;
OnClickBindingInfo(int viewId, String methodName, boolean hasViewParam) {
this.viewId = viewId;
this.methodName = methodName;
this.hasViewParam = hasViewParam;
}
}
}
4.5 步骤 3:在 app 模块中使用自定义注解
(1)app 模块 build.gradle 配置
java
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdk 33
defaultConfig {
applicationId "com.example.app"
minSdk 21
targetSdk 33
// ... 其他配置
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
// 1. 依赖自定义注解模块
implementation project(':annotation')
// 2. 依赖注解处理器(Kotlin用kapt,Java用annotationProcessor)
kapt project(':processor')
// 3. 基础依赖
implementation 'androidx.appcompat:appcompat:1.6.1'
}
(2)Activity 代码实现
java
package com.example.app;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.example.annotation.compiler.MyBindView;
import com.example.annotation.compiler.OnClick;
class BinderDemoActivity : AppCompatActivity() {
// 1. @MyBindView:自动绑定View,无需findViewById
@MyBindView(R.id.tv_title)
lateinit var tvTitle: TextView
@MyBindView(R.id.btn_submit)
lateinit var btnSubmit: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_binder_demo)
// 2. 调用生成的绑定类,完成视图+事件绑定
BinderDemoActivity_ViewBinder.bind(this)
}
/**
* 3. @OnClick:绑定btn_submit的点击事件(无参方法)
*/
@OnClick(R.id.btn_submit)
private fun onSubmitClick() {
tvTitle.text = "提交成功!"
btnSubmit.text = "已提交"
}
/**
* 4. @OnClick:绑定tv_title的点击事件(带View参数)
*/
@OnClick(R.id.tv_title)
private fun onTitleClick(view: View) {
(view as TextView).text = "标题被点击!"
}
}
(3)布局文件(activity_binder_demo.xml)
java
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="视图+事件绑定Demo"
android:textSize="18sp"/>
<TextView
android:id="@+id/btn_submit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="提交"
android:layout_marginTop="16dp"
android:padding="8dp"
android:background="@android:color/darker_gray"
android:textColor="@android:color/white"/>
</LinearLayout>
4.6 查看生成的代码
编译 app 模块后,在app/build/generated/kapt/debug/com/example/app/目录下生成BinderDemoActivity_ViewBinder.java:
java
package com.example.app;
public final class BinderDemoActivity_ViewBinder {
public static void bind(BinderDemoActivity activity) {
activity.tvTitle = (android.widget.TextView) activity.findViewById(2131231015); // R.id.tv_title
activity.btnSubmit = (android.widget.TextView) activity.findViewById(2131231016); // R.id.btn_submit
((android.view.View) activity.findViewById(2131231016)).setOnClickListener(new android.view.View.OnClickListener() {
@Override
public void onClick(android.view.View v) {
activity.onSubmitClick();
}
});
((android.view.View) activity.findViewById(2131231015)).setOnClickListener(new android.view.View.OnClickListener() {
@Override
public void onClick(android.view.View v) {
activity.onTitleClick(v);
}
});
}
}
4.7 核心优势
- 无反射开销:生成的代码是原生 Java 调用,性能与手写代码一致;
- 编译时校验:方法签名错误(如非 void 返回值)直接报编译错误,提前规避 bug;
- 扩展性强:可扩展@OnLongClick、@OnTextChanged等注解,只需新增注解和处理器逻辑。
五、注解开发最佳实践与避坑指南
5.1 选型原则:运行时 vs 编译时
|---------------|-------|----------------------|
| 场景特征 | 推荐类型 | 示例 |
| 逻辑简单、非高频调用 | 运行时注解 | 权限检查、日志埋点 |
| 高频调用(UI / 列表) | 编译时注解 | 视图绑定、事件绑定 |
| 需动态修改配置 | 运行时注解 | 调试模式开关 |
| 性能敏感、无反射依赖 | 编译时注解 | RecyclerView 适配、网络拦截 |
5.2 避坑技巧
1.运行时注解避坑:
- 避免在onBindViewHolder等高频方法中使用,反射开销放大;
- 反射访问 private 成员后,无需手动恢复setAccessible(false)(不影响后续调用);
- 用try-catch包裹反射代码,避免因方法 / 字段不存在导致崩溃。
2.编译时注解避坑:
- 处理器需处理 "多轮编译"(RoundEnvironment),避免重复生成代码;
- 生成类名加固定后缀(如_ViewBinder),避免与业务类重名;
- 用JavaPoet生成代码,避免手动拼接字符串导致语法错误。
3.混淆兼容:
- 运行时注解:反射依赖类名 / 方法名,需在混淆规则中保留:
java
-keep class com.example.app.** { *; } // 保留业务类
- 编译时注解:生成的类需保留,避免混淆后无法调用:
java
-keep class com.example.app.**_ViewBinder { *; } // 保留绑定类
六、总结
Android 注解的核心价值是 **"用元数据提升效率,用合理选型保障性能"**:
- 基础层面:理解生命周期(SOURCE/CLASS/RUNTIME)和元注解,是自定义注解的前提;
- 框架层面:熟练使用 Room、Hilt 等框架的注解,减少重复代码;
- 实战层面:运行时注解适合简单场景,编译时注解适合性能敏感场景,两者结合可覆盖大部分业务需求。
通过本文的实战案例,你可根据业务扩展更多自定义注解(如@LogMethod日志注解、@TimeCost耗时统计注解),进一步提升开发效率。注解虽不直接实现业务逻辑,但却是现代 Android 开发中 "提质增效" 的关键工具。