Android工程中FTP加密传输与非加密传输的深度解析

详细的FTP传输实现方案,包括完整代码、安全实践、性能优化和实际应用场景分析。

一、FTP传输类型对比表(增强版)

特性 非加密FTP FTPS (FTP over SSL/TLS) SFTP (SSH File Transfer Protocol)
协议基础 FTP (RFC 959) FTP + SSL/TLS (RFC 4217) SSH-2 (RFC 4253)
默认端口 21 (控制), 20 (数据) 990 (隐式), 21 (显式) 22
加密方式 SSL/TLS加密 SSH加密隧道
数据完整性 SSL/TLS保证 SSH保证
认证机制 用户名/密码 用户名/密码 + 证书 用户名/密码 + 密钥对
防火墙友好 差 (需要多个端口) 中等 (需要控制+数据端口) 优 (单一端口)
Android实现库 Apache Commons Net Apache Commons Net JSch/SSHJ
性能开销 最低 中等 (10-15%) 中等 (8-12%)
推荐场景 内部测试网络 企业级文件传输 互联网文件传输

二、完整实现方案

1. 项目配置

build.gradle (模块级):

gradle 复制代码
dependencies {
    // FTP/FTPS 实现
    implementation 'commons-net:commons-net:3.9.0'
    
    // SFTP 实现 (选择其一)
    implementation 'com.jcraft:jsch:0.1.55' // 方案1
    implementation 'com.hierynomus:sshj:0.35.0' // 方案2(更现代)
    
    // 后台任务
    implementation 'androidx.work:work-runtime:2.7.1'
    
    // 安全存储
    implementation 'androidx.security:security-crypto:1.1.0-alpha03'
    
    // 日志
    implementation 'com.jakewharton.timber:timber:5.0.1'
}

AndroidManifest.xml:

xml 复制代码
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- 存储权限处理 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                 android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
                 tools:ignore="ScopedStorage" />

2. 非加密FTP实现(增强版)

java 复制代码
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class FtpUploader {

    private static final int CONNECT_TIMEOUT = 30; // 秒
    private static final int DATA_TIMEOUT = 120; // 秒

    public static FtpResult uploadFile(String server, int port, String username, 
                                      String password, String localPath, 
                                      String remoteDir, String remoteFileName) {
        
        FTPClient ftpClient = new FTPClient();
        FileInputStream inputStream = null;
        FtpResult result = new FtpResult();
        
        try {
            // 1. 配置客户端
            ftpClient.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(CONNECT_TIMEOUT));
            ftpClient.setDataTimeout((int) TimeUnit.SECONDS.toMillis(DATA_TIMEOUT));
            ftpClient.setControlEncoding("UTF-8");
            
            // 2. 连接服务器
            ftpClient.connect(server, port);
            int replyCode = ftpClient.getReplyCode();
            if (!FTPReply.isPositiveCompletion(replyCode)) {
                result.error = "FTP服务器拒绝连接。响应代码: " + replyCode;
                return result;
            }
            
            // 3. 登录认证
            if (!ftpClient.login(username, password)) {
                result.error = "FTP登录失败。请检查凭据。";
                return result;
            }
            
            // 4. 配置传输模式
            ftpClient.enterLocalPassiveMode(); // 重要:应对防火墙/NAT
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            
            // 5. 创建远程目录(如果需要)
            if (remoteDir != null && !remoteDir.isEmpty()) {
                createDirectoryTree(ftpClient, remoteDir);
            }
            
            // 6. 上传文件
            File localFile = new File(localPath);
            if (!localFile.exists()) {
                result.error = "本地文件不存在: " + localPath;
                return result;
            }
            
            inputStream = new FileInputStream(localFile);
            String remotePath = (remoteDir != null ? remoteDir + "/" : "") + remoteFileName;
            
            long startTime = System.currentTimeMillis();
            boolean success = ftpClient.storeFile(remotePath, inputStream);
            long duration = System.currentTimeMillis() - startTime;
            
            if (success) {
                result.success = true;
                result.fileSize = localFile.length();
                result.durationMs = duration;
                Timber.d("FTP上传成功: %d bytes, 耗时: %d ms", 
                         result.fileSize, result.durationMs);
            } else {
                result.error = "文件存储失败。服务器响应: " + ftpClient.getReplyString();
            }
            
        } catch (Exception e) {
            result.error = "FTP异常: " + e.getMessage();
            Timber.e(e, "FTP上传失败");
        } finally {
            // 7. 清理资源
            try {
                if (inputStream != null) inputStream.close();
                if (ftpClient.isConnected()) {
                    ftpClient.logout();
                    ftpClient.disconnect();
                }
            } catch (IOException e) {
                Timber.e(e, "FTP清理资源时出错");
            }
        }
        
        return result;
    }
    
    private static void createDirectoryTree(FTPClient ftpClient, String path) throws IOException {
        String[] pathElements = path.split("/");
        if (pathElements.length > 0 && pathElements[0].isEmpty()) {
            pathElements[0] = "/";
        }
        
        for (String element : pathElements) {
            if (element.isEmpty()) continue;
            
            // 检查目录是否存在
            if (!ftpClient.changeWorkingDirectory(element)) {
                // 目录不存在则创建
                if (ftpClient.makeDirectory(element)) {
                    ftpClient.changeWorkingDirectory(element);
                } else {
                    throw new IOException("无法创建目录: " + element);
                }
            }
        }
    }
    
    public static class FtpResult {
        public boolean success = false;
        public long fileSize = 0;
        public long durationMs = 0;
        public String error = null;
    }
}

3. FTPS实现(增强安全版)

java 复制代码
import org.apache.commons.net.ftp.FTPSClient;
import org.apache.commons.net.util.TrustManagerUtils;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.File;
import java.io.FileInputStream;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;

public class FtpsUploader {

    public static FtpResult uploadFile(String server, int port, String username, 
                                      String password, String localPath, 
                                      String remoteDir, String remoteFileName, 
                                      boolean explicit, boolean validateCert) {
        
        FTPSClient ftpsClient;
        if (explicit) {
            // 显式 FTPS (FTPES)
            ftpsClient = new FTPSClient("TLS");
        } else {
            // 隐式 FTPS
            ftpsClient = new FTPSClient(true); 
        }
        
        // 配置SSL上下文
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            
            if (validateCert) {
                // 生产环境:使用系统默认信任管理器
                sslContext.init(null, null, null);
            } else {
                // 测试环境:接受所有证书(不推荐生产使用)
                TrustManager[] trustAllCerts = new TrustManager[]{
                    new X509TrustManager() {
                        public X509Certificate[] getAcceptedIssuers() {
                            return new X509Certificate[0];
                        }
                        public void checkClientTrusted(X509Certificate[] certs, String authType) {}
                        public void checkServerTrusted(X509Certificate[] certs, String authType) {}
                    }
                };
                sslContext.init(null, trustAllCerts, null);
            }
            
            ftpsClient.setSSLContext(sslContext);
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            FtpResult result = new FtpResult();
            result.error = "SSL配置失败: " + e.getMessage();
            return result;
        }
        
        // 设置其他参数
        ftpsClient.setConnectTimeout(30000);
        ftpsClient.setDataTimeout(120000);
        
        // 启用服务器主机名验证
        ftpsClient.setHostnameVerifier((hostname, session) -> true); // 生产环境应实现验证
        
        try {
            // 连接服务器
            ftpsClient.connect(server, port);
            
            // 显式模式需要发送"AUTH TLS"命令
            if (explicit) {
                ftpsClient.execPROT("P"); // 保护数据通道
            }
            
            // 登录和文件传输逻辑与非加密FTP类似...
            // 参考FtpUploader的实现,添加以下安全步骤:
            
            // 登录后启用安全数据通道
            ftpsClient.execPBSZ(0); // 设置保护缓冲区大小
            ftpsClient.execPROT("P"); // 设置数据通道保护
            
            // ... 其余上传逻辑与FtpUploader相同
            
        } catch (Exception e) {
            // 错误处理
        } finally {
            // 清理资源
        }
        
        // 返回结果...
    }
}

4. SFTP实现(使用SSHJ - 现代方案)

java 复制代码
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.sftp.SFTPClient;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
import net.schmizz.sshj.xfer.FileSystemFile;

import java.io.File;
import java.io.IOException;
import java.security.PublicKey;

public class SftpUploader {

    public static SftpResult uploadFile(String server, int port, String username,
                                      String password, String localPath,
                                      String remoteDir, String remoteFileName,
                                      boolean verifyHostKey) {
        
        SSHClient sshClient = new SSHClient();
        SftpResult result = new SftpResult();
        
        try {
            // 1. 配置SSH客户端
            sshClient.addHostKeyVerifier(new PromiscuousVerifier() {
                @Override
                public boolean verify(String hostname, int port, PublicKey key) {
                    if (verifyHostKey) {
                        // 生产环境应验证主机密钥
                        // 实现方式:将已知主机密钥存储在安全位置并比较
                        return super.verify(hostname, port, key);
                    }
                    return true; // 测试环境跳过验证
                }
            });
            
            sshClient.setConnectTimeout(30000);
            sshClient.setTimeout(120000);
            
            // 2. 连接服务器
            sshClient.connect(server, port);
            
            // 3. 认证
            sshClient.authPassword(username, password);
            // 可选:密钥认证
            // sshClient.authPublickey(username, "path/to/private/key");
            
            // 4. 创建SFTP客户端
            try (SFTPClient sftpClient = sshClient.newSFTPClient()) {
                
                // 5. 创建远程目录(如果需要)
                if (remoteDir != null && !remoteDir.isEmpty()) {
                    createRemoteDirectory(sftpClient, remoteDir);
                }
                
                // 6. 上传文件
                String remotePath = remoteDir + "/" + remoteFileName;
                File localFile = new File(localPath);
                
                long startTime = System.currentTimeMillis();
                sftpClient.put(new FileSystemFile(localFile), remotePath);
                long duration = System.currentTimeMillis() - startTime;
                
                result.success = true;
                result.fileSize = localFile.length();
                result.durationMs = duration;
                Timber.d("SFTP上传成功: %d bytes, 耗时: %d ms", 
                         result.fileSize, result.durationMs);
            }
            
        } catch (Exception e) {
            result.error = "SFTP错误: " + e.getMessage();
            Timber.e(e, "SFTP上传失败");
        } finally {
            try {
                sshClient.disconnect();
            } catch (IOException e) {
                Timber.e(e, "关闭SSH连接时出错");
            }
        }
        
        return result;
    }
    
    private static void createRemoteDirectory(SFTPClient sftp, String path) throws IOException {
        String[] folders = path.split("/");
        String currentPath = "";
        
        for (String folder : folders) {
            if (folder.isEmpty()) continue;
            
            currentPath += "/" + folder;
            try {
                sftp.lstat(currentPath); // 检查目录是否存在
            } catch (IOException e) {
                // 目录不存在则创建
                sftp.mkdir(currentPath);
            }
        }
    }
    
    public static class SftpResult {
        public boolean success = false;
        public long fileSize = 0;
        public long durationMs = 0;
        public String error = null;
    }
}

5. 后台任务管理(WorkManager增强版)

java 复制代码
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

public class FileUploadWorker extends Worker {

    private static final String KEY_SERVER = "server";
    private static final String KEY_PORT = "port";
    private static final String KEY_USERNAME = "username";
    private static final String KEY_PASSWORD = "password";
    private static final String KEY_LOCAL_PATH = "local_path";
    private static final String KEY_REMOTE_DIR = "remote_dir";
    private static final String KEY_REMOTE_FILE = "remote_file";
    private static final String KEY_PROTOCOL = "protocol"; // "ftp", "ftps", "sftp"
    private static final String KEY_VERIFY_CERT = "verify_cert"; // 仅FTPS/SFTP
    
    public FileUploadWorker(@NonNull Context context, @NonNull WorkerParameters params) {
        super(context, params);
    }

    @NonNull
    @Override
    public Result doWork() {
        Data inputData = getInputData();
        
        // 从输入数据中提取参数
        String server = inputData.getString(KEY_SERVER);
        int port = inputData.getInt(KEY_PORT, 21);
        String username = inputData.getString(KEY_USERNAME);
        String password = inputData.getString(KEY_PASSWORD);
        String localPath = inputData.getString(KEY_LOCAL_PATH);
        String remoteDir = inputData.getString(KEY_REMOTE_DIR);
        String remoteFile = inputData.getString(KEY_REMOTE_FILE);
        String protocol = inputData.getString(KEY_PROTOCOL);
        boolean verifyCert = inputData.getBoolean(KEY_VERIFY_CERT, false);
        
        // 根据协议选择上传方法
        try {
            boolean success;
            switch (protocol) {
                case "ftps":
                    // FTPS可以使用显式或隐式模式
                    boolean explicit = port == 21; // 通常显式模式使用21端口
                    FtpsUploader.FtpResult ftpsResult = FtpsUploader.uploadFile(
                        server, port, username, password, localPath, 
                        remoteDir, remoteFile, explicit, verifyCert);
                    success = ftpsResult.success;
                    break;
                    
                case "sftp":
                    SftpUploader.SftpResult sftpResult = SftpUploader.uploadFile(
                        server, port, username, password, localPath, 
                        remoteDir, remoteFile, verifyCert);
                    success = sftpResult.success;
                    break;
                    
                case "ftp":
                default:
                    FtpUploader.FtpResult ftpResult = FtpUploader.uploadFile(
                        server, port, username, password, localPath, 
                        remoteDir, remoteFile);
                    success = ftpResult.success;
            }
            
            return success ? Result.success() : Result.failure();
        } catch (Exception e) {
            return Result.failure();
        }
    }
    
    // 创建上传任务的方法
    public static void enqueueUpload(Context context, String protocol, 
                                    String server, int port, String username, 
                                    String password, String localPath, 
                                    String remoteDir, String remoteFile,
                                    boolean verifyCert) {
        
        Data inputData = new Data.Builder()
            .putString(KEY_PROTOCOL, protocol)
            .putString(KEY_SERVER, server)
            .putInt(KEY_PORT, port)
            .putString(KEY_USERNAME, username)
            .putString(KEY_PASSWORD, password)
            .putString(KEY_LOCAL_PATH, localPath)
            .putString(KEY_REMOTE_DIR, remoteDir)
            .putString(KEY_REMOTE_FILE, remoteFile)
            .putBoolean(KEY_VERIFY_CERT, verifyCert)
            .build();
        
        Constraints constraints = new Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .build();
        
        OneTimeWorkRequest uploadRequest = new OneTimeWorkRequest.Builder(FileUploadWorker.class)
            .setInputData(inputData)
            .setConstraints(constraints)
            .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
            .build();
        
        WorkManager.getInstance(context).enqueue(uploadRequest);
    }
}

6. 安全凭证管理(使用Android Keystore)

java 复制代码
import android.content.Context;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;

import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey;

import java.nio.charset.StandardCharsets;
import java.security.KeyStore;

public class SecureCredentialManager {

    private static final String PREFS_NAME = "secure_ftp_prefs";
    private static final String KEY_SERVER = "server";
    private static final String KEY_USERNAME = "username";
    private static final String KEY_PASSWORD = "password";

    public static void saveCredentials(Context context, String server, 
                                      String username, String password) {
        try {
            MasterKey masterKey = new MasterKey.Builder(context)
                .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
                .build();

            EncryptedSharedPreferences sharedPreferences =
                (EncryptedSharedPreferences) EncryptedSharedPreferences.create(
                    context,
                    PREFS_NAME,
                    masterKey,
                    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
                    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
                );

            sharedPreferences.edit()
                .putString(KEY_SERVER, server)
                .putString(KEY_USERNAME, username)
                .putString(KEY_PASSWORD, password)
                .apply();
            
        } catch (Exception e) {
            Timber.e(e, "保存凭证失败");
        }
    }

    public static Credentials getCredentials(Context context) {
        try {
            MasterKey masterKey = new MasterKey.Builder(context)
                .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
                .build();

            EncryptedSharedPreferences sharedPreferences =
                (EncryptedSharedPreferences) EncryptedSharedPreferences.create(
                    context,
                    PREFS_NAME,
                    masterKey,
                    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
                    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
                );

            String server = sharedPreferences.getString(KEY_SERVER, null);
            String username = sharedPreferences.getString(KEY_USERNAME, null);
            String password = sharedPreferences.getString(KEY_PASSWORD, null);
            
            if (server != null && username != null && password != null) {
                return new Credentials(server, username, password);
            }
        } catch (Exception e) {
            Timber.e(e, "获取凭证失败");
        }
        return null;
    }
    
    public static class Credentials {
        public final String server;
        public final String username;
        public final String password;
        
        public Credentials(String server, String username, String password) {
            this.server = server;
            this.username = username;
            this.password = password;
        }
    }
}

三、文件路径处理(兼容Android 11+)

java 复制代码
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;

public class FileUtils {

    public static String getRealPath(Context context, Uri uri) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            return handleScopedStorage(context, uri);
        } else {
            return handleLegacyStorage(context, uri);
        }
    }
    
    private static String handleScopedStorage(Context context, Uri uri) {
        if (DocumentsContract.isDocumentUri(context, uri)) {
            final String docId = DocumentsContract.getDocumentId(uri);
            final String[] split = docId.split(":");
            final String type = split[0];
            
            if ("primary".equalsIgnoreCase(type)) {
                return Environment.getExternalStorageDirectory() + "/" + split[1];
            }
        } 
        // 处理其他存储提供程序...
        return null;
    }
    
    private static String handleLegacyStorage(Context context, Uri uri) {
        if ("content".equalsIgnoreCase(uri.getScheme())) {
            String[] projection = { MediaStore.Images.Media.DATA };
            try (Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null)) {
                if (cursor != null && cursor.moveToFirst()) {
                    int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
                    return cursor.getString(columnIndex);
                }
            }
        }
        // 处理文件URI
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }
        
        return null;
    }
    
    public static File createTempFile(Context context, InputStream inputStream, String fileName) throws IOException {
        File outputDir = context.getCacheDir();
        File outputFile = new File(outputDir, fileName);
        
        try (FileOutputStream outputStream = new FileOutputStream(outputFile)) {
            byte[] buffer = new byte[4 * 1024];
            int read;
            
            while ((read = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, read);
            }
            
            outputStream.flush();
        }
        
        return outputFile;
    }
}

四、最佳实践与高级主题

1. 安全实践

  • 证书固定:对于FTPS/SFTP,实现证书固定以防止中间人攻击
  • 双因素认证:使用密钥+密码组合进行SFTP认证
  • 连接复用:为频繁传输建立持久连接
  • 传输加密:即使使用SFTP,也可在应用层对敏感文件进行额外加密

2. 性能优化

  • 分块传输:大文件使用分块上传/下载
  • 并行传输:多个文件同时传输
  • 压缩传输:在传输前压缩文本/日志文件
  • 增量同步:仅传输变化部分

3. 错误处理与重试

java 复制代码
public class UploadManager {
    private static final int MAX_RETRIES = 3;
    private static final long RETRY_DELAY_MS = 5000;
    
    public static boolean uploadWithRetry(String protocol, /* 参数 */) {
        int attempt = 0;
        boolean success = false;
        
        while (attempt < MAX_RETRIES && !success) {
            try {
                switch (protocol) {
                    case "ftp":
                        success = FtpUploader.uploadFile(/* 参数 */).success;
                        break;
                    case "ftps":
                        success = FtpsUploader.uploadFile(/* 参数 */).success;
                        break;
                    case "sftp":
                        success = SftpUploader.uploadFile(/* 参数 */).success;
                        break;
                }
            } catch (Exception e) {
                Timber.e(e, "上传尝试 %d 失败", attempt + 1);
            }
            
            if (!success) {
                attempt++;
                if (attempt < MAX_RETRIES) {
                    try {
                        Thread.sleep(RETRY_DELAY_MS);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
        
        return success;
    }
}

4. 协议选择指南

场景 推荐协议 理由
内部网络,非敏感数据 标准FTP 简单、高效、低开销
企业级文件传输 FTPS (显式) 兼容性好,企业防火墙通常支持
互联网文件传输 SFTP 单一端口,高安全性,NAT穿透性好
需要严格审计 SFTP + 密钥认证 提供强身份验证和不可否认性
移动网络环境 SFTP 更好的连接稳定性,单一端口

五、完整工作流程

sequenceDiagram participant A as Android应用 participant KS as Keystore participant F as FTP/FTPS/SFTP服务器 A->>KS: 1. 获取安全凭证 activate KS KS-->>A: 返回加密凭证 deactivate KS A->>F: 2. 建立连接 activate F alt 标准FTP A->>F: 发送USER/PASS F-->>A: 登录确认 else FTPS A->>F: 发起SSL/TLS握手 F-->>A: 服务器证书 A->>A: 验证证书 A->>F: 发送USER/PASS F-->>A: 登录确认 else SFTP A->>F: SSH握手 F-->>A: 服务器主机密钥 A->>A: 验证主机密钥 A->>F: 用户认证(密码/密钥) F-->>A: 认证成功 end A->>F: 3. 准备传输(设置目录/模式) F-->>A: 确认 A->>F: 4. 传输文件数据 F-->>A: 传输进度确认 A->>F: 5. 关闭连接 F-->>A: 断开确认 deactivate F

六、常见问题解决方案

  1. 连接超时问题

    • 增加超时设置:ftpClient.setConnectTimeout(60000)
    • 检查网络策略:确保应用不在后台受限
    • 尝试被动/主动模式切换
  2. 文件权限问题

    java 复制代码
    // 在AndroidManifest.xml中添加
    <application
         android:requestLegacyExternalStorage="true"
         ...>
  3. 证书验证失败

    • 开发环境:使用TrustManagerUtils.getAcceptAllTrustManager()
    • 生产环境:将服务器证书打包到应用中并验证
  4. 大文件传输稳定性

    • 实现分块传输
    • 添加进度保存和断点续传
    • 使用WorkManager的持久化工作
  5. Android 12+ 网络限制

    • AndroidManifest.xml中添加:

      xml 复制代码
      <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
      <service android:name=".FileTransferService"
               android:foregroundServiceType="dataSync" />

这个增强版实现方案提供了完整的FTP传输解决方案,包括安全实践、性能优化和兼容性处理,适合在生产环境中使用。

相关推荐
AD钙奶-lalala2 小时前
android:foregroundServiceType详解
android
编程乐学(Arfan开发工程师)3 小时前
56、原生组件注入-原生注解与Spring方式注入
java·前端·后端·spring·tensorflow·bug·lua
周某某~4 小时前
七.适配器模式
java·设计模式·适配器模式
大胃粥5 小时前
Android V app 冷启动(13) 焦点窗口更新
android
奔跑的小十一5 小时前
JDBC接口开发指南
java·数据库
刘大猫.5 小时前
业务:资产管理功能
java·资产管理·资产·资产统计·fau·bpb·mcb
YuTaoShao6 小时前
Java八股文——JVM「内存模型篇」
java·开发语言·jvm
开开心心就好6 小时前
电脑扩展屏幕工具
java·开发语言·前端·电脑·php·excel·batch
零叹7 小时前
篇章十 数据结构——排序
java·数据结构·算法·排序算法