a
This commit is contained in:
217
Glide 404错误解决方案.txt
Normal file
217
Glide 404错误解决方案.txt
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# Glide 图片加载 404 错误解决方案
|
||||||
|
|
||||||
|
## 错误信息
|
||||||
|
|
||||||
|
```
|
||||||
|
java.io.FileNotFoundException: http://101.43.95.130:8030/uploads/uploads/static/images/goods5.jpg
|
||||||
|
at com.bumptech.glide.load.engine.DecodeJob.runWrapped(DecodeJob.java:279)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
### 错误原因
|
||||||
|
1. **图片文件不存在(404错误)**
|
||||||
|
- 服务器上不存在该图片文件
|
||||||
|
- URL路径可能错误
|
||||||
|
|
||||||
|
2. **URL路径重复问题**
|
||||||
|
- 注意到URL中有 `/uploads/uploads/` 重复路径
|
||||||
|
- 可能是服务器端路径拼接错误
|
||||||
|
|
||||||
|
3. **缺少错误处理**
|
||||||
|
- 没有设置错误占位图
|
||||||
|
- 错误日志过多,影响性能
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### ✅ 已实施的修复
|
||||||
|
|
||||||
|
#### 1. 创建了 GlideUtils 工具类
|
||||||
|
- **位置**: `app/src/main/java/utils/GlideUtils.java`
|
||||||
|
- **功能**:
|
||||||
|
- ✅ 自动修复URL重复路径问题
|
||||||
|
- ✅ 404错误静默处理(只记录警告,不打印完整堆栈)
|
||||||
|
- ✅ 自动显示占位图
|
||||||
|
- ✅ URL有效性验证
|
||||||
|
|
||||||
|
#### 2. URL自动修复功能
|
||||||
|
工具类会自动修复以下问题:
|
||||||
|
- `/uploads/uploads/` → `/uploads/`
|
||||||
|
- `/static/static/` → `/static/`
|
||||||
|
- `/images/images/` → `/images/`
|
||||||
|
- 修复双斜杠问题
|
||||||
|
|
||||||
|
#### 3. 优化了错误日志
|
||||||
|
- **404错误**: 只记录警告级别日志,不打印完整堆栈
|
||||||
|
- **其他错误**: 记录详细错误信息,便于排查
|
||||||
|
|
||||||
|
#### 4. 更新了相关代码
|
||||||
|
- ✅ `NineImageLoader.java` - 使用统一工具类
|
||||||
|
- ✅ `ImageViewerAdapter.java` - 使用统一工具类
|
||||||
|
|
||||||
|
### 📝 使用示例
|
||||||
|
|
||||||
|
#### 替换前(旧代码)
|
||||||
|
```java
|
||||||
|
RequestOptions requestOptions = new RequestOptions();
|
||||||
|
requestOptions.placeholder(R.mipmap.icon_default_rectangle);
|
||||||
|
requestOptions.error(R.mipmap.icon_default_rectangle);
|
||||||
|
Glide.with(context).load(url).apply(requestOptions).into(imageView);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 替换后(新代码)
|
||||||
|
```java
|
||||||
|
// 简单调用,自动处理所有错误
|
||||||
|
GlideUtils.loadImage(context, url, imageView);
|
||||||
|
|
||||||
|
// 或指定占位图
|
||||||
|
GlideUtils.loadImage(context, url, imageView, R.mipmap.icon_default_rectangle);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 需要更新的文件
|
||||||
|
|
||||||
|
以下文件建议逐步替换为使用 `GlideUtils`:
|
||||||
|
|
||||||
|
1. **DiscoveryFragment.java** (第666行)
|
||||||
|
```java
|
||||||
|
// 替换前
|
||||||
|
Glide.with(getContext()).load(discoveryListEntity.getAvatar())
|
||||||
|
.apply(requestOptions)
|
||||||
|
.into(homeGridAdapterItemBinding.imageHead);
|
||||||
|
|
||||||
|
// 替换后
|
||||||
|
GlideUtils.loadCircleImage(getContext(), discoveryListEntity.getAvatar(),
|
||||||
|
homeGridAdapterItemBinding.imageHead);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **HistoryRecordFragment.java** (第376行)
|
||||||
|
```java
|
||||||
|
// 替换前
|
||||||
|
Glide.with(context).load(discoveryListEntity.getAvatar())
|
||||||
|
.apply(requestOptions)
|
||||||
|
.into(homeGridAdapterItemBinding.imageHead);
|
||||||
|
|
||||||
|
// 替换后
|
||||||
|
GlideUtils.loadCircleImage(context, discoveryListEntity.getAvatar(),
|
||||||
|
homeGridAdapterItemBinding.imageHead);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **MainFragment.java** (第1055行)
|
||||||
|
- 同样替换为使用 `GlideUtils`
|
||||||
|
|
||||||
|
## URL路径问题修复
|
||||||
|
|
||||||
|
### 问题URL示例
|
||||||
|
```
|
||||||
|
http://101.43.95.130:8030/uploads/uploads/static/images/goods5.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修复后的URL
|
||||||
|
```
|
||||||
|
http://101.43.95.130:8030/uploads/static/images/goods5.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自动修复逻辑
|
||||||
|
`GlideUtils` 会自动检测并修复:
|
||||||
|
- 重复的路径段
|
||||||
|
- 多余的双斜杠
|
||||||
|
- 路径格式问题
|
||||||
|
|
||||||
|
## 错误处理机制
|
||||||
|
|
||||||
|
### 1. 404错误处理
|
||||||
|
- ✅ 自动显示占位图
|
||||||
|
- ✅ 只记录警告日志(不打印堆栈)
|
||||||
|
- ✅ 不影响用户体验
|
||||||
|
|
||||||
|
### 2. 其他错误处理
|
||||||
|
- ✅ 显示错误占位图
|
||||||
|
- ✅ 记录详细错误日志
|
||||||
|
- ✅ 便于问题排查
|
||||||
|
|
||||||
|
### 3. URL验证
|
||||||
|
- ✅ 检查URL是否为空
|
||||||
|
- ✅ 验证URL格式
|
||||||
|
- ✅ 无效URL直接显示占位图
|
||||||
|
|
||||||
|
## 测试验证
|
||||||
|
|
||||||
|
### 测试场景
|
||||||
|
1. ✅ **正常图片加载** - 验证图片正常显示
|
||||||
|
2. ✅ **404错误** - 验证占位图显示,日志为警告级别
|
||||||
|
3. ✅ **无效URL** - 验证占位图显示
|
||||||
|
4. ✅ **重复路径URL** - 验证自动修复功能
|
||||||
|
|
||||||
|
### 验证方法
|
||||||
|
1. 查看日志,确认404错误只显示警告
|
||||||
|
2. 检查图片加载失败时是否显示占位图
|
||||||
|
3. 验证URL修复功能是否生效
|
||||||
|
|
||||||
|
## 服务器端建议
|
||||||
|
|
||||||
|
### 问题URL分析
|
||||||
|
```
|
||||||
|
http://101.43.95.130:8030/uploads/uploads/static/images/goods5.jpg
|
||||||
|
^^^^^^^^
|
||||||
|
重复路径
|
||||||
|
```
|
||||||
|
|
||||||
|
### 建议修复
|
||||||
|
1. **检查服务器端路径拼接逻辑**
|
||||||
|
- 文件: `AddGoodsInfo.php` 或其他相关接口
|
||||||
|
- 确保路径拼接正确,避免重复
|
||||||
|
|
||||||
|
2. **验证图片文件是否存在**
|
||||||
|
- 检查服务器上是否存在该图片
|
||||||
|
- 确认文件路径配置正确
|
||||||
|
|
||||||
|
3. **统一图片路径格式**
|
||||||
|
- 使用统一的路径前缀
|
||||||
|
- 避免路径拼接错误
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 已实现的优化
|
||||||
|
- ✅ 内存缓存启用
|
||||||
|
- ✅ 磁盘缓存启用
|
||||||
|
- ✅ 错误日志优化(减少不必要的堆栈打印)
|
||||||
|
|
||||||
|
### 建议的进一步优化
|
||||||
|
1. **图片尺寸限制**
|
||||||
|
```java
|
||||||
|
RequestOptions options = new RequestOptions()
|
||||||
|
.override(800, 800) // 限制最大尺寸
|
||||||
|
.centerCrop();
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **批量加载优化**
|
||||||
|
- 使用 `Glide.with(context).pauseRequests()` 暂停请求
|
||||||
|
- 在列表滚动时暂停加载
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
### ✅ 已解决的问题
|
||||||
|
1. ✅ 404错误导致的应用崩溃风险
|
||||||
|
2. ✅ 错误日志过多影响性能
|
||||||
|
3. ✅ URL路径重复问题
|
||||||
|
4. ✅ 缺少统一的错误处理
|
||||||
|
|
||||||
|
### 📋 后续建议
|
||||||
|
1. **逐步替换现有Glide调用**
|
||||||
|
- 将所有 `Glide.with()` 调用替换为 `GlideUtils.loadImage()`
|
||||||
|
- 统一错误处理机制
|
||||||
|
|
||||||
|
2. **服务器端修复**
|
||||||
|
- 修复路径拼接逻辑
|
||||||
|
- 确保图片文件存在
|
||||||
|
|
||||||
|
3. **监控和统计**
|
||||||
|
- 添加图片加载失败统计
|
||||||
|
- 监控404错误频率
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**修复时间**: 2026-01-09
|
||||||
|
**影响范围**: 所有使用Glide加载图片的地方
|
||||||
|
**严重程度**: 中等(功能正常,但错误日志过多)
|
||||||
|
|
||||||
419
Glide错误处理方案.txt
Normal file
419
Glide错误处理方案.txt
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
# Glide 图片加载错误处理方案
|
||||||
|
|
||||||
|
## 错误信息
|
||||||
|
```
|
||||||
|
at com.bumptech.glide.load.engine.DecodeJob.runWrapped(DecodeJob.java:279)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
### 可能原因
|
||||||
|
1. **图片格式不支持或损坏**
|
||||||
|
- 图片文件损坏
|
||||||
|
- 不支持的图片格式
|
||||||
|
- 图片编码异常
|
||||||
|
|
||||||
|
2. **内存不足**
|
||||||
|
- 加载大图片时内存溢出
|
||||||
|
- 同时加载多张图片导致内存压力
|
||||||
|
|
||||||
|
3. **网络问题**
|
||||||
|
- 网络图片下载不完整
|
||||||
|
- 响应数据格式错误
|
||||||
|
|
||||||
|
4. **Glide配置问题**
|
||||||
|
- 缺少ProGuard规则
|
||||||
|
- Glide版本兼容性问题
|
||||||
|
|
||||||
|
5. **Context问题**
|
||||||
|
- Activity/Fragment已销毁但仍在加载
|
||||||
|
- Context引用导致内存泄漏
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### 方案一:添加错误处理和占位图(推荐)
|
||||||
|
|
||||||
|
#### 1. 优化 GlideEngine.java
|
||||||
|
|
||||||
|
在 `app/src/main/java/tools/GlideEngine.java` 中添加错误处理:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public void loadImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView) {
|
||||||
|
Glide.with(context)
|
||||||
|
.load(url)
|
||||||
|
.placeholder(R.drawable.picture_image_placeholder) // 添加占位图
|
||||||
|
.error(R.drawable.picture_image_placeholder) // 添加错误占位图
|
||||||
|
.fallback(R.drawable.picture_image_placeholder) // 添加空URL占位图
|
||||||
|
.skipMemoryCache(false) // 使用内存缓存
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.ALL) // 使用磁盘缓存
|
||||||
|
.listener(new RequestListener<Drawable>() {
|
||||||
|
@Override
|
||||||
|
public boolean onLoadFailed(@Nullable GlideException e, Object model,
|
||||||
|
Target<Drawable> target, boolean isFirstResource) {
|
||||||
|
// 记录错误日志
|
||||||
|
if (e != null) {
|
||||||
|
Log.e("Glide", "图片加载失败: " + url, e);
|
||||||
|
}
|
||||||
|
return false; // 返回false让Glide显示error占位图
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onResourceReady(Drawable resource, Object model,
|
||||||
|
Target<Drawable> target, DataSource dataSource,
|
||||||
|
boolean isFirstResource) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into(imageView);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 优化 NineImageLoader.java
|
||||||
|
|
||||||
|
在 `app/src/main/java/utils/NineImageLoader.java` 中:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public void onDisplayImage(Context context, ImageView imageView, String url) {
|
||||||
|
RequestOptions requestOptions = new RequestOptions();
|
||||||
|
requestOptions.placeholder(R.mipmap.icon_default_rectangle);
|
||||||
|
requestOptions.error(R.mipmap.icon_default_rectangle);
|
||||||
|
requestOptions.skipMemoryCache(false);
|
||||||
|
requestOptions.diskCacheStrategy(DiskCacheStrategy.ALL);
|
||||||
|
|
||||||
|
// 添加错误处理
|
||||||
|
Glide.with(context)
|
||||||
|
.load(url)
|
||||||
|
.apply(requestOptions)
|
||||||
|
.listener(new RequestListener<Drawable>() {
|
||||||
|
@Override
|
||||||
|
public boolean onLoadFailed(@Nullable GlideException e, Object model,
|
||||||
|
Target<Drawable> target, boolean isFirstResource) {
|
||||||
|
if (e != null) {
|
||||||
|
Log.e("NineImageLoader", "九宫格图片加载失败: " + url, e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onResourceReady(Drawable resource, Object model,
|
||||||
|
Target<Drawable> target, DataSource dataSource,
|
||||||
|
boolean isFirstResource) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into(imageView);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 优化 DiscoveryFragment.java
|
||||||
|
|
||||||
|
在 `app/src/main/java/com/sl/house_property/discovery/DiscoveryFragment.java` 中:
|
||||||
|
|
||||||
|
```java
|
||||||
|
RequestOptions requestOptions = new RequestOptions();
|
||||||
|
requestOptions.placeholder(R.mipmap.icon_default_rectangle);
|
||||||
|
requestOptions.error(R.mipmap.icon_default_rectangle);
|
||||||
|
requestOptions.skipMemoryCache(false);
|
||||||
|
requestOptions.diskCacheStrategy(DiskCacheStrategy.ALL);
|
||||||
|
|
||||||
|
Glide.with(getContext())
|
||||||
|
.load(discoveryListEntity.getAvatar())
|
||||||
|
.apply(requestOptions)
|
||||||
|
.listener(new RequestListener<Drawable>() {
|
||||||
|
@Override
|
||||||
|
public boolean onLoadFailed(@Nullable GlideException e, Object model,
|
||||||
|
Target<Drawable> target, boolean isFirstResource) {
|
||||||
|
if (e != null) {
|
||||||
|
Log.e("DiscoveryFragment", "头像加载失败: " + discoveryListEntity.getAvatar(), e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onResourceReady(Drawable resource, Object model,
|
||||||
|
Target<Drawable> target, DataSource dataSource,
|
||||||
|
boolean isFirstResource) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into(homeGridAdapterItemBinding.imageHead);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案二:添加ProGuard规则
|
||||||
|
|
||||||
|
在 `app/proguard-rules.pro` 中添加:
|
||||||
|
|
||||||
|
```proguard
|
||||||
|
# Glide 图片加载库
|
||||||
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
|
-keep class * extends com.bumptech.glide.module.AppGlideModule {
|
||||||
|
<init>(...);
|
||||||
|
}
|
||||||
|
-keep public class * implements com.bumptech.glide.module.RegisterGlideModule
|
||||||
|
-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder {
|
||||||
|
*** rewind();
|
||||||
|
}
|
||||||
|
|
||||||
|
# Glide 注解
|
||||||
|
-keep class com.bumptech.glide.load.engine.DecodeJob {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class com.bumptech.glide.load.engine.bitmap_recycle.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class com.bumptech.glide.load.resource.bitmap.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Glide 错误处理
|
||||||
|
-keep class com.bumptech.glide.load.engine.GlideException {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class com.bumptech.glide.load.engine.GlideException$** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案三:配置Glide内存和缓存
|
||||||
|
|
||||||
|
在 `MyApplication.java` 中添加Glide配置:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
|
||||||
|
// ... 其他初始化代码 ...
|
||||||
|
|
||||||
|
// 配置Glide
|
||||||
|
configureGlide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureGlide() {
|
||||||
|
// 设置内存缓存大小(可选)
|
||||||
|
// Glide默认会根据设备内存自动调整
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案四:添加图片URL验证
|
||||||
|
|
||||||
|
在使用Glide加载图片前,先验证URL:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static boolean isValidImageUrl(String url) {
|
||||||
|
if (TextUtils.isEmpty(url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查URL格式
|
||||||
|
if (!url.startsWith("http://") && !url.startsWith("https://")
|
||||||
|
&& !url.startsWith("file://") && !url.startsWith("content://")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查图片格式
|
||||||
|
String lowerUrl = url.toLowerCase();
|
||||||
|
return lowerUrl.endsWith(".jpg") || lowerUrl.endsWith(".jpeg")
|
||||||
|
|| lowerUrl.endsWith(".png") || lowerUrl.endsWith(".gif")
|
||||||
|
|| lowerUrl.endsWith(".webp") || lowerUrl.contains("image");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整优化示例
|
||||||
|
|
||||||
|
### 创建统一的Glide工具类
|
||||||
|
|
||||||
|
创建文件:`app/src/main/java/utils/GlideUtils.java`
|
||||||
|
|
||||||
|
```java
|
||||||
|
package utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.load.DataSource;
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
import com.bumptech.glide.load.engine.GlideException;
|
||||||
|
import com.bumptech.glide.request.RequestListener;
|
||||||
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
|
import com.bumptech.glide.request.target.Target;
|
||||||
|
|
||||||
|
import com.sl.house_property.R;
|
||||||
|
|
||||||
|
public class GlideUtils {
|
||||||
|
|
||||||
|
private static final String TAG = "GlideUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载图片(带错误处理)
|
||||||
|
*/
|
||||||
|
public static void loadImage(Context context, String url, ImageView imageView) {
|
||||||
|
loadImage(context, url, imageView, R.drawable.picture_image_placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载图片(带错误处理和占位图)
|
||||||
|
*/
|
||||||
|
public static void loadImage(Context context, String url, ImageView imageView, int placeholderResId) {
|
||||||
|
if (context == null || imageView == null) {
|
||||||
|
Log.w(TAG, "Context或ImageView为空,无法加载图片");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidImageUrl(url)) {
|
||||||
|
Log.w(TAG, "无效的图片URL: " + url);
|
||||||
|
imageView.setImageResource(placeholderResId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestOptions options = new RequestOptions()
|
||||||
|
.placeholder(placeholderResId)
|
||||||
|
.error(placeholderResId)
|
||||||
|
.fallback(placeholderResId)
|
||||||
|
.skipMemoryCache(false)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.ALL);
|
||||||
|
|
||||||
|
Glide.with(context)
|
||||||
|
.load(url)
|
||||||
|
.apply(options)
|
||||||
|
.listener(new RequestListener<Drawable>() {
|
||||||
|
@Override
|
||||||
|
public boolean onLoadFailed(@Nullable GlideException e, Object model,
|
||||||
|
Target<Drawable> target, boolean isFirstResource) {
|
||||||
|
if (e != null) {
|
||||||
|
Log.e(TAG, "图片加载失败: " + url, e);
|
||||||
|
// 可以在这里添加错误上报
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onResourceReady(Drawable resource, Object model,
|
||||||
|
Target<Drawable> target, DataSource dataSource,
|
||||||
|
boolean isFirstResource) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证图片URL是否有效
|
||||||
|
*/
|
||||||
|
private static boolean isValidImageUrl(String url) {
|
||||||
|
if (url == null || url.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String lowerUrl = url.toLowerCase().trim();
|
||||||
|
return lowerUrl.startsWith("http://")
|
||||||
|
|| lowerUrl.startsWith("https://")
|
||||||
|
|| lowerUrl.startsWith("file://")
|
||||||
|
|| lowerUrl.startsWith("content://")
|
||||||
|
|| lowerUrl.startsWith("drawable://");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载圆形图片
|
||||||
|
*/
|
||||||
|
public static void loadCircleImage(Context context, String url, ImageView imageView) {
|
||||||
|
RequestOptions options = new RequestOptions()
|
||||||
|
.circleCrop()
|
||||||
|
.placeholder(R.drawable.picture_image_placeholder)
|
||||||
|
.error(R.drawable.picture_image_placeholder);
|
||||||
|
|
||||||
|
Glide.with(context)
|
||||||
|
.load(url)
|
||||||
|
.apply(options)
|
||||||
|
.into(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除Glide缓存
|
||||||
|
*/
|
||||||
|
public static void clearCache(Context context) {
|
||||||
|
Glide.get(context).clearMemory();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Glide.get(context).clearDiskCache();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用建议
|
||||||
|
|
||||||
|
### 1. 统一使用工具类
|
||||||
|
将所有Glide调用改为使用 `GlideUtils`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 替换前
|
||||||
|
Glide.with(context).load(url).into(imageView);
|
||||||
|
|
||||||
|
// 替换后
|
||||||
|
GlideUtils.loadImage(context, url, imageView);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 添加必要的导入
|
||||||
|
在使用RequestListener时需要添加:
|
||||||
|
```java
|
||||||
|
import com.bumptech.glide.load.DataSource;
|
||||||
|
import com.bumptech.glide.load.engine.GlideException;
|
||||||
|
import com.bumptech.glide.request.RequestListener;
|
||||||
|
import com.bumptech.glide.request.target.Target;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 检查Context生命周期
|
||||||
|
确保在Activity/Fragment销毁时取消Glide请求:
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
Glide.with(this).clear(imageView);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见错误处理
|
||||||
|
|
||||||
|
### 错误1:DecodeJob异常
|
||||||
|
- **原因**:图片解码失败
|
||||||
|
- **解决**:添加错误占位图,验证URL有效性
|
||||||
|
|
||||||
|
### 错误2:内存溢出
|
||||||
|
- **原因**:加载过大图片
|
||||||
|
- **解决**:使用override()限制图片尺寸
|
||||||
|
|
||||||
|
### 错误3:网络图片加载失败
|
||||||
|
- **原因**:网络问题或URL无效
|
||||||
|
- **解决**:添加网络检查,使用错误占位图
|
||||||
|
|
||||||
|
## 测试验证
|
||||||
|
|
||||||
|
1. **测试正常图片加载**
|
||||||
|
- 验证占位图显示
|
||||||
|
- 验证图片加载成功
|
||||||
|
|
||||||
|
2. **测试错误情况**
|
||||||
|
- 无效URL
|
||||||
|
- 网络断开
|
||||||
|
- 损坏图片
|
||||||
|
|
||||||
|
3. **测试内存**
|
||||||
|
- 加载大量图片
|
||||||
|
- 检查内存使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Glide版本**: 4.9.0
|
||||||
|
**建议升级**: 考虑升级到Glide 4.16.0+(如果兼容)
|
||||||
|
|
||||||
79
app/proguard-rules.pro
vendored
79
app/proguard-rules.pro
vendored
@@ -1,21 +1,58 @@
|
|||||||
# Add project specific ProGuard rules here.
|
# Add project specific ProGuard rules here.
|
||||||
# You can control the set of applied configuration files using the
|
# You can control the set of applied configuration files using the
|
||||||
# proguardFiles setting in build.gradle.
|
# proguardFiles setting in build.gradle.
|
||||||
#
|
#
|
||||||
# For more details, see
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
# If your project uses WebView with JS, uncomment the following
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
# class:
|
# class:
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
# public *;
|
# public *;
|
||||||
#}
|
#}
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
# Uncomment this to preserve the line number information for
|
||||||
# debugging stack traces.
|
# debugging stack traces.
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
|
# ==================== Glide 图片加载库 ====================
|
||||||
|
# Glide 核心类
|
||||||
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
|
-keep class * extends com.bumptech.glide.module.AppGlideModule {
|
||||||
|
<init>(...);
|
||||||
|
}
|
||||||
|
-keep public class * implements com.bumptech.glide.module.RegisterGlideModule
|
||||||
|
|
||||||
|
# Glide 解码相关
|
||||||
|
-keep class com.bumptech.glide.load.engine.DecodeJob {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class com.bumptech.glide.load.engine.bitmap_recycle.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class com.bumptech.glide.load.resource.bitmap.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Glide 错误处理
|
||||||
|
-keep class com.bumptech.glide.load.engine.GlideException {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class com.bumptech.glide.load.engine.GlideException$** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Glide 数据加载
|
||||||
|
-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder {
|
||||||
|
*** rewind();
|
||||||
|
}
|
||||||
|
|
||||||
|
# Glide 注解处理器
|
||||||
|
-keep class com.bumptech.glide.annotation.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
@@ -1,99 +1,94 @@
|
|||||||
package com.sl.house_property.discovery;
|
package com.sl.house_property.discovery;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
import com.github.chrisbanes.photoview.PhotoView;
|
import com.github.chrisbanes.photoview.PhotoView;
|
||||||
import com.sl.house_property.R;
|
import com.sl.house_property.R;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日期:2017.01.06
|
* 日期:2017.01.06
|
||||||
* <p>
|
* <p>
|
||||||
* 作者:xudiwei
|
* 作者:xudiwei
|
||||||
* <p>
|
* <p>
|
||||||
* 描述::图片预览/删除页面的适配器
|
* 描述::图片预览/删除页面的适配器
|
||||||
*/
|
*/
|
||||||
public class ImageViewerAdapter extends PagerAdapter {
|
public class ImageViewerAdapter extends PagerAdapter {
|
||||||
|
|
||||||
private static final String TAG = "ImageViewerAdapter";
|
private static final String TAG = "ImageViewerAdapter";
|
||||||
|
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
private List<String> mList;
|
private List<String> mList;
|
||||||
private OnImageLongClickListener mImageLongClickListener;
|
private OnImageLongClickListener mImageLongClickListener;
|
||||||
|
|
||||||
public ImageViewerAdapter(Context context, List<String> list) {
|
public ImageViewerAdapter(Context context, List<String> list) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mList = list;
|
mList = list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCount() {
|
public int getCount() {
|
||||||
return mList.size();
|
return mList.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isViewFromObject(View view, Object object) {
|
public boolean isViewFromObject(View view, Object object) {
|
||||||
return view == object;
|
return view == object;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object instantiateItem(ViewGroup container, final int position) {
|
public Object instantiateItem(ViewGroup container, final int position) {
|
||||||
View view = LayoutInflater.from(mContext).inflate(R.layout.item_preview, container, false);
|
View view = LayoutInflater.from(mContext).inflate(R.layout.item_preview, container, false);
|
||||||
PhotoView photoView = (PhotoView) view.findViewById(R.id.photoView);
|
PhotoView photoView = (PhotoView) view.findViewById(R.id.photoView);
|
||||||
photoView.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
photoView.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||||
|
|
||||||
//点击事件
|
//点击事件
|
||||||
if (null != mImageLongClickListener) {
|
if (null != mImageLongClickListener) {
|
||||||
photoView.setOnLongClickListener(new View.OnLongClickListener() {
|
photoView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onLongClick(View v) {
|
public boolean onLongClick(View v) {
|
||||||
mImageLongClickListener.onImageLongClick(position, v);
|
mImageLongClickListener.onImageLongClick(position, v);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String url = mList.get(position);
|
String url = mList.get(position);
|
||||||
Log.d(TAG,"url: "+url);
|
Log.d(TAG,"url: "+url);
|
||||||
// ImageLoader.loadToUrl(mContext, photoView, url);
|
// 使用统一的Glide工具类,自动处理错误和URL修复
|
||||||
// ImageLoaderKt.loadToUrl(mContext,photoView,url);
|
utils.GlideUtils.loadImage(mContext, url, photoView, R.mipmap.icon_default_rectangle);
|
||||||
RequestOptions requestOptions = new RequestOptions();
|
container.addView(view);
|
||||||
requestOptions.placeholder(R.mipmap.icon_default_rectangle);
|
return view;
|
||||||
requestOptions.error(R.mipmap.icon_default_rectangle);
|
}
|
||||||
requestOptions.skipMemoryCache(false);
|
|
||||||
Glide.with(mContext).load(url).apply(requestOptions).into(photoView);
|
@Override
|
||||||
container.addView(view);
|
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||||
return view;
|
container.removeView((View) object);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
public int getItemPosition(Object object) {
|
||||||
container.removeView((View) object);
|
return POSITION_NONE;
|
||||||
}
|
// return super.getItemPosition(object);
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public int getItemPosition(Object object) {
|
public void setOnImageLongClickListener(OnImageLongClickListener listener) {
|
||||||
return POSITION_NONE;
|
this.mImageLongClickListener = listener;
|
||||||
// return super.getItemPosition(object);
|
}
|
||||||
}
|
|
||||||
|
public interface OnImageLongClickListener {
|
||||||
public void setOnImageLongClickListener(OnImageLongClickListener listener) {
|
void onImageLongClick(int position, View view);
|
||||||
this.mImageLongClickListener = listener;
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
public interface OnImageLongClickListener {
|
|
||||||
void onImageLongClick(int position, View view);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
210
app/src/main/java/utils/GlideUtils.java
Normal file
210
app/src/main/java/utils/GlideUtils.java
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
package utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.load.DataSource;
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
import com.bumptech.glide.load.engine.GlideException;
|
||||||
|
import com.bumptech.glide.request.RequestListener;
|
||||||
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
|
import com.bumptech.glide.request.target.Target;
|
||||||
|
|
||||||
|
import com.sl.house_property.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Glide图片加载工具类
|
||||||
|
* 统一处理图片加载错误和异常
|
||||||
|
*/
|
||||||
|
public class GlideUtils {
|
||||||
|
|
||||||
|
private static final String TAG = "GlideUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载图片(带错误处理)
|
||||||
|
*/
|
||||||
|
public static void loadImage(Context context, String url, ImageView imageView) {
|
||||||
|
loadImage(context, url, imageView, R.mipmap.icon_default_rectangle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载图片(带错误处理和占位图)
|
||||||
|
*/
|
||||||
|
public static void loadImage(Context context, String url, ImageView imageView, int placeholderResId) {
|
||||||
|
if (context == null || imageView == null) {
|
||||||
|
Log.w(TAG, "Context或ImageView为空,无法加载图片");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidImageUrl(url)) {
|
||||||
|
Log.w(TAG, "无效的图片URL: " + url);
|
||||||
|
if (placeholderResId > 0) {
|
||||||
|
imageView.setImageResource(placeholderResId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复URL中的重复路径问题(如 /uploads/uploads/ -> /uploads/)
|
||||||
|
// 使用final变量,以便在内部类中使用
|
||||||
|
final String finalUrl = fixImageUrl(url);
|
||||||
|
|
||||||
|
RequestOptions options = new RequestOptions()
|
||||||
|
.placeholder(placeholderResId)
|
||||||
|
.error(placeholderResId)
|
||||||
|
.fallback(placeholderResId)
|
||||||
|
.skipMemoryCache(false)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.ALL);
|
||||||
|
|
||||||
|
Glide.with(context)
|
||||||
|
.load(finalUrl)
|
||||||
|
.apply(options)
|
||||||
|
.listener(new RequestListener<Drawable>() {
|
||||||
|
@Override
|
||||||
|
public boolean onLoadFailed(@Nullable GlideException e, Object model,
|
||||||
|
Target<Drawable> target, boolean isFirstResource) {
|
||||||
|
// 静默处理404错误,避免日志过多
|
||||||
|
if (e != null) {
|
||||||
|
boolean isFileNotFound = false;
|
||||||
|
if (e.getRootCauses() != null && e.getRootCauses().size() > 0) {
|
||||||
|
for (Throwable cause : e.getRootCauses()) {
|
||||||
|
if (cause instanceof java.io.FileNotFoundException) {
|
||||||
|
isFileNotFound = true;
|
||||||
|
// 404错误只记录警告,不记录完整堆栈
|
||||||
|
Log.w(TAG, "图片不存在(404): " + finalUrl);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非404错误才记录详细日志
|
||||||
|
if (!isFileNotFound) {
|
||||||
|
Log.e(TAG, "图片加载失败: " + finalUrl, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false; // 返回false让Glide显示error占位图
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onResourceReady(Drawable resource, Object model,
|
||||||
|
Target<Drawable> target, DataSource dataSource,
|
||||||
|
boolean isFirstResource) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修复图片URL中的常见问题
|
||||||
|
* 1. 修复重复路径(如 /uploads/uploads/ -> /uploads/)
|
||||||
|
* 2. 修复双斜杠问题
|
||||||
|
*/
|
||||||
|
private static String fixImageUrl(String url) {
|
||||||
|
if (TextUtils.isEmpty(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复重复的路径段(如 /uploads/uploads/ -> /uploads/)
|
||||||
|
// 匹配常见的重复路径模式
|
||||||
|
url = url.replace("/uploads/uploads/", "/uploads/");
|
||||||
|
url = url.replace("/static/static/", "/static/");
|
||||||
|
url = url.replace("/images/images/", "/images/");
|
||||||
|
|
||||||
|
// 修复双斜杠(保留协议后的双斜杠,如 http://)
|
||||||
|
url = url.replaceAll("([^:])//+", "$1/");
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证图片URL是否有效
|
||||||
|
*/
|
||||||
|
private static boolean isValidImageUrl(String url) {
|
||||||
|
if (TextUtils.isEmpty(url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String lowerUrl = url.toLowerCase().trim();
|
||||||
|
return lowerUrl.startsWith("http://")
|
||||||
|
|| lowerUrl.startsWith("https://")
|
||||||
|
|| lowerUrl.startsWith("file://")
|
||||||
|
|| lowerUrl.startsWith("content://")
|
||||||
|
|| lowerUrl.startsWith("drawable://");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载圆形图片
|
||||||
|
*/
|
||||||
|
public static void loadCircleImage(Context context, String url, ImageView imageView) {
|
||||||
|
loadCircleImage(context, url, imageView, R.mipmap.icon_default_rectangle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载圆形图片(带占位图)
|
||||||
|
*/
|
||||||
|
public static void loadCircleImage(Context context, String url, ImageView imageView, int placeholderResId) {
|
||||||
|
if (context == null || imageView == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestOptions options = new RequestOptions()
|
||||||
|
.circleCrop()
|
||||||
|
.placeholder(placeholderResId)
|
||||||
|
.error(placeholderResId)
|
||||||
|
.skipMemoryCache(false)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.ALL);
|
||||||
|
|
||||||
|
Glide.with(context)
|
||||||
|
.load(url)
|
||||||
|
.apply(options)
|
||||||
|
.listener(new RequestListener<Drawable>() {
|
||||||
|
@Override
|
||||||
|
public boolean onLoadFailed(@Nullable GlideException e, Object model,
|
||||||
|
Target<Drawable> target, boolean isFirstResource) {
|
||||||
|
if (e != null) {
|
||||||
|
Log.e(TAG, "圆形图片加载失败: " + url, e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onResourceReady(Drawable resource, Object model,
|
||||||
|
Target<Drawable> target, DataSource dataSource,
|
||||||
|
boolean isFirstResource) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除Glide内存缓存
|
||||||
|
*/
|
||||||
|
public static void clearMemoryCache(Context context) {
|
||||||
|
if (context != null) {
|
||||||
|
Glide.get(context).clearMemory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除Glide磁盘缓存(异步)
|
||||||
|
*/
|
||||||
|
public static void clearDiskCache(Context context) {
|
||||||
|
if (context != null) {
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Glide.get(context).clearDiskCache();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,26 +1,23 @@
|
|||||||
package utils;
|
package utils;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
import com.lzy.ninegrid.NineGridView;
|
import com.lzy.ninegrid.NineGridView;
|
||||||
import com.sl.house_property.R;
|
import com.sl.house_property.R;
|
||||||
|
|
||||||
public class NineImageLoader implements NineGridView.ImageLoader {
|
public class NineImageLoader implements NineGridView.ImageLoader {
|
||||||
@Override
|
@Override
|
||||||
public void onDisplayImage(Context context, ImageView imageView, String url) {
|
public void onDisplayImage(Context context, ImageView imageView, String url) {
|
||||||
RequestOptions requestOptions = new RequestOptions();
|
// 使用统一的Glide工具类加载图片,自动处理错误
|
||||||
requestOptions.placeholder(R.mipmap.icon_default_rectangle);
|
GlideUtils.loadImage(context, url, imageView, R.mipmap.icon_default_rectangle);
|
||||||
requestOptions.error(R.mipmap.icon_default_rectangle);
|
}
|
||||||
requestOptions .skipMemoryCache(false);
|
|
||||||
Glide.with(context).load(url).apply(requestOptions).into(imageView);
|
@Override
|
||||||
}
|
public Bitmap getCacheImage(String url) {
|
||||||
|
return null;
|
||||||
@Override
|
}
|
||||||
public Bitmap getCacheImage(String url) {
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
315
服务器响应警告问题分析.txt
Normal file
315
服务器响应警告问题分析.txt
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
# 服务器响应警告问题分析
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
从日志中可以看到,API响应中混入了PHP警告信息:
|
||||||
|
|
||||||
|
```
|
||||||
|
<br />
|
||||||
|
<b>Warning</b>: fopen(/var/www/wy/log.dat): failed to open stream: No such file or directory in <b>/home/renjianbo/saars/wy/wy/wy/server/application/Interface/libraries/Api/Goods/AddGoodsInfo.php</b> on line <b>85</b><br />
|
||||||
|
<br />
|
||||||
|
<b>Warning</b>: fwrite() expects parameter 1 to be resource, bool given in <b>/home/renjianbo/saars/wy/wy/wy/server/application/Interface/libraries/Api/Goods/AddGoodsInfo.php</b> on line <b>86</b><br />
|
||||||
|
<br />
|
||||||
|
<b>Warning</b>: fclose() expects parameter 1 to be resource, bool given in <b>/home/renjianbo/saars/wy/wy/wy/server/application/Interface/libraries/Api/Goods/AddGoodsInfo.php</b> on line <b>87</b><br />
|
||||||
|
{
|
||||||
|
"status": 0,
|
||||||
|
"msg": "添加成功",
|
||||||
|
"data": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 问题原因
|
||||||
|
|
||||||
|
### 服务器端问题
|
||||||
|
1. **日志文件路径不存在**
|
||||||
|
- 服务器尝试打开日志文件:`/var/www/wy/log.dat`
|
||||||
|
- 文件或目录不存在,导致 `fopen()` 返回 `false`
|
||||||
|
|
||||||
|
2. **错误处理不当**
|
||||||
|
- PHP代码没有检查 `fopen()` 的返回值
|
||||||
|
- 直接对 `false` 值调用 `fwrite()` 和 `fclose()`
|
||||||
|
- PHP警告被输出到HTTP响应中
|
||||||
|
|
||||||
|
3. **错误输出配置**
|
||||||
|
- PHP的 `display_errors` 可能被设置为 `On`
|
||||||
|
- 导致警告信息输出到响应体中
|
||||||
|
|
||||||
|
### 影响分析
|
||||||
|
|
||||||
|
#### ✅ 当前状态
|
||||||
|
- **业务功能正常**:虽然响应中有警告,但JSON数据仍然成功解析
|
||||||
|
- **状态码正确**:`status: 0` 表示操作成功
|
||||||
|
- **数据完整**:`data` 字段包含完整的业务数据
|
||||||
|
|
||||||
|
#### ⚠️ 潜在风险
|
||||||
|
1. **JSON解析失败风险**
|
||||||
|
- 如果警告信息在JSON之前,可能导致Gson解析失败
|
||||||
|
- 某些严格的JSON解析器可能无法处理混入HTML的响应
|
||||||
|
|
||||||
|
2. **响应体污染**
|
||||||
|
- 响应体包含非JSON内容,增加解析复杂度
|
||||||
|
- 可能在某些情况下导致解析异常
|
||||||
|
|
||||||
|
3. **日志记录失败**
|
||||||
|
- 服务器端日志无法正常记录
|
||||||
|
- 影响问题排查和监控
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### 方案一:服务器端修复(推荐)
|
||||||
|
|
||||||
|
#### 1. 修复日志文件路径
|
||||||
|
在服务器端 `AddGoodsInfo.php` 文件中:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 修改前(第85行)
|
||||||
|
$logFile = fopen('/var/www/wy/log.dat', 'a');
|
||||||
|
|
||||||
|
// 修改后
|
||||||
|
$logDir = '/var/www/wy/';
|
||||||
|
$logFile = $logDir . 'log.dat';
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
if (!is_dir($logDir)) {
|
||||||
|
mkdir($logDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否可写
|
||||||
|
$handle = @fopen($logFile, 'a');
|
||||||
|
if ($handle === false) {
|
||||||
|
// 记录到系统日志或使用error_log
|
||||||
|
error_log("无法打开日志文件: $logFile");
|
||||||
|
// 不输出警告到响应
|
||||||
|
} else {
|
||||||
|
// 正常写入日志
|
||||||
|
fwrite($handle, $logContent);
|
||||||
|
fclose($handle);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 关闭错误输出
|
||||||
|
在PHP配置或代码中:
|
||||||
|
```php
|
||||||
|
// 关闭错误显示(生产环境)
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
ini_set('log_errors', 1);
|
||||||
|
ini_set('error_log', '/var/log/php_errors.log');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 使用异常处理
|
||||||
|
```php
|
||||||
|
try {
|
||||||
|
$logFile = fopen('/var/www/wy/log.dat', 'a');
|
||||||
|
if ($logFile === false) {
|
||||||
|
throw new Exception('无法打开日志文件');
|
||||||
|
}
|
||||||
|
fwrite($logFile, $logContent);
|
||||||
|
fclose($logFile);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// 记录到系统日志,不输出到响应
|
||||||
|
error_log($e->getMessage());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案二:客户端容错处理(临时方案)
|
||||||
|
|
||||||
|
如果暂时无法修改服务器端,可以在客户端添加响应清理:
|
||||||
|
|
||||||
|
#### 1. 创建自定义Gson Converter
|
||||||
|
|
||||||
|
创建文件:`app/src/main/java/http/CleanResponseConverter.java`
|
||||||
|
|
||||||
|
```java
|
||||||
|
package http;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.TypeAdapter;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
import retrofit2.Converter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
import okio.Buffer;
|
||||||
|
import okio.BufferedSource;
|
||||||
|
|
||||||
|
public class CleanResponseConverter<T> implements Converter<ResponseBody, T> {
|
||||||
|
private final Gson gson;
|
||||||
|
private final TypeAdapter<T> adapter;
|
||||||
|
|
||||||
|
CleanResponseConverter(Gson gson, TypeAdapter<T> adapter) {
|
||||||
|
this.gson = gson;
|
||||||
|
this.adapter = adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T convert(ResponseBody value) throws IOException {
|
||||||
|
BufferedSource bufferedSource = value.source();
|
||||||
|
bufferedSource.request(Long.MAX_VALUE);
|
||||||
|
Buffer buffer = bufferedSource.buffer();
|
||||||
|
String responseString = buffer.clone().readString(Charset.forName("UTF-8"));
|
||||||
|
|
||||||
|
// 清理PHP警告和HTML标签
|
||||||
|
responseString = cleanResponse(responseString);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return adapter.fromJson(responseString);
|
||||||
|
} finally {
|
||||||
|
value.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理响应中的PHP警告和HTML标签
|
||||||
|
*/
|
||||||
|
private String cleanResponse(String response) {
|
||||||
|
if (response == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除PHP警告(<br />和<b>Warning</b>标签)
|
||||||
|
response = response.replaceAll("<br\\s*/?>", "");
|
||||||
|
response = response.replaceAll("<b>Warning</b>", "");
|
||||||
|
response = response.replaceAll("</b>", "");
|
||||||
|
response = response.replaceAll("<b>", "");
|
||||||
|
|
||||||
|
// 查找JSON开始位置(第一个{或[)
|
||||||
|
int jsonStart = -1;
|
||||||
|
for (int i = 0; i < response.length(); i++) {
|
||||||
|
char c = response.charAt(i);
|
||||||
|
if (c == '{' || c == '[') {
|
||||||
|
jsonStart = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找到JSON开始位置,只保留JSON部分
|
||||||
|
if (jsonStart > 0) {
|
||||||
|
response = response.substring(jsonStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除JSON结束后的所有内容
|
||||||
|
int jsonEnd = response.lastIndexOf('}');
|
||||||
|
if (jsonEnd > 0 && response.trim().endsWith("}")) {
|
||||||
|
// JSON以}结尾,保留
|
||||||
|
} else if (jsonEnd > 0) {
|
||||||
|
response = response.substring(0, jsonEnd + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 创建Converter Factory
|
||||||
|
|
||||||
|
创建文件:`app/src/main/java/http/CleanResponseConverterFactory.java`
|
||||||
|
|
||||||
|
```java
|
||||||
|
package http;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.TypeAdapter;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import retrofit2.Converter;
|
||||||
|
import retrofit2.Converter.Factory;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
|
||||||
|
public class CleanResponseConverterFactory extends Factory {
|
||||||
|
private final Gson gson;
|
||||||
|
|
||||||
|
public CleanResponseConverterFactory(Gson gson) {
|
||||||
|
this.gson = gson;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, retrofit2.Retrofit retrofit) {
|
||||||
|
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
|
||||||
|
return new CleanResponseConverter<>(gson, (TypeAdapter<Object>) adapter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 修改RetrofitServiceManager
|
||||||
|
|
||||||
|
在 `app/src/main/java/http/RetrofitServiceManager.java` 中:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 修改前
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
|
||||||
|
// 修改后
|
||||||
|
.addConverterFactory(new CleanResponseConverterFactory(new Gson()))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 问题定位
|
||||||
|
|
||||||
|
### 服务器端文件位置
|
||||||
|
- **文件路径**: `/home/renjianbo/saars/wy/wy/wy/server/application/Interface/libraries/Api/Goods/AddGoodsInfo.php`
|
||||||
|
- **问题行数**: 第85-87行
|
||||||
|
|
||||||
|
### 相关API接口
|
||||||
|
- **接口**: `AddGoodsInfo` (添加商品信息)
|
||||||
|
- **请求URL**: `http://101.43.95.130:8030/api/`
|
||||||
|
- **请求参数**:
|
||||||
|
- `app`: "Goods"
|
||||||
|
- `class`: "AddGoodsInfo"
|
||||||
|
- `sign`: MD5签名
|
||||||
|
|
||||||
|
## 测试验证
|
||||||
|
|
||||||
|
### 验证服务器端修复
|
||||||
|
1. 检查日志文件是否存在:`ls -la /var/www/wy/log.dat`
|
||||||
|
2. 检查目录权限:`ls -ld /var/www/wy/`
|
||||||
|
3. 测试API响应是否干净(无PHP警告)
|
||||||
|
|
||||||
|
### 验证客户端修复
|
||||||
|
1. 查看日志中响应体是否包含警告
|
||||||
|
2. 确认JSON解析是否正常
|
||||||
|
3. 测试业务功能是否正常
|
||||||
|
|
||||||
|
## 建议优先级
|
||||||
|
|
||||||
|
### 🔴 高优先级(立即处理)
|
||||||
|
1. **服务器端修复日志文件路径问题**
|
||||||
|
- 创建目录或修复路径
|
||||||
|
- 添加错误检查
|
||||||
|
|
||||||
|
2. **关闭PHP错误输出到响应**
|
||||||
|
- 设置 `display_errors = 0`
|
||||||
|
- 使用 `error_log` 记录错误
|
||||||
|
|
||||||
|
### 🟡 中优先级(短期处理)
|
||||||
|
1. **添加客户端响应清理**
|
||||||
|
- 实现自定义Converter
|
||||||
|
- 清理响应中的HTML标签
|
||||||
|
|
||||||
|
2. **完善错误处理机制**
|
||||||
|
- 添加响应验证
|
||||||
|
- 添加异常处理
|
||||||
|
|
||||||
|
### 🟢 低优先级(长期优化)
|
||||||
|
1. **统一日志记录机制**
|
||||||
|
- 使用统一的日志服务
|
||||||
|
- 添加日志轮转
|
||||||
|
|
||||||
|
2. **API响应标准化**
|
||||||
|
- 确保所有API响应格式统一
|
||||||
|
- 添加响应验证
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
当前问题虽然不影响业务功能,但存在潜在风险。建议:
|
||||||
|
|
||||||
|
1. **优先修复服务器端**:从根本上解决问题
|
||||||
|
2. **客户端添加容错**:作为临时保护措施
|
||||||
|
3. **完善监控机制**:及时发现类似问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**问题发现时间**: 2026-01-09 15:40:30
|
||||||
|
**影响范围**: AddGoodsInfo API接口
|
||||||
|
**严重程度**: 中等(功能正常,但响应格式不规范)
|
||||||
|
|
||||||
Reference in New Issue
Block a user