Android入门第66天-使用AOP
开篇
这篇恐怕又是一篇补足网上超9成关于这个领域实际都是错的、用不起来的一个知识点了。网上太多太多教程和案例用的是一个叫hujiang的AOP组件-com.hujiang.aspectjx:gradle-android-plugin-aspectjx。
首先这些错的文章我不知道是怎么来的,其次那些案例真的运行成功过吗?它们用的都是aspectjx功能,是为了兼容Kotlin。
还是那句话,Java开发和Kotlin还有Flutter开发到底有什么区别?整一堆无用功、把框架搞得很复杂、一堆错误的无用的代码混在一起,这才是本质。开发了好用什么语言都可以开发出好东西来,开发了不好就只剩下了:我用的开发语言是最先进的。因此我们坚持使用Java。
扯回来,我们继续来看AOP功能在Android中的使用,由于Android是Java语言,因此它也支持aspectj功能,用法和在spring系里使用一模一样的简单。只不过都是被了这个叫:com.hujiang.aspectjx:gradle-android-plugin-aspectjx搞了太复杂了,甚至有人几天都调不通一个AOP功能。
因此本篇就是交给大家正确的、纯净的、纯真的AOP使用方法。
AOP的使用场景
AOP使用场景太多了,如:拦截方法打印日志,特别是:APP里有很多activity,它要求用户必须登录才可以打开,如:用户积分、我的历史订单等。对于这些界面,我们要求如果用户不登录那么APP自动跳到登录页。
这种操作就是用的AOP功能去实现的。
如何使用一个AOP来判断用户打开界面前是否已经登录
一个activity从进入到展示界面在onCreate方法里一般会经历这么几个方法,这里的initMain()就是用来展示界面的。

我们为了做这个界面在展示前判断用户是否已经登录,我们会这么“切”一刀。

然后把这个判断用户是否已经登录做成一个和后端交互的API,整个是否登录的判断逻辑如下截图:

动手使用AOP实现
我们假设后台的这个接口如下所示(这边用了上一篇讲到的retrofit2+okhttp3+rxjava)。
package com.mkyuan.aset.mall.android.login.api;import com.mkyuan.aset.mall.android.login.model.LoginResponseBean;import io.reactivex.Observable;
import okhttp3.RequestBody;
import retrofit2.http.Body;
import retrofit2.http.Header;
import retrofit2.http.POST;public interface LoginAPI {@POST("/api/account/checkLoginUT")Observable <LoginResponseBean> checkLoginUT(@Header("ut") String ut);
}
所以我们现在开始动手制作我们的AOP了。
先引入AOP包
我们这边不会使用网上的已经不维护的、一堆问题的“com.hujiang.aspectjx:gradle-android-plugin-aspectjx”。
请直接使用“org.aspectj:aspectjrt:1.9.6”。为此
第一步:编程全局build.gradle
加入以下语句:
dependencies {classpath 'org.aspectj:aspectjtools:1.9.6'
}第二步:编辑我们的模块级别的build.gradle文件
先引入aspectjrt包
implementation 'org.aspectj:aspectjrt:1.9.6' //引入 aspectj然后再要在同一个build.gradle中加入如下语句
//使得aop生效
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main// 获取log打印工具和构建配置
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->if (!variant.buildType.isDebuggable()) {// 判断是否debug,如果打release把return去掉就可以log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")// return;}// 使aspectj配置生效JavaCompile javaCompile = variant.javaCompilejavaCompile.doLast {String[] args = ["-showWeaveInfo","-1.8","-inpath", javaCompile.destinationDir.toString(),"-aspectpath", javaCompile.classpath.asPath,"-d", javaCompile.destinationDir.toString(),"-classpath", javaCompile.classpath.asPath,"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]log.debug "ajc args: " + Arrays.toString(args)MessageHandler handler = new MessageHandler(true);new Main().run(args, handler);//在编译时打印信息如警告、error等等for (IMessage message : handler.getMessages(null, true)) {switch (message.getKind()) {case IMessage.ABORT:case IMessage.ERROR:case IMessage.FAIL:log.error message.message, message.thrownbreak;case IMessage.WARNING:log.warn message.message, message.thrownbreak;case IMessage.INFO:log.info message.message, message.thrownbreak;case IMessage.DEBUG:log.debug message.message, message.thrownbreak;}}}
}加完后的build.gradle长这个样
/*** following plugins is the original version*/
/*
plugins {id 'com.android.application'
}
*/
/*** by using aspectj must ad following lines tart*/
apply plugin: 'com.android.application'
//apply plugin: 'android-aspectjx' //暂时注了
/*** by using aspectj must ad following lines tart*/
android {namespace 'com.mkyuan.aset.mall.android'compileSdk 32defaultConfig {applicationId "com.mkyuan.aset.mall.android"minSdk 27targetSdk 32versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}dataBinding {enabled = true}//aspectjx {// exclude "**/module-info.class"// exclude "META-INF.versions.9.module-info"// exclude "META-INF/versions/9/*.class"// exclude 'com.google', 'com.squareup', 'org.apache','com.taobao','com.ut'// exclude 'androidx', 'com.squareup', 'com.alipay', 'org.apache', 'org.jetbrains.kotlin',// "module-info", 'versions.9'//}}dependencies {implementation 'org.aspectj:aspectjrt:1.9.6' //引入 aspectjimplementation 'com.github.bumptech.glide:glide:4.11.0'annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'implementation 'org.aspectj:aspectjrt:1.9.6'implementation 'io.github.youth5201314:banner:2.2.2'//com.google.android.material.theme//implementation 'com.google.android.material:material:<version>'//retrofit2implementation 'com.squareup.retrofit2:retrofit:2.9.0'implementation 'com.squareup.retrofit2:converter-gson:2.9.0'//日志拦截器implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'//rxjavaimplementation 'io.reactivex.rxjava2:rxandroid:2.1.1'implementation 'io.reactivex.rxjava2:rxjava:2.2.12'//gsonimplementation 'com.google.code.gson:gson:2.8.7'implementation 'androidx.appcompat:appcompat:1.4.1'implementation 'com.google.android.material:material:1.5.0'implementation 'androidx.constraintlayout:constraintlayout:2.1.3'testImplementation 'junit:junit:4.13.2'androidTestImplementation 'androidx.test.ext:junit:1.1.3'androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}//使得aop生效
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main// 获取log打印工具和构建配置
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->if (!variant.buildType.isDebuggable()) {// 判断是否debug,如果打release把return去掉就可以log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")// return;}// 使aspectj配置生效JavaCompile javaCompile = variant.javaCompilejavaCompile.doLast {String[] args = ["-showWeaveInfo","-1.8","-inpath", javaCompile.destinationDir.toString(),"-aspectpath", javaCompile.classpath.asPath,"-d", javaCompile.destinationDir.toString(),"-classpath", javaCompile.classpath.asPath,"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]log.debug "ajc args: " + Arrays.toString(args)MessageHandler handler = new MessageHandler(true);new Main().run(args, handler);//在编译时打印信息如警告、error等等for (IMessage message : handler.getMessages(null, true)) {switch (message.getKind()) {case IMessage.ABORT:case IMessage.ERROR:case IMessage.FAIL:log.error message.message, message.thrownbreak;case IMessage.WARNING:log.warn message.message, message.thrownbreak;case IMessage.INFO:log.info message.message, message.thrownbreak;case IMessage.DEBUG:log.debug message.message, message.thrownbreak;}}}
}制作AOP相关的代码

Login.java-定义一个基于方法切面的annotation
package com.mkyuan.aset.mall.android.util.aop.login;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;//不需要回调的处理
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
LoginAspect.java -使用Around模式对含有@Login的方法进行切面
package com.mkyuan.aset.mall.android.util.aop.login;import android.util.Log;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;@Aspect
public class LoginAspect {private static final String TAG = "LoginAspect";@Pointcut("execution(@com.mkyuan.aset.mall.android.util.aop.login.Login * *(..))")public void executionCheckLogin() {}//不带回调的注解处理@Around("executionCheckLogin()")public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {Log.i(TAG, ">>>>>>走进AOP方法");Signature signature = joinPoint.getSignature();if (!(signature instanceof MethodSignature)) {throw new RuntimeException("该注解只能用于方法上");}Login login = ((MethodSignature) signature).getMethod().getAnnotation(Login.class);if (login == null)return;//判断当前是否已经登录LoginManager.isLogin(new LoginCheckCallBack() {@Overridepublic void changeValue(boolean loginResult) {if(loginResult) {Log.i(TAG, ">>>>>>用户己登录走入下一步");try {joinPoint.proceed();} catch (Throwable e) {Log.e(TAG,e.getMessage(),e);}}else{//如果未登录,去登录页面Log.i(TAG, ">>>>>>用户未登录去登录页面");LoginManager.gotoLoginPage();}}});}
}
LoginManager.java
内含有和后台是否登录接口交互以及判断用户如果已经登录那么继续“走下去-打开界面”,否则跳到Login登录界面的逻辑
package com.mkyuan.aset.mall.android.util.aop.login;import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;import com.google.gson.Gson;
import com.mkyuan.aset.mall.android.home.MainActivity;
import com.mkyuan.aset.mall.android.login.LoginActivity;
import com.mkyuan.aset.mall.android.login.SmsLoginActivity;
import com.mkyuan.aset.mall.android.login.api.LoginAPI;
import com.mkyuan.aset.mall.android.login.model.LoginResponseBean;
import com.mkyuan.aset.mall.android.network.BaseObserver;
import com.mkyuan.aset.mall.android.network.NetworkApi;
import com.mkyuan.aset.mall.android.util.ContextUtils;
import com.mkyuan.aset.mall.android.util.SharedPreferenceHelper;
import com.mkyuan.aset.mall.android.util.activity.ActivityCollector;import java.util.Map;import okhttp3.FormBody;
import okhttp3.MediaType;
import okhttp3.RequestBody;public class LoginManager {private static final String TAG = "LoginAspect";public static void isLogin(LoginCheckCallBack loginCheckCallBack) {Context ctx = null;try {ctx = ContextUtils.getCurApplicationContext();SharedPreferenceHelper spHelper = new SharedPreferenceHelper(ctx);Map<String, String> data = spHelper.read();if (data.get("ut") != null && data.get("ut").trim().length() > 0) {String utValue = data.get("ut");//开始调用okhttp3, retrofit, rxjava框架LoginAPI loginAPI = NetworkApi.createService(LoginAPI.class);//loginAPI.checkLoginUT().loginAPI.checkLoginUT(utValue).compose(NetworkApi.applySchedulers(new BaseObserver<LoginResponseBean>() {@Overridepublic void onSuccess(LoginResponseBean loginResponseBean) {Log.i(TAG, ">>>>>>" + new Gson().toJson(loginResponseBean));int returnCode = loginResponseBean.getCode();String returnMsg = loginResponseBean.getMessage();if (returnCode == 0) {//result = true;loginCheckCallBack.changeValue(true);Log.i(TAG,">>>>>>get verifiedCode->" + loginResponseBean.getData());//startActivity(new Intent(SmsLoginActivity.this, MainActivity// .class));} else {loginCheckCallBack.changeValue(false);}}@Overridepublic void onFailure(Throwable e) {Log.e(TAG, ">>>>>>Network Error: " + e.toString(), e);loginCheckCallBack.changeValue(false);}}));} else {loginCheckCallBack.changeValue(false);}} catch (Exception e) {Log.e(TAG, ">>>>>>isLogin error: " + e.getMessage());loginCheckCallBack.changeValue(false);}}public static void gotoLoginPage() {Context ctx = null;try {ctx = ContextUtils.getCurApplicationContext();Intent intent = new Intent();intent.setClass(ctx, LoginActivity.class);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);ctx.startActivity(intent);ActivityCollector.getInstance().quitCurrentProcess("com.mkyuan.aset.mall.android.home.MainActivity");} catch (Exception e) {Log.e(TAG, ">>>>>>gotoLoginPage error: " + e.getMessage(), e);}}
}
使用这个AOP
在MainActivity代码里有一个initMain方法,渲染界面的逻辑全部在这个initMain方法里。

接着我们来看这个initMain()方法。在方法前我们加入了自定义的“切入点”。

效果演示
第一次打开APP,用户未登录,因此直接被跳到了Login界面


然后输入手机,点获取验证码

按提交,登录成功。
然后关闭APP,再次打开APP
由于之前用户已经登录过了,因此AOP直接会把用户带入到主页


结合着我上一篇:retrofit2+okhttp3+rxjava自己不妨动动手试试看吧
相关文章:
Android入门第66天-使用AOP
开篇这篇恐怕又是一篇补足网上超9成关于这个领域实际都是错的、用不起来的一个知识点了。网上太多太多教程和案例用的是一个叫hujiang的AOP组件-com.hujiang.aspectjx:gradle-android-plugin-aspectjx。首先这些错的文章我不知道是怎么来的,其次那些案例真的运行成功…...
pl/sql篇之触发器
简述本文將具体简述触发器的语法,触发条件及其适用场景,希望对读者理解,使用触发器能起到作用。触发器的定位触发器是数据库独立编译,存储的对象,是数据库重要的技术。和函数不同,触发器的执行是主动的&…...
黑马《数据结构与算法2023版》正式发布
有人的地方就有江湖。 在“程序开发”的江湖之中,各种技术流派风起云涌,变幻莫测,每一位IT侠客,对“技术秘籍”的追求和探索也从未停止过。 要论开发技术哪家强,可谓众说纷纭。但长久以来,确有一技&#…...
Spring的创建和使用
目录 创建Spring项目 步骤 1)使用Maven的方式创建Spring项目 2)添加Spring依赖 3)创建启动类 存Bean对象 1.创建Bean对象 2.将Bean注册到Spring中 取Bean对象并使用 步骤 1.先得到Spring上下文对象 2.从Spring中获取Bean对象 3.使用Bean ApplicationContext VS Bea…...
如何实现外网跨网远程控制内网计算机?快解析来解决
远程控制,是指管理人员在异地通过计算机网络异地拨号或双方都接入Internet等手段,连通需被控制的计算机,将被控计算机的桌面环境显示到自己的计算机上,通过本地计算机对远方计算机进行配置、软件安装程序、修改等工作。通俗来讲&a…...
【跟着ChatGPT学深度学习】ChatGPT教我文本分类
【跟着ChatGPT学深度学习】ChatGPT教我文本分类 ChatGPT既然无所不能,我为啥不干脆拜他为师,直接向他学习,岂不是妙哉。说干就干,我马上就让ChatGPT给我生成了一段文本分类的代码,不看不知道,一看吓一跳&am…...
IM即时通讯架构技术:可靠性、有序性、弱网优化等
消息的可靠性是IM系统的典型技术指标,对于用户来说,消息能不能被可靠送达(不丢消息),是使用这套IM的信任前提。 换句话说,如果这套IM系统不能保证不丢消息,那相当于发送的每一条消息都有被丢失的…...
【算法】三道算法题两道难度中等一道困难
算法目录只出现一次的数字(中等难度)java解答参考二叉树的层序遍历(难度中等)java 解答参考给表达式添加运算符(比较困难)java解答参考大家好,我是小冷。 上一篇是算法题目 接下来继续看下算法题…...
正交实验与极差分析
正交试验极差分析流程如下图: 正交试验说明 正交试验是研究多因素试验的设计方法。对于多因素、多水平的实验要求,如果每个因素的每个水平都要进行试验,这样就会耗费大量的人力和时间,正交试验可以选择出具有代表性的少数试验进行…...
DEXTUpload .NET增强的上传速度和可靠性
DEXTUpload .NET增强的上传速度和可靠性 DEXTUpload.NET Pro托管在Windows操作系统上的Internet Information Server(IIS)上,服务器端组件基于HTTP协议,支持从web浏览器到web服务器的文件上载。它也可以在ASP.NET服务器应用程序平台开发的任何网站上使用…...
SkyWalking 将方法加入追踪链路(@Trace)
SkyWalking8 自定义链路追踪@Trace 自定义链路,需要依赖skywalking官方提供的apm-toolkit-trace包.在pom.xml的dependencies中添加如下依赖: <dependency><groupId>org.apache.skywalking</groupId><artifactId>apm-toolkit-trace</artifactId>&…...
MySQL Administrator定时备份MySQL数据库
1、下载并安装软件mysql-gui-tools-5.0-r17-win32.exe 2、将汉化包zh_CN文件夹拷贝到软件安装目录 3、菜单中打开MySql Adminstrator,见下图,初次打开无服务实例。 点击已存储连接右侧按钮①,打开下图对话框。点击“新连接”按钮ÿ…...
Kubernetes入门教程 --- 使用二进制安装
Kubernetes入门教程 --- 使用二进制安装1. Introduction1.1 架构图1.2 关键字介绍1.3 简述2. 使用Kubeadm Install2.1 申请三个虚拟环境2.2 准备安装环境2.3 配置yum源2.4 安装Docker2.4.1 配置docker加速器并修改成k8s驱动2.5 时间同步2.6 安装组件3. 基础知识3.1 Pod3.2 控制…...
深度学习模型压缩方法概述
一,模型压缩技术概述 1.1,模型压缩问题定义 因为嵌入式设备的算力和内存有限,因此深度学习模型需要经过模型压缩后,方才能部署到嵌入式设备上。 模型压缩问题的定义可以从 3 角度出发: 模型压缩的收益: 计算: 减少浮点运算量(FLOPs),降低延迟(Latency)存储: 减少内…...
《NFL橄榄球》:坦帕湾海盗·橄榄1号位
坦帕湾海盗(英语:Tampa Bay Buccaneers)是一支位于佛罗里达州的坦帕湾职业美式橄榄球球队。他们是全国橄榄球联盟的南区其中一支球队。在1976年,与西雅图海鹰成为NFL的球队。球队在最初的两个球季连败26场,在二十世纪七…...
Xmake v2.7.7 发布,支持 Haiku 平台,改进 API 检测和 C++ Modules 支持
layout: post.cn title: “Xmake v2.7.7 发布,支持 Haiku 平台,改进 API 检测和 C Modules 支持” tags: xmake lua C/C package modules haiku cmodules categories: xmake Xmake 是一个基于 Lua 的轻量级跨平台构建工具。 它非常的轻量,没…...
苹果ios签名证书的生成方法
在使用hbuilderx打包uniapp或html5应用的时候,假如是打包ios应用,是需要ios签名证书,和证书profile文件的,这个证书要求是p12格式的证书,profile文件又叫描述文件。 这两个文件,需要在苹果开发者中心生成&…...
c++开发配置常用网站记录
1.ubuntu 镜像源: (1) 清华源:https://mirror.tuna.tsinghua.edu.cn/help/ubuntu/ (2) 阿里源:https://developer.aliyun.com/mirror/ubuntu?spma2c6h.13651102.0.0.3e221b11VuM27s 包含了ubuntu各个版本的source源 2.ubuntu iso镜像下载…...
DC-1 靶场学习
以前写过了,有一些忘了,快速的重温一遍。 DC一共九个靶场,目标一天一个。 文章目录环境配置:信息搜集:漏洞复现:FLAG获取环境配置: 最简单的办法莫过于将kali和DC-1同属为一个nat的网络下。 信…...
oracle 不使用索引深入解析
首先,我们要确定数据库运行在何种优化模式下,相应的参数是:optimizer_mode。缺省的设置应是"choose",即如果对已分析的表查询的话选择CBO,否则选择RBO。如果该参数设为“rule”,则不论表是否分析…...
VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...
【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...
【JavaWeb】Docker项目部署
引言 之前学习了Linux操作系统的常见命令,在Linux上安装软件,以及如何在Linux上部署一个单体项目,大多数同学都会有相同的感受,那就是麻烦。 核心体现在三点: 命令太多了,记不住 软件安装包名字复杂&…...
html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...
初探Service服务发现机制
1.Service简介 Service是将运行在一组Pod上的应用程序发布为网络服务的抽象方法。 主要功能:服务发现和负载均衡。 Service类型的包括ClusterIP类型、NodePort类型、LoadBalancer类型、ExternalName类型 2.Endpoints简介 Endpoints是一种Kubernetes资源…...
无人机侦测与反制技术的进展与应用
国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机(无人驾驶飞行器,UAV)技术的快速发展,其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统,无人机的“黑飞”&…...
AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...
