信息发布系统(安卓+PC)实战:从HTTP服务到WebView播放器

📅 发布时间:2026/10/16 18:26:22
信息发布系统(安卓+PC)实战:从HTTP服务到WebView播放器
简介面向餐饮、零售等门店的多媒体信息发布场景这套「信息发布系统安卓PC」提供可在安卓大屏终端运行的播放端App及配套PC端管理软件支持视频、图片、字幕等多格式文件播放并具备场景编排、节目单定时播放、后台预下载、播放数据校验等功能适配单机或局域网环境源码完全开源便于开发者二次集成与部署。压缩包共134个文件约111.62MB以Java源码、XML配置、PNG图标、TTF字体及Gradle构建脚本为主另含少量C/JAR辅助模块与配置文件项目结构清晰导入Android Studio或对应IDE即可梳理主干逻辑。资源已吸引5564人学习浏览适合有安卓开发基础、希望快速搭建或定制信息发布系统的技术人员。通过阅读源码可掌握串口通信、播放校验、后台下载及多场景调度等关键实现为后续功能扩展提供直接参考。1. 信息发布系统安卓PC先搞清它到底要帮你解决什么一家连锁门店想把促销海报、公告、排队叫号信息投到挂在墙上的安卓平板上内容每周更新但每次都得店员拿着U盘一台一台去拷。这个场景听上去很常见解决它的东西就是「信息发布系统安卓PC」一套PC端负责内容制作和下发、安卓端负责播放展示的组合方案。它不是什么新概念行业里也叫数字标牌但在中小规模项目里至今是最务实的选择。我见过不少团队绕开它去做一整套复杂的云平台结果运维成本把项目拖垮。真正决定这套系统能不能用的不是App界面做得多么精致而是PC端那几个HTTP接口是否稳定、安卓终端断网重连时能不能自己恢复。这篇文章就按「先拆架构、再分别落地PC端和安卓端、最后讲坑和进阶」的顺序讲清楚这类系统从零跑通要过哪些坎。2. 架构拆解为什么让PC扛服务端安卓端只做一个能干的播放器拿到一个信息发布系统的工程包常见交付形态是一份PC端工程加一份安卓工程外加部署说明。PC端工程里通常同时包含了内容管理界面和HTTP下发服务安卓工程则是一个独立播放器App。很多人第一次看这个结构会疑惑为什么不干脆把内容管理做成网页为什么要放一个App在终端上下面从三个角度看清楚。2.1 三个角色谁负责做什么信息发布系统安卓PC里的角色划分很清晰。PC端承担两个职能一是内容管理后台运营人员在这里上传图片、视频编辑节目单二是下发服务把编辑好的节目单和素材通过HTTP接口暴露给局域网内的安卓终端。安卓端则只承担一个职能做一个忠诚的播放器按节目单要求把内容一段一段展示出来。这个划分意味着安卓端不需要做内容编辑也不需要复杂的业务逻辑它要处理的只有三件事拉取节目单、判断素材是否有更新、按时间顺序播放。把职责砍到这个程度终端的稳定性压力就小了很多。实际项目里安卓播放器经常被塞进各种低端设备配置只有几百兆内存越简单的App反而越能长久运行。2.2 为什么PC端兼任服务端规模决定架构常见做法是让PC端在局域网里兼任HTTP服务端安卓终端通过轮询方式向它要数据。这样做的前提是项目规模有限通常几十块屏以内、同一个局域网内一台普通PC完全扛得住。我一般会建议先按这个方案做因为独立服务端意味着要额外维护数据库、部署服务、处理公网安全对一个中小型门店项目来说这些成本会吃掉大部分预算。这套架构的边界也由此而来。终端数量超过五六十台或者门店分布在不同的城市局域网方案就会失效那时候才需要换成公网云服务器。但大部分拿着「信息发布系统安卓PC」这类工程包的人面对的正是单店或几个店面的场景局域网轮询足够。PC端在承担HTTP服务的同时素材文件直接存放在本地磁盘目录里不用数据库去存二进制这既省事也避免了数据库文件越撑越大带来的备份问题。2.3 为什么安卓端用WebView而不是原生渲染安卓终端这边节目内容的呈现方式有两条路用原生View拼页面或者用WebView加载HTML。实际工程里绝大多数选后者。原因很直接门店的节目版式经常变有时要加个二维码、有时要改字体颜色原生View每改一次样式就得重新打包APK让运营人员去现场更新。WebView则把版式做成HTML模板后台改一下模板内容终端下次轮询就拉到了完全不用碰APK。WebView的代价也必须要正视。它对大图片的加载、网页资源的跨域都有各种限制低端设备上WebView版本碎片化还会导致同样的HTML在不同平板上显示效果不一样。这些坑会在第5章详细展开。架构上先记住一点信息发布系统的安卓端本质是一个网络拉取 WebView渲染的容器能少用原生UI就少用这决定了后期的维护成本。3. PC端落地用HTTP服务把节目单安全下发到每块屏PC端是整个信息发布系统的中枢。它要解决的问题有两个内容管理界面怎么操作以及终端通过什么协议拿到内容。管理界面各家做法不同有做成本地桌面程序的也有做成网页的但底层下发逻辑高度一致。这里以Python的Flask框架为例做一个最小实现把接口约定讲清楚生产环境换成C#或Java只是语言层面的迁移接口语义不用变。3.1 接口约定终端与PC之间到底交换什么终端轮询PC端无非回答三个问题我是谁、我要播什么、素材去哪里下载。对应的接口就三类终端注册与心跳、获取节目单、下载素材文件。节目单是核心它必须是终端能直接解析的JSON里面包含播放素材的URL、校验值、播放时长以及生效时间段。下面是一个典型的节目单结构{ terminal_id: A001, poll_interval: 600, channels: [ { name: 工作日白天, schedule: { start: 09:00, end: 22:00, days: [1, 2, 3, 4, 5] }, items: [ { type: image, url: http://192.168.1.100:8080/media/A001/promo_01.jpg, md5: a3f2..., duration: 30 }, { type: html, url: http://192.168.1.100:8080/media/A001/page_news.html, md5: b7c1..., duration: 0 } ] } ] }poll_interval告诉终端每隔多少秒来拉一次md5字段用于让终端判断素材是否变化这是增量更新机制的关键后面会细讲。type区分图片和HTML页面HTML类型的duration设为0表示整页持续显示由页面内部自己控制刷新。3.2 HTTP服务端代码一个最小可用的下发服务用Flask搭这个服务非常快完整代码也就六十行左右。这段代码里我做了三件事提供节目单查询接口、提供素材文件下载接口、用目录结构隔离不同终端的素材。import hashlib import os from flask import Flask, jsonify, request, send_from_directory, abort app Flask(__name__) PC_IP 0.0.0.0 # 监听所有网卡方便局域网访问 PORT 8080 MEDIA_ROOT ./media # 素材根目录子目录按终端ID命名 def file_md5(path): 计算素材文件的MD5供终端做更新判断 h hashlib.md5() with open(path, rb) as f: for chunk in iter(lambda: f.read(8192), b): h.update(chunk) return h.hexdigest() def build_channel_items(terminal_id): 扫描终端素材目录生成节目单 items 列表 term_dir os.path.join(MEDIA_ROOT, terminal_id) items [] if not os.path.isdir(term_dir): return items for name in sorted(os.listdir(term_dir)): full os.path.join(term_dir, name) if not os.path.isfile(full): continue ext name.rsplit(., 1)[-1].lower() item_type html if ext in (html, htm) else image items.append({ type: item_type, url: fhttp://{request.host}/media/{terminal_id}/{name}, md5: file_md5(full), duration: 0 if item_type html else 30, }) return items app.route(/api/terminal/terminal_id/playlist) def playlist(terminal_id): 终端轮询时获取自己的节目单 return jsonify({ terminal_id: terminal_id, poll_interval: 600, channels: [{ name: default, schedule: {start: 00:00, end: 23:59, days: [0, 1, 2, 3, 4, 5, 6]}, items: build_channel_items(terminal_id), }] }) app.route(/media/terminal_id/filename) def media(terminal_id, filename): 素材下载接口终端用这个地址拉图片和HTML页面 term_dir os.path.join(MEDIA_ROOT, terminal_id) if not os.path.isfile(os.path.join(term_dir, filename)): abort(404) return send_from_directory(term_dir, filename) if __name__ __main__: os.makedirs(MEDIA_ROOT, exist_okTrue) app.run(hostPC_IP, portPORT, threadedTrue)代码逻辑不复杂终端来请求节目单时服务端扫描该终端目录下的所有文件按扩展名判断是图片还是HTML模板同时计算每个文件的MD5最终拼成一个JSON返回。这样运营人员只要把新素材文件放进去终端下一轮轮询就会拿到新的节目单。参数上有几个点需要注意。poll_interval设300秒到600秒比较合适太短会增加PC压力太长会让内容更新显得迟钝。request.host会自动带上PC的IP和端口素材URL不用硬编码。threadeTrue是必须的否则多个终端同时请求时会互相阻塞。生产环境如果终端数量多我会把flask自带的开发服务器换成waitress或gunicorn接口代码完全不用改。3.3 素材更新机制MD5比对是增量的关键很多人在这个阶段犯的错误是终端每次轮询都把素材全部重新下载一遍。图片和HTML文件可能动辄几兆几十台终端全部重拉PC带宽和磁盘IO很快被打满。正确做法是节目单里带上MD5终端先比对本地文件的MD5一致就跳过不一致才重新下载。# 终端侧伪代码逻辑 def need_download(local_md5, remote_md5): return local_md5 ! remote_md5PC端在响应节目单时实时计算MD5文件一变MD5就变终端自然能感知。这个方案比用文件修改时间可靠因为某些终端下载文件后修改时间会变成下载时间用时间戳比对会误判为已更新。MD5的代价是计算耗时大文件会占一点CPU但目录里素材文件通常是几十张图片影响可以忽略。实际运营中还有一个小细节更新素材时不要直接覆盖原文件名建议用带版本号的新文件名上传到终端目录同时把旧文件删掉。这样即使终端正在播放旧文件也不会出现读取到一半被截断的问题。PC端的管理后台在负责替换文件时先复制到临时名再rename覆盖这个习惯能避免不少播放器卡在坏素材上的事故。4. 安卓端落地从开机自启到节目渲染的关键代码安卓端是这套系统能不能稳的关键。PC端挂了可以重启终端在现场跑着没人管所以终端的所有逻辑都要围绕自恢复来设计。下面这部分代码示例基于Java实现兼容Android 7到11的常见设备低端板子也能跑。4.1 播放器主流程轮询拉取节目单再渲染终端启动后第一件事不是播放而是先拉一次节目单拿到节目单之后再渲染。渲染组件是WebView图片和HTML都通过它加载。MainActivity的核心流程如下public class MainActivity extends Activity { private WebView webView; private Handler loopHandler new Handler(); private String playlistUrl http://192.168.1.100:8080/api/terminal/A001/playlist; private Runnable pollTask new Runnable() { Override public void run() { fetchPlaylist(); // 拉节目单并解析 loopHandler.postDelayed(this, 600_000); // 10分钟轮询一次 } }; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); webView new WebView(this); setContentView(webView); setupWebView(); pollTask.run(); } private void renderPlaylist(JSONObject playlist) { JSONArray items playlist.getJSONArray(channels) .getJSONObject(0).getJSONArray(items); JSONObject first items.getJSONObject(0); String url first.getString(url); // 根据type决定用WebView加载还是用ImageView组件轮播 webView.loadUrl(url); } }这段代码只展示了骨架但主流程已经清楚了onCreate里注册常亮Flag启动轮询任务每次轮询解析节目单后取第一项加载。轮询间隔直接写死在Runnable里和PC端返回的poll_interval其实应该联动生产环境建议读PC返回的值动态调整。这里有一个选型要点图片轮播和HTML页面加载到底走WebView还是走原生我一般建议全部走WebView。原因很简单终端播放的素材形态会越来越复杂今天是图片加文字明天可能就要放一个跑马灯公告、一个天气组件统一走WebView后这些功能都只是后台改HTML的事。原生图片轮播虽然省内存但每次改样式都要改App对于信息发布系统来说是得不偿失的。4.2 前台服务与看门狗让终端7x24小时不假死安卓设备在息屏、长时间运行后系统会回收后台Activity的资源尤其是一些国产低端设备省电策略非常激进。如果只是起一个Activity而不做前台服务很可能夜里息屏后第二天屏幕就黑了轮询任务被系统杀掉。解决这个问题必须用前台服务public class PlayService extends Service { private static final int NOTIFY_ID 1001; private Handler watchdog new Handler(); private Runnable rebootTask new Runnable() { Override public void run() { // 检测WebView是否还活着超过5分钟没响应就重启Activity sendBroadcast(new Intent(com.pub.REBOOT_PLAYER)); watchdog.postDelayed(this, 300_000); } }; Override public int onStartCommand(Intent intent, int flags, int startId) { startForeground(NOTIFY_ID, buildNotification()); watchdog.postDelayed(rebootTask, 300_000); return START_STICKY; } }START_STICKY保证服务被系统杀掉后能重建。watchdog每5分钟广播一次重启播放器的指令MainActivity收到广播后检查自身状态如果WebView长时间无响应就reload当前页面。这个兜底机制看起来很粗暴但现场维护常常就靠它兜住那些偶发黑屏。还要在Manifest里声明权限RECEIVE_BOOT_COMPLETED、WAKE_LOCK、FOREGROUND_SERVICE、INTERNET。开机自启用一个BOOT_COMPLETED广播接收器收到广播后启动MainActivity常见的做法是给Activity配置android:launchModesingleInstance避免多次广播后重复创建播放界面。所有权限和组件声明缺一不可少了任何一个终端都可能在一觉醒来后变成一块哑屏。4.3 WebView配置里的关键参数WebView的配置直接决定页面能不能正常显示尤其是HTML模板里引用了本地CSS或图片资源时稍不注意就会白屏。下面的配置是跑通这套系统的基础private void setupWebView() { WebSettings s webView.getSettings(); s.setJavaScriptEnabled(true); s.setDomStorageEnabled(true); s.setCacheMode(WebSettings.LOAD_DEFAULT); webView.setWebViewClient(new WebViewClient() { Override public boolean shouldOverrideUrlLoading(WebView view, String url) { return false; // 允许在同一个WebView内加载 } Override public void onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { webView.loadUrl(about:blank); // 渲染进程崩溃后重置 } }); }setJavaScriptEnabled是HTML页面脚本运行的前提setDomStorageEnabled让localStorage可用后续做离线兜底就依赖它。onRenderProcessGone回调是Android 8以后WebView独立进程机制的产物渲染进程崩溃后如果不处理整个App直接闪退在这里重置WebView是必要的保护。缓存模式建议用LOAD_DEFAULT而不是LOAD_NO_CACHE。因为素材更新依赖HTTP响应的ETag或MD5判断完全没有缓存会导致每次轮询都重新下载大图反而拖慢速度。WebView加载的HTML页面里如果引用了外部的CSS或JS要注意这些资源在PC端的URL必须能被终端直接访问不要使用localhost必须写PC的实际IP地址。5. 联调避坑信息发布系统最容易翻车的5个现场这套系统联调时踩坑几乎不可避免。下面按「现象 → 原因 → 解决」把最常见的几类问题列出来每一条都是现场用真金白银换来的血泪教训。5.1 本地HTML页面里的接口请求被拦白屏不给任何提示现象终端把HTML模板下载到本地用WebView的loadUrl(file:///sdcard/...)加载页面里的JS去请求http://192.168.1.100:8080的图片或接口结果全部加载失败Logcat里报net::ERR_FAILED。原因file://协议下的页面浏览器安全策略会把Origin视为null默认禁止页面发起跨域网络请求。这是WebView的安全机制不是代码写错了。解决最简单的方案是不要让WebView加载file协议统一用http加载PC端素材URL。也就是说节目单里的HTML类型素材URL直接指向http://192.168.1.100:8080/media/A001/page.html不要下载到本地再加载。这样页面和它引用的资源同源就不会有跨域限制。如果你的场景必须离线播放那就需要配置setAllowFileAccessFromFileURLs(true)但这个方法在Android 7以上默认关闭而且会有安全隐患能不用尽量不用。5.2 换了一批高清大图终端播放十几分钟就闪退现象运营人员把促销海报从1MB换成了5MB的1920x1080大图结果播放器运行一会儿就闪退Logcat里反复出现OutOfMemoryError。原因WebView加载大图时内存开销远大于图片文件本身大小一张5MB的JPG解码成Bitmap后可能占用几十MB内存低端设备总内存只有1GB多翻几张图就崩了。解决从PC端入口做限制上传图片时统一压一遍。我一般要求单张图片不超过1MB推荐用PNG或高质量JPG分辨率不超过终端屏幕分辨率的2倍。终端侧不要手动用BitmapFactory去解码网络图片交给WebView自己处理但可以在HTML模板里给img标签加上width:100%;height:auto样式避免WebView按原始尺寸分配内存。5.3 终端跑着跑着就失联节目不更新了现象白天正常第二天早上到店发现屏幕还停在前一天的画面手动打开App又能恢复。PC端看终端在线状态全是离线。原因这是最典型的安卓省电策略问题。设备息屏后系统进入深度休眠WiFi被断开Service被回收轮询任务全部停摆。解决两个动作缺一不可。一是前台服务START_STICKY保证进程存活二是获取WiFi锁和唤醒锁在播放服务启动时申请PARTIAL_WAKE_LOCK。另外把轮询逻辑从Activity里挪到前台服务里执行Activity被回收后服务还在等下一个轮询周期自然就把画面恢复了。设置里还要关掉设备的自动休眠选项或者插着充电器运行。5.4 素材更新了终端还在播旧图片现象PC端已经把旧图删掉、新图放进来了节目单也生成了终端轮询也正常但屏幕上就是旧画面。原因检查后发现终端本地缓存了旧图片WebView加载相同URL时直接命中缓存。而且节目单里如果文件名字不变终端侧根本没有重新下载的触发条件。解决PC端更新素材时换文件名而不是覆盖同名文件比如promo_01.jpg变成promo_02.jpg。这样URL变了WebView自然不会用旧缓存。终端侧在解析节目单时也应比对MD5发现变化先删除本地缓存文件再重新下载。强制刷新可以用webView.clearCache(true)配合但这个操作很重建议只在页面异常时用。5.5 JSON编码乱码中文标题全部变成问号现象PC端生成的节目单JSON里中文的频道名称、公告文字到了终端全部乱码英文和数字正常。原因PC端用UTF-8编码生成JSON但终端在解析时用了系统默认的GBK编码或者PC端写入文件时没指定编码用了系统ANSI。两边编码不一致导致乱码。解决PC端统一用UTF-8生成文件Python里写文件时明确encodingutf-8。终端侧用BufferedReader读取时指定UTF-8不要用默认编码。这个坑在Windows上最容易出现因为Windows中文系统的默认编码是GBK稍不留神就踩进去。建议PC端管理后台打包导出节目单时固定写死UTF-8编码并做校验。6. 进阶技巧断网兜底、时段节目与远程排障基础联调通了之后这套系统就可以交付了但还有三个进阶能力能大幅降低运维负担建议在第二版加上。断网兜底是第一优先级。门店的网络不稳定一旦PC端暂时不可达终端绝不能黑屏。做法是终端每次成功拉取节目单后把内容和素材清单保存在本地下一轮轮询如果连接失败就用本地保存的上一份节目单继续播。代码上只需在轮询catch异常时加载本地缓存URL播放器体验完全不受影响。这个能力对终端可靠性来说优先级高于其他一切。时段节目单是第二个值得做的方向。PC端下发节目单时带上schedule字段终端解析出每天的生效时间段按时间切换不同频道的节目。比如早上9点到晚上10点播商场的促销信息下班后切换到品牌宣传页。终端的实现逻辑控制在五十行以内但运营体验提升非常明显。远程排障是最后一个建议。给终端加一个日志上报能力把每次轮询结果、播放异常、素材下载失败的记录追加到本地文件同时每N次轮询将该文件POST回PC端。这样哪块屏出了问题在PC端就能看到它的播放历史不用再跑现场用Logcat抓日志了。我自己的习惯是日志接口永远最先实现因为联调阶段依赖它交付后也依赖它。这套系统做完后我最深的体会是信息发布系统的成败通常不在功能多全而在终端够不够「笨」——它能自愈、能离线兜底、能不依赖人工干预才是真正可交付的系统。上述断网兜底、时段切换和日志回传都是围绕这个原则补上去的。如果你正打算基于这类工程包改造自己的方案先把这几个能力做完再谈界面优化少走很多弯路。希望帮到你。本文还有配套的精品资源点击获取