爬坑小集锦第四期

开场致辞

最近没更新,主要是最近没怎么做笔记。目前在学Erlang,一门函数式语言,在并发上也有很优异的表现。编程语言可分为三种:静态语言(C、Java等)、动态语言(JavaScript、Python之类)还有函数式语言(Lisp、Erlang这些)。传说程序员只要学齐这三种语言……也召唤不了神龙。但在思维上会有很大的升华(真的假的?)。至少当我看到在Erlang里只要短短五行代码就能写出一个快排的时候,好后悔没早点学会,面试的时候就能炫技了。一起来感受一下:

qsort([]) -> [];
qsort([Pivot|T]) ->
    qsort([X || X<-T, X<Pivot])
    ++[Pivot]++
    qsort([X || X<-T, X>Pivot]).

废话不再多说,马上开始我们本次专题——Android爬坑小合集。

Q1: 如何解决android.os.NetworkOnMainThreadException异常?

事件回放

使用Apache提供的common-net包进行网络编程时,获取网络数据时出现这个异常,Demo程序直接崩溃。这个异常的意思是在主线程中访问网络,所以出了异常。

原因分析

众所周知,Android的应用程序里有一个称为主线程的东东在对程序的执行进行管理,负责刷新程序的UI,调用程序的模块功能等。因此,倘若主线程有耗时的操作,将会给用户一种程序很卡的感觉,要是主线程阻塞了,更会造成程序没有响应的情况,形似在终端下执行了一个死循环。因此,像网络请求、大量的数据计算、图像处理等操作不应该放主线程运行,而要另外开启一个线程来处理它们。但在Android4.0之前,主线程是支持执行访问网络这类耗时的操作的,4.0之后对这部分程序进行了优化,因此在主线程有网络的相关操作就会出现上述异常。

两种解决方法

方法一:如果执意要在主线程运行的话,可以用下面这种方法绕过系统的限制(不推荐使用):

// 添加在Activity的OnCreate(Bundle saveInstanceState)方法中
if(android.os.Build.VERSION.SDK_INT>9){
    StrictMode.ThreadPolicy tp = new StrictMode.ThreadPolicy.Builder.permitAll().build();
    StrictMode.setThread.Policy(tp);
}

方法二:使用Thread或Handler,将这类耗时操作从主线程中分离出来(强烈推荐)。只要是学过Java编程的人都应该懂怎么使用线程(不懂赶紧补上),这里就不贴代码了。

Q2: Android网络编程之FTP上传下载

在Android中实现FTP功能,需要使用到Apache提供的一个包:Comment-Net,这个包对基本网络协议的功能进行了实现,使用它可以方便的进行网络编程。当然同样的提供网络编程相关的包的公司还有Sun公司,但Sun公司的提供的包似乎风评不怎样,详见Why developers should not write programs that call sun packages这篇文章。

FTP编程需要用到的核心类是FTPClient,这个类提供了基本的FTP客户端操作,如连接/断开服务器、登录/注销、设置连接模式、切换目录、上传/下载等功能。以下是简短的程序示例:

import java.io.FileInputStream;
import java.io.FileOutputStream;

import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.tftp.TFTP;

public class FTP {
    public static void main(String[] args){

        String hostname = "ftp.sr1.me";
        int port = 21;
        String username = "anonymous";
        String password = "";
        String pathname = "/libs";

        String upload_file_name = "upload.file";
        String download_file_name = "commons-net-3.0.1.jar";

        FTPClient client = new FTPClient();
        try {
            // 连接FTP服务器
            client.connect(hostname, port);
            // 登录
            client.login(username, password);

            // 创建 /libs 目录
            client.makeDirectory(pathname);

            // 切换到 /libs 目录下"
            client.changeWorkingDirectory(pathname);

            // 默认是以ASCII编码方式上传文件
            // 对于非ASCII编码的文件,需要将它设置为二进制模式
            client.setFileTransferMode(TFTP.BINARY_MODE);
            // 设置缓存大小
            client.setBufferSize(256);
            // 进入被动模式
            client.enterLocalPassiveMode();

            // 上传文件
            boolean upload_status;
            FileInputStream upload = new FileInputStream(upload_file_name);
            upload_status = client.storeFile(upload_file_name, upload);
            if(upload_status==true)
                System.out.println("上传成功");
            else
                System.out.println("上传失败");
            // 最后记得关闭输入流
            upload.close();

            // 列出FTP服务器当前目录下文件
            FTPFile[] file_list = client.listFiles();
            System.out.println("当前目录下有以下文件:");
            for (FTPFile file : file_list) {
                System.out.println(file.getName());
            }

            // 下载文件
            boolean download_status;
            FileOutputStream download = new FileOutputStream(download_file_name);
            download_status = client.retrieveFile(download_file_name, download);
            if(download_status==true)
                System.out.println("下载成功");
            else
                System.out.println("下载失败");
            // 同样记得关闭输出流
            download.close();

            // 注销
            client.logout();
            // 断开连接
            client.disconnect();
        } catch (Exception e) {
            // 为简单起见我们捕获所有异常
            e.printStackTrace();
        }
    }
}

实际使用的话应该把这些操作封装成一个类,方便整合到程序中去。更多功能,可以查阅Common-net库的文档

Q3: 上传时,纯文本文件正常,非纯文本文件损坏?

FTP协议里,文件传输有两种模式,一种是ASCII码模式,另一种是二进制编码模式。因此为了上传非ASCII编码的文件,我们需要在传输文件之前设置传输模式,即调用“FTPClient.setFileTransferMode(int Mode)”方法。在前面的示例代码里,上传之前设置了传输模式为二进制文件传输,即将参数的值设为“TFCP.BINARY_MODE”。

Q4: 下载也出问题了,带图的Word、PPT文件损坏无法打开

这个问题我在Android上出现了,下载下来的文件用WPS打不开,但在本地Java程序上不能再现,可能是无线网络传输导致的问题。因此,这个问题的坑目前还爬不出来,解决方案:绕坑。

绕坑之: 使用FTP框架ftp4j实现FTP功能

说是实现FTP功能有点不妥当,毕竟ftp4j已经把所需要的功能实现了,我们所做的只是把功能集成进程序而已。修改后的实例程序如下:

import it.sauronsoftware.ftp4j.FTPClient;
import it.sauronsoftware.ftp4j.FTPFile;

import java.io.File;

public class FTP {
    public static void main(String[] args){

        String hostname = "ftp.sr1.me";
        int port = 21;
        String username = "anonymous";
        String password = "";
        String upload_path = "/uploads";
        String download_path = "/libs";

        String upload_file_name = "upload.file";
        String download_file_name = "commons-net-3.0.1.jar";

        FTPClient client = new FTPClient();
        try {
            // 连接FTP服务器
            client.connect(hostname, port);
            // 登录
            client.login(username, password);

            // 创建 /uploads 目录
            client.createDirectory(upload_path);

            // 切换到 /uploads 目录下"            
            client.changeDirectory(upload_path);

            // 默认是以ASCII编码方式上传文件
            // 对于非ASCII编码的文件,需要将它设置为二进制模式
            client.setType(FTPClient.TYPE_BINARY); 
            // 进入被动模式
            client.setPassive(true);

            // 上传文件
            File upload = new File(upload_file_name);
            client.upload(upload);

            // 切换到 /libs 目录下"           
            client.changeDirectory(download_path);

            // 列出FTP服务器当前目录下文件
            FTPFile[] file_list = client.list();
            System.out.println("当前目录下有以下文件:");
            for (FTPFile file : file_list) {
                System.out.println(file.getName());
            }

            // 下载文件
            File download = new File(download_file_name);
            client.download(download_file_name, download);

            // 注销
            client.logout();
            // 断开连接
            client.disconnect(true);
        } catch (Exception e) {
            // 为简单起见我们捕获所有异常
            e.printStackTrace();
        }
    }
}

经测试,用这种方式能够解决下载文件损坏的问题。可以看到,使用框架确实方便了很多,简化了开发流程和难度,有效降低了代码量,避免了一些莫名其妙的Bug。但使用框架也会引入一些新的隐患:倘若框架不够健壮,出现Bug(这时候的Bug可能就不是一般的Bug了)不好调试;框架可能没有针对移动端进行性能优化,容易出现瓶颈问题;使用框架降低了难度的同时,也让程序员对具体实现缺少认识,无法实现个人成长。不管怎样,Q3的坑,就算是绕过去了。

对了,ftp4j的文档在这里

Q4: Android里如何调用系统自带的文件管理器选择文件?

调用系统的文件管理器选择文件这个操作在实现上分为以下三个步骤:

  1. 启动文件管理器选择文件;
  2. 文件管理器将被选中的文件的相关信息返回给应用;
  3. 接收到这些信息,进行相应处理。

其中第二步不需要我们处理,因此需要编程实现的是第一和第三步:

// 第一步:启动文件管理器选择文件
private void start_file_chooser(){
    // 表示这个操作是想要获取系统中的数据
    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
    // 设置文件类型,这里是所有类型文件
    intent.setType("*/*");
    // 表示取回来的文件要能打开进行操作
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    try {
        // startActivityForResult表示需要返回数据
        startActivityForResult(Intent.createChooser(intent, "请选择一个要上传的文件"),
            Activity.RESULT_OK);
    } catch (android.content.ActivityNotFoundException ex) {
        Toast.makeText(this, "请安装文件管理器", Toast.LENGTH_SHORT).show();
    }
}

// 第三步:接收数据进行处理
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    // 如果正常返回了数据
    if (resultCode == Activity.RESULT_OK) {
        // 获取文件的地址
        String file_path = data.getData().getPath();
        // 进行处理
        return;
    }
    super.onActivityResult(requestCode, resultCode, data);
}

先这样,没了。

That's all, But NOT ALL.