转载请注明出处:http://blog.csdn.net/a512337862/article/details/76035464

其实关于Android的串口通信Google早已提供了一个开源的工具,具体地址如下:https://github.com/cepr/android-serialport-api,大家可以自行下载。但是本人觉得这个demo有点过于复杂,所以在这个demo的基础上进行了一些封装,方便大家使用,勉强算得上是一个工具类。

串口操作

对于Android串口操作,基本上就是对应串口文件的读写,所以基本思路就是:
1.对串口文件进行配置(波特率等),打开串口文件
2.读写串口
3.关闭串口文件
但是这里需要注意的是Android/Java中读写串口需要用到FileDescriptor类(文件描述符),具体的大家可以参考:http://blog.csdn.net/morningsun1990/article/details/19639583。

代码分析

JNI

因为本篇是基于android-serialport-api 的,JNI自然是无法跳过的坑,至于JNI如何使用,网上的教程多不胜数,这里也不班门弄斧了,可以自行上网搜索。这里提醒一下大家注意下面这点:

  • 不同的项目,包含native方法的类对应的路径一般都不相同,所以通过javah -jni指令生成的.h文件名以及方法名也不同,那对应的.c文件方法名以及包含的.h头文件也需要修改。例如,android-serialport-api中的.h文件生成的方法名如下:

但是我这边的.h中的方法名则是:

下面贴一下跟JNI层相关的代码:

.c文件

//// Created by ZhangHao on 2016/7/29.//#include #include #include #include #include #include "com_serialPort_SerialPort.h"static const char *TAG = "serial_port";#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)static speed_t getBaudrate(jint baudrate) {    switch (baudrate) {        case 0:            return B0;        case 50:            return B50;        case 75:            return B75;        case 110:            return B110;        case 134:            return B134;        case 150:            return B150;        case 200:            return B200;        case 300:            return B300;        case 600:            return B600;        case 1200:            return B1200;        case 1800:            return B1800;        case 2400:            return B2400;        case 4800:            return B4800;        case 9600:            return B9600;        case 19200:            return B19200;        case 38400:            return B38400;        case 57600:            return B57600;        case 115200:            return B115200;        case 230400:            return B230400;        case 460800:            return B460800;        case 500000:            return B500000;        case 576000:            return B576000;        case 921600:            return B921600;        case 1000000:            return B1000000;        case 1152000:            return B1152000;        case 1500000:            return B1500000;        case 2000000:            return B2000000;        case 2500000:            return B2500000;        case 3000000:            return B3000000;        case 3500000:            return B3500000;        case 4000000:            return B4000000;        default:            return -1;    }}/* * Class:     com_sona_utils_SerialPort * Method:    open * Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor; */JNIEXPORT jobject JNICALL Java_com_serialPort_SerialPort_open        (JNIEnv *env, jclass thiz, jstring path, jint baudrate, jint flags) {       LOGE("JNI START");    int fd;    speed_t speed;    jobject mFileDescriptor;    /* Check arguments */    {        speed = getBaudrate(baudrate);        if (speed == -1) {            /* TODO: throw an exception */            LOGE("Invalid baudrate");            return NULL;        }    }    /* Opening device */    {        jboolean iscopy;        const char *path_utf = (*env)->GetStringUTFChars(env, path, &iscopy);        LOGD("Opening serial port %s with flags 0x%x", path_utf, O_RDWR | flags);        fd = open(path_utf, O_RDWR | O_NOCTTY | O_NDELAY);//O_RDWR | flags        LOGD("open() fd = %d", fd);        (*env)->ReleaseStringUTFChars(env, path, path_utf);        if (fd == -1) {            /* Throw an exception */            LOGE("Cannot open port");            /* TODO: throw an exception */            return NULL;        }        if (fcntl(fd, F_SETFL, 0) < 0)            LOGD("fcntl failed!\n");        else            LOGD("fcntl=%d\n", fcntl(fd, F_SETFL, 0));    }    /* Configure device */    {        struct termios cfg;        LOGD("Configuring serial port");        if (tcgetattr(fd, &cfg)) {            LOGE("tcgetattr() failed");            close(fd);            /* TODO: throw an exception */            return NULL;        }        cfmakeraw(&cfg);        cfg.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);        tcflush(fd, TCIFLUSH);        cfg.c_cc[VTIME] = 0;         cfg.c_cc[VMIN] = 0; //Update the Opt and do it now        cfsetispeed(&cfg, speed);        cfsetospeed(&cfg, speed);        if (tcsetattr(fd, TCSANOW, &cfg)) {            LOGE("tcsetattr() failed");            close(fd);            /* TODO: throw an exception */            return NULL;        }    }    /* Create a corresponding file descriptor */    {        jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor");        jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "",                                                        "()V");        jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I");        mFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor);        (*env)->SetIntField(env, mFileDescriptor, descriptorID, (jint) fd);    }    return mFileDescriptor;}/* * Class:     com_sona_utils_SerialPort * Method:    close * Signature: ()V */JNIEXPORT void JNICALL Java_com_serialPort_SerialPort_close        (JNIEnv *env, jobject thiz) {    jclass SerialPortClass = (*env)->GetObjectClass(env, thiz);    jclass FileDescriptorClass = (*env)->FindClass(env, "java/io/FileDescriptor");    jfieldID mFdID = (*env)->GetFieldID(env, SerialPortClass, "mFd", "Ljava/io/FileDescriptor;");    jfieldID descriptorID = (*env)->GetFieldID(env, FileDescriptorClass, "descriptor", "I");    jobject mFd = (*env)->GetObjectField(env, thiz, mFdID);    jint descriptor = (*env)->GetIntField(env, mFd, descriptorID);    LOGD("close(fd = %d)", descriptor);    close(descriptor);}

.h文件

/* DO NOT EDIT THIS FILE - it is machine generated */#include /* Header for class com_serialPort_SerialPort */#ifndef _Included_com_serialPort_SerialPort#define _Included_com_serialPort_SerialPort#ifdef __cplusplusextern "C" {#endif/* * Class:     com_serialPort_SerialPort * Method:    open * Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor; */JNIEXPORT jobject JNICALL Java_com_serialPort_SerialPort_open  (JNIEnv *, jclass, jstring, jint, jint);/* * Class:     com_serialPort_SerialPort * Method:    close * Signature: ()V */JNIEXPORT void JNICALL Java_com_serialPort_SerialPort_close  (JNIEnv *, jobject);#ifdef __cplusplus}#endif#endif

Android.mk

LOCAL_PATH:= $(call my-dir)include $(CLEAR_VARS)LOCAL_CERTIFICATE :=platformLOCAL_MODULE := serialPortLibLOCAL_LDLIBS := \    -llog \    -lz \    -lm \LOCAL_SRC_FILES := com_serialPort_SerialPort.cinclude $(BUILD_SHARED_LIBRARY)

c文件基本上原封不动照搬android-serialport-api,本人对此也是一知半解,这里就不多做阐述。

JAVA代码

SerialPort.java

public class SerialPort {    private static final String TAG = "SerialPort";    /*     * Do not remove or rename the field mFd: it is used by native method     * close();     */    private FileDescriptor mFd;    private FileInputStream mFileInputStream;    private FileOutputStream mFileOutputStream;    public SerialPort(File device, int baudrate, int flags)            throws SecurityException, IOException {        /* Check access permission *///        if (!device.canRead() && !device.canWrite()) {//            try {//                /* Missing read/write permission, trying to chmod the file *///                Process su;//                Log.e(TAG, "Process su");//                su = Runtime.getRuntime().exec("/system/bin/su");//                String cmd = "chmod 666 " + device.getAbsolutePath() + "\n"//                        + "exit\n";//                su.getOutputStream().write(cmd.getBytes());//                if ((su.waitFor() != 0) || !device.canRead()//                        || !device.canWrite()) {//                    if (!device.canRead() || !device.canWrite()) {//                        Log.e(TAG, "串口无法读写!");//                    }//                    throw new SecurityException();//                }//            } catch (Exception e) {//                e.printStackTrace();//                Log.e(TAG, e.toString());//                throw new SecurityException();//            }//        } else {//            Log.e(TAG, "start Open");//        }        mFd = open(device.getAbsolutePath(), baudrate, flags);        if (mFd == null) {            Log.e(TAG, "native open returns null");            throw new IOException();        }        mFileInputStream = new FileInputStream(mFd);        mFileOutputStream = new FileOutputStream(mFd);    }    // Getters and setters    public InputStream getInputStream() {        return mFileInputStream;    }    public OutputStream getOutputStream() {        return mFileOutputStream;    }    // JNI    private native static FileDescriptor open(String path, int baudrate,                                              int flags);    public native void close();}

SerialPort也是来自android-serialport-api,主要就是native方法打开以及关闭指定的串口文件,获取对应输出输入流。而我们对串口的读写也是通过输出输入流来完成的。

这里我把检查对串口文件读写权限以及尝试添加权限的相关代码给注释掉了,因为我这边基本上用不到,大家需要的话可以自己加上。这里需要注意的是:

  1. su = Runtime.getRuntime().exec(“/system/bin/su”),并不是所有的系统su路径都是/system/bin/su,也有的路径是/system/xbin/su。可能也会有其他的路径,这个要根据实际情况而定。

  2. 这段代码我不知道其他的设备能不能成功,但是我这边是没办法成功的,最后还是通过修改源码来实现的。

  3. 还有一种临时性的办法,是通过临时关闭SELinux,重启就会失效。主要是在命令行执行以下代码:

    • adb shell setenforce 0
    • adb shell
    • chmod 0666 /dev/ttyS2

其实这里只是新增了关闭SELinux的命令,关于Android SELinux,大家可以参考老罗的博客:http://blog.csdn.net/luoshengyang/article/details/37613135。
ps:这种方法我也不敢保证在所有的设备上都能成功,但是至少我在全志的几块板子测试是可行的。其实,最好的办法还是修改源码,一劳永逸。

SerialPortUtil.java

/** * Created by ZhangHao on 2017/7/24. * 串口操作工具类 */public class SerialPortUtil {    //    private SerialPort mSerialPort;    private OutputStream mOutputStream;    private InputStream mInputStream;    //串口返回回调    private SerialPortDataCallBack serialPortDataCallBack;    //flag 结束串口发送线程    private boolean sendFlag = true;    //Dream音效命令List    private List<byte[]> dreamProList = new ArrayList<>();    //超时时间(超时检测线程是每次休眠固定时间,通过休眠次数来判断超时)    private int timeOutCount = 10;    //休眠次数    private int sleepCount = 0;    //是否返回标识    private boolean isReturn = true;    public void initSerialPort(final String filePath, final int baudRate, final int flags) {        new Thread(new Runnable() {            @Override            public void run() {                try {                    if (mSerialPort == null) {                        mSerialPort = new SerialPort(new File(filePath), baudRate, flags);                    }                    mOutputStream = mSerialPort.getOutputStream();                    mInputStream = mSerialPort.getInputStream();                    //开启线程循环接收串口返回的指令                    new ReadThread().start();                    //开始发送串口指令线程以及超时判断线程                    new SendOrderThread().start();                    new TimeOutThread().start();                    if (serialPortDataCallBack != null) {                        //初始化完成,回调结果                        serialPortDataCallBack.onSerialPortInitFinish(true);                    }                } catch (Exception e) {                    e.printStackTrace();                    if (serialPortDataCallBack != null) {                        //初始化完成,回调结果                        serialPortDataCallBack.onSerialPortInitFinish(false);                    }                }            }        }).start();    }    public void closeSerialPort() {        if (mSerialPort != null) {            mSerialPort.close();            mSerialPort = null;        }        //将sendFlag置为false        sendFlag = false;    }    //发送串口命令    private void sendOrder(byte[] bytes) {        //发送byte[]指令        try {            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();            byteArrayOutputStream.write(bytes);            mOutputStream.write(byteArrayOutputStream.toByteArray());        } catch (IOException e) {            e.printStackTrace();        }    }    //发送最前面的指令    private synchronized void sendFirst() {        if (dreamProList.size() > 0 && isReturn) {            isReturn = false;            byte[] buffer = dreamProList.get(0);            sendOrder(buffer);        }    }    //移除最前面的指令    private synchronized void removeFirst() {        if (dreamProList.size() > 0) {            dreamProList.remove(0);        }        isReturn = true;        sleepCount = 0;    }    //清空指令    public synchronized void clearOrders() {        if (dreamProList != null) {            dreamProList.clear();        }    }    //添加指令    public synchronized void addOrder(byte[] bytes) {        if (dreamProList != null) {            dreamProList.add(bytes);        }    }    //添加指令集合    public synchronized void addOrder(List<byte[]> bytes) {        if (dreamProList != null) {            dreamProList.addAll(bytes);        }    }    //循环读取串口数据    private class ReadThread extends Thread {        @Override        public void run() {            super.run();            while (!isInterrupted()) {                int size;                try {                    if (mInputStream == null) {                        return;                    }                    size = mInputStream.available();                    byte[] buffer = new byte[size];                    size = mInputStream.read(buffer);                    if (size > 0) {                        isReturn = true;                        removeFirst();                        if (serialPortDataCallBack != null) {                            serialPortDataCallBack.onDataReceived(buffer, size);                        }                    }                    Thread.sleep(5);                } catch (Exception e) {                    e.printStackTrace();                    return;                }            }        }    }    //超时检测线程    private class TimeOutThread extends Thread {        @Override        public void run() {            super.run();            while (sendFlag) {                try {                    Thread.sleep(50);                    if (sleepCount > timeOutCount) {                        if (serialPortDataCallBack != null) {                            serialPortDataCallBack.onDataReceived(null, -1);                        }                        //超时移除指令                        removeFirst();                    } else if (dreamProList.size() > 0) {                        sleepCount++;                    }                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }    //命令发送线程    private class SendOrderThread extends Thread {        @Override        public void run() {            super.run();            while (sendFlag) {                try {                    sendFirst();                    Thread.sleep(50);                } catch (Exception e) {                    e.printStackTrace();                }            }        }    }    //设置回调接口    public void setSerialPortDataCallBack(SerialPortDataCallBack serialPortDataCallBack) {        this.serialPortDataCallBack = serialPortDataCallBack;    }    //设置超时时间    public void setTomeOut(long mesc){        //超时检测线程是每次休眠固定时间,通过休眠次数来判断超时        timeOutCount = (int) (mesc / 50);    }}

这里简单介绍一下我的思路:

  1. 通过一个List来保存指令集合,通过指定方法来添加指令(集)或者清空指令。

  2. 同时利用两个线程分别进行串口的读写操作,并增加一个超时检测线程。

  3. 读串口文件的线程没有太多可以说的,就是通过InputStream循环读取串口数据,当有数据时就将数据通过接口回调到指定的位置。

  4. 写串口文件的线程,通过循环去发送List中的第一条指令(List.get(0)),但当前指令接收到返回或者超时的情况,移除List中第一条指令(这里也可以稍微修改一下代码,进行重发),然后继续发送第一条指令。

  5. 超时检测线程就是通过判断指令发送完成之后指定时间内是否收到返回指令,未收到则移除List中第一条指令,继续发送下一条指令。ps:超时时间可以自行设置

SerialPortUtil封装了对串口的操作,简化了很多步骤,方便调用。主要的步骤如下:

  1. 初始化:new 一个SerialPortUtil对象,然后调用initSerialPort方法,打开串口文件。建议在Application中初始化,然后在application提供一个相应get方法

  2. 在需要调用的界面设置回调接口,方便获取返回的数据

  3. 通过SerialPortUtil.addOrder添加需要发送的指令(集)

  4. 通过SerialPortUtil.clearOrders()清空所有未发送的指令

  5. 关闭串口,SerialPortUtil.closeSerialPort()来关闭串口以及结束所有线程,建议放在application的onTerminate()中

回调接口

/** * Created by ZhangHao on 2017/6/14. * 串口数据回调 */public interface SerialPortDataCallBack {    //接收数据回调    void onDataReceived(byte[] buffer, int size);    //串口初始化完成回调    void onSerialPortInitFinish(boolean result);}

测试

Application

/** * Created by ZhangHao on 2017/6/23. */public class HaoApp extends Application {    private SerialPortUtil util;    @Override    public void onCreate() {        super.onCreate();        util = new SerialPortUtil();        util.initSerialPort("/dev/ttyS4", 115200, 0);    }    //获取SerialPortUtil    public SerialPortUtil getSerialPortUtil() {        return util;    }    @Override    public void onTerminate() {        //程序终止关闭串口        util.closeSerialPort();        super.onTerminate();    }    /**     * 初始化时加载JNI     */    static {        try {            System.loadLibrary("serialPortLib");        } catch (UnsatisfiedLinkError ule) {            Log.e("HaoApp", "loadLibrary(API) : " + ule.getMessage());            ule.printStackTrace();        }    }}

Activity

public class SerialPortActivity extends AppCompatActivity implements SerialPortDataCallBack {    private SerialPortUtil util;    //测试指令,从单片机读取时间    private byte[] test = new byte[]{(byte) 0xff, (byte) 0x26, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,            (byte) 0x12, (byte) 0x06, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00};    private TextView tv;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_serial_port);        tv = (TextView) findViewById(R.id.show_receive);        util = ((HaoApp) getApplication()).getSerialPortUtil();        util.setSerialPortDataCallBack(this);    }    public void onClick(View view) {        switch (view.getId()) {            case R.id.send_order:                new SendTestThread().start();                break;        }    }    @Override    public void onDataReceived(byte[] buffer, int size) {        if (buffer.length > 10) {            if (buffer[6] == (byte) 0x12 && buffer[7] == (byte) 0x06) {                byte[] timeBytes = Arrays.copyOfRange(buffer, 12, buffer.length);                int time = ByteBuffer.wrap(timeBytes).order(ByteOrder.LITTLE_ENDIAN).getInt();                final int[] dates = getTimeInfoByMsec((long) time * 1000);                //在主线程更新UI                runOnUiThread(new Runnable() {                    @Override                    public void run() {                        String text = dates[0] + "-" + dates[1] + "-" + dates[2] + " "                                + dates[3] + ":" + dates[4] + ":" + dates[5];                        tv.setText(text);                    }                });            }        }    }    @Override    public void onSerialPortInitFinish(boolean result) {        Log.e("SerialPortActivity", "SerialPort Init " + result);    }    private class SendTestThread extends Thread {        @Override        public void run() {            super.run();            for (int i = 0; i < 10; i++) {                util.addOrder(test);                try {                    //每秒添加一次                    Thread.sleep(1000);                } catch (InterruptedException e) {                    Log.e("SendTestThread", e.toString());                    e.printStackTrace();                }            }        }    }    /**     * 根据毫秒数返回年月日时分秒     *     * @return     */    public static int[] getTimeInfoByMsec(long msec) {        int[] dates = new int[6];        Time t = new Time();        t.set(msec);        dates[0] = t.year;        dates[1] = t.month;        dates[2] = t.monthDay;        dates[3] = t.hour;        dates[4] = t.minute;        dates[5] = t.second;        return dates;    }}public class SerialPortActivity extends AppCompatActivity implements SerialPortDataCallBack {    private SerialPortUtil util;    //测试指令,从单片机读取时间    private byte[] test = new byte[]{(byte) 0xff, (byte) 0x26, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,            (byte) 0x12, (byte) 0x06, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00};    private TextView tv;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_serial_port);        tv = (TextView) findViewById(R.id.show_receive);        util = ((HaoApp) getApplication()).getSerialPortUtil();        util.setSerialPortDataCallBack(this);    }    public void onClick(View view) {        switch (view.getId()) {            case R.id.send_order:                new SendTestThread().start();                break;        }    }    @Override    public void onDataReceived(byte[] buffer, int size) {        if (buffer.length > 10) {            if (buffer[6] == (byte) 0x12 && buffer[7] == (byte) 0x06) {                byte[] timeBytes = Arrays.copyOfRange(buffer, 12, buffer.length);                int time = ByteBuffer.wrap(timeBytes).order(ByteOrder.LITTLE_ENDIAN).getInt();                final int[] dates = getTimeInfoByMsec((long) time * 1000);                //在主线程更新UI                runOnUiThread(new Runnable() {                    @Override                    public void run() {                        String text = dates[0] + "-" + dates[1] + "-" + dates[2] + " "                                + dates[3] + ":" + dates[4] + ":" + dates[5];                        tv.setText(text);                    }                });            }        }    }    @Override    public void onSerialPortInitFinish(boolean result) {        Log.e("SerialPortActivity", "SerialPort Init " + result);    }    private class SendTestThread extends Thread {        @Override        public void run() {            super.run();            for (int i = 0; i < 10; i++) {                util.addOrder(test);                try {                    //每秒添加一次                    Thread.sleep(1000);                } catch (InterruptedException e) {                    Log.e("SendTestThread", e.toString());                    e.printStackTrace();                }            }        }    }    /**     * 根据毫秒数返回年月日时分秒     *     * @return     */    public static int[] getTimeInfoByMsec(long msec) {        int[] dates = new int[6];        Time t = new Time();        t.set(msec);        dates[0] = t.year;        dates[1] = t.month;        dates[2] = t.monthDay;        dates[3] = t.hour;        dates[4] = t.minute;        dates[5] = t.second;        return dates;    }}

layout

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <Button        android:id="@+id/send_order"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:onClick="onClick"        android:textColor="@android:color/black"        android:textSize="16sp"        android:text="添加指令"/>    <TextView        android:id="@+id/show_receive"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:gravity="center"        android:textColor="@android:color/holo_blue_bright"        android:textSize="18sp"        android:layout_margin="20dp"        android:layout_below="@id/send_order"/>RelativeLayout>

我这里的测试代码,主要功能就是通过SerialPortUtil来发送串口指令,从我的单片机中读取时间,并显示在TextView中。效果图如下:

这里需要强调一点:我的测试代码只是为了表明我的SerialPortUtil是可行的,对于其他的设备完全不适用!因为每个人的硬件环境都是不相同的,需要根据自己的硬件环境自行修改。

结语

  1. 如果有任何问题,可以留言。

  2. android-serialport-api中还有一个SerialPortFinder类,主要是用来搜索系统里可用的串口文件,因为正常情况下串口文件都是指定的,所以这里直接给去掉了,如果有需要可以自行去android-serialport-api中查看。

更多相关文章

  1. Android(安卓)HttpGet和HttpPost设置超时
  2. Android逆向基础之Dalvik指令集
  3. android 8.1 mtk fota差分包指令
  4. Android(安卓)完全退出的实例详解
  5. NDK官方开发指南翻译之 CPU_ARM_Neon
  6. Android(安卓)Http请求框架一:Get 和 Post 请求
  7. Android实现应用下载并自动安装apk包
  8. Android串口通信:基本知识梳理
  9. Lock-free atomic operations in Android

随机推荐

  1. 2019 Android开发工程师面经
  2. [置顶] Android的AlertDialog详解
  3. Android(安卓)Debug Tools
  4. 2018 I/O Android(安卓)详解
  5. Android中Listview通过适配器设置Item的
  6. Android工程中配置OpenCV
  7. 自动化测试 Appium之Python运行环境搭建
  8. [置顶] Android——编译安装Module的控制
  9. Android(安卓)system document
  10. Android(安卓)6.0 Marshmallow root 方法