Android 注解完全指南:从基础概念到自定义实战

在 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 开发中 "提质增效" 的关键工具。

相关推荐
Roye_ack2 小时前
【项目实战 Day5】springboot + vue 苍穹外卖系统(Redis + 店铺经营状态模块 完结)
java·spring boot·redis·学习·mybatis
JIngJaneIL3 小时前
记账本|基于SSM的家庭记账本小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·家庭记账本小程序
Ting-yu3 小时前
Nginx快速入门
java·服务器·前端·nginx
用户2018792831673 小时前
浅析RecyclerView的DiffUtill实现
android
文言一心3 小时前
MySQL脚本转换为StarRocks完整指南
android·数据库·mysql
小虎l3 小时前
李兴华-JavaWEB就业编程实战
java
卡卡酷卡BUG3 小时前
Redis 面试常考问题(高频核心版)
java·redis·面试
青云交3 小时前
Java 大视界 -- Java 大数据机器学习模型在金融衍生品定价与风险管理中的应用(415)
java·机器学习·金融衍生品·dl4j·信用风控·spark mllib·期权定价