Android端使用JSch SFTP上传DCIM文件失败排查及方案咨询
让我帮你排查下问题,你遇到的上传失败大概率是几个Android+JSch的常见坑导致的,咱们一步步来解决:
1. 动态权限申请缺失(最可能的核心原因)
你在AndroidManifest.xml里声明了READ_EXTERNAL_STORAGE,但你的Build SDK是24(对应Android 7.0),而从Android 6.0(API23)开始,危险权限必须动态申请——仅仅在Manifest里声明是不够的,系统不会自动授予这类权限,必须在运行时向用户主动请求。
解决方法:
在启动上传任务前,先检查并请求权限:
private static final int REQUEST_STORAGE_PERMISSION = 100; // 在调用上传任务前执行权限检查 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_STORAGE_PERMISSION); } else { // 权限已授予,执行上传任务 new UploadTask().execute(); } // 处理权限请求结果 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_STORAGE_PERMISSION) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { new UploadTask().execute(); } else { Toast.makeText(this, "需要存储权限才能上传图片", Toast.LENGTH_SHORT).show(); } } }
2. 异常处理的错误操作
你在doInBackground里直接调用Toast.makeText是错误的!doInBackground运行在后台线程,而Toast必须在UI线程执行,这会触发异常但你没捕获到有效日志。另外ex.printStackTrace()在Android环境下可能不会输出到Logcat的可见位置,应该用Android的Log类打印完整异常栈。
修改异常处理部分:
catch (Exception ex) { Log.e("SFTP_UPLOAD_ERROR", "上传失败详情", ex); // 打印完整异常信息到Logcat // 回到UI线程显示提示 runOnUiThread(() -> Toast.makeText(getApplicationContext(), "上传失败:" + ex.getMessage(), Toast.LENGTH_LONG).show() ); }
这样你就能在Logcat里看到具体错误原因(比如权限拒绝、文件不存在、连接超时等)。
3. 硬编码文件路径不可靠
直接写/storage/emulated/0/DCIM/Camera/...或者/sdcard/...是不推荐的,不同设备的外部存储路径可能存在差异。应该用Android官方API获取标准路径:
// 获取DCIM/Camera的正确路径 File cameraDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "Camera"); File imageFile = new File(cameraDir, "IMG_20180118_122224.jpg"); String localFilePath = imageFile.getAbsolutePath();
这能保证在绝大多数Android设备上都能定位到正确的图片文件。
4. 未关闭资源导致的潜在问题
你的代码没有关闭ChannelSftp、Channel和Session,这会导致资源泄漏,甚至后续连接失败。应该在finally块里统一关闭这些资源:
finally { if (channelSftp2 != null && channelSftp2.isConnected()) { channelSftp2.disconnect(); } if (channel2 != null && channel2.isConnected()) { channel2.disconnect(); } if (session2 != null && session2.isConnected()) { session2.disconnect(); } }
完整优化后的代码示例
整合所有修改后的上传任务代码如下:
private class UploadTask extends AsyncTask<Void, Void, Boolean> { @Override protected Boolean doInBackground(Void... params) { String SFTPHOST2 = "192.168.0.1"; int SFTPPORT2 = 22; String SFTPUSER2 = "pi"; String SFTPPASS2 = "raspberry"; String SFTPWORKINGDIR2 = "/home/pi/"; Session session2 = null; Channel channel2 = null; ChannelSftp channelSftp2 = null; try { // 获取正确的本地图片路径 File cameraDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "Camera"); File imageFile = new File(cameraDir, "IMG_20180118_122224.jpg"); if (!imageFile.exists()) { Log.e("SFTP_UPLOAD", "图片文件不存在:" + imageFile.getAbsolutePath()); return false; } JSch jsch2 = new JSch(); session2 = jsch2.getSession(SFTPUSER2, SFTPHOST2, SFTPPORT2); session2.setPassword(SFTPPASS2); session2.setConfig("StrictHostKeyChecking", "no"); session2.setTimeout(10000); session2.connect(); channel2 = session2.openChannel("sftp"); channel2.connect(); channelSftp2 = (ChannelSftp) channel2; channelSftp2.cd(SFTPWORKINGDIR2); // 执行上传 channelSftp2.put(imageFile.getAbsolutePath(), imageFile.getName()); return true; } catch (Exception ex) { Log.e("SFTP_UPLOAD_ERROR", "上传失败详情", ex); return false; } finally { // 关闭资源 if (channelSftp2 != null && channelSftp2.isConnected()) { channelSftp2.disconnect(); } if (channel2 != null && channel2.isConnected()) { channel2.disconnect(); } if (session2 != null && session2.isConnected()) { session2.disconnect(); } } } @Override protected void onPostExecute(Boolean success) { super.onPostExecute(success); if (success) { Toast.makeText(getApplicationContext(), "上传成功", Toast.LENGTH_LONG).show(); } else { Toast.makeText(getApplicationContext(), "上传失败,请查看Logcat获取详细信息", Toast.LENGTH_LONG).show(); } } }
内容的提问来源于stack exchange,提问作者Omid1989

