如何通过URLConnection实现PDF缓存以提升加载速度?
Android PDF文件本地缓存实现方案
问题场景
使用HttpURLConnection加载远程PDF时速度缓慢,需要实现本地缓存机制,再次访问同一PDF时直接读取本地文件,无需重复下载。
核心实现思路
- 本地文件缓存:根据PDF的URL生成唯一文件名,将下载后的文件保存到APP私有目录,下次访问优先读取本地缓存。
- 服务器缓存校验:通过
If-Modified-Since请求头询问服务器文件是否更新,避免下载未修改的文件。 - 优化IO操作:使用缓冲流提升读写效率,避免内存溢出。
修改后的完整代码
import android.app.Activity; import android.content.pm.ActivityInfo; import android.os.AsyncTask; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.github.barteksc.pdfviewer.PDFView; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.FirebaseDatabase; import com.google.firebase.database.ValueEventListener; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.CookieHandler; import java.net.CookieManager; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.TimeZone; public class YourPDFActivity extends Activity { private ListView myPDFListView; private PDFView pdfView; private DatabaseReference databaseReference; private List<uploadPDF> uploadPDFS = new ArrayList<>(); @Override protected void onCreate(@Nullable android.os.Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.your_main_layout); myPDFListView = findViewById(R.id.your_list_view_id); viewAllFiles(); myPDFListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) { setContentView(R.layout.viewfile_activity); if(getResources().getBoolean(R.bool.portrait_only)){ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } pdfView = findViewById(R.id.pdfView); uploadPDF targetPDF = uploadPDFS.get(position); new RetrievePDFStream().execute(targetPDF.url); } }); } class RetrievePDFStream extends AsyncTask<String, Void, InputStream> { private File cacheFile; @Override protected InputStream doInBackground(String... strings) { String pdfUrl = strings[0]; // 提前初始化Cookie管理 CookieManager cookieManager = new CookieManager(); CookieHandler.setDefault(cookieManager); // 1. 获取本地缓存文件 cacheFile = getCacheFile(pdfUrl); if (cacheFile.exists()) { try { // 校验服务器文件是否未更新 if (isFileUnchangedOnServer(pdfUrl, cacheFile.lastModified())) { return new FileInputStream(cacheFile); } } catch (IOException e) { e.printStackTrace(); } } // 2. 无有效缓存则重新下载 InputStream inputStream = null; HttpURLConnection urlConnection = null; try { URL urlx = new URL(pdfUrl); urlConnection = (HttpURLConnection) urlx.openConnection(); // 添加缓存校验请求头 if (cacheFile.exists()) { urlConnection.setRequestProperty("If-Modified-Since", formatLastModifiedTime(cacheFile.lastModified())); } int responseCode = urlConnection.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { inputStream = new BufferedInputStream(urlConnection.getInputStream()); // 将下载内容写入缓存 saveStreamToCache(inputStream, cacheFile); // 返回缓存文件的输入流 return new FileInputStream(cacheFile); } else if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { // 文件未修改,直接用本地缓存 return new FileInputStream(cacheFile); } } catch (IOException e) { e.printStackTrace(); } finally { // 确保资源释放 if (urlConnection != null) { urlConnection.disconnect(); } try { if (inputStream != null) { inputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } return null; } @Override protected void onPostExecute(InputStream inputStream) { if (inputStream != null) { pdfView.fromStream(inputStream) .enableSwipe(true) .load(); // 关闭流避免资源泄漏 try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } // 根据URL生成唯一缓存文件名 private File getCacheFile(String url) { String fileName = generateMD5(url) + ".pdf"; return new File(getCacheDir(), fileName); } // MD5加密URL生成唯一标识 private String generateMD5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(input.getBytes()); StringBuilder hexBuilder = new StringBuilder(); for (byte b : digest) { String hex = Integer.toHexString(0xFF & b); if (hex.length() == 1) { hexBuilder.append('0'); } hexBuilder.append(hex); } return hexBuilder.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } // 校验服务器文件是否未更新 private boolean isFileUnchangedOnServer(String url, long localLastModified) throws IOException { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestMethod("HEAD"); connection.setRequestProperty("If-Modified-Since", formatLastModifiedTime(localLastModified)); int responseCode = connection.getResponseCode(); connection.disconnect(); return responseCode == HttpURLConnection.HTTP_NOT_MODIFIED; } // 格式化时间为HTTP标准格式 private String formatLastModifiedTime(long timestamp) { if (timestamp <= 0) return ""; SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); sdf.setTimeZone(TimeZone.getTimeZone("GMT")); return sdf.format(new Date(timestamp)); } // 将输入流写入缓存文件 private void saveStreamToCache(InputStream inputStream, File cacheFile) throws IOException { BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(cacheFile)); byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } outputStream.flush(); outputStream.close(); } } private void viewAllFiles() { databaseReference = FirebaseDatabase.getInstance().getReference("uploads"); databaseReference.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { uploadPDFS.clear(); // 避免重复加载数据 for(DataSnapshot postSnapshot : snapshot.getChildren()){ uploadPDF uploadPDF = postSnapshot.getValue(com.tokha.diplom.uploadPDF.class); uploadPDFS.add(uploadPDF); } String[] uploads = new String[uploadPDFS.size()]; for(int i = 0;i<uploads.length;i++){ uploads[i] = uploadPDFS.get(i).getName(); } ArrayAdapter<String> adapter = new ArrayAdapter<String>(getApplicationContext(), android.R.layout.simple_list_item_1, uploads){ @NonNull @Override public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { View view = super.getView(position, convertView, parent); TextView myText = view.findViewById(android.R.id.text1); myText.setTextColor(getResources().getColor(R.color.autorization)); return view; } }; myPDFListView.setAdapter(adapter); } @Override public void onCancelled(@NonNull DatabaseError error) { // 可添加错误提示逻辑 } }); } // 假设的uploadPDF实体类 public static class uploadPDF { public String url; public String name; public uploadPDF() {} public uploadPDF(String url, String name) { this.url = url; this.name = name; } } }
关键优化点说明
- 缓存文件管理:通过MD5加密URL生成唯一文件名,避免缓存冲突;使用APP私有缓存目录
getCacheDir(),系统会在存储空间不足时自动清理。 - 服务器校验:发送
HEAD请求和If-Modified-Since头,仅在文件更新时才重新下载,节省带宽。 - IO优化:使用8KB缓冲流提升读写速度,减少IO操作次数。
- 资源安全:在finally块中强制关闭流和网络连接,避免资源泄漏。
额外建议
- 替换
AsyncTask:AsyncTask已被标记为过时,建议改用Kotlin协程或ExecutorService实现异步操作,代码更简洁且生命周期更安全。 - 缓存清理:可添加手动清理缓存的功能,或定期清理超过7天的过期缓存文件。
内容的提问来源于stack exchange,提问作者SmyhkS Channe
相关产品推荐
相关产品推荐

