ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java安卓物联网开发入门:从Gradle构建到传感器数据上报

Java安卓物联网开发入门:从Gradle构建到传感器数据上报

简介:面向移动物联网入门者的Android开发起步资源,基于Java语言,系统梳理从开发环境搭建、SDK配置到Activity与Intent、布局设计、事件监听、数据存储、网络通信、传感器接入等12项核心技能,适合零基础或刚接触Android的学生开发者,也可作为高校移动开发课程的辅助材料。压缩包内共132个文件,其中xml配置与布局文件77个、java源码与gradle脚本各3个,另有properties、png、jar等辅助资源,整体945KB,结构紧凑,便于快速下载查阅。压缩包提供“FirstApp”示例工程,将Android生命周期、组件通信、UI布局、数据存储及网络请求等知识点融入可运行代码,读者可对照描述逐步拆解,也可结合AndroidManifest.xml、gradle配置和单元测试用例理解工程组织方式,掌握物联网App从界面到硬件交互的完整链路。已有283人学习下载,适合作为课堂补充或自学入门的第一份演练材料。

1. 拿到 "01_FirstApp" 这个包,先别急着找 Activity

"01_FirstApp_java安卓移动物联网开发app教程_" 这个资源包,第一次解压会吓到不少人:根目录下躺着一堆 gradlew.bat、executionHistory.bin、taskHistory.bin、fileHashes.bin,看起来像项目损坏或者打包打错了。其实这些是 Gradle 构建缓存,和源码没有关系,真正要看的代码在 app/src/main 下面。这份资源的核心价值,是把 Java 安卓移动物联网开发的地基部分——界面、事件、网络、存储、传感器——浓缩成一个可运行的入门 APP 示例。适合刚掌握 Java 语法想转安卓开发的人,也适合做过普通 APP 但没碰过 IoT 上报链路的人。这篇笔记按实际拆包的顺序来写:先解决环境与构建,再让界面和 Activity 跑起来,最后打通网络与传感器的数据链路,收在坑和验证上。

2. 搭建环境与理清项目结构:gradlew.bat、SDK 路径与构建缓存的分工

2.1 项目包里那堆 .bin 文件到底是什么

解压后如果看到 executionHistory.bin、fileSnapshots.bin、fileHashes.bin、jarAnalysis.bin、taskHistory.bin 这些文件,先别动。它们存放在 .gradle 或 build 目录里,是 Gradle 增量构建时用来对比输入输出、判断哪些 task 需要重新执行的本地缓存。举个例子,fileHashes.bin 记录每个源码文件的哈希,你改了一个类,Gradle 对比哈希后只重编译被影响的部分,能省不少时间。

# 在项目根目录执行,先看清目录结构 tree . -L 2 --dirsfirst

如果系统没有 tree,用 find 替代:

find . -maxdepth 2 -type d | sort

-L 2限定只显示两层,避免刷屏;--dirsfirst让目录排在文件前面。find 的-maxdepth 2也是限制深度,-type d只列目录,配合sort排序。

一个合格的项目结构应该长这样:

settings.gradle build.gradle gradle.properties gradlew.bat app/ build.gradle src/main/ AndroidManifest.xml java/com/example/firstapp/MainActivity.java res/layout/activity_main.xml

这里只有 settings.gradle、build.gradle 和 app/src/main 下的文件是手写的核心,其它都能自动生成。如果你只想复现工程效果,把这些 .bin 缓存删掉再重新构建完全没问题。常见做法是:拿到这类项目后先执行一次 clean——把别机器上的缓存路径带到你本地,反而会因为路径不一致导致构建异常。

2.2 local.properties 与 SDK 路径对齐

命令行构建和 IDE 构建都依赖 SDK 路径。项目根目录的 local.properties 是本地配置文件,里面就一行关键内容:

sdk.dir=D\:/Android/Sdk

注意 Windows 下盘符的冒号要转义成\:,否则解析会出问题。如果这个文件缺失,gradlew 会直接报SDK location not found。用 Android Studio 打开项目时,通常会自动生成这个文件;但如果你是在命令行下用 gradlew.bat 构建,就必须保证它存在且路径正确。

路径里有中文或空格也是常见翻车点。某开发者曾把 SDK 放在D:\开发环境\Android Sdk这类目录下,构建工具解析时对空格和中文的处理不一致,最后直接把 SDK 挪到纯英文路径才解决。我一般会提前确认sdk.dir指向的目录下存在 platforms 和 build-tools 子目录,再继续下一步。

2.3 用 gradlew.bat 完成第一次构建

项目里的 gradlew.bat 是 Gradle Wrapper 的 Windows 入口,它的作用是读取 gradle/wrapper/gradle-wrapper.properties 里指定的 Gradle 版本,自动下载对应发行版并执行构建,保证所有人用同一个构建版本:

cd /d D:\projects\01_FirstApp .\gradlew.bat assembleDebug

cd /d可以跨盘符切换,在 cmd 里必须带这个参数才能从 C 盘切到 D 盘。assembleDebug是生命周期任务,生成带调试签名的 debug 包,产物在 app/build/outputs/apk/debug/app-debug.apk。

首次构建会下载 Gradle 发行版和项目依赖,耗时长短取决于网络。如果卡在下载依赖上,常见做法是修改 settings.gradle 里 pluginManagement 的仓库配置,或者 build.gradle 的 repositories,换成国内可达的 Maven 仓库地址。换完仓库后,最好把.gradle目录下的缓存删掉再重试,避免旧缓存干扰。构建失败的另一个高频原因,是compileSdk要求的 SDK 版本本地没装,看 Android Studio 的 SDK Manager 安装对应 platform 即可。

3. Activity 与布局:让第一个界面从 XML 变成可点击的页面

3.1 Activity 生命周期:界面加载的前后顺序

第一个 APP 里通常就一个 MainActivity。App 启动时,系统创建 Activity 实例并回调一系列生命周期方法,先onCreate,再onStart,再onResume。控件初始化的代码要放onCreate,因为它是整个生命周期里最早、且只会执行一次的入口:

public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 将 XML 布局解析成 View 树,setContentView 之后才能 findViewById setContentView(R.layout.activity_main); // 如果系统回收过 Activity,这里能拿到之前保存的状态 if (savedInstanceState != null) { String saved = savedInstanceState.getString("last_value"); Log.d("FirstApp", "恢复状态:" + saved); } } }

savedInstanceState在屏幕旋转、进程被系统回收后会携带之前保存的数据,其它情况是 null。这里有一个新手容易踩的认知偏差:onCreate只执行一次没问题,但onResume每次回到前台都会执行,所以注册传感器监听这类动作放onResume比放onCreate更稳妥。onStart到onResume之间的间隔很短,通常不用重写。

3.2 XML 布局:LinearLayout 与 ConstraintLayout 怎么选

教程里的第一个界面,十有八九是 LinearLayout 堆出来的。它理解成本低,控件从上到下或从左到右排列,适合初学。activity_main.xml 的核心部分:

<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_temperature" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="当前温度:--" /> <Button android:id="@+id/btn_collect" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="采集一次" /> </LinearLayout>

match_parent表示和父容器一样宽,wrap_content表示只包裹内容本身。padding用 dp 而不是 px,因为 dp 在不同密度的屏幕上显示尺寸一致。LinearLayout 的问题是嵌套多了会影响性能,三层以上嵌套会明显增加测量时间。界面复杂度上来之后,我会换成 ConstraintLayout,它用约束关系把布局拍平,嵌套层级少得多,缺点是新手写起来容易绕。初学先用 LinearLayout 理解 View 的测量和摆放,不丢人。

3.3 按钮事件与 Intent 跳转

界面静止没有意义,按钮点击要能触发动作。给按钮设置监听器是标准做法:

Button collectBtn = findViewById(R.id.btn_collect); collectBtn.setOnClickListener(v -> { // 显式 Intent,明确指定要打开的目标 Activity Intent intent = new Intent(MainActivity.this, SensorActivity.class); intent.putExtra("mode", "single"); startActivity(intent); });

findViewById根据@+id/btn_collect找到按钮实例,setOnClickListener接收一个回调接口,点击时执行 Lambda 中的逻辑。这里的 Intent 是显式的,直接指定了目标 Activity 的类名。与之相对的是隐式 Intent,你需要定义一个 action 字符串,并在 AndroidManifest.xml 里配置 intent-filter,常用于跨应用跳转,比如调起系统相机。同 APP 内部跳转,显式 Intent 足够。

putExtra是给目标 Activity 传参的标准方式,支持字符串、整型、序列化对象等。目标 Activity 里用getIntent().getStringExtra("mode")取回。老教程里会看到startActivityForResult,用于跳转后拿返回值,新项目已经改用 Activity Result API,初学阶段知道onActivityResult是旧写法就行,不用刻意去学。

4. 打通物联网数据链路:HTTP 上报、本地存储与传感器接入

4.1 用 OkHttp 上报数据:请求与回调线程要注意

物联网场景的核心动作,是定时把设备状态上报到服务器。Java 原生自带的 HttpURLConnection 能用但写法啰嗦,我一般直接用 OkHttp。先在 app/build.gradle 的 dependencies 里加依赖:

dependencies { implementation 'com.squareup.okhttp3:okhttp:4.12.0' }

上报一段 JSON 数据的完整写法:

private void reportTemperature() { OkHttpClient client = new OkHttpClient(); String json = "{\"device_id\":\"dev_001\",\"temperature\":25.6}"; RequestBody body = RequestBody.create( json, MediaType.parse("application/json; charset=utf-8") ); Request request = new Request.Builder() .url("http://10.0.2.2:8080/api/data") .post(body) .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e("FirstApp", "上报失败", e); } @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody respBody = response.body()) { if (response.isSuccessful() && respBody != null) { JSONObject obj = new JSONObject(respBody.string()); String code = obj.optString("code"); Log.d("FirstApp", "服务器返回 code=" + code); } } catch (JSONException e) { Log.e("FirstApp", "JSON 解析失败", e); } } }); }

10.0.2.2是 Android 模拟器访问宿主机 localhost 的特殊地址,真机调试要换成开发机的局域网 IP。enqueue是异步请求,回调发生在 OkHttp 的工作线程,不能在这里直接更新 UI,要改界面得用runOnUiThread或 Handler。ResponseBody必须关闭,用 try-with-resources 写法最稳,否则连接池会被占满。optString和getString的差别是:前者取不到返回空串或默认值,后者直接抛异常,解析服务端响应时我习惯用 opt 系列。

4.2 SharedPreferences 做轻量存储:保存设备编号与上报间隔

物联网设备通常需要记住设备 ID、上报间隔这类小配置,用 SharedPreferences 最合适,它本质是写入一个 XML 文件:

SharedPreferences sp = getSharedPreferences("device_config", MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putString("device_id", "dev_001"); editor.putLong("report_interval", 5000L); // apply 异步落盘,主线程调用不会卡 IO editor.apply();

getSharedPreferences的第一个参数是文件名,第二个MODE_PRIVATE表示只有本应用能读。数据实际存在/data/data/包名/shared_prefs/device_config.xml里。apply是异步提交,先把数据更新到内存,再异步写盘,日常使用足够;commit是同步提交并返回布尔值,只有当你需要立刻知道写入结果时才用它。这里要注意边界:SharedPreferences 只适合小体量键值对,设备历史数据、传感器采集序列要想清楚再决定是否入库,数据量大就上 SQLite。

4.3 接入加速度计:传感器监听与生命周期绑定

开始之前先想清楚你的 APP 要读什么数据。加速度计是最常见的入门传感器,不需要额外权限,但不同设备硬件能力差异很大,接入前务必判空:

SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE); Sensor accelerometer = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); if (accelerometer == null) { Log.w("FirstApp", "设备没有加速度计"); return; } SensorEventListener listener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { float x = event.values[0]; float y = event.values[1]; float z = event.values[2]; float magnitude = (float) Math.sqrt(x * x + y * y + z * z); Log.d("FirstApp", "加速度模值=" + magnitude); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } }; sm.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_GAME);

SENSOR_DELAY_UI对应约 60ms 的采样间隔,SENSOR_DELAY_GAME更密集,适合运动检测。采样频率越高越耗电,实际项目要按上报需求调,不要盲目用最快档。onSensorChanged的回调频率可能高达每秒几十上百次,不要在回调里做 JSON 拼接或网络请求,只做轻量计算,再按阈值或定时器触发上报。registerListener之后,必须在 Activity 的onPause或onStop里注销监听器,否则 Activity 销毁后回调仍然活着,会造成泄漏或持续耗电。

5. 避坑与排查:安卓入门最常见的五个翻车点

5.1 双击 gradlew.bat 窗口一闪而过

现象:Windows 上双击 gradlew.bat,黑窗口一闪就结束,看起来像什么都没发生。

原因:批处理脚本执行完自动关闭终端,你根本来不及看输出;而且双击时工作目录不一定在项目根目录,脚本找不到项目文件,直接退出。

解决:打开 cmd,先切到项目目录再执行:

cd /d D:\projects\01_FirstApp .\gradlew.bat assembleDebug

cd /d负责跨盘符切换,执行后注意观察输出里有没有BUILD SUCCESSFUL,看到这个才算构建通过。

5.2 报错 SDK location not found

现象:执行 gradlew 或打开工程,提示SDK location not found. Define a valid SDK location with an ANDROID_HOME environment variable or by setting the sdk.dir path in your project's local.properties file.

原因:local.properties 文件缺失,或者 sdk.dir 写的位置根本不存在;路径里的中文、空格也会让解析出问题。

解决:在项目根目录新建或修改 local.properties:

sdk.dir=D\:/Android/Sdk

把路径指向你本机实际安装的 SDK 目录,确认里面有 platforms 和 build-tools 子目录。我把 SDK 放在纯英文路径下之后,这类报错基本绝迹。

5.3 网络请求报 Cleartext HTTP traffic not permitted

现象:OkHttp 请求直接抛异常,日志里出现Cleartext HTTP traffic not permitted。这里说的是开发调试时用明文 HTTP 访问内网服务器的正常场景,不涉及任何代理或工具,只是 Android 对加密流量的默认策略。

原因:Android 9 开始默认禁止应用使用明文 HTTP,而开发阶段我常用http://访问本机或内网服务,于是撞上限制。

解决:开发阶段临时在 AndroidManifest.xml 的 application 标签里放开:

<application android:usesCleartextTraffic="true" ...>

需要注意,这只适合开发调试环境;正式上线的 IoT 服务,应当用 HTTPS。另外,模拟器里用localhost访问宿主机是走不通的,必须用10.0.2.2,这点和明文限制叠加在一起,经常是两个人排查半天才发现问题。

5.4 模拟器上传感器没反应

现象:真机上能正常回调onSensorChanged,换到模拟器上一次都不触发。

原因:模拟器默认没有模拟加速度计硬件,getDefaultSensor(TYPE_ACCELEROMETER)返回 null,或者注册了监听但底层没有数据源。

解决:用支持传感器的真机调试。代码里先判空,给用户提示,避免崩溃:

if (getDefaultSensor(Sensor.TYPE_ACCELEROMETER) == null) { // 提示当前设备不支持加速度计 }

5.5 旋转屏幕后 Activity 重建,状态全丢

现象:竖屏采集了一组数据,屏幕一转,界面回到初始状态,刚才的数据没了。

原因:屏幕方向是配置变更的一种,默认行为是销毁当前 Activity 再重建,所有未持久化的内存状态都丢失。

解决:看场景选方案。简单状态用onSaveInstanceState保存:

@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString("device_id", currentDeviceId); }

如果是传感器采集页面,转屏本身没价值,直接在 AndroidManifest.xml 里固定竖屏更省事:

<activity android:name=".SensorActivity" android:screenOrientation="portrait" />

固定方向是取舍题,适合单一数据采集场景;如果界面需要适配横屏,老老实实做状态保存和恢复。

6. 验证方法:从 Logcat 到 ADB,一个不能少的确认步骤

带过几次项目之后,我特别怕听到一句"我代码应该没问题吧"——代码有没有问题,得上设备看数据流动才算数。我自己的验证顺序固定成一套,拿到这类教程工程先按这个流程走:

第一步,确认设备连接:

adb devices

输出里有List of devices attached,下面出现一行设备串号加device状态才算连上。如果找不到设备,先检查 USB 调试开关和驱动。

第二步,直接装包:

adb install -r app/build/outputs/apk/debug/app-debug.apk

-r表示覆盖安装且保留数据,反复调试时不用先卸载。

第三步,过滤日志。我不推荐无脑adb logcat,刷屏太快。统一用 Tag 过滤:

adb logcat -s FirstApp:D

-s是静默模式,只显示指定 Tag 的日志;FirstApp:D表示只输出 Tag 为 FirstApp 且级别不低于 Debug 的日志。要带时间戳就加-v time。如果传感器数据、服务器返回 code 都按顺序出现在日志里,这条链路才算真的通了。

验证通过之后,我会顺手看一眼 Android Studio 的 Profiler 里的内存曲线,确认没有明显的锯齿形泄漏。从那以后,我每次拿到类似的项目包,都会强制走一遍这个顺序:看结构、清缓存、构建、装到真机、拉日志、确认数据真的从传感器走到服务器,才敢说代码没问题。希望这篇文章能帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进