【Android】答题系统Web服务器APP应用开发流程详解
之前开发出来的答题系统服务器放在PC台式机或者笔记本电脑上存在明显不足,无法24小时持续运行,存在高能耗及设备寿命短等问题,持续高负荷运行不仅耗电量大,还会加速硬件老化,反应迟钝,如何解决呢。
为了更节能和延长设备寿命,笔者思考着更优解决方案
- 从电子设备的发展趋势显示,微型化设备在功耗和便携性方面具有显著优势;
- 体积更小的设备通常能耗更低,且便于携带和管理;
- 安卓系统手机或闲置旧手机可作为理想替代品;
- 安卓系统这些设备体积小、功耗低,适合长时间运行服务器应用;
- 闲置手机重新利用起来既环保又经济,能够有效解决原有方案的不足;
文章目录
- 新建项目
- 新建页面
- 实现功能
- 1. HTTP服务
- 开始服务
- 安装模块
- 后台运行
- 2. 访问服务
- 打开浏览器
- 二维码访问
- 3. 更新题库
- 下载题库
- 题库来源
- 下载服务器
- 下载失败问题
- 运行效果
- 项目源码
对新手读者来说,手里也有闲置的手机话,对软件开发很感兴趣,但是不知道怎么给其开发安卓应用,这里就展开讲一下,方便新手学习入门。
新手或许有点感觉陌生,接下来说的Android系统也叫安卓系统
开发答题服务器APP,这对设备性能要求较低,普通智能手机即可胜任服务器角色,通过家庭网络局域网实现随时都可访问答题系统。
这里以安卓系统的手机APP开发为例,笔者使用开发工具是Android Studio 版本2024,
若新手想要安装AndroidStudio开发工具,可参考以下文章:
- 【Android】安装2025版AndroidStudio开发工具开发老安卓旧版App
开发安卓系统的手机APP需要有以下基础
- 用Java语言开发过任意程序,
- 有学习过用旧的Eclipse ADT工具开发过安卓app,
新手有此基础,学习使用Android Studio开发工具会顺利上手的
新建项目
打开Android Studio开发工具,选择新建项目New Project时,
选择其中No Activity
,这一项,开发的App项目支持在老旧的安卓系统版本上运行,
点击Next
下一步,到新建项目的信息填写里,如下图,
- Language - 开发语言,选择最早支持的Java;
- Minimum SDK - 最低系统版本,选择API 19, 也就是Android 4.4;
- Build configuration language - 构建工具的语言, 开发工具最早使用的Groovy DSL(build.gradle);
笔者手里有好几个闲置的老旧安卓系统手机,最低安卓系统版本是4.4.4,
为了兼顾到最低的安卓系统版本,所以这里就选择最小Android 4.4,
点击最后Finish
完成,
然后观察开发工具的处理进度条,通常在右下角位置,等待它处理完成,
准备就绪,可以开始了,
新建页面
在选择Android的项目结构下,展开java分支,
选中包名(Package name),按鼠标右键选择,新建第一个页面,如下图
新建空白的页面按照步骤:
New
→Activity
→Empty Views Activity
出现的窗口,如下图,
点击OK确定新建,接下来又是等待进度条完成,
页面自动创建好了,Android项目结构下,会多出来以下两个文件待编辑,分别说明如下
- MainActivity.java - 页面的代码逻辑文件,这里写代码;
- activity_main.xml - 页面的布局文件,这里设计页面;
那个页面的布局文件要先做好,系统会自动创建好页面上的所有布局控件供开发者调用,这样能提高开发效率,让开发者专注于实现功能逻辑代码;
修改布局文件activity_main.xml
,
页面布局做好后,运行效果如下图:
知道怎样按上图布局控件吧,布局很简单的,这可是新手必备的基础操作,这里就不贴布局内容了
然后,修改页面逻辑文件MainActivity.java
,
写页面的逻辑代码,处理初始化,代码如下
public class MainActivity extends AppCompatActivity {//...@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);//...tvAddress = findViewById(R.id.textViewServerAddress);btnStart = findViewById(R.id.buttonStartServer);btnStop = findViewById(R.id.buttonStopServer);btnOpen = findViewById(R.id.buttonOpenBrowser);btnStart.setOnClickListener(s->startServer());btnStop.setOnClickListener(s->stopServer());btnOpen.setOnClickListener(s->openBrowser());//...}//...
}
这看起来也很简单吧,页面控件已经初始化好了,这里就是把页面的布局控件对象都获取出来,方便后面调用
还有其它初始化逻辑,不是重要的,例如以前输入了什么内容会保存起来,这里等下次打开App需要读取出来设置到控件中,还有处理授权,防止CPU休眠等等. 有很多需要自己实现,这都是小问题,所以,继续讲那些没必要吧。
实现功能
用户操作的那些主要功能,需要一个个实现,
1. HTTP服务
当点击开始服务按钮时,需要去开启一个服务器,
开始服务
调用的方法是startServer()
,实现代码如下
private void startServer(){if (!isServiceBound) {// 绑定服务Intent intent = new Intent(this, WebServerService.class);bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);}// 启动服务(会创建前台通知)Intent serviceIntent = new Intent(this, WebServerService.class);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {startForegroundService(serviceIntent);} else {startService(serviceIntent);}showToast( "服务器开启");}
}
从代码中看出,它是绑定一个服务类
WebServerService.class
来启用一个HTTP服务,
这服务器是在后台运行的,为了好控制,需要通过页面逻辑来绑定它
需要写一个类文件WebServerService.java
,这里面写逻辑,实现服务,代码如下
public class WebServerService extends Service {//...@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {// 启动Web服务器startWebServer();// ...return START_STICKY; // 系统会重启服务}//...
}
从上面代码看出,该类继承了服务类
Service
,服务类的一些方法可选择哪些实现,
上面其中方法onStartCommand
就是前面调用startService(serviceIntent)
时就会触发的,
调用方法startWebServer()
,代码如下
private void startWebServer() {if (webServer == null) {try {webServer = new AndroidWebServer(SERVER_PORT,this);webServer.start();// 启动成功...} catch (Exception e) {// 启动失败...}}}
从代码中看出,
AndroidWebServer
类是要自己实现的服务逻辑类文件,传入的第一个参数是端口号,如数字类型8080,
这上面执行了方法start()
,它启动了HTTP(web)服务器,
如何实现HTTP服务功能呢,细节过于复杂,不过没关系,利用现有的插件就行(用别人写好的轮子)
这里就借助开发的构建工具,去安装一个模块插件 implementation libs.nanohttpd
来使用,里面集成了服务器的功能,如何使用呢;
要写一个服务逻辑文件AndroidWebServer.java
,在里面实现如何使用它,
public class AndroidWebServer extends NanoHTTPD {private final Context context;public AndroidWebServer(int port, Context context) {super(port);this.context = context;}@Overridepublic Response serve(IHTTPSession session) {return super.serve(session);}
}
从上面代码看出,继承一个类
NanoHTTPD
,实现它的方法serve(session)
,去处理请求页面路由和返回内容就可
类NanoHTTPD
关键词是模块插件里的,如果没有安装好这个模块,代码里就会飘红,上面提示报错引用了不存在的类,
安装模块
这里用一个能实现HTTP服务器的模块插件,
如何安装模块插件NanoHTTPD
,
打开项目下的libs.versions.toml
文件,在对应项下面添加它的版本号和模块信息,
[versions]
//...
nanohttpd = "2.3.1"[libraries]
//..
nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
还要在app模块文件build.gradle
中添加,内容如下
//...
dependencies {//...implementation libs.nanohttpd
}
添加后,编辑器上方会出现一个提示,可点击Sync Now
,执行Gradle同步,开始联网安装,
等模块安装成功后,再看看一个类文件AndroidWebServer.java
,会发现里面继承的模块NanoHTTPD
类关键词不会飘红了,
接着实现服务器的功能,代码如下,在方法里serve(session)
实现
public class AndroidWebServer extends NanoHTTPD {//...@Overridepublic Response serve(IHTTPSession session) {//return super.serve(session);//这里实现对答题题库资源的读取和处理网页请求返回...}
}
处理请求返回的所有资源包括答题文件都放在Android项目里的assets
文件夹里面的,
在里面还有个文件夹wwwroot
,是存放H5页面的,由以下uniapp项目发布H5生成
- 答题-答卷系统-小程序-uniapp-项目源码
这就是服务器实现大概过程,上面调用它的方法start()
就会开始服务,停止服务就调用它的方法stop()
,
启用一个服务器功能,是需要每天24小时运行的,
如果不在后台保持运行,等安卓设备屏幕自动一关,或进入待机模式,那服务器就会停止,访问时突然停止响应,需要你来重新打开,这样很闹心;
后台运行
要想保持后台运行,
需要在上面的一个服务类中继续完善,添加对应权限,这样锁屏后会继续运行,能保持在24小时长期运行,
使用后台服务,就需要往配置文件AndroidManifest.xml
里添加allowBackup=true
,
允许后台运行,内容如下
<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools">...<uses-permission android:name="android.permission.WAKE_LOCK" /><uses-permission android:name="android.permission.FOREGROUND_SERVICE" />...<application...android:allowBackup="true"... /></application>
</manifest>
仅仅上面的保持应用在后台长期运行的条件是不够的,还需要你打开手机系统设置里面的省电策略,还有安全管理下的锁屏清理程序,只要不kill掉本应用就基本没问题了。
2. 访问服务
要想看访问服务器的答题页面查看效果,就需要打开浏览器,
打开浏览器
当点击其中的打开浏览器按钮,就是调用方法openBrowser()
,
实现代码如下
private void openBrowser(){String url = webServerService.getServerUrl();if (url==null || url.isEmpty()) {showToast( "无法获取网络IP地址");} else {// 打开浏览器访问Web服务器LinkUtils.openUrl(this, url);}}
从上面代码中看出,获取到服务器地址,就调用了LinkUtils
类中方法openUrl()
打开浏览器,
这个LinkUtils类也需要自己实现,实现不来的话,就看看项目源码吧,
不过,太旧的安卓手机自带的浏览器可能无法正常加载服务器的H5页面,是因为浏览器内核是Chrome的版本太旧,已不能支持执行现代的js脚本
经过笔者测试,旧安卓手机系统版本在
Android 4.4
,Android 5.0
,Android 6.0
上的自带浏览器是无法正常加载H5页面
要能正常访问,可试试安装用新的浏览器打开,
也可以用你自己常用的手机浏览器扫码访问(若还访问不了,笔者可以肯定你仍在用旧手机的浏览器!=.=)
二维码访问
要让另外的手机访问答题服务器的答题页面,需要手动输入IP地址,
输入IP感觉太麻烦吧,就弄个二维码显示,这样用手机扫码就可直接访问了,
当服务器的手机每次连接WIFI热点,其IP可能会变动,弄个二维码就没事;
要使用它,需要安装谷歌的zxing
模块插件,
就在app模块文件build.gradle
中添加,内容如下
//...
dependencies {//...implementation 'com.google.zxing:core:3.5.2'implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
}
添加后,编辑器上方会出现一个提示,点击Sync Now
,执行Gradle同步,开始联网安装,
安装好了,写一个类QRCodeUtils.java
文件,
这里面实现如何调用这个模块,代码如下
public class QRCodeUtils {public static Bitmap generateQRCode(String content, int width, int height){try {Hashtable<EncodeHintType, Object> hints = new Hashtable<>();hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);hints.put(EncodeHintType.MARGIN, 1);BitMatrix bitMatrix = new QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);return createBitmapFromBitMatrix(bitMatrix);} catch (WriterException e) {e.printStackTrace();return null;}}//...
}
然后,在服务器成功开启的事件中添加如下代码
Bitmap qrCode = QRCodeUtils.generateQRCode(serverUrl, 300, 300);
if (qrCode!=null){ImageView iv = findViewById(R.id.imageView);iv.setImageBitmap(qrCode);ll.setVisibility(View.VISIBLE);
}
从上面代码中看出,调用方法generateQRCode()
就能生成图片对象,把图片设置到页面的控件ImageView
中,
在页面的一排按钮下面,展示二维码图片,
这个二维码可以方便让另外的手机浏览器或微信扫码就能访问
是不是没意思,想象一下:
- 在学校,模拟考试场,老师让学生用手机扫码进入考试;
- 在家里,家长要考察孩子的学习成绩,让孩子用手机扫码进入答题;
- 我
不会告诉你很多…
3. 更新题库
服务器的题库是会经常改动的,
在往后的使用中,我们会从电脑上添加很多资料到题库资源中,
因此需要服务器会更新题库才行,
实现更新题库功能,可以让答题服务器的题库更快的更新,
下载题库
更新题库的过程就是,需要先实现下载压缩文件,然后解压它,
最新的题库是一个压缩包,是从电脑上的桌面程序做出来的,
如何实现呢,先实现一个请求服务器下载一个文件,
然后实现解压文件,放到App的缓存文件目录中,
把原来的题库文件都清空了,再放新的题库在里面,
让服务器从这个题库目录中读取文件,用来响应页面的请求返回内容,
按照这个步骤实现,要做的细节很多,这里就不讲,自己能实现吧,
剩下的一些代码还有很多,这里不详细讲了,完整代码可以看文章提供相关项目源码,
题库来源
有个电脑桌面程序就是用来做题库文件的,在之前的文章就讲过,可以看这篇文章
- WPF-答题系统题库编辑工具桌面程序开发项目流程详解
桌面程序的菜单下有个题库
选项,展开后有个生成压缩文件
可点击,如下图
可以将自己做好的题库打包,生成的压缩文件就是打包好的题库资源文件,
若需要桌面程序,可自己做一个来用,参考以下源码
- WPF-桌面程序-答题系统-题库编辑器 项目源码
也可以用作者已建好的题库资源,就放在GitCode代码托管平台上
- resource_mp_answer
题库就是一个ZIP压缩文件
下载服务器
把压缩文件放到电脑的下载服务器上,可以用电脑自带的IIS服务器来下载文件,需要自己开启,
怎么开启呢,从电脑上搜索IIS即可找到,如下图
找不到IIS?电脑系统需要安装IIS,参考 Windows 11 IIS服务器安装与配置
打开后,选择启用默认的网站Default Web Site
,如下图
点击图中右侧的浏览,会打开一个文件夹,
然后把那个题库压缩包文件放在电脑启用的IIS服务器上的默认wwwroot
文件夹里,如下图
只要电脑在服务器的局域网内,就可访问到电脑的IP地址,地址格式像这样:
http://[IP地址]:80/resource_mp_answer.zip
不会查看自己的电脑IP地址?参考 查看电脑的局域网IP地址,
在安卓APP的答题服务器里粘贴输入这个地址,就能访问下载更新题库,
下载失败问题
都弄好了,运行看效果吧,可能会遇到以下问题,看如何解决,
当点击更新题库按钮,请求访问下载时,可能报错如下:
ClearText HTTP traffic to IP地址 not permitted
原因是联网安全导致的,原http请求需要改成https请求,
由于是在家庭网络局域网内访问,这里用不到https(这会降低设备性能,执行加解密过程,会消耗计算资源),
使用http请求,就需要往配置文件AndroidManifest.xml
里添加usesCleartextTraffic=true
,
解除安全限制,内容如下
<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools">...<application...android:usesCleartextTraffic="true"... /></application>
</manifest>
如果不是上面的问题,那可能是你填入的下载地址有问题吧,再仔细看看,笔者当时没遇到还有别的问题呢。
解决后,重新编译运行看看
运行效果
这里笔者做出来后,运行在真机上,效果图如下
点击开启服务按钮后,就是上面的效果截图,同时展示了二维码方便另外的手机扫码访问,该有的功能都有;
需要注意:
- 不要运行在开发工具自带的模拟器上,因为IP地址不在一个局域网段上,电脑与模拟器的服务器无法互相访问,
- 扫码访问需要那些手机设备和服务器同在一个局域网段哦,也就是一起连接一个WIFI路由器(WIFI热点),
写到这里,知道笔者做出来这个是为了什么吧,打造良好的学习工具可以帮助整理思路、节省时间,并提高理解深度。无论是打造什么学习工具软件,正确的选择都能让学习事半功倍。
闲置手机弃之可惜,留着自用,对开发者而言仍具实用价值。通过合理配置,闲置设备可转化为持续运行的答题服务器,为家庭成员提供便捷的学习与复习工具,实现随时访问的教育资源。
为了不让它运行到没电而自动关机,需要插上充电器,
长期插上充电器不会有事吧,担心忘记取下来,有担心一会没电,这是可以解决的:
- 需要将充电器连接至智能(定时)插座,设定充电时间段,定时插座按预设时间自动开启充电,到达截止时间后自动断电,形成循环充电模式;
- 定时断电机制防止手机长时间处于满电状态,减少电池损耗,循环充电模式维持电量在合理区间,避免过充或深度放电,延长电池寿命;
- 自动开关功能无需手动操作,节省电力消耗,通过固定时间段的充放电循环,确保设备随时可用,同时减少用户频繁管理的麻烦;
项目源码
- 相关项目源码 点此查看,
- 更多的源码 点此查看,
- 更多的资源 点此查看,
如果下载了项目源码,会发现一个问题,也是笔者后来发现的,需要改一下项目源码,
问题是在:
点击更新题库后,提示更新成功,实际上访问答题页面时发现没有更新,
调查出结果:
题库更新没问题,是修改更新前端H5项目源码时写错了,需要改下,
解决步骤:
在项目中找到这个文件index-BEnBRmpL.js
,这个是编译后的源码文件,
其位置在项目文件夹的 app/src/main/assets/wwwroot/assets/
目录下,
在开发工具上打开这个文件,按Ctrl+F快捷键,调出查找工具,输入查找文本内容如下
/api/version`,fail:t,success:t=>{console.log("ready res",t),"string"==typeof t.data&&/^\d+(\.\d+)+?$/.test(t.data)?e({url:`${location.origin}/api`
编译后的源码文件如果看不懂,不用管它,那代码是压缩了,把多条语句写到一行上,把文件变小了
在第二个输入框输入替换内容,将其替换为
/assets/index-BEnBRmpL.js`,fail:t,success:t=>{"string"==typeof t.data?e({url:`${location.origin}/resource_mp_answer`
输入后,点击查找,找到后如下图:
然后,点击 Replace
按钮,替换完成,
保存修改的文件后,重新编译运行试试看问题是否解决了。