普通视图

发现新文章,点击刷新页面。
昨天以前道轲

回顾Java中经典的生产/消费者模型

2023年4月29日 19:00

概述

生产者-消费者模型是 Java 并发编程中比较常见的加锁应用场景之一,以下是维基百科的对于该名词的定义:

生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多进程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

问题分析

java_producer_consumer

结合上图及定义我们可以提取出该模型具有以下特点:

  • 生产者和消费者可以同时并发运作
  • 缓冲区满则阻塞等待消费者消费数据
  • 缓冲区空则阻塞等待生产者生产数据

我们知道,生产者和消费者可以同时运作,并且二者执行效率和规模也很有可能出现严重的不对等性。那么为了保证生产和消费操作的原子性和共享数据的可见性,我们需要借助一种同步机制来保障该模型的正常运行。

实现方式

以下来介绍几种常见的生产-消费模型的实现方式。

1. synchorized + wait/notify
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class GoodsStorageDispatcher {
    // 仓库容纳上限
    private static final int MAX_SIZE = 100;
    // 仓库容纳的货物集合
    private LinkedList<Good> list = new LinkedList();


    public void produce(Good good) {
        synchronized (list) {
            while (list.size() == MAX_SIZE) {
                System.out.println("仓库已满 >> 生产暂停,等待消费");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.add(good);
            System.out.println("生产了一个新产品,现库存为:" + list.size());
            list.notifyAll();
        }
    }
    public void consume() {
        synchronized (list) {
            while (list.size() == 0) {
                System.out.println("库存已清仓 >> 消费暂停,等待生产");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.remove();
            System.out.println("消费了一个产品,现库存为:" + list.size());
            list.notifyAll();
        }
    }

}

可以看到,我们采用对象锁以及等待/唤醒的方式来实现生产和消费的同步机制。这里的 wait 和 notifyAll 在示例中含义如下:

  • wait:在缓冲区满/空的情况下先挂起生产/消费线程并释放锁,进入等待状态,让其他线程执行
  • notify:当生产/消费一个商品时,通知其他等待的线程继续执行并释放锁,进入等待状态

值得注意的是,produce 和 consume 方法中都使用了 while 循环来判断缓冲区的存储状态(空/满),这样处理的原因是什么呢,为何不直接用 if 呢?

其实这里用 while 是为了防止虚假唤醒,我们结合一个例子更好理解一些:

fake_notify

如上图所示,假设我们现在有 1 个生产者,3 个消费者在执行生产和消费任务,而目前缓冲区(Warehouse)仅有一个数据:A。

如果当 Consumer-1 消费了 A 后,缓冲区就为空了,Consumer-2 和 Consumer-3 从缓冲区取数据消费时就会陷入等待状态(因为 Consumer-2 消费时缓冲区为空会执行 wait 方法并释放锁,Consumer-3也会重蹈覆辙)。而此时又有一个生产者 Producer-2 生产了数据 B 并加入缓冲区,通过 notifyAll 去唤醒所有等待的消费者,假设消费者 Consumer-2 优先抢到了使用权将 B 消费后缓冲区又恢复到空空如也的状态。接下来的情况就值得注意了:消费者 Consumer-3 想终于可以消费了,然而由于使用的是 if,所以唤醒后继续执行到 list.remove(),毫无疑问的抛出了 IndexOutOfBoundsException,因为此时 list 为空了,这就是所谓的“虚假唤醒”。其实这里我们不能直接消费数据,而是要继续等待。因此这里使用 while 循环判断,当唤醒继续执行代码时重新进入 while 内判断缓冲区数据是否为空。

2. Lock + Condition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class GoodsStorageDispatcher {
    // 仓库容纳上限
    private static final int MAX_SIZE = 100;
    // 仓库容纳的货物集合
    private LinkedList<Good> list = new LinkedList();
    // 锁
    final Lock lock = new ReentrantLock();
    // 用于等待或唤醒线程(仓库满后需要等待被消费,成功消费后需要唤醒线程继续生产)
    final Condition notFull = lock.newCondition();
    // 用于等待或唤醒线程(仓库清仓后需要等待生产,生产成功后需要唤醒线程继续消费)
    final Condition notEmpty = lock.newCondition();


    public void produce(Good good) throws InterruptedException {
        lock.lock();
        try {
            while (list.size() == MAX_SIZE)  { //防止虚假唤醒,Condition的await调用一般会放在一个循环判断中
                System.out.println("仓库已满 >> 生产暂停,等待消费");
                notFull.await();
            }
            list.add(good);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }
    public void consume() throws InterruptedException {
        lock.lock();
        try {
            while (list.size() == 0) {
                System.out.println("库存已清仓 >> 消费暂停,等待生产");
                notEmpty.await();
            }
            list.remove();
            notFull.signal();
        } finally {
            lock.unlock();
        }
    }

}

通过 ReentrantLock + Condition 来替代实现上面的等待/唤醒,但无疑它的功能性更加齐全和灵活(有限等待、公平锁、读写锁分离等)。

3. BlockingQueue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class GoodsStorageDispatcher {
    // 仓库容纳上限
    private static final int MAX_SIZE = 100;
    // 仓库容纳的货物集合
    private LinkedBlockingQueue<Good> list = new LinkedBlockingQueue<>(MAX_SIZE);
  

    public void produce(Good good) {
        if (list.size() == MAX_SIZE) {
            System.out.println("仓库已满 >> 生产暂停,等待消费");
        }
        try {
            list.put(good);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void consume() {
        if (list.size() == 0) {
            System.out.println("库存已清仓 >> 消费暂停,等待生产");
        }
        try {
            list.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    

}

BlockingQueue 其实就是阻塞队列,是基于阻塞机制实现的线程安全的队列。而阻塞机制的实现是通过在入队和出队时加锁的方式避免并发操作。我们可以理解它内部其实已经实现了上述的加锁和等待/唤醒这一套流程,只不过帮我们封装好了这一切。

应用场景

我们所熟知的线程池,它的内部其实就是构建了一个生产者-消费者模型用于将任务管理线程管理两部分工作解耦,很大程度上提高了可扩展性。

threadpool_running_process

平时业务中也会有生产消费模型的用武之地,比如多固件更新过程就符合生产-消费模型,用户通过手动操作选中特定设备的固件包加入升级队列,随后由 UpdateDispatcher 根据具体设备类型进行不同策略的分发消费。

相关参考

AOSP在Mac上的编译实践(上)

2023年4月21日 22:15

AOSP,即 Android Open Source Project,对于 Android 开发者来说再熟悉不过的项目,本文将着重介绍如何在 MacOS Monterey 环境上下载完整的 AOSP 源码。

Git & Python

1. Git 安装和配置

在安装了 Homebrew 的情况下,安装 git 我们只需要执行如下命令:

1
brew install git

接着初始化一下 Git 的全局配置信息:

1
2
git config --global user.name "User Name"
git config --global user.email "your@example.com"

2. 配置 python 环境

一般情况下我们电脑上都会安装了 python 环境,如果没有安装,可以前往 python官方站点下载。安装成功并做好解释器关联后执行以下命令查看版本信息:

1
2
➜  ~ python --version
Python 3.10.4

目前的 repo 都是基于 python 3.0,我们需要留意最新的 repo 版本中的 main.py 文件中定义的版本和我们本地的 python 环境是否一致。这里能跑通并不代表整个环境就万事大吉了,后面在安装 repo 的时候会提到因 python 配置而遇到的一个问题。

安装 repo

Repo 是建立在 Git 上的一个多仓库管理工具,可以组织多个仓库的上传和下载。它是 Google 基于 Python 和 Git 编写的脚本工具,可以协助我们管理多个 Git 存储仓库。首先,我们通过如下命令安装和配置 repo:

1
2
3
mkdir ~/DevKit/bin
➜ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/DevKit/bin/repo
➜ chmod a+x ~/DevKit/bin/repo

如果提示网络问题可能需要翻墙,可尝试使用国内镜像:

1
2
curl https://mirrors.tuna.tsinghua.edu.cn/git/git-repo -o ~/DevKit/bin/repo
chmod a+x ~/DevKit/bin/repo

接着执行 repo --version 查看是否安装成功:

1
2
3
4
5
6
7
8
9
10
➜  ~ repo --version
<repo not installed>
repo launcher version 2.32
       (from /Users/dorck/DevKit/bin/repo)
git 2.32.0 (Apple Git-132)
Python 3.10.4 (v3.10.4:9d38120e33, Mar 23 2022, 17:29:05) [Clang 13.0.0 (clang-1300.0.29.30)]
OS Darwin 21.4.0 (Darwin Kernel Version 21.4.0: Fri Mar 18 00:46:32 PDT 2022; root:xnu-8020.101.4~15/RELEASE_ARM64_T6000)
CPU arm64 (arm)
Bug reports: https://bugs.chromium.org/p/gerrit/issues/entry?template=Repo+tool+issue
➜  ~ 

若出现上述版本提示信息说明安装成功,可继续进行下一步操作。But,在笔者的 M1 电脑上遇到了一个坑,在执行 repo --version 时提示 python 配置问题:

1
env: python: No such file or directory

出现这类问题的常见原因一般是本地有多个 python 版本环境,导致链接出错,于是我先定位一下本地机器中的 python 解释器版本有哪些:

1
2
➜ where python
/Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10

可以看到我电脑指向的是 /Library/Frameworks 下的内置 python 环境,然而我又翻看了一下 ~/.bash_profile 中的 python 环境配置:

1
2
3
4
5
6
# Python 配置
alias python="/Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10"
export PATH="/Library/Frameworks/Python.framework/Versions/3.10/bin:$PATH"
export PATH=/usr/local/bin:$PATH
export PATH=/Users/{YOUR_NAME}/DevKit/bin:$PATH
export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo'

可以看到,我这边配置了不止一项 python 环境,/usr/local/bin 下还存在一个 3.0 版本的 python,那么为什么 repo 中的 python 代码执行不了呢?简单看了下 repo 的内部代码,发现他里面是通过 sys 库来获取的当前运行的 python 解释器。以下是笔者的个人猜测:在 macOS 系统中,系统默认的 Python 解释器是 Python 2.x 版本,如果安装了 Python 3.x 版本,而且没有设置符号链接将其路径添加到系统 PATH 中,那么系统就无法找到 Python 3.x 版本的可执行文件。因此,当您在终端中输入 python3 命令时,系统会去默认的路径 /usr/bin/ 中查找 Python 解释器,而无法找到 Python 3.x 版本的解释器,从而导致 “env: python: No such file or directory” 错误的发生。 要解决这个问题,我们可以执行以下命令:

1
ln -sf /Library/Frameworks/Python.framework/Versions/3.10/bin/python3 /usr/local/bin/python

这个命令的作用是将 Python 3.10 版本的可执行文件添加到了 /usr/local/bin 目录下,并创建了一个符号链接(软链接),使得 python 命令可以正确地指向 Python 3.10 版本的可执行文件,从而避免在使用 python 命令时出现因为找不到 Python 3.10 版本的可执行文件而报错的问题。这样,当我们在终端中输入 pythonpython3 命令时,系统会优先在 /usr/local/bin 目录下查找,找到该目录下的 pythonpython3 命令,然后使用其中的 Python 3.10 版本的可执行文件来执行程序,从而保证我们使用的是正确的 Python 版本。

接下来,安装完成 repo 后,我可以将以下 REPO_URL 地址替换为国内的镜像地址,方便后续下载 AOSP 源码,由于该项目较大,需要保证稳定的网络环境,不具备翻墙条件的此步骤为必选项:

1
2
export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo'
export PATH=/Users/{YOUR_NAME}/DevKit/bin:$PATH

以上配置请复制到 ~/.bash_profile 中,并执行 source ~/.bash_profile 使其生效。这里需要将 repo 的 path 也放置到环境变量中,方便我们全局调用。

关于 repo 的具体用法可以参考:多仓库管理工具—Repo

下载 AOSP

接下来我们就可以愉快的下载 AOSP 代码了:

1
2
3
mkdir AOSP
➜ cd ~/AOSP
➜ repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b android-13.0.0_r30

通过 repo init 命令来初始化、关联具体的 repo 项目,并指定了下载分支为 Android 13 版本的代码。当然,事情可能并不会一帆风顺,出现了下面的报错信息:

1
2
fatal: Cannot get https://gerrit.googlesource.com/git-repo/clone.bundle 
fatal: error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:590)

我们需要执行一下 Python 安装包下的 Install Certificates.command 脚本来安装证书,然后重新执行上述 init 命令:

1
2
3
4
5
6
7
8
Downloading Repo source from https://mirrors.tuna.tsinghua.edu.cn/git/git-repo
remote: Enumerating objects: 4463, done.
remote: Counting objects: 100% (4463/4463), done.
remote: Compressing objects: 100% (2123/2123), done.
remote: Total 8034 (delta 3979), reused 2340 (delta 2340), pack-reused 3571
Receiving objects: 100% (8034/8034), 3.74 MiB | 13.53 MiB/s, done.
Resolving deltas: 100% (5171/5171), done.
repo: Updating release signing keys to keyset ver 2.3

输出以上信息说明 repo 的 manifest 配置下载成功,接下来只需要同步一下代码即可:

1
➜ repo sync

代码同步时间会比较长,我们能做的就是耐心等待了。最终下载完成的 AOSP 大小直逼 150 GB:

aosp_download_suc

下一篇文章将会一起探索如何在 Mac 上编译 Android Open Source Project 项目,拭目以待。

参考

Android系统中线程的创建过程

2023年4月7日 21:15

我们都知道,Android 中线程创建过程需要追溯到 Native 层面,最终是委托给一个 Linux 标准线程 pthread 来执行的,所以 Android 中线程状态本质上是 Native 线程的一种映射。Android 中运行的线程可以分为两种:一种是 attach 到虚拟机的,即虚拟机线程;另一种是没有 attach 到虚拟机的。今天我们就分别从源码层面来看看 Android 系统中 Java 和 Native 层线程的创建过程。

以下分析基于 Android 13 最新源码

Java 线程创建过程

首先,我们需要知道的是:当我们通过 new 关键字创建一个 Thread 时其实并没有真正创建一个线程,只有调用 start 方法后才会去创建线程。先来看下 start 方法内部实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        // Android-changed: Replace unused threadStatus field with started field.
        // The threadStatus field is unused on Android.
        if (started)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        // Android-changed: Use field instead of local variable.
        // It is necessary to remember the state of this across calls to this method so that it
        // can throw an IllegalThreadStateException if this method is called on an already
        // started thread.
        started = false;
        try {
            // Android-changed: Use Android specific nativeCreate() method to create/start thread.
            // start0();
            nativeCreate(this, stackSize, daemon);
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

可以看到,最终线程的创建和执行是在 nativeCreate 方法中,而它是个 native 方法,对应的实现在 /art/runtime/native/java_lang_Thread.cc 文件中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size,
                                jboolean daemon) {
  // There are sections in the zygote that forbid thread creation.
  Runtime* runtime = Runtime::Current();
  if (runtime->IsZygote() && runtime->IsZygoteNoThreadSection()) {
    jclass internal_error = env->FindClass("java/lang/InternalError");
    CHECK(internal_error != nullptr);
    env->ThrowNew(internal_error, "Cannot create threads in zygote");
    return;
  }

  Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
}

继续跟踪到 /art/runtime/thread.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
  CHECK(java_peer != nullptr);
  Thread* self = static_cast<JNIEnvExt*>(env)->GetSelf();

  ......

  int pthread_create_result = 0;
  if (child_jni_env_ext.get() != nullptr) {
    pthread_t new_pthread;
    pthread_attr_t attr;
    child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();
    CHECK_PTHREAD_CALL(pthread_attr_init, (&attr), "new thread");
    CHECK_PTHREAD_CALL(pthread_attr_setdetachstate, (&attr, PTHREAD_CREATE_DETACHED),
                       "PTHREAD_CREATE_DETACHED");
    CHECK_PTHREAD_CALL(pthread_attr_setstacksize, (&attr, stack_size), stack_size);
    pthread_create_result = pthread_create(&new_pthread,
                                           &attr,
                                           gUseUserfaultfd ? Thread::CreateCallbackWithUffdGc
                                                           : Thread::CreateCallback,
                                           child_thread);
    CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread");

    if (pthread_create_result == 0) {
      // pthread_create started the new thread. The child is now responsible for managing the
      // JNIEnvExt we created.
      // Note: we can't check for tmp_jni_env == nullptr, as that would require synchronization
      //       between the threads.
      child_jni_env_ext.release();  // NOLINT pthreads API.
      return;
    }
  }

  ......
}

Thread::CreateNativeThread 方法实现比较多,过滤了一些代码,我们重点关注下里面调用了 pthread_create 方法:

bionic/libc/bionic/pthread_create.cpp

1
2
3
4
int pthread_create(pthread_t* thread_out, pthread_attr_t const* attr,
                   void* (*start_routine)(void*), void* arg) {
  ......
}

POSIX 线程(POSIX threads)又简称 Pthreads 是线程的 POSIX 标准,该标准定义了创建和操纵线程的一整套 API,在类 Unix 操作系统(Unix、Linux、Mac OS X等)中都使用 Pthreads 作为操作系统的线程,Windows操作系统也有其移植版 pthreads-win32。简而言之该标准定义内部 API 创建和操纵线程, Pthreads 定义了一套 C 程序语言类型、函数与常量,它以 pthread.h 头文件和一个线程库实现,所以在 Android Studio 使用时直接在 C/C++ 文件中 #include < pthread.h > 引入即可。

该函数是一个线程阻塞函数,调用方将一直等待到线程结束为止,当函数返回时,被等待线程的资源被收回。如果执行成功,将返回 0,如果失败则返回一个错误码。

该函数参数释义如下:

  • thread_out:线程标识符的指针,pthread_t 类型,即线程 ID(线程创建成功后会将分配的线程 ID 赋值给 thread_out)
  • attr:它是一个结构体类型,用于表示创建线程的相关属性信息,如线程优先级、调度策略等等
  • start_routine:表示线程运行函数的地址
  • arg:代表线程运行函数的参数

看到这里,基本代表 Java 层面的线程创建路径分析完成了,想更多了解关于 pthread 内容可自行查阅资料。此外,Android 中还有一种 native 线程,即 C/C++ 平台的 Thread 特供版。

Native 线程创建

刚刚我们分析了 Java 层面的 Thread 创建流程,下面来简单看下 Android Native 层中的 Thread 是如何创建的。首先我们需要关注一下 system/core/libutils/Threads.cpp 这个文件,里面是针对 Android 平台的 native 层如何创建一个线程的相关封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
status_t Thread::run(const char* name, int32_t priority, size_t stack)
{
    Mutex::Autolock _l(mLock);
    if (mRunning) {
      	// thread already started
        return INVALID_OPERATION;
    }
    ...
    mRunning = true;

    bool res;

    if (mCanCallJava) {
        // 创建能调用Java代码的Native线程
        res = createThreadEtc(_threadLoop,
                this, name, priority, stack, &mThread);
    } else {
        // 创建只能调用C/C++代码的Native线程
        res = androidCreateRawThreadEtc(_threadLoop,
                this, name, priority, stack, &mThread);
    }

    if (res == false) {
        return UNKNOWN_ERROR;
    }
    return NO_ERROR;
}

mCanCallJava 是在 Thread 对象创建时的构造参数,在构造函数中默认设置值为 true

  • 当 mCanCallJava 为 true 时,则代表创建的是不仅能调用 C/C++ 代码,还能调用 Java 代码的 Native 线程。
  • 当 mCanCallJava 为 false 时,则代表创建的是只能调用 C/C++ 代码的 Native 线程。

有关两种创建线程模式的方法实现分别在 createThreadEtc 和 androidCreateRawThreadEtc 中,下面来简单分析一下它们的实现链路。

androidCreateRawThreadEtc

本方法用于创建仅允许调用 C/C++ 代码的线程。

Threads.cpp#androidCreateRawThreadEtc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
int androidCreateRawThreadEtc(android_thread_func_t entryFunction,
                               void *userData,
                               const char* threadName __android_unused,
                               int32_t threadPriority,
                               size_t threadStackSize,
                               android_thread_id_t *threadId)
{
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

#if defined(__ANDROID__)  /* valgrind is rejecting RT-priority create reqs */
    if (threadPriority != PRIORITY_DEFAULT || threadName != NULL) {
        // Now that the pthread_t has a method to find the associated
        // android_thread_id_t (pid) from pthread_t, it would be possible to avoid
        // this trampoline in some cases as the parent could set the properties
        // for the child.  However, there would be a race condition because the
        // child becomes ready immediately, and it doesn't work for the name.
        // prctl(PR_SET_NAME) only works for self; prctl(PR_SET_THREAD_NAME) was
        // proposed but not yet accepted.
        thread_data_t* t = new thread_data_t;
        t->priority = threadPriority;
        t->threadName = threadName ? strdup(threadName) : NULL;
        t->entryFunction = entryFunction;
        t->userData = userData;
        entryFunction = (android_thread_func_t)&thread_data_t::trampoline;
        userData = t;
    }
#endif

    if (threadStackSize) {
        pthread_attr_setstacksize(&attr, threadStackSize);
    }

    errno = 0;
    pthread_t thread;
    int result = pthread_create(&thread, &attr,
                    (android_pthread_entry)entryFunction, userData);
    pthread_attr_destroy(&attr);
    if (result != 0) {
        ALOGE("androidCreateRawThreadEtc failed (entry=%p, res=%d, %s)\n"
             "(android threadPriority=%d)",
            entryFunction, result, strerror(errno), threadPriority);
        return 0;
    }

    // Note that *threadID is directly available to the parent only, as it is
    // assigned after the child starts.  Use memory barrier / lock if the child
    // or other threads also need access.
    if (threadId != nullptr) {
        *threadId = (android_thread_id_t)thread; // XXX: this is not portable
    }
    return 1;
}

可以看到,上面代码首先做了一些线程对象的属性赋值,然后分配线程的栈空间,接着是通过 pthread 来创建一个线程,线程创建成功则返回 1。值得注意的是,这里的 entryFunction 其实就是之前代码中传进来的 _threadLoop 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int Thread::_threadLoop(void* user)
{
    Thread* const self = static_cast<Thread*>(user);

    sp<Thread> strong(self->mHoldSelf);
    wp<Thread> weak(strong);
    self->mHoldSelf.clear();

#if defined(__ANDROID__)
    // this is very useful for debugging with gdb
    self->mTid = gettid();
#endif

    bool first = true;

    do {
        bool result;
        if (first) {
            first = false;
            self->mStatus = self->readyToRun();
            result = (self->mStatus == OK);

            if (result && !self->exitPending()) {
                // Binder threads (and maybe others) rely on threadLoop
                // running at least once after a successful ::readyToRun()
                // (unless, of course, the thread has already been asked to exit
                // at that point).
                // This is because threads are essentially used like this:
                //   (new ThreadSubclass())->run();
                // The caller therefore does not retain a strong reference to
                // the thread and the thread would simply disappear after the
                // successful ::readyToRun() call instead of entering the
                // threadLoop at least once.
                result = self->threadLoop();
            }
        } else {
            result = self->threadLoop();
        }

        // establish a scope for mLock
        {
        Mutex::Autolock _l(self->mLock);
        if (result == false || self->mExitPending) {
            self->mExitPending = true;
            self->mRunning = false;
            // clear thread ID so that requestExitAndWait() does not exit if
            // called by a new thread using the same thread ID as this one.
            self->mThread = thread_id_t(-1);
            // note that interested observers blocked in requestExitAndWait are
            // awoken by broadcast, but blocked on mLock until break exits scope
            self->mThreadExitedCondition.broadcast();
            break;
        }
        }

        // Release our strong reference, to let a chance to the thread
        // to die a peaceful death.
        strong.clear();
        // And immediately, re-acquire a strong reference for the next loop
        strong = weak.promote();
    } while(strong != nullptr);

    return 0;
}

通过上述代码不难发现:_threadLoop 内部会循环调用 Thread 对象的成员方法 threadLoop,该成员方法一般由派生类来自行实现。该线程将会在下面几种情况退出循环:

  • 线程状态错误,即 mStatus != OK
  • 线程即将退出,调用 Thread::requestExit() 时会触发
  • 线程的强引用被释放,无法继续将弱引用提升为强引用
createThreadEtc

本方法用于创建允许调用 C/C++ 和 Java 代码的线程。

system/core/libutils/include/utils/AndroidThreads.h#createThreadEtc

1
2
3
4
5
6
7
8
9
10
11
// Create thread with lots of parameters
inline bool createThreadEtc(thread_func_t entryFunction,
                            void *userData,
                            const char* threadName = "android:unnamed_thread",
                            int32_t threadPriority = PRIORITY_DEFAULT,
                            size_t threadStackSize = 0,
                            thread_id_t *threadId = nullptr)
{
    return androidCreateThreadEtc(entryFunction, userData, threadName,
        threadPriority, threadStackSize, threadId) ? true : false;
}

继续跟踪 androidCreateThreadEtc 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static android_create_thread_fn gCreateThreadFn = androidCreateRawThreadEtc;

int androidCreateThreadEtc(android_thread_func_t entryFunction,
                            void *userData,
                            const char* threadName,
                            int32_t threadPriority,
                            size_t threadStackSize,
                            android_thread_id_t *threadId)
{
    return gCreateThreadFn(entryFunction, userData, threadName,
        threadPriority, threadStackSize, threadId);
}

void androidSetCreateThreadFunc(android_create_thread_fn func)
{
    gCreateThreadFn = func;
}

看到这里可以发现继续调用了 gCreateThreadFn 函数,而该函数会被调用方在其他地方赋值代理掉,所以我们需要找到调用 androidSetCreateThreadFunc 函数的地方,最终锁定在了 frameworks/base/core/jni/AndroidRuntime.cpp 这个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
 * Register android native functions with the VM.
 */
/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
    ATRACE_NAME("RegisterAndroidNatives");
    /*
     * This hook causes all future threads created in this process to be
     * attached to the JavaVM.  (This needs to go away in favor of JNI
     * Attach calls.)
     */
    androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);

    ALOGV("--- registering native functions ---\n");

    /*
     * Every "register" function calls one or more things that return
     * a local reference (e.g. FindClass).  Because we haven't really
     * started the VM yet, they're all getting stored in the base frame
     * and never released.  Use Push/Pop to manage the storage.
     */
    env->PushLocalFrame(200);

    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);
        return -1;
    }
    env->PopLocalFrame(NULL);

    //createJavaThread("fubar", quickTest, (void*) "hello");

    return 0;
}

由此看来,Android 系统会在向虚拟机中注册 native 方法时将 gCreateThreadFn 指定向 javaCreateThreadEtc 这个函数。那么我们就来看下 javaCreateThreadEtc 内部实现如何:

frameworks/base/core/jni/AndroidRuntime.cpp#javaCreateThreadEtc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
 * This is invoked from androidCreateThreadEtc() via the callback
 * set with androidSetCreateThreadFunc().
 *
 * We need to create the new thread in such a way that it gets hooked
 * into the VM before it really starts executing.
 */
/*static*/ int AndroidRuntime::javaCreateThreadEtc(
                                android_thread_func_t entryFunction,
                                void* userData,
                                const char* threadName,
                                int32_t threadPriority,
                                size_t threadStackSize,
                                android_thread_id_t* threadId)
{
    void** args = (void**) malloc(3 * sizeof(void*));   // javaThreadShell must free
    int result;

    LOG_ALWAYS_FATAL_IF(threadName == nullptr, "threadName not provided to javaCreateThreadEtc");

    args[0] = (void*) entryFunction;
    args[1] = userData;
    args[2] = (void*) strdup(threadName);   // javaThreadShell must free

    result = androidCreateRawThreadEtc(AndroidRuntime::javaThreadShell, args,
        threadName, threadPriority, threadStackSize, threadId);
    return result;
}

根据官方给出的注释可以知道:javaCreateThreadEtc 函数从 androidCreateThreadEtc 中通过 androidSetCreateThreadFunc 设置的回调来进行调用的,即在线程真正开始执行之前被 Hook 到虚拟机中。我们接着来看下真正实现的 androidCreateRawThreadEtc 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
int androidCreateRawThreadEtc(android_thread_func_t entryFunction,
                               void *userData,
                               const char* threadName __android_unused,
                               int32_t threadPriority,
                               size_t threadStackSize,
                               android_thread_id_t *threadId)
{
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

#if defined(__ANDROID__)  /* valgrind is rejecting RT-priority create reqs */
    if (threadPriority != PRIORITY_DEFAULT || threadName != NULL) {
        // Now that the pthread_t has a method to find the associated
        // android_thread_id_t (pid) from pthread_t, it would be possible to avoid
        // this trampoline in some cases as the parent could set the properties
        // for the child.  However, there would be a race condition because the
        // child becomes ready immediately, and it doesn't work for the name.
        // prctl(PR_SET_NAME) only works for self; prctl(PR_SET_THREAD_NAME) was
        // proposed but not yet accepted.
        thread_data_t* t = new thread_data_t;
        t->priority = threadPriority;
        t->threadName = threadName ? strdup(threadName) : NULL;
        t->entryFunction = entryFunction;
        t->userData = userData;
        entryFunction = (android_thread_func_t)&thread_data_t::trampoline;
        userData = t;
    }
#endif

    if (threadStackSize) {
        pthread_attr_setstacksize(&attr, threadStackSize);
    }

    errno = 0;
    pthread_t thread;
    int result = pthread_create(&thread, &attr,
                    (android_pthread_entry)entryFunction, userData);
    pthread_attr_destroy(&attr);
    if (result != 0) {
        ALOGE("androidCreateRawThreadEtc failed (entry=%p, res=%d, %s)\n"
             "(android threadPriority=%d)",
            entryFunction, result, strerror(errno), threadPriority);
        return 0;
    }

    // Note that *threadID is directly available to the parent only, as it is
    // assigned after the child starts.  Use memory barrier / lock if the child
    // or other threads also need access.
    if (threadId != nullptr) {
        *threadId = (android_thread_id_t)thread; // XXX: this is not portable
    }
    return 1;
}

该函数实现在 androidCreateRawThreadEtc 章节就已经分析过了,只不过这里的 entryFunction 传过来的是 AndroidRuntime::javaThreadShell,我们来看下它内部如何实现的:

frameworks/base/core/jni/AndroidRuntime.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
 * When starting a native thread that will be visible from the VM, we
 * bounce through this to get the right attach/detach action.
 * Note that this function calls free(args)
 */
/*static*/ int AndroidRuntime::javaThreadShell(void* args) {
    void* start = ((void**)args)[0];
    void* userData = ((void **)args)[1];
    char* name = (char*) ((void **)args)[2];        // we own this storage
    free(args);
    JNIEnv* env;
    int result;

    /* hook us into the VM */
    if (javaAttachThread(name, &env) != JNI_OK)
        return -1;

    /* start the thread running */
    result = (*(android_thread_func_t)start)(userData);

    /* unhook us */
    javaDetachThread();
    free(name);

    return result;
}

javaThreadShell 函数比较重要,综合上下文不难发现,代码中的局部变量 start 代表的是 _threadLoop,userData 代表 Thread 对象,而 name 指向线程的名称。接着继续调用了 javaAttachThread 函数,用于将线程 hook 到当前进程的虚拟机中,进而执行 Java 代码。紧接着继续执行线程自身的逻辑,即调用成员函数 threadLoop;线程执行完毕后调用了 javaDetachThread 函数用于将线程从虚拟机中剥离。下面重点看下 javaAttachThread 内部是如何将线程 hook 至虚拟机的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
 * Makes the current thread visible to the VM.
 *
 * The JNIEnv pointer returned is only valid for the current thread, and
 * thus must be tucked into thread-local storage.
 */
static int javaAttachThread(const char* threadName, JNIEnv** pEnv)
{
    JavaVMAttachArgs args;
    JavaVM* vm;
    jint result;

    vm = AndroidRuntime::getJavaVM();
    assert(vm != NULL);

    args.version = JNI_VERSION_1_4;
    args.name = (char*) threadName;
    args.group = NULL;

    result = vm->AttachCurrentThread(pEnv, (void*) &args);
    if (result != JNI_OK)
        ALOGI("NOTE: attach of thread '%s' failed\n", threadName);

    return result;
}

可以看见 javaAttachThread 中又调用了 AttachCurrentThread 函数,该函数可以将 native 线程附加到进程的 VM 中,详细参见官方 JNI 文档:JNI 提示

接着可以在 runtime 中找到 CheckAttachThread 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Check whether the current thread is attached. This is usually required
// to be the first check, as ScopedCheck needs a ScopedObjectAccess for
// checking heap values (and that will fail with unattached threads).
bool CheckAttachedThread(const char* function_name) {
  Thread* self = Thread::Current();
  if (UNLIKELY(self == nullptr)) {
    // Need to attach this thread for a proper abort to work. We prefer this
    // to get reasonable stacks and environment, rather than relying on
    // tombstoned.
    JNIEnv* env;
    Runtime::Current()->GetJavaVM()->AttachCurrentThread(&env, /* thr_args= */ nullptr);

    std::string tmp = android::base::StringPrintf(
        "a thread (tid %" PRId64 " is making JNI calls without being attached",
        static_cast<int64_t>(GetTid()));
    Runtime::Current()->GetJavaVM()->JniAbort(function_name, tmp.c_str());

    CHECK_NE(Runtime::Current()->GetJavaVM()->DetachCurrentThread(), JNI_ERR);
    return false;
  }
  return true;
}

好吧,里面又调用了 GetJavaVM()-> AttachCurrentThread,看来我们还得继续跟下去,不过为了节省中间若干个类文件跳转环节,这里直接指出最终目的地在 Runtime::AttachCurrentThread 函数里面:

art/runtime/runtime.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool Runtime::AttachCurrentThread(const char* thread_name, bool as_daemon, jobject thread_group,
                                  bool create_peer, bool should_run_callbacks) {
  ScopedTrace trace(__FUNCTION__);
  Thread* self = Thread::Attach(thread_name,
                                as_daemon,
                                thread_group,
                                create_peer,
                                should_run_callbacks);
  // Run ThreadGroup.add to notify the group that this thread is now started.
  if (self != nullptr && create_peer && !IsAotCompiler()) {
    ScopedObjectAccess soa(self);
    self->NotifyThreadGroup(soa, thread_group);
  }
  return self != nullptr;
}

这里其实就是将线程在 JavaVM 层面包装成一个 VM 上的线程再返回给 self。接下来就不继续往下深究了,篇幅有限,本文先讲到这里,里面还涉及到很多 JavaVM 相关的细节及原理将在后续文章中做一个补充。

顺带提一嘴,Android SDK 中的 Thread 与 JDK 中的 java.lang.Thread 实际上是有区别的,虽然 Android 也使用 Java 语言开发,但Android 基于平台特殊性对 JDK 进行了一些删减和改造。我们都知道 Java 是具有跨平台特性的,同一套代码它完全可以在 Windows、Linux 等操作系统上正常运作,但其实内部的线程创建等细节是基于 OS 的特性进行各自实现的:

java_thread_creation

想了解更多关于 Java 线程的启动过程可以参考此文

总结

我们通过在 Java 层面创建线程的时候,VM 会包装成一个 VM 的 Thread,然后启动 pthread,再调用 run 方法,但我们通过 pthread 独立创建的线程,是没有和 VM 里面的线程对象建立关联的,VM 压根不知道它的存在。但是如果该线程想要访问 Java代码,这就得需 VM 帮忙,故而需要将 native 层自己创建的线程包装成一个 VM 层面的 Thread 对象,然后添加到 VM 的 thread 集合中去,如此一来,Android 的虚拟机就可以感知到 native 中这个线程对象的存在了。

相关参考

简单聊聊 ChatGPT

2023年3月29日 18:50

关于国内如何注册和使用 ChatGPT 可以参考这篇文章:国内注册一个属于自己的免费ChatGPT账号。本文不是扫盲贴,只是通过这两天对 ChatGPT 的使用体验来随便聊聊。

首先,对于他所具备的能力想必大家已经心照不宣了,从它(ChatGPT 3.0)问世以来的各项 DAU、MAU 数据就可以知道其火爆程度了。它的出现及发展代表着自然语言处理提升到了新的高度,甚至有人称其为第三次工业革命的前兆。当然,目前大家对于 ChatGPT 的赞美之词是”甚嚣尘上“,其中可能夹杂着部分夸大之词,但不可否认的是: AI 即将步入寻常百姓家。

ChatGPT 能为开发做什么

说了这么多,那么对于我们普通开发人员来说,它的出现意味着什么呢?于我而言,它其实是一位可敬的对手,也是一位可靠的伙伴。

例如,我想找出 LeetCode 上排名 Hot 100 的题目,不出三十秒,ChatGPT 就能为我列举出来所有题目,比我们自己找题要节省很多时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
Q:请整理出Leetcode hot100的题目

A:(From ChatGPT)
1. Two Sum
2. Add Two Numbers
3. Longest Substring Without Repeating Characters
4. Median of Two Sorted Arrays
5. Longest Palindromic Substring
6. ZigZag Conversion
7. Reverse Integer
8. String to Integer (atoi)
9. Palindrome Number
10. Regular Expression Matching
11. Container With Most Water
12. Integer to Roman
13. Roman to Integer
14. 3Sum
15. 3Sum Closest
16. Letter Combinations of a Phone Number
17. 4Sum
18. Remove Nth Node From End of List
19. Valid Parentheses
20. Merge Two Sorted Lists
21. Generate Parentheses
22. Merge k Sorted Lists
23. Swap Nodes in Pairs
24. Group Anagrams
25. Reverse Nodes in k-Group
26. Remove Duplicates from Sorted Array
27. Search Insert Position
28. Maximum Subarray
29. Length of Last Word
30. Climbing Stairs
31. Best Time to Buy and Sell Stock
32. Same Tree
33. Symmetric Tree
34. Maximum Depth of Binary Tree
35. Count and Say
36. Construct Binary Tree from Preorder and Inorder Traversal
37. Binary Tree Level Order Traversal
38. Maximum Depth of Binary Tree
39. Binary Tree Zigzag Level Order Traversal
40. Path Sum
41. Path Sum II
42. Sum Root to Leaf Numbers
43. Flatten Binary Tree to Linked List
44. Minimum Depth of Binary Tree
45. Balanced Binary Tree
46. Convert Sorted Array to Binary Search Tree
47. Unique Binary Search Trees
48. Unique Binary Search Trees II
49. Recover Binary Search Tree
50. Populating Next Right Pointers in Each Node
51. Populating Next Right Pointers in Each Node II
52. Sum Root to Leaf Numbers
53. Convert Sorted List to Binary Search Tree
54. Insert Interval
55. Merge Intervals
56. Longest Consecutive Sequence
57. LRU Cache
58. Gas Station
59. Jump Game
60. Jump Game II
61. Spiral Matrix
62. Permutations
63. Permutations II
64. Rotate Image
65. N-Queens
66. N-Queens II
67. Combinations
68. Combinations II
69. Minimum Path Sum
70. Unique Paths
71. Unique Paths II
72. Word Search
73. Restore IP Addresses
74. Evaluate Reverse Polish Notation
75. Sort Colors
76. Subsets
77. Subsets II
78. Scramble String
79. Partition Equal Subset Sum
80. Search a 2D Matrix
81. Search a 2D Matrix II
82. Remove Duplicates from Sorted List
83. Remove Duplicates from Sorted List II
84. Wildcard Matching
85. Edit Distance
86. Sum of Left Leaves
87. Find All Anagrams in a String
88. Remove Invalid Parentheses
89. Word Ladder
90. Sliding Window Maximum
91. Kth Largest Element in an Array
92. LRU Cache
93. Number of Islands
94. Verify Preorder Sequence in Binary Search Tree
95. Decode String
96. Sum of Two Integers
97. Design TinyURL
98. Maximum Product Subarray
99. Best Time to Buy and Sell Stock with Cooldown
100. Unique Binary Search Trees III

或者,我还可以让他模拟面试官的身份与我对话(场景模拟),考察我岗位职责内需要掌握的知识点,查漏补缺,协助我在后面的真实面试过程中取得好成绩。

p2

上图来源于网络,仅做举例展示,侵权删。

又或者,我们在编码时经常会遇到各种各样的问题,ChatGPT 完全有能力可以协助我们做一些简单的编码、单元测试、函数功能检测等工作。前几天我遇到一个问题,一个两年前的项目中有一个功能函数(从 /proc/stat 中获取 CPU 数据)在高版本的机器上无法正常运行,抛出了异常,最终通过 ChatGPT 两分钟就排查清楚问题出现的原因。下面来展示一下 ChatGPT 与我们的对话过程:

  1. 请帮我检查一下下面的函数存在什么问题?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    private fun readProcFile(file: String): Array<String>? {
            var procFile: RandomAccessFile? = null
            var procFileContents: String
            return try {
                procFile = RandomAccessFile(file, "r")
                procFileContents = procFile.readLine()
                val rightIndex = procFileContents.indexOf(")")
                if (rightIndex > 0) {
                    procFileContents = procFileContents.substring(rightIndex + 2)
                }
       
                procFileContents.split(" ").toTypedArray()
            } catch (ioe: IOException) {
                ioe.printStackTrace()
                return null
            } finally {
                procFile?.close()
            }
        }
    

    Answer:

    1
    2
    3
    4
    
    1. 没有处理读取文件可能抛出的异常;
    2. 没有检查读取到的字符串是否为空;
    3. 没有处理RandomAccessFile在关闭时可能抛出的异常;
    4. 没有判断右括号 ')' 的位置是否合法,以防止越界访问。
    
  2. 上面函数的作用是什么?

    Answer:

    1
    
    这个函数的作用是从给定文件中读取内容,并根据空格分割成字符串数组返回。
    
  3. 一般用来获取什么?

    Answer:

    1
    
    一般用来获取系统信息,比如文件状态、进程信息等。
    
  4. 如果获取的 /proc/stat 虚拟文件信息,会有什么问题吗?

    Answer:

    1
    
    如果获取的是 /proc/stat 虚拟文件信息,那么可能会出现越界访问的问题,因为procFileContents.indexOf(")")的结果可能会大于procFileContents的长度。
    
  5. 在Android系统中会有版本兼容问题吗?

    Answer:

    1
    
    在Android系统中如果用到 /proc/stat 虚拟文件信息,可能会出现版本兼容问题,因为/proc/stat 虚拟文件的格式在不同的Android版本中可能会有所不同。
    
  6. 比如说呢?

    Answer:

    1
    2
    
    比如:Android 8.0 之前的版本中,/proc/stat 虚拟文件的格式是:cpu  user  nice  system  idle  iowait  irq  softirq  ...
    而Android 8.0 及以后的版本中,/proc/stat 虚拟文件的格式改变了:cpu  user  nice  system  idle  iowait  irq  softirq  steal  guest  guest_nice  ...
    
  7. 那么 /proc/stat 文件访问权限有变化吗?

    Answer:

    1
    
    /proc/stat 文件的访问权限在Android 8.0 之前的版本中是公开的,任何应用都可以访问,但从Android 8.0 开始,/proc/stat 文件的访问权限仅限于系统应用。
    

自始至终,我们通过 7 轮问答最终得到了想要的答案。然而事实上,如果我们在向 ChatGPT 提问时能够尽可能描述清楚问题,提供足够多的现场信息,则能够大幅度缩短获取我们所期望答案的时间。上述问题在经过优化好的话术,例如开头给出问题发生的背景是在 Android 手机上,Android 6.0 执行没有问题等等,新一轮回答只经过 3 轮就得到了我们期望的结果。由此可见,“提问的艺术”对于 ChatGPT 来说也是至关重要的,它能够左右我们距离最终结果的长短以及结果的质量。

在 ChatGPT 的帮助下,单元测试将变得简单易得,收益率大幅提高。为什么这么说呢?实际上,据笔者统计,目前开发人员中日常编写单元测试的占比不到 30%,原因大抵是浪费时间,出力不讨好,不在公司绩效考察范围内等。而根据软件工程长期的历史数据统计和测试经验来看,单元测试却能帮我们解决 80% 的编码缺陷。单元测试重要性由此可见。我们让 ChatGPT 为上述代码编写单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Q:请帮我为优化后的代码编写单元测试?

// A:单元测试代码如下:
@Test
fun testReadProcFile() {
    val fileName = "/proc/stat"

    // 测试文件存在
    val contents = readProcFile(fileName)
    assertNotNull(contents)

    // 测试文件不存在
    val notExistsFileName = "/not/exists/file"
    val notExistsContents = readProcFile(notExistsFileName)
    assertNull(notExistsContents)
}

自此,在 ChatGPT 的加持下,单元测试成本大幅度降低,开发人员也不用被繁琐的测试用例所缠身,只需要丢给 ChatGPT 去完成就好了,它有着得天独厚的优势。

ChatGPT 未来趋势

对于我们开发者而言,除以上能力以外,ChatGPT 还可以在源码阅读、API 文档生成、文档翻译、代码补全&纠错、协作编程等方面为我们提供帮助,能够显著提高我们的工作和学习效率。笔者预测,未来 ChatGPT 势必会走入寻常百姓家,每个人都会感受到 AI 带给我们的变化,行业可能会重新洗牌。以后每个人的身边势必会有一个私人定制的 “ChatGPT” 与我们形影不离,不论是开发者、医生、司机、金融工作者、学生还是老师,每个人都会享受 AI 的便利。

我们能做什么?

作为一名普通人,不要盲目的抵制,也无需毫无保留的拥抱。对技术心存敬畏,虚心向学,也要保持距离。AI 可以辅助我们走向辉煌,也可以引领我们走向灭亡,我们需要提防过度依赖 AI,避免基本语法能力、编程能力和动手能力的缺失,不要在安逸中停止了思考。毕竟,AI 的产生就是为了服务于人,倘若有一天你不能从 ta 身上感受到积极的变化或无法占据主导地位,那就疏远 ta 吧。

❌
❌