腾讯Hardcoder-Android通讯框架简介

  • APP 的功能和业务特性不依赖于该框架。

总而言之,由于Hardcoder是腾讯主导的,所以我们不用太担心兼容性问题,腾讯会和手机厂商进行洽谈并提供解决方案,并且目前已经支持Hardcoder框架的手机厂商有OPPO、vivo、华为、小米、三星、魅族等。

Hardcoder 性能优化技术方案

Hardcoder 优化基础

Hardcoder 在Android系统侧主要优化的方法有提高 CPU 频率、提高 IO 频率, CPU 锁核以及提高 GPU 频率。

提高 CPU 频率

一般来说,移动设备为了降低功耗,会不同程度地抑制 cpu 频率,同时内核会根据当前的负载动态调整 cpu 频率。这就导致 APP 在执行一些需要资源的操作的时候,不能最大限度利用到 cpu 资源,可能出现卡顿等情况。

在Android系统中,通过修改Android内核配置,就可以达到提高 cpu 频率的目标。我们可以使用下面的命令来查看当前系统的 cpu 核数。

ls -l /sys/devices/system/cpu

如果要查看 cpu 支持的频率,可以使用下面的命令。

cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies

上面的命令是查看某个核(cpu0)支持的频率。主流的 cpu 都会有大小核的区分,所以并不能保证每个核的支持频率是一样的。一般来说,大核支持更高的频率。要修改某个核的频率,需要有 root 权限。以下操作就是直接 echo 指定频率到 cpu0 上,以达到提频的效果。

echo 1440000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq

一般来说,为了方便测试,我们会禁用某些核,甚至只给系统留下一个核,以方便做些基准测试,下面的命令可禁用某个指定的内核。

echo 0 > /sys/devices/system/cpu/cpu0/online

同理,我们可以把 0 改成 1,即可启用对应的内核。

提高 IO 频率

就 Hardcoder 项目与部分厂商的沟通过程中,发现厂商并不把 IO 提频看作重点,认为 IO 提频本身效果有限,且必须同时提高 cpu 频率效果才会明显,而部分厂商比如 vivo 则把 IO 频率长期锁定为高频状态。

cat /sys/class/mmc_host/mmc0/clk_scaling/enable

如果值为1 , 则 emmc 运行在 50MHz 和 200MHz 之间变化; 如果值为0 , 则 emmc 运行在 200MHz 上,也就是高频上了,如使用下面的提频命令即可。

echo 0 > /sys/class/mmc_host/mmc0/clk_scaling/enable

CPU 锁核

锁核操作实际上操作的是 cpu 亲和度,设置某个线程的 cpu 亲和度,即可变相把线程锁定在某个大核上运行。实际操作中,线程可固定在某个核上运行,但不独占,也就是这个核同样可以执行其他线程的指令。这样就减少了内核切换带来的性能开销。

提高 GPU 频率

GPU主要用于渲染图像,部分厂商提供了调节 GPU 芯片参数的能力,提高 GPU 频率,从而更好地支持对 GPU 需求较高的场景。

Hardcoder 通讯方式 —— LocalSocket

Hardcoder 采用 LocalSocket 实现手机本地双方 Native 层实时通信机制,关于LocalSocket可以自行查阅相关的资料。Android中的LocalSocket其实是Linux中Socket进行了封装,采用JNI方式调用,进而实现进程间通信。与普通的Socket不同,LocalSocket实现的是Android和Framework直接的通信。

LocalSocket 通信框架

目前在 Android 上进程通信的 IPC 机制主要有 Binder(Java INTENT/AIDL)、共享内存、Socket(TCP/UDP)等,同时在 Java 层 Android 提供了一套 LocalSocket 的API。LocalSocket 的本质是基于对 Linux 层 Socket 的封装,用来进行进程间的通信。LocalSocket 根据是否命名分为两种类型:非命名 LocalSocket 只能在父进程和子进程之间通信(因为其他进程无法获得 Server 端 Socket 名,只有保存了未命名 Socket 的文件描述符的父子进程之间可以使用);命名的 LocalSocket 则可以在任意进程间进行通信。

Android 中的 LocalSocket 在 Unix 域名空间创建一个 Socket 进行通信。Unix 域名空间 Socket 是在 Socket 基础上衍生的 IPC 通信机制,因此 LocalSocket 是为了解决同一台主机上不同进程间的通信问题,其对应接口和 TCP 等跨网络 Socket 方式一致,但是无需实现复杂的 TCP/IP 协议栈,不需要打包拆包、计算校验,只需通信双方协商好套接字地址(Android 上即为文件描述符)即可。因而 LocketSocket 在 Android 系统中作为 IPC 通信手段被广泛使用,如常见的Binder、Ashmem和LocalSocket等。

IPC通信机制安全性传输效率实现及推广难易度
Binder支持UID鉴权安全性高单次拷贝缺少C层公开API,实现难度大
Ashmem需实现鉴权无需拷贝,效率最高需额外实现同步,实现难度大
Socket(TCP/UDP)需实现鉴权两次拷贝技术成熟,系统侧接入难度小
LocalSocket需实现鉴权两次拷贝,面向本地通信,无需协议栈技术成熟,面向Android设计,系统侧接入难度小

由于 Hardcoder 采用 Native 实现,自然无法直接使用 Android SDK 提供的 Java 类的 LocalSocket 和 LocalSocketServer API。当然在 Hardcoder 的设计预期中,使用者无需关心通信细节,所以项目在 C 层使用 Linux 的 Socket 接口实现了一套类 Java 类的 LocalSocket 机制。

Hardcoder 子项目 libapp2sys 中 localsocket.h 定义了C++类 Localsocket 用于实现对通信流程的接口封装和逻辑控制,工作流程如下所示。

基类 Localsocket

基类 Localsocket 定义了作为 Socket 双方通信的基本行为接口,主要包括创建接口、循环处理收发数据和回调函数。

CreateSocket() 创建接口

int createSocket(const char *localPath, const char *remotePath)

其中,CreateSocket() 创建套接字,传入参数为 Server 端的 Socket 名。

对 Server 端,传入的 Socket 名为空,则默认创建 Server 端。先新建套接字 new Socket(),然后 bind() 绑定地址,再调用 listen() 监听端口,一系列系统调用成功后,则进入 loop() 循环等待处理数据。

对 Client 端,传入的 Socket 名有效,则创建 Client 端。先新建套接字 new Socket(),然后 connect() 尝试通过 Socket 名连接 Server 端。连接成功后,Client 端创建子线程进入 loop() 循环等待处理数据。

Loop() 循环处理收发数据

int loop(const int isServer)

Client 端和 Server 端分别维护一个发送队列 sendQueue,当发送队列不为空则发送数据,否则调用 select() 判断是否有待处理的接收数据,如果有则调用 read() 读取数据然后处理接收数据。对 Server 端该函数还会调用 accept() 处理 Client 端的连接请求。

RecvEvent()回调函数

int recvEvent(Event event, int fd, uid_t uid, const char *path, uint8_t *data, int len)

RecvEvent() 为虚函数,数据接收处理完后回调上层进行对应的业务处理,具体实现由各自派生类自行完成。

客户端 LocalsocketClient

客户端 LocalsocketClient 继承自 LocalSocket,是 Client 端实现的 LocalSocket 类,实现在 client.h 文件。除了包括基类的基础函数外,主要包括 start() 方法创建连接。LocalSocketClient 对应实例 Client 由客户端代理类负责创建,主要方法包括初始化和启动。

初始化 init()

int init(const char *remote, const int port, const char *local, IC2JavaCallback *callback)

上层 JNI 入口调用初始化创建 Client 端,当前版本 Localsocket 实现会忽略 port 和 local 两个参数(UDP 历史实现遗留), remote 为约定的 Server 端 Socket 名,callback 为 APP 端 Java 层监听 server 端回调函数。

启动函数 tryStartEngine()

int tryStartEngine()

启动函数 tryStartEngine() 创建本地的 LocalSocketClient 引擎实例 clientEngine,并调用 LocalSocketClient 类 start() 方法创建连接并负责断开超时重连逻辑。

服务端 LocalsocketServer

服务端 LocalsocketServer 继承自 LocalSocket,是 Server 端实现的 LocalSocket 类,实现在 server.h 文件,对应实例由服务端代理类负责创建。其创建和 start() 方法由实现了相应服务能力接口的系统级别进程负责调用。

Native 层实现的 LocalSocket 流程

对 Server 端,创建 Socket 后,调用 bind() 绑定地址和 listen() 监听端口,进入 loop() 循环。首先通过 accept() 处理来自 Client 端的连接请求,通过 recvEvent() 的回调通知 Client 端是否连接成功;然后检查发送队列 sendQueue 是否有待发送数据,若有则调用 send() 发送数据;再调用 select() 查找是否有待处理的接收数据,有则调用 read() 读取数据进行相应处理;然后重新进行 loop() 循环。

对 Client 端,创建 Socket 后,调用 connect() 通过 Socket 名尝试连接 Server 端,从 recvEvent() 回调获取是否连接成功。若连接成功,则进入 loop() 循环,与 Server 端类似循环 send()、select()、read() 过程。

Hardcoder 数据格式 —— proto + JSON

Hardcoder 使用 LocalSocket 机制完成 C/S 双向实时通信,client 端到 server 端传输数据简称为请求(request),server 端到 client 端数据简称为响应(response)。双向数据包均由包头和包体两部分组成,考虑到为本地通信且已通过 UID 方式实现鉴权认证,当前版本所有数据均无加密。

请求数据格式

请求的数据格式如下所示。


| AMCReqHeaderV2 | body(业务请求结构体序列化数据) |

AMCReqHeaderV2 定义在 libapp2sys 子项目的 header.h 中。

const static uint16_t HEADER_PROTOCAL_VERSION_2 = 16;
const static uint32_t HEADER_BEGIN = 0x48444352;

typedef struct AMCReqHeaderV2 {
uint32_t begin; // 包头其起始字段
uint16_t version; // 协议版本
uint16_t funcid; // 请求对应的function ID
uint32_t bodylen; // 包体序列化数据长度
int64_t requestid; // 当前请求包ID
uint32_t callertid; // 上层JNI调用者所在线程ID
int64_t timestamp; // 当前请求时间戳
uint32_t headerlen; // 包头数据长度(Ver2新增)
uint32_t bodyformat; //包体数据序列化格式枚举值(Ver2新增)

}attribute ((packed)) AMCReqHeaderV2;

其中:

  • begin 字段固定标识 Hardcoder 通信。
  • version 字段支持协议版本扩展。
  • funcid 定义在 protocol.h,表明本次客户端请求对应的系统操作,例如申请 CPU 提频或者线程锁核等。

const static uint32_t FUNC_BASE = 1000;

const static uint32_t FUNC_CHECK_PERMISSION = FUNC_BASE + 1;

const static uint32_t FUNC_CPU_HIGH_FREQ = FUNC_BASE + 2;
const static uint32_t FUNC_CANCEL_CPU_HIGH_FREQ = FUNC_BASE + 3;

const static uint32_t FUNC_CPU_CORE_FOR_THREAD = FUNC_BASE + 4;
const static uint32_t FUNC_CANCEL_CPU_CORE_FOR_THREAD = FUNC_BASE + 5;

const static uint32_t FUNC_HIGH_IO_FREQ = FUNC_BASE + 6;
const static uint32_t FUNC_CANCEL_HIGH_IO_FREQ = FUNC_BASE + 7;

const static uint32_t FUNC_SET_SCREEN_RESOLUTION = FUNC_BASE + 8;
const static uint32_t FUNC_RESET_SCREEN_RESOLUTION = FUNC_BASE + 9;

const static uint32_t FUNC_REG_ANR_CALLBACK = FUNC_BASE + 10;

const static uint32_t FUNC_REG_PRELOAD_BOOT_RESOURCE = FUNC_BASE + 11;

const static uint32_t FUNC_TERMINATE_APP = FUNC_BASE + 12;

const static uint32_t FUNC_UNIFY_CPU_IO_THREAD_CORE = FUNC_BASE + 13;
const static uint32_t FUNC_CANCEL_UNIFY_CPU_IO_THREAD_CORE = FUNC_BASE + 14;

static const uint32_t FUNC_REG_SYSTEM_EVENT_CALLBACK = FUNC_BASE + 15;

static const uint32_t FUNC_GPU_HIGH_FREQ = FUNC_BASE + 16;
static const uint32_t FUNC_CANCEL_GPU_HIGH_FREQ = FUNC_BASE + 17;

static const uint32_t FUNC_CONFIGURE = FUNC_BASE + 18;
static const uint32_t FUNC_GET_PARAMETERS = FUNC_BASE + 19;

其中,requestid 字段从 0 开始递增保持唯一性。headerlen 字段为包头长度。bodyformat 为包体序列化格式,当前取值为:1-raw(byte数组)、2-protobuf 序列化字节流 、3-JSON 序列化字节流。

请求 body 部分如果使用 google protobuf 协议格式实现(C 库),请参见项目amc.proto 文件中请求结构体定义,例如申请 CPU 提频的业务请求结构体:

message RequestCPUHighFreq{
required int32 scene = 1;
required int32 level = 2;
required int32 timeoutMs = 3;
required int64 action = 4;
}

编译后会自动在 gen/cpp/amc.pb.h 文件中生成对应的 C++ 类,主要公开成员函数为序列化/反序列化接口,以及数据属性的 get/set 访问接口。

class RequestCPUHighFreq : public ::google::protobuf::MessageLite {

RequestCPUHighFreq* New() const;
void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
void CopyFrom(const RequestCPUHighFreq& from);
void MergeFrom(const RequestCPUHighFreq& from);

// required int32 scene = 1;
inline bool has_scene() const;
inline void clear_scene();
static const int kSceneFieldNumber = 1;
inline ::google::protobuf::int32 scene() const;
inline void set_scene(::google::protobuf::int32 value);

//…

调用 header.h 的静态函数 genReqPack 即可以完成请求数据包的完整封包逻辑。

static int64_t genReqPack(uint32_t funcid, uint8_t *data, int dataLen, uint8_t **outPack, uint32_t *outLen, uint32_t callertid, int64_t timestamp)

如果采用 JSON 格式,则使用 key-value 方式,其中 key 统一为字符串,当前版本已有属性定义如下:

  • “funcid” 和包头中的 funcid 保持一致,int32 格式。
  • “scene” 场景值,int32 格式,表示 APP 具体业务场景,和 Ver1 已有定义保持一致。
  • “status” 状态值,int32 格式,表示该操作是一个请求/置位(1)还是取消/复位(2)操作。
  • “timouts” 超时值,int32 格式,表示该操作任务最长时间,单位 ms。
  • “cpulevel” 请求 cpu level 值,分为 Level 0~3,具体定义请参见 protocol.h。
  • “iolevel” 请求 io level 值,分为 Level 0~3,具体定义请参见 protocol.h。
  • “gpulevel” 请求 gpu level 值,分为 Level 0和 Level 1,具体定义请参见 protocol.h。
  • “bindtids” 需要绑核的线程,int32格式数组。
  • “unbindtids” 需要解绑的线程,int32格式数组。
响应数据格式

响应数据的格式如下:


| AMCRespHeaderV2 | payload |

AMCRespHeaderV2 同样定义在 libapp2sys 子项目的 header.h 中。

typedef struct AMCRespHeaderV2 {
uint32_t begin; // 包头其起始字段
uint16_t version; // 协议版本
uint16_t funcid; // 响应请求对应的function ID
uint32_t retCode; // 请求处理结果
uint32_t bodylen; // 包体序列化数据长度
int64_t requestid; // 响应对应的请求包ID
int64_t timestamp; // 当前响应时间戳
uint32_t headerlen; // 包头数据长度(Ver2新增)
uint32_t bodyformat;//包体数据序列化格式枚举值(Ver2新增)
}attribute ((packed)) AMCRespHeaderV2;

begin、version、bodylen、timestamp、headerlen 和 bodyformat 等字段含义与 AMCReqHeader2 中各字段一样用于标识响应包自身属性,而 funcid、requestid 则表示其对应处理的请求包属性,便于请求端确认;如果请求包在解包或者校验方面不通过,则对应的 retCode 会返回响应的全局错误码(负值,定义在 protocal.h),否则返回 0 值或者是具体业务处理结果。

const static int32_t RET_OK = 0;

//requestCpuHighFreq,requestHighIOFreq 直接返回level n
const static int32_t RET_LEVEL_1 = 1;
const static int32_t RET_LEVEL_2 = 2;
const static int32_t RET_LEVEL_3 = 3;

//预留返回值最后三位作为level,倒数第四位代表cpu level,倒数第五位代表io level,新增值继续左移
const static int32_t RET_CPU_HIGH_FREQ = 1 << 3;// 1000,即8
const static int32_t RET_HIGH_IO_FREQ = 1 << 4; // 10000,即16

//requestUnifyCpuIOThreadCore使用复合标识位
const static int32_t RET_CPU_HIGH_FREQ_LEVEL_1 = RET_CPU_HIGH_FREQ | RET_LEVEL_1; //Unify接口返回cpu level 1,1000 | 01 = 1001
const static int32_t RET_CPU_HIGH_FREQ_LEVEL_2 = RET_CPU_HIGH_FREQ | RET_LEVEL_2; //Unify接口返回cpu level 2,1000 | 10 = 1010
const static int32_t RET_CPU_HIGH_FREQ_LEVEL_3 = RET_CPU_HIGH_FREQ | RET_LEVEL_3; //Unify接口返回cpu level 3,1000 | 11 = 1011

const static int32_t RET_HIGH_IO_FREQ_LEVEL_1 = RET_HIGH_IO_FREQ | RET_LEVEL_1; //Unify接口返回io level 1,10000 | 01 = 10001
const static int32_t RET_HIGH_IO_FREQ_LEVEL_2 = RET_HIGH_IO_FREQ | RET_LEVEL_2; //Unify接口返回io level 2,10000 | 10 = 10010
const static int32_t RET_HIGH_IO_FREQ_LEVEL_3 = RET_HIGH_IO_FREQ | RET_LEVEL_3; //Unify接口返回io level 3,10000 | 11 = 10011

const static int32_t ERR_UNAUTHORIZED = -10001;
const static int32_t ERR_FUNCTION_NOT_SUPPORT = -10002;
const static int32_t ERR_SERVICE_UNAVAILABLE = -10003;
const static int32_t ERR_FAILED_DEPENDENCY = -10004;
const static int32_t ERR_PACKAGE_DECODE_FAILED = -10005;
const static int32_t ERR_PARAMETERS_WRONG = -10006;
const static int32_t ERR_CLIENT_UPGRADE_REQUIRED = -10007;

const static int32_t ERR_CLIENT_DISCONNECT = -20001;
const static int32_t ERR_CLIENT_RESPONSE = -20002;

请求 body 部分如果使用 protobuf 定义,请参见项目amc.proto 文件中响应结构体定义;如果采用 JSON 格式,属性定义与 AMCReqHeaderV2 保持一致。调用 header.h 的静态函数 genRespPack 即可以完成响应数据包的完整封包逻辑。

static int64_t genRespPack(uint32_t funcid, uint32_t retCode, uint64_t requestid, uint8_t *data, int dataLen, uint8_t **outPack, uint32_t *outLen)

Native 层实现的 LocalSocket 鉴权方式

由于 Native 层实现的 LocalSocket 通信方案为本地进程间通信,因而只需要在 Server 端接收到 Client 端请求时,通过调用 getsockopt() 方法获取到 Client 端的 UID,然后通过 UID 反查出 Client 端对应的 APP 信息,进而完成响应的鉴权处理。

Hardcoder 厂商接入指南

Hardcoder 项目工程中提供了 Server 端的实现例子,代码主要参见 server.h ,server.cpp 以及 protocol.h。

设置系统属性

APP 判断手机是否支持 Hardcoder 会读取 persist.sys.hardcoder.name 的 property,若不为空则手机取 property 字段作为 server 端 socket name 请求建立 socket 连接。厂商侧需设置 persist.sys.hardcoder.name 属性为系统 server 侧 socket name。

实现主要接口函数

protocol.h 的 HardCoder 类定义了所有 Hardcoder 接口为虚函数,厂商侧需继承 HardCoder 类实现相关接口。代码例子中 server.h 的 ManufacturerCoder 继承了 HardCoder 实现了相关接口,例子中为空函数,具体实现需要厂商侧编写。此部分接口可同时参照 Hardcoder 接入指南中接口说明。

int getUidByAddress(const char *ip, int port);

获取 APP UID,每次在 socket 连接中收到 APP 请求都会检查 APP UID。由于 UID 有唯一性,可作为鉴权用。

bool checkPermission(std::vectorstd::string manufactures, std::vectorstd::string certs, int funcid, int uid, int callertid, int64_t timestamp);

checkPermission方法的作用是否允许 APP 使用 Hardcoder,允许返回 true,否则返回 false。若允许所有 APP 使用,直接返回 true 即可。

若需要限制 APP 接入,应实现对应的 checkPermission,其中 manufactures 为厂商名数组,certs 为鉴权​
值数组,可提供鉴权值给允许使用的 APP 作为参数传入。注意若限制应用使用,需告知 APP 开发者如何申请权限接入。

int requestCpuHighFreq(int scene, int64_t action, int level, int timeoutms, int callertid, int64_t timestamp);

requestCpuHighFreq的主要作用是提高CPU频率。其中,scene参数为APP 场景值;action参数为APP 场景值扩展,为保留字段;level,请求的 CPU level,定义在 protocol.h;目前共分为三个 level,LEVEL 0 为不变,LEVEL 1 最高,默认为 CPU 最高频率,LEVEL 2 次之,LEVEL 3 最低,但仍比当前频率会提高,LEVEL 2 和 LEVEL 3 具体频率可由厂商自行决定。timeoutms,从 timestamp 开始请求 timeoutms 时间的资源;callertid,请求线程 id;timestamp,请求时间戳;如果要取消提频请求,可以使用下面的函数。

int cancelCpuHighFreq(int callertid, int64_t timestamp);

如果要请求绑定指定线程到cpu大核,可以使用下面的函数。

int requestCpuCoreForThread(int scene, int64_t action, std::vectorbindtids, int timeoutms, int callertid, int64_t timestamp);

其中,bindtids,需要绑定到大核的线程 id 数组。参考实现,部分厂商会直接把请求线程所在进程的所有线程同时绑定到大核。

int cancelCpuCoreForThread(std::vectorbindtids, int callertid, int64_t timestamp);

上面的方法用于取消绑定线程请求。

int requestHighIOFreq(int scene, int64_t action, int level, int timeoutms, int callertid, int64_t timestamp);

requestHighIOFreq方法用于提高 IO 频率请求。当然,我们还可以使用混合请求,同时请求提高 CPU 频率,提高 IO 频率,提高 GPU 频率以及线程绑核。

int requestUnifyCpuIOThreadCoreGpu(int scene, int64_t action, int cpulevel, int iolevel, std::vectorbindtids, int gpulevel, int timeoutms, int callertid, int64_t timestamp);

取消的时候使用cancelUnifyCpuIOThreadCoreGpu方法。

int cancelUnifyCpuIOThreadCoreGpu(int cancelcpu, int cancelio, int cancelthread, std::vectorbindtids, int cancelgpu, int callertid, int64_t timestamp);

Hardcoder 接入指南

接入步骤

  1. 下载 Hardcoder 工程编译 aar;
  2. 项目 build.gradle 引入 Hardcoder aar;
  3. 进程启动时调用 initHardCoder 建立 socket
    连接(一般进程启动时需要请求资源,因而推荐在进程启动时调用)。每个进程都是独立的,都需要调用 initHardCoder 建立 socket 连接,建立连接后每个进程维持一个 socket,进程退出时 socket 也会断开;
  4. initHardCoder 回调成功后调用 checkPermission,传入 APP 已申请的各个厂商鉴权值;
  5. 在需要请求资源的场景调用 startPerformance,传入请求资源的参数。若场景位于进程启动阶段,比如 APP 启动,需要在initHardCoder 的回调成功以后再调用 startPerformance,确保连接已成功建立,或者判断HardCoderJNI 的 isConnect() 检查 socket 是否已连接。
  6. 场景结束时主动调用 stopPerformance,传入对应场景 startPerformance 时的返回值 hashCode作为参数,停止本次请求。
  7. 测试性能,APP 可对打开/关闭 Hardcoder 的情况做对比实验,测试性能数据。

编译 Hardcoder aar

Hardcoder项目给开发环境为 AndroidStudio,采用 gradle 构建,其中 Native 部分代码使用 CMAKE 编译。编译aar的步骤如下:

  1. 根目录下运行命令行 gradlew assembleDebug 触发子工程 libapp2sys 编译,编译成功后生成 Hardcoder aar, 输出目录为hardcoder/libapp2sys/build/outputs/aar/libapp2sys-debug.aar;
  2. 若需要把 aar publish 到本地 maven 库,以便自身项目使用,在根目录下运行如下命令行./gradlew publishToMavenLocal,输出 aar 目录为 User
    目录/.m2/repository/com/tencent/mm/hardcoder/app2sys/。

当然,也可以使用Android Studio自带的Gradle工具进行构建,构建完成后可以在outputs/aar目录下看到生成的aar。

App 引入 Hardcoder aar

Hardcoder 工程以 aar 方式引入,把 hardcoder:app2sys 添加到主工程目录下的 build.gradle 文件的依赖库中。

dependencies {
api(‘com.tencent.mm.hardcoder:app2sys:1.0.0’)
}

其中 1.0.0 为当前 Hardcoder 版本号,当前版本号在 Hardcoder 主工程目录下 gradle.properties 文件。

HC_VERSION_NAME=1.0.0

Hardcoder 方法

最后

小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

资料⬅专栏获取
m.hardcoder:app2sys:1.0.0’)
}

其中 1.0.0 为当前 Hardcoder 版本号,当前版本号在 Hardcoder 主工程目录下 gradle.properties 文件。

HC_VERSION_NAME=1.0.0

Hardcoder 方法

最后

小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。

[外链图片转存中…(img-ROnqxgFv-1719115137588)]一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

资料⬅专栏获取

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/32533.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++】————类和对象(中)

作者主页&#xff1a; 作者主页 本篇博客专栏&#xff1a;C 创作时间 &#xff1a;2024年6月22日 一、类的6个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。空类中什么都没有吗&#xff1f;并不是的&#xff0c;任何一个类在我们不写的情 况下&#x…

链接脚本文件入门介绍

本文框架 1. Why<为什么需要链接文件>2.What<是什么及组成>2.1 MEMORY介绍2.2 SECTIONS介绍 3.How<链接文件应用>3.1 定义特定字段3.2 将变量定义在指定段3.3 将变量定义在不同的段3.4 将变量定义在指定地址3.5 将函数定义在指定段3.6 将函数定义在指定地址 …

【PyQt5】python可视化开发:PyQt5介绍,开发环境搭建快速入门

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

IDEA 中 Maven 报错 Cannot resolve xxx(全网试完,亲测有效的方法汇总)

问题&#xff1a; pom中已经添加相关依赖&#xff0c;maven刷新也没有用&#xff0c;依旧是疯狂报错。 可能原因&#xff1a; 在IDEA中的pom文件中添加了依赖&#xff0c;并且正确加载了相应依赖&#xff0c;pom文件没有报红&#xff0c;看起来像是把所有依赖库全部加载进来了&…

csp 2023 入门级题解(上)

csp 2023 入门级题解 上 第一题第二题第三题结构体联合体 第四题第五题第六题第7题第八题 第一题 unsigned是指无符号,用于int类型,是指自然数. const是定义常量,定义后的值不可修改. static是将系统栈中的变量放入内存,可以让其他程序调用 答案是c 第二题 答案d 第三题 s…

基于YOLOv5的PCB板缺陷检测系统的设计与实现

简介 随着电子设备的广泛应用,PCB(印刷电路板)作为其核心部件,其质量和可靠性至关重要。然而,PCB生产过程中常常会出现各种缺陷,如鼠咬伤、开路、短路、杂散、伪铜等。这些缺陷可能导致设备故障,甚至引发严重的安全问题。为了提高PCB检测的效率和准确性,我们基于YOLOv…

数组移除元素算法(以JS为例)

题目&#xff1a;LeeCode第27题 答案&#xff1a; 算法思想&#xff1a;双指针 这段代码实际上使用了一种简化版的双指针技术来实现元素的移除。这里的双指针技术并不是传统意义上的两个指针&#xff0c;而是一个索引k作为辅助指针&#xff0c;用来记录新数组&#xff08;或原…

Android 开发必备知识点及面试题汇总(Android+Java+算法+性能优化+四大组件……

**虚引用&#xff1a;**顾名思义&#xff0c;就是形同虚设&#xff0c;如果一个对象仅持有虚引用&#xff0c;那么它相当于没有引用&#xff0c;在任何时候都可能被垃圾回收器回收。 7.介绍垃圾回收机制 **标记回收法&#xff1a;**遍历对象图并且记录可到达的对象&#xff0c…

Jenkins定时构建自动化(一):Jenkins下载安装配置

目录 ​编辑 一、jdk下载安装 1. 已下载安装jdk 2. 未下载安装jdk 二、jenkins安装 1. .war包安装 三、获取IP地址 四、jenkins网页配置 一、jdk下载安装 1. 已下载安装jdk &#xff08;1&#xff09;查询jdk版本命令&#xff1a;java -version &#xff08;2&#xff09;…

idea2022激活

下载激活脚本 解压后&#xff0c;打开文件夹如下&#xff1a;ja-netfilter.jar 为激活补丁&#xff1a; 复制补丁所在的整个文件夹到硬盘某个位置 将 ja-netfilter补丁所在的整个文件夹移动到电脑上某个位置&#xff0c;我是放到了 D 盘下&#xff1a; &#xff08;路径中不…

Java面试题:内存管理、类加载机制、对象生命周期及性能优化

1. 说一下 JVM 的主要组成部分及其作用? JVM包含两个子系统和两个组件:Class loader(类装载)、Execution engine(执行引擎)、Runtime data area(运行时数据区)、Native Interface(本地接口)。 Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)装载class文…

Google Earth Engine(GEE)——ui.DateSlider时间进度条的设置

结果 函数: ui.DateSlider(start, end, value, period, <

基于ACO蚁群优化的城市最佳出行路径规划matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于ACO蚁群优化的城市最佳出行路径规划matlab仿真&#xff0c;可以修改城市个数&#xff0c;输出路径规划结果和ACO收敛曲线。 2.测试软件版本以及运行结果展示…

Google trend搜索关键词

Google trend地址&#xff1a;https://trends.google.com/trends/?geoUS&hlzh-CN 1、具体的操作步骤如下&#xff1a; 2、Google trend搜索页面如下&#xff1a;

2024-06-23 编译原理实验3——语义分析

文章目录 一、实验要求二、实验设计三、实验结果四、附完整代码 补录与分享本科实验&#xff0c;以示纪念。 一、实验要求 基于前面的实验&#xff0c;编写一个程序对使用 C—语言书写的源代码进行语义分析&#xff0c;输出语义分析中发现的错误&#xff08;涉及 17 种错误类…

mac电脑守护神CleanMyMac2024免费版本下载

&#x1f31f; 电脑的守护神&#xff1a;CleanMyMac&#x1f47e; 亲爱的数码控们&#xff0c;是不是每次看到电脑上满满的垃圾文件和缓慢的运行速度就感到头疼呢&#xff1f;别怕&#xff0c;今天我要来给你们安利一款神奇的小帮手——CleanMyMac&#xff01;它可是我们电脑的…

【驱动篇】龙芯LS2K0300之LED驱动

实验目的 点亮龙芯开发板上面的用户自定义LED灯&#xff0c;编写LED驱动以及测试用例验证实现效果&#xff0c;LED位于开发板左下方&#xff08;靠近USB口&#xff09;第二个 原理图 LED通过电阻上拉至电源P3V3&#xff0c;低电平时LED被点亮 设备树 打开arch/loongarch/boo…

【职场人】如何与同事有效沟通

在职场中&#xff0c;沟通如同桥梁&#xff0c;连接着每一位职场人士的心灵与智慧。有效的沟通不仅能让工作更加顺畅&#xff0c;还能让团队关系更加和谐。那么&#xff0c;如何与同事进行有效沟通呢&#xff1f;下面&#xff0c;我将结合个人经验和一些幽默的比喻&#xff0c;…

音频数据集1--LJSpeech单人语音

LJ Speech Dataset 版本号: 1.1 , 文件大小: 2.6GB 1.简介 1. 1 内容简介 LJS是一个语音数据集&#xff0c;包含 13,100 个音频片段&#xff0c;内容为Linda Johnson(欧美女性)朗读的 7 本书籍段落(非小说类)。每个片段都提供文本转录&#xff0c;片段长度从 1 到 10 秒不等&…

八大排序之希尔排序

一、概念及其介绍 希尔排序(Shell Sort)是插入排序的一种&#xff0c;它是针对直接插入排序算法的改进。 希尔排序又称缩小增量排序&#xff0c;因 DL.Shell 于 1959 年提出而得名。 它通过比较相距一定间隔的元素来进行&#xff0c;各趟比较所用的距离随着算法的进行而减小…