简介这份资源是面向Android应用开发初学者与进阶者的图片分享应用设计源码基于Java语言实现适合作为课程设计、毕业设计或自学练手的参考案例。压缩包共26个文件约131KB其中11个Java源文件承载登录注册、搜索、图片保存、图文详情等业务逻辑10个XML文件负责界面布局与控件样式另有keep占位文件与说明文档辅助工程管理。已有399人学习下载说明其在同类练手项目中具备一定参考价值。读者可从中获得一套结构完整的图片分享应用框架理解Activity生命周期、适配器与列表/网格视图的配合方式掌握用户交互、图片上传下载与评论展示等常见任务的实现思路并借助说明文档快速梳理项目结构与设计意图为后续扩展功能或优化体验打下基础。1. 从零拆解一个 Java 版 Android 图片分享应用它到底解决什么问题很多人第一次看到「基于 Java 语言的 Android 图片分享应用设计源码」这个标题脑子里蹦出来的画面是一个能选图、能上传、能刷列表的 App代码量不大随便找个模板改改就能跑。但真正动手做过的人会告诉你图片分享类应用是所有移动端项目里最容易「看起来简单、做起来翻车」的一类。它牵扯到本地媒体库读取、图片压缩与缓存、网络上传与断点续传、列表滑动性能、权限适配、内存泄漏排查这一整条链路任何一环没处理好用户看到的就是白屏、卡顿、闪退、上传失败。这个方向适合两类人一类是想拿一个完整项目练手 Android 四大组件、OkHttp、Glide、RecyclerView 这些核心技能的开发者另一类是需要快速搭出一个图片社区原型、验证产品思路的小团队。它不追求炫技追求的是把「选图 → 处理 → 上传 → 展示」这条主链路做扎实。下面我按自己实际搭过的一个模拟项目 X 的路径把选型理由、关键代码、参数设置和踩过的坑一条条讲清楚你照着改就能跑起来。2. 技术选型与工程结构为什么用 Java 而不是 Kotlin为什么坚持 OkHttp Glide2.1 Java 在这个场景下依然站得住脚的理由现在新项目默认 Kotlin但「基于 Java 语言的 Android 图片分享应用」这个标题本身就锁定了 Java。从落地角度看Java 版本的优势在于大量高校课程、老项目维护、外包交付仍然以 Java 为主源码可读性和接手成本低。我一般会建议如果团队里有人对 Kotlin 协程不熟强行上 Kotlin 反而会在异步上传和生命周期处理上写出更多 bug。Java 配合 ExecutorService 和 Handler虽然啰嗦但行为可预测排查问题时有明确的调用栈。工程结构上我习惯按功能分包而不是按类型分包。很多教程喜欢把 Activity 全放一个包、Adapter 全放一个包项目一大就找不到对应关系。模拟项目 X 的结构是这样的com.example.imageshare ├── ui │ ├── MainActivity.java // 图片列表入口 │ ├── PickActivity.java // 选图与预览 │ └── UploadActivity.java // 上传进度与结果 ├── adapter │ └── ImageGridAdapter.java // RecyclerView 适配器 ├── net │ ├── ApiClient.java // OkHttp 单例与拦截器 │ └── UploadTask.java // 上传任务封装 ├── cache │ └── ImageCacheManager.java // 本地缓存与磁盘管理 ├── model │ └── ImageItem.java // 图片数据模型 └── util ├── ImageCompressor.java // 压缩工具 └── PermissionHelper.java // 权限请求封装这个结构的好处是找上传逻辑就去 net找列表渲染就去 adapter找压缩参数就去 util不用在几十个文件里翻。对于图片分享应用来说net 和 cache 是两个最容易出问题的包单独拆出来便于加日志和做单元测试。2.2 OkHttp 与 Glide 的职责边界网络层选 OkHttp图片加载选 Glide这是目前 Java 项目里最稳的组合。有人会问能不能用 Retrofit 把上传也包进去可以但图片上传涉及 multipart 进度回调Retrofit 原生支持得绕一层不如直接用 OkHttp 的 MultipartBody 来得直接。Glide 负责列表里的缩略图加载和内存缓存OkHttp 负责原图上传和接口请求两者互不干扰。ApiClient 的单例写法要注意别每次请求都 new 一个 OkHttpClient连接池和线程池会爆。下面是我常用的初始化代码public class ApiClient { private static volatile OkHttpClient instance; public static OkHttpClient get() { if (instance null) { synchronized (ApiClient.class) { if (instance null) { instance new OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) // 连接超时 .writeTimeout(60, TimeUnit.SECONDS) // 上传大图写超时 .readTimeout(30, TimeUnit.SECONDS) // 读取响应超时 .retryOnConnectionFailure(true) // 弱网重试 .build(); } } } return instance; } }参数说明writeTimeout 必须放大到 60 秒以上因为一张 3MB 的图在 4G 弱网下写入时间可能超过 30 秒默认 10 秒必挂。retryOnConnectionFailure 在移动网络切换时能救回不少失败请求但要注意它只重试连接失败不重试业务错误。connectTimeout 设 15 秒是折中值太短在电梯里直接失败太长用户等得焦躁。2.3 最小可跑的依赖配置在 app 模块的 build.gradle 里核心依赖就这几项别贪多dependencies { implementation com.squareup.okhttp3:okhttp:4.12.0 implementation com.github.bumptech.glide:glide:4.16.0 annotationProcessor com.github.bumptech.glide:compiler:4.16.0 implementation androidx.recyclerview:recyclerview:1.3.2 implementation androidx.exifinterface:exifinterface:1.3.7 }exifinterface 这个依赖很多人会漏导致读取相机拍的照片方向错误图片显示时被旋转 90 度。Glide 虽然能自动处理一部分但自己压缩时如果不读 EXIF 方向压缩后的图方向就乱了。这个坑后面避坑章节还会细说。3. 图片选择、压缩与上传主链路每一步的代码和参数怎么定3.1 从系统相册选图并拿到真实路径Android 10 以后分区存储强制生效直接拿 MediaStore 的 DATA 字段已经不可靠。常见做法是用 ActivityResultContracts 或者手动处理 onActivityResult拿到 Uri 后再通过 ContentResolver 查询。下面这段是查询真实路径和文件大小的核心逻辑public static String getPathFromUri(Context context, Uri uri) { String path null; if (content.equals(uri.getScheme())) { try (Cursor cursor context.getContentResolver().query( uri, new String[]{MediaStore.Images.Media.DATA}, null, null, null)) { if (cursor ! null cursor.moveToFirst()) { int idx cursor.getColumnIndex(MediaStore.Images.Media.DATA); if (idx 0) { path cursor.getString(idx); // 部分机型仍可拿到真实路径 } } } } return path ! null ? path : uri.toString(); // 拿不到就退回 Uri 字符串 }逻辑说明query 返回的 DATA 列在 Android 10 以上很多机型返回 null所以必须做兜底。兜底用 uri.toString() 后后续上传时直接以流的方式读取不再依赖文件路径。参数上projection 只查 DATA 一列减少 IO。注意 cursor 必须用 try-with-resources 关闭否则频繁选图会泄漏文件描述符最终导致「Too many open files」崩溃。3.2 压缩参数质量、尺寸、格式三者的取舍图片分享应用最核心的参数就是压缩。不压缩上传慢、流量大、服务端存储压力大压太狠用户投诉画质糊。我一般按「长边限制 质量递减」两步走public static Bitmap compress(String srcPath, int maxSide, int quality) { BitmapFactory.Options opts new BitmapFactory.Options(); opts.inJustDecodeBounds true; // 只读边界不加载像素 BitmapFactory.decodeFile(srcPath, opts); int sample 1; while (opts.outWidth / sample maxSide || opts.outHeight / sample maxSide) { sample * 2; // 采样率按 2 的幂递增 } opts.inJustDecodeBounds false; opts.inSampleSize sample; Bitmap bitmap BitmapFactory.decodeFile(srcPath, opts); ByteArrayOutputStream baos new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos); return bitmap; }参数说明maxSide 我通常设 1280这是分享场景下清晰度和体积的平衡点长边 1280 的 JPEG 质量 80 大约 200KB 到 400KB。quality 设 80低于 70 肉眼可见块状噪点高于 90 体积增长陡峭但画质提升有限。inSampleSize 必须是 2 的幂否则解码器会向下取整到最近的 2 的幂你以为设了 3 实际生效 2尺寸算错。压缩完记得 recycle 原 bitmap否则连续选十几张图必 OOM。3.3 用 OkHttp 做 multipart 上传并回调进度上传部分OkHttp 的 RequestBody 默认没有进度回调需要自己包一层。下面是一个可复用的进度 RequestBodypublic class ProgressRequestBody extends RequestBody { private final File file; private final String mimeType; private final ProgressListener listener; public interface ProgressListener { void onProgress(long bytesWritten, long total); } public ProgressRequestBody(File file, String mimeType, ProgressListener listener) { this.file file; this.mimeType mimeType; this.listener listener; } Override public MediaType contentType() { return MediaType.parse(mimeType); } Override public long contentLength() { return file.length(); } Override public void writeTo(BufferedSink sink) throws IOException { long total file.length(); long written 0; byte[] buffer new byte[8192]; try (Source source Okio.source(file)) { long read; while ((read source.read(buffer)) ! -1) { sink.write(buffer, 0, (int) read); written read; if (listener ! null) { listener.onProgress(written, total); // 回调进度 } } } } }逻辑说明writeTo 是 OkHttp 真正写 socket 的地方在这里按块读取文件并累加 written就能拿到真实上传进度。buffer 设 8192 字节是 IO 的常用块大小太小系统调用频繁太大内存占用高。注意 onProgress 是在 OkHttp 的 IO 线程回调的更新 UI 必须切回主线程否则直接崩。上传时组装请求MultipartBody.Builder builder new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart(file, file.getName(), new ProgressRequestBody(file, image/jpeg, listener)); Request request new Request.Builder() .url(https://your.server/api/upload) .post(builder.build()) .build();参数上addFormDataPart 的第二个参数是服务端接收的字段名必须和后端约定一致否则收到 400。文件名建议用时间戳加随机数避免并发上传同名覆盖。3.4 列表展示RecyclerView 与 Glide 的配合列表用 RecyclerView图片加载交给 Glide关键是尺寸和占位符要设对Glide.with(holder.imageView.getContext()) .load(item.getThumbUrl()) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .override(300, 300) // 按控件实际像素加载省内存 .centerCrop() .into(holder.imageView);override 设成控件实际显示尺寸的像素值不要用原图尺寸否则一张 4000x3000 的图加载进 300x300 的格子内存直接多占十几倍。centerCrop 保证格子填满不变形。placeholder 和 error 必须有否则滑动时白块闪烁体验很差。4. 避坑与排查图片分享应用最容易翻车的 5 个点4.1 上传大图必现 OOM现象选了几张相机原图点上传后 App 闪退日志里是 OutOfMemoryError。原因BitmapFactory.decodeFile 直接按原尺寸加载一张 4000x3000 的图占内存约 48MB连续几张就爆。解决先 inJustDecodeBounds 读边界再按目标尺寸算 inSampleSize压缩完立即 recycle上传用文件流而不是 Bitmap 对象。4.2 图片方向旋转 90 度现象相机竖拍的照片上传后显示是横的。原因相机传感器方向与显示方向不一致EXIF 里记录了 Orientation但压缩时没读取。解决用 ExifInterface 读取 TAG_ORIENTATION根据值对 Bitmap 做 matrix 旋转再压缩上传。这个坑在模拟项目 X 里让我调了一下午血泪经验就是压缩前先处理方向。4.3 弱网下上传卡死无回调现象地铁里上传进度条卡在 30% 不动也没有失败提示。原因writeTimeout 设得太短socket 写超时后 OkHttp 抛异常但代码里没在 onFailure 里更新 UI。解决writeTimeout 放大到 60 秒onFailure 里必须回调失败状态并给出重试按钮同时用 EventBus 或 Handler 切主线程更新。4.4 频繁选图后崩溃「Too many open files」现象连续选图十几次后查询相册直接崩。原因Cursor 没有关闭文件描述符泄漏。解决所有 ContentResolver.query 返回的 Cursor 必须用 try-with-resources 或 finally 关闭这是硬性纪律没有例外。4.5 列表滑动卡顿掉帧现象图片列表快速滑动时明显卡顿。原因Glide 没有设置 override每张图按原图尺寸解码或者 RecyclerView 的 item 布局层级太深。解决override 设成控件实际尺寸item 布局用 ConstraintLayout 压平层级图片加载加 crossFade 动画时间设短一点避免滑动时动画堆积。5. 进阶技巧把上传成功率从 80% 拉到 95% 的几个习惯做到上面这些主链路基本能跑通。但真正上线后你会发现上传成功率才是图片分享应用的命门。我自己的习惯是加三层保险。第一层是分片上传把大于 1MB 的图切成 256KB 的块逐块上传失败只重传当前块不用整图重来。第二层是本地待上传队列用 Room 存一条待上传记录App 被杀后重启能续传这是后悔药级别的设计。第三层是失败重试策略不要固定间隔重试用指数退避第一次等 2 秒第二次 4 秒第三次 8 秒最多三次避免弱网下疯狂重试把电量耗光。验证方法上我一般用 Android Studio 的 Network Profiler 看上传时的流量曲线正常应该是平稳上升如果出现长时间平坦再突然跳变说明分片或缓冲有问题。另外用 adb shell dumpsys meminfo 包名 观察压缩前后的内存峰值超过 200MB 就要警惕。参数调优没有银弹1280 长边和 80 质量是我在多个模拟项目里试出来的甜点值你可以根据自己服务端的存储成本和用户网络情况微调但别偏离太远。最后说一个我踩过的坑早期我为了省事把压缩和上传放在同一个线程里顺序执行结果用户选完图要等两三秒才开始上传体感极差。后来改成压缩用单独的线程池压缩完一张就丢进上传队列边压边传整体耗时缩短了将近一半。这个习惯我一直保留到现在希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?