1. 解决的问题

在Android开发过程中,如果需要进行C++方面的开发,选择好用的IDE非常重要,很多人会使用Source Insight进行开发,首先它是收费软件,其次Source Insight对于资源消耗挺大,会造成电脑的卡顿。

我们知道,Android的模块使用的是Android.bp或者Android.mk进行构建的,目前并没有IDE可以解析这些配置文件。

本质上来说,需要解决的问题还是C++工程的索引问题,抛砖引玉可以参考我的另一篇文章:vscode配置c++代码跳转demo

2. 实践步骤

下面以Android12为例说明

解决C++工程索引问题,需要得到compile_commands.json,然后配置vscode + clangd进行索引。

2.1. 生成compile_commands.json

官方文档:build/soong/docs/compdb.md

需要导出以下环境变量:

$ export SOONG_GEN_COMPDB=1
$ export SOONG_GEN_COMPDB_DEBUG=1

建议加入到~/.bashrc文件,默认导入到环境变量中。

使用如下命令触发编译生成compile_commands.json

$ make nothing

生成的compile_commands.json目录:

out/soong/development/ide/compdb/compile_commands.json

注意:这种方式生成的compile_commands.json包含了所有使用Android.bp定义的模块的编译数据库,使用Android.mk编译的模块并不包含在内。

2.2. 配置vscode

参考文章:vscode配置c++代码跳转demo

首先需要使用vscode远程登录到服务器,然后增加clangd配置,索引一遍即可。

2.3. 热更新

添加新模块、删除模块、变更源代码的头文件后,只需要编译该模块即可,使用mm或者mmm或者make <target>都可以,不需要编译整个系统。

2.4. 精简compile_commands.json

你可能会发现,make nothing后生成的compile_commands.json大概有300M,如果打开整个Android目录会非常慢,一般来说,我们只会打开一个仓库,如frameworks仓库,所以我们需要精简compile_commands.json,然后打开特定仓库进行索引,以加快vscode的响应速度。

以下给出的是精简compile_commands.json的python脚本:

import json
import sys
from typing import *


def simply_compile_commands_json(completed_json_file: 'str', simplified_json_file: 'str', repositories: 'List[str]'):
    with open(completed_json_file) as input_file:
        command_content = input_file.read()
        command_json = json.loads(command_content)
        target_command_list = []
        for command in command_json:
            file: 'str' = command['file']
            if any((file.startswith(repository) for repository in repositories)):
                target_command_list.append(command)

        with open(simplified_json_file, "w") as output_file:
            output_file.write(json.dumps(target_command_list, indent=4))


if __name__ == '__main__':
    if len(sys.argv) != 4:
        print('Usage: python3 {} <complete.json> <simply.json> <repo[,repo[,repo]...]>'.format(sys.argv[0]))
        print('Eg: python3 {} ./compile_commands.json.big ./compile_commands.json system,hardware,frameworks'.format(sys.argv[0]))
        exit(1)
    else:
        input_compile_commands = sys.argv[1]
        output_compile_commands = sys.argv[2]
        repositories = sys.argv[3].split(',')
        simply_compile_commands_json(input_compile_commands, output_compile_commands, repositories)

2.5. for kernel

对于驱动部分的代码,其实也可以使用compile_commands.json配合vscode进行索引,从Kernel 5.10开始就预置了脚本用于生成compile_commands.json。

kernel-5.10$ python3 ./scripts/clang-tools/gen_compile_commands.py

生成的compile_commands.json在kernel-5.10目录下。

2.6. for Android.mk module

前面提到,对于Android.mk定义的模块,以上的方式是不会生成到compile_commands.json中的。

对于这类模块,我提出的想法是自己解析编译的命令,自己生成compile_commands.json!

第一个问题是如何得到编译的完整命令:

其实只需要在Android.mk中加入如下参数,就可以打印出完整的编译命令:

LOCAL_CFLAGS += -v
LOCAL_CXXFLAGS += -v

如何批量往Android.mk中添加以上flag?可以通过如下脚本:

import os
import re
import sys
import time

def toggle_verbose_cflag(path: 'str'):
    lines = []
    try:
        with open(path, 'r') as input:
            lines = input.readlines()
    except Exception:
        return

    verbose_cflag_count = 0
    for line in lines:
        verbose_cflag_count += line.count('LOCAL_CFLAGS += -v')
        verbose_cflag_count += line.count('LOCAL_CXXFLAGS += -v')

    if verbose_cflag_count > 0:
        # remove LOCAL_CFLAGS += -v
        # remove LOCAL_CXXFLAGS += -v
        temp = []
        for line in lines:
            if line.count('LOCAL_CFLAGS += -v') > 0 or line.count('LOCAL_CXXFLAGS += -v') > 0:
                pass
            else:
                temp.append(line)
        lines = temp
    else:
        # add LOCAL_CFLAGS += -v
        # add LOCAL_CXXFLAGS += -v
        temp = []
        for line in lines:
            if line.strip().startswith('include') and not line.strip().startswith(r'include $(CLEAR_VARS)'):
                m = re.match(r'^\s*', line)
                spaces = m.group(0) if m else ''
                temp.append(spaces + 'LOCAL_CFLAGS += -v' + os.linesep)
                temp.append(spaces + 'LOCAL_CXXFLAGS += -v' + os.linesep)
            temp.append(line)
        lines = temp

    with open(path, 'w') as output:
        output.writelines(lines)


def update_androidmk(directory):
    # 获取当前时间戳(以秒为单位)
    current_time = int(time.time())

    # 遍历指定目录下的所有文件和子目录
    for filename in os.listdir(directory):
        # 构建完整的文件路径或子目录路径
        path = os.path.join(directory, filename)

        # 如果这是一个文件(而不是一个目录),则更新其修改时间
        if os.path.isfile(path):
            os.utime(path, (current_time, current_time))
            if filename == 'Android.mk':
                toggle_verbose_cflag(path)
        # 否则,如果这是一个目录,则递归地处理其内容
        elif os.path.isdir(path):
            update_androidmk(path)

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: python3 {} directory".format(sys.argv[0]))
        exit(1)
    else:
        directory = sys.argv[1]
        update_androidmk(directory)

通过以上脚本,给定目录,可以一键往其中的Android.mk添加/删除verbose flag。

第二个问题是如何解析编译命令,生成compile_commands.json

可以通过如下脚本实现:

import json
import sys

def build_compile_commands(input_file: 'str', output_file: 'str', android_top: 'str', compiler='clang'):
    with open(input_file) as input:
        target_lines = []
        compile_command_list = []
        for line in input:
            if line.count(compiler) > 0 and ( line.count('.c') > 0 or line.count('.cpp') > 0 ):
                target_lines.append(line.strip())
        # print(len(target_lines))
        for rule in target_lines:
            if rule.count('"') >= 2:
                compile_command = {}
                compile_command['directory'] = android_top
                compile_command['arguments'] = []
                compile_command_file = ''
                items = rule.split(' ')
                for item in items:
                    item = item.replace('"', '')
                    if item.count(compiler) > 0:
                        item = android_top + '/' + item
                    compile_command['arguments'].append(item)
                    if item.count('.c') > 0 or item.count('.cpp') > 0:
                        if item.count('/') > 0:
                            compile_command_file = item
                if compile_command_file != '':
                    compile_command['file'] = compile_command_file
                    compile_command_list.append(compile_command)

        with open(output_file, 'w') as output:
            print(json.dumps(compile_command_list, indent=4), file=output)

if __name__ == '__main__':
    if len(sys.argv) < 4:
        print("Usage: python3 {} <input_file> <output_file> <android_top> [compiler]".format(sys.argv[0]))
        exit(1)
    else:
        input_file = sys.argv[1]
        output_file = sys.argv[2]
        android_top = sys.argv[3]
        compiler = 'clang'
        if len(sys.argv) > 4:
            compiler = sys.argv[4]
        build_compile_commands(input_file, output_file, android_top, compiler)

简单来说,就是先从编译日志中找到编译命令的行,这种行最基本的特征的会包含clang或者clang++关键字,compile_commands.json中有3项需要填,directory指的是安卓根目录,这个可以直接传入该脚本,注意需要是绝对路径;file指的是这条命令编译的是哪个源文件,这个可以从编译命令中拿到,特征是以.c或者.cpp结尾的那一项,最后是arguments,这一项是从编译命令中拷贝过去的,注意其中需要处理的是clang或者clang++,需要加上安卓根目录路径。

以上就是使用vscode配置android 开发c++的全部内容!