adb android源码分析,Android Adb 源码解析(base on Android 9.0)

Adb 框架

a47e1c90b9bf

Adb架构

Android Adb 一共分为三个部分:adb、adb server、adbd,源码路径:system⁩/⁨core⁩/⁨adb。

adb和adb server 是运行在PC端,adb就是大家所熟悉的控制台命令adb,adb server是由adb fork出的一个常驻后台的子进程大家再看到

* daemon not running. starting it now on port 5037 *

* daemon started successfully *

的时候就是adb在启动 adb server,adb与adb server 通过local socket进行通信。

adbd运行在Android端,是在内核初始化完毕之后,init进程启动

service adbd /sbin/adbd --root_seclabel=u:r:su:s0 --device_banner=recovery

disabled

socket adbd stream 660 system system

seclabel u:r:adbd:s0

adbd是一个linux程序,不依赖Android framework,通过tcp或者usb与PC端的adb server通信,调用logcat shell 等等程序实现各种功能。

1. 初始化

adbd main 入口在system⁩/⁨core⁩/⁨adb/daemon/main.cpp中,main函数获取selinux标签、banner名称、版本信息参数以及设置一些调试信息后,调用adbd_main函数:

int adbd_main(int server_port) {

umask(0);

signal(SIGPIPE, SIG_IGN);

init_transport_registration();

// We need to call this even if auth isn't enabled because the file

// descriptor will always be open.

adbd_cloexec_auth_socket();

if (ALLOW_ADBD_NO_AUTH && !android::base::GetBoolProperty("ro.adb.secure", false)) {

auth_required = false;

}

adbd_auth_init();

// Our external storage path may be different than apps, since

// we aren't able to bind mount after dropping root.

const char* adb_external_storage = getenv("ADB_EXTERNAL_STORAGE");

if (adb_external_storage != nullptr) {

setenv("EXTERNAL_STORAGE", adb_external_storage, 1);

} else {

D("Warning: ADB_EXTERNAL_STORAGE is not set. Leaving EXTERNAL_STORAGE"

" unchanged.\n");

}

drop_privileges(server_port);

bool is_usb = false;

if (access(USB_FFS_ADB_EP0, F_OK) == 0) {

// Listen on USB.

usb_init();

is_usb = true;

}

// If one of these properties is set, also listen on that port.

// If one of the properties isn't set and we couldn't listen on usb, listen

// on the default port.

std::string prop_port = android::base::GetProperty("service.adb.tcp.port", "");

if (prop_port.empty()) {

prop_port = android::base::GetProperty("persist.adb.tcp.port", "");

}

int port;

if (sscanf(prop_port.c_str(), "%d", &port) == 1 && port > 0) {

D("using port=%d", port);

// Listen on TCP port specified by service.adb.tcp.port property.

setup_port(port);

} else if (!is_usb) {

// Listen on default port.

setup_port(DEFAULT_ADB_LOCAL_TRANSPORT_PORT);

}

D("adbd_main(): pre init_jdwp()");

init_jdwp();

D("adbd_main(): post init_jdwp()");

D("Event loop starting");

fdevent_loop();

return 0;

}

init_transport_registration 函数完成adbd接受PC端adb server连接功能的初始化工作

adbd_auth_init 函数负责完成对连入的PC端身份验证功能的初始化工作

drop_privileges 函数负责是否要去掉adbd的root权限,降级为shell权限

usb_init 如果可以usb调试,初始化usb,等待连接

setup_port 如果开启了网络调试,初始化端口,等待连接

init_jdwp 初始化 java 调试框架

fdevent_loop 对监听的fd进行消息处理(死循环)

这里我们重点看一下 init_transport_registration 函数

void init_transport_registration(void) {

int s[2];

if (adb_socketpair(s)) {

fatal_errno("cannot open transport registration socketpair");

}

D("socketpair: (%d,%d)", s[0], s[1]);

transport_registration_send = s[0];

transport_registration_recv = s[1];

fdevent_install(&transport_registration_fde, transport_registration_recv,

transport_registration_func, 0);

fdevent_set(&transport_registration_fde, FDE_READ);

}

通过adb_socketpair 函数建立了一个管道,并且把管道的一头放入fdevent中进行读监听,当有数据可读时候调用transport_registration_func 函数

static void transport_registration_func(int _fd, unsigned ev, void* data) {

tmsg m;

int s[2];

atransport* t;

if (!(ev & FDE_READ)) {

return;

}

if (transport_read_action(_fd, &m)) {

fatal_errno("cannot read transport registration socket");

}

t = m.transport;

if (m.action == 0) {

D("transport: %s removing and free'ing %d", t->serial, t->transport_socket);

/* IMPORTANT: the remove closes one half of the

** socket pair. The close closes the other half.

*/

fdevent_remove(&(t->transport_fde));

adb_close(t->fd);

{

std::lock_guard<:recursive_mutex> lock(transport_lock);

transport_list.remove(t);

}

if (t->product) free(t->product);

if (t->serial) free(t->serial);

if (t->model) free(t->model);

if (t->device) free(t->device);

if (t->devpath) free(t->devpath);

delete t;

update_transports();

return;

}

/* don't create transport threads for inaccessible devices */

if (t->GetConnectionState() != kCsNoPerm) {

/* initial references are the two threads */

t->ref_count = 2;

if (adb_socketpair(s)) {

fatal_errno("cannot open transport socketpair");

}

D("transport: %s socketpair: (%d,%d) starting", t->serial, s[0], s[1]);

t->transport_socket = s[0];

t->fd = s[1];

fdevent_install(&(t->transport_fde), t->transport_socket, transport_socket_events, t);

fdevent_set(&(t->transport_fde), FDE_READ);

std::thread(write_transport_thread, t).detach();

std::thread(read_transport_thread, t).detach();

}

{

std::lock_guard<:recursive_mutex> lock(transport_lock);

pending_list.remove(t);

transport_list.push_front(t);

}

update_transports();

}

1.建立连接(adb connect ip举例)

adb入口函数system⁩/⁨core⁩/⁨adb/client/main.cpp,在启动时候会调用adb_commandline函数,如果命令能adb进程处理就直接处理返回,如果不能处理则尝试连接adb server,adb server如果未启动则fork出adb server

调用adb_server_main 完成各种初始化工作(类似adbd初始化),adb server启动后发起命令"host:connect:"经过smart_socket_enqueue 最终调用

else if (!strncmp(name, "connect:", 8)) {

char* host = strdup(name + 8);

int fd = create_service_thread("connect", connect_service, host);

if (fd == -1) {

free(host);

}

return create_local_socket(fd);

}

调用

void connect_device(const std::string& address, std::string* response) {

if (address.empty()) {

*response = "empty address";

return;

}

std::string serial;

std::string host;

int port = DEFAULT_ADB_LOCAL_TRANSPORT_PORT;

if (!android::base::ParseNetAddress(address, &host, &port, &serial, response)) {

return;

}

std::string error;

int fd = network_connect(host.c_str(), port, SOCK_STREAM, 10, &error);

if (fd == -1) {

*response = android::base::StringPrintf("unable to connect to %s: %s",

serial.c_str(), error.c_str());

return;

}

D("client: connected %s remote on fd %d", serial.c_str(), fd);

close_on_exec(fd);

disable_tcp_nagle(fd);

// Send a TCP keepalive ping to the device every second so we can detect disconnects.

if (!set_tcp_keepalive(fd, 1)) {

D("warning: failed to configure TCP keepalives (%s)", strerror(errno));

}

int ret = register_socket_transport(fd, serial.c_str(), port, 0);

if (ret < 0) {

adb_close(fd);

*response = android::base::StringPrintf("already connected to %s", serial.c_str());

} else {

*response = android::base::StringPrintf("connected to %s", serial.c_str());

}

}

至此通过network_connect发起tcp连接请求并返回fd通过register_socket_transport注册到transport列表中。

而adbd端

static void server_socket_thread(int port) {

int serverfd, fd;

adb_thread_setname("server socket");

D("transport: server_socket_thread() starting");

serverfd = -1;

for(;;) {

if(serverfd == -1) {

std::string error;

serverfd = network_inaddr_any_server(port, SOCK_STREAM, &error);

if(serverfd < 0) {

D("server: cannot bind socket yet: %s", error.c_str());

std::this_thread::sleep_for(1s);

continue;

}

close_on_exec(serverfd);

}

D("server: trying to get new connection from %d", port);

fd = adb_socket_accept(serverfd, nullptr, nullptr);

if(fd >= 0) {

D("server: new connection on fd %d", fd);

close_on_exec(fd);

disable_tcp_nagle(fd);

std::string serial = android::base::StringPrintf("host-%d", fd);

if (register_socket_transport(fd, serial.c_str(), port, 1) != 0) {

adb_close(fd);

}

}

}

D("transport: server_socket_thread() exiting");

}

接收到TCP连接请求,调用register_transport函数

至此adb server和adbd物理连接建立完成

2. 消息流转

当register_transport函数被调用,管道一端被写入tmsg消息

static void register_transport(atransport* transport) {

tmsg m;

m.transport = transport;

m.action = 1;

D("transport: %s registered", transport->serial);

if (transport_write_action(transport_registration_send, &m)) {

fatal_errno("cannot write transport registration socket\n");

}

}

触发transport_registration_func 函数从管道另一端读取tmsg消息同时再建立了一条管道和两个线程并把tmsg中的atransport记录到传输队列中,atransport记录了三个文件描述符 sfd、fd、transport_socket,他们的关系如图

a47e1c90b9bf

adbd 消息流

图中的apacket为各种消息载体。

struct amessage {

uint32_t command; /* command identifier constant */

uint32_t arg0; /* first argument */

uint32_t arg1; /* second argument */

uint32_t data_length; /* length of payload (0 is allowed) */

uint32_t data_check; /* checksum of data payload */

uint32_t magic; /* command ^ 0xffffffff */

};

struct apacket {

amessage msg;

std::string payload;

};

3. 消息处理

重点看handle_packet 函数

void handle_packet(apacket *p, atransport *t)

{

D("handle_packet() %c%c%c%c", ((char*) (&(p->msg.command)))[0],

((char*) (&(p->msg.command)))[1],

((char*) (&(p->msg.command)))[2],

((char*) (&(p->msg.command)))[3]);

print_packet("recv", p);

CHECK_EQ(p->payload.size(), p->msg.data_length);

switch(p->msg.command){

case A_SYNC:

if (p->msg.arg0){

send_packet(p, t);

#if ADB_HOST

send_connect(t);

#endif

} else {

t->SetConnectionState(kCsOffline);

handle_offline(t);

send_packet(p, t);

}

return;

case A_CNXN: // CONNECT(version, maxdata, "system-id-string")

handle_new_connection(t, p);

break;

case A_AUTH:

switch (p->msg.arg0) {

#if ADB_HOST

case ADB_AUTH_TOKEN:

if (t->GetConnectionState() == kCsOffline) {

t->SetConnectionState(kCsUnauthorized);

}

send_auth_response(p->payload.data(), p->msg.data_length, t);

break;

#else

case ADB_AUTH_SIGNATURE:

if (adbd_auth_verify(t->token, sizeof(t->token), p->payload)) {

adbd_auth_verified(t);

t->failed_auth_attempts = 0;

} else {

if (t->failed_auth_attempts++ > 256) std::this_thread::sleep_for(1s);

send_auth_request(t);

}

break;

case ADB_AUTH_RSAPUBLICKEY:

adbd_auth_confirm_key(p->payload.data(), p->msg.data_length, t);

break;

#endif

default:

t->SetConnectionState(kCsOffline);

handle_offline(t);

break;

}

break;

case A_OPEN: /* OPEN(local-id, 0, "destination") */

if (t->online && p->msg.arg0 != 0 && p->msg.arg1 == 0) {

asocket* s = create_local_service_socket(p->payload.c_str(), t);

if (s == nullptr) {

send_close(0, p->msg.arg0, t);

} else {

s->peer = create_remote_socket(p->msg.arg0, t);

s->peer->peer = s;

send_ready(s->id, s->peer->id, t);

s->ready(s);

}

}

break;

case A_OKAY: /* READY(local-id, remote-id, "") */

if (t->online && p->msg.arg0 != 0 && p->msg.arg1 != 0) {

asocket* s = find_local_socket(p->msg.arg1, 0);

if (s) {

if(s->peer == 0) {

/* On first READY message, create the connection. */

s->peer = create_remote_socket(p->msg.arg0, t);

s->peer->peer = s;

s->ready(s);

} else if (s->peer->id == p->msg.arg0) {

/* Other READY messages must use the same local-id */

s->ready(s);

} else {

D("Invalid A_OKAY(%d,%d), expected A_OKAY(%d,%d) on transport %s",

p->msg.arg0, p->msg.arg1, s->peer->id, p->msg.arg1, t->serial);

}

} else {

// When receiving A_OKAY from device for A_OPEN request, the host server may

// have closed the local socket because of client disconnection. Then we need

// to send A_CLSE back to device to close the service on device.

send_close(p->msg.arg1, p->msg.arg0, t);

}

}

break;

case A_CLSE: /* CLOSE(local-id, remote-id, "") or CLOSE(0, remote-id, "") */

if (t->online && p->msg.arg1 != 0) {

asocket* s = find_local_socket(p->msg.arg1, p->msg.arg0);

if (s) {

/* According to protocol.txt, p->msg.arg0 might be 0 to indicate

* a failed OPEN only. However, due to a bug in previous ADB

* versions, CLOSE(0, remote-id, "") was also used for normal

* CLOSE() operations.

*

* This is bad because it means a compromised adbd could

* send packets to close connections between the host and

* other devices. To avoid this, only allow this if the local

* socket has a peer on the same transport.

*/

if (p->msg.arg0 == 0 && s->peer && s->peer->transport != t) {

D("Invalid A_CLSE(0, %u) from transport %s, expected transport %s",

p->msg.arg1, t->serial, s->peer->transport->serial);

} else {

s->close(s);

}

}

}

break;

case A_WRTE: /* WRITE(local-id, remote-id, ) */

if (t->online && p->msg.arg0 != 0 && p->msg.arg1 != 0) {

asocket* s = find_local_socket(p->msg.arg1, p->msg.arg0);

if (s) {

unsigned rid = p->msg.arg0;

if (s->enqueue(s, std::move(p->payload)) == 0) {

D("Enqueue the socket");

send_ready(s->id, rid, t);

}

}

}

break;

default:

printf("handle_packet: what is %08x?!\n", p->msg.command);

}

put_apacket(p);

}

这部分代码是adb server和adbd共用的,通过ADB_HOST宏来控制是否编译,我们先看apacket命令种类

A_SYNC 握手命令,当PC开始连接建立时交换SYNC字段,其意义类似于TCP协议的握手

A_CNXN adb server握手完成后直接发送A_CNXN 协商版本、最大负载等信息,adbd收到A_CNXN后还会判断是够要对adb server进行认证,认证通过之后才会发送A_CNXN 协商版本、最大负载等信息

A_AUTH adbd对adb server进行验证

以上均完成之后,adb连接完全建立,PC端查看adb devices命令devices显示device

A_OPEN 当需要流完成的命令(例如:logat)需要通知接收方建立一个套接字,local-id用来标识套接字

A_OKAY 通知发送方已经准备好了接收流

A_CLSE 通知接收方关闭套接字

A_WRTE 发送的流消息

再重点看一下A_OPEN流程,在接收到A_OPEN后,先创建一个本地的套接字(create_local_service_socket),成功后再创建一个远程套接字(create_remote_socket),并关联两个

套接字互为peer,完成后通知远端A_OKAY。

远端收到A_OKAY后同时也会关联对方的套接字。

大家可能已经看出来了,这里所谓的本地套接字远程套接字并非真的套接字,而是通过local-id字段在逻辑上建立相关联的一个结构(peer),具体参见sockets.cpp。

至此针对不同服务(logcat、shell等)的链接就建立完成了。

在services.cpp文件中

int service_to_fd(const char* name, atransport* transport) {

int ret = -1;

if (is_socket_spec(name)) {

std::string error;

ret = socket_spec_connect(name, &error);

if (ret < 0) {

LOG(ERROR) << "failed to connect to socket '" << name << "': " << error;

}

#if !ADB_HOST

} else if(!strncmp("dev:", name, 4)) {

ret = unix_open(name + 4, O_RDWR | O_CLOEXEC);

} else if(!strncmp(name, "framebuffer:", 12)) {

ret = create_service_thread("fb", framebuffer_service, nullptr);

} else if (!strncmp(name, "jdwp:", 5)) {

ret = create_jdwp_connection_fd(atoi(name+5));

} else if(!strncmp(name, "shell", 5)) {

ret = ShellService(name + 5, transport);

} else if(!strncmp(name, "exec:", 5)) {

ret = StartSubprocess(name + 5, nullptr, SubprocessType::kRaw, SubprocessProtocol::kNone);

} else if(!strncmp(name, "sync:", 5)) {

ret = create_service_thread("sync", file_sync_service, nullptr);

} else if(!strncmp(name, "remount:", 8)) {

ret = create_service_thread("remount", remount_service, nullptr);

} else if(!strncmp(name, "reboot:", 7)) {

void* arg = strdup(name + 7);

if (arg == NULL) return -1;

ret = create_service_thread("reboot", reboot_service, arg);

if (ret < 0) free(arg);

} else if(!strncmp(name, "root:", 5)) {

ret = create_service_thread("root", restart_root_service, nullptr);

} else if(!strncmp(name, "unroot:", 7)) {

ret = create_service_thread("unroot", restart_unroot_service, nullptr);

} else if(!strncmp(name, "backup:", 7)) {

ret = StartSubprocess(android::base::StringPrintf("/system/bin/bu backup %s",

(name + 7)).c_str(),

nullptr, SubprocessType::kRaw, SubprocessProtocol::kNone);

} else if(!strncmp(name, "restore:", 8)) {

ret = StartSubprocess("/system/bin/bu restore", nullptr, SubprocessType::kRaw,

SubprocessProtocol::kNone);

} else if(!strncmp(name, "tcpip:", 6)) {

int port;

if (sscanf(name + 6, "%d", &port) != 1) {

return -1;

}

ret = create_service_thread("tcp", restart_tcp_service, reinterpret_cast(port));

} else if(!strncmp(name, "usb:", 4)) {

ret = create_service_thread("usb", restart_usb_service, nullptr);

} else if (!strncmp(name, "reverse:", 8)) {

ret = reverse_service(name + 8, transport);

} else if(!strncmp(name, "disable-verity:", 15)) {

ret = create_service_thread("verity-on", set_verity_enabled_state_service,

reinterpret_cast(0));

} else if(!strncmp(name, "enable-verity:", 15)) {

ret = create_service_thread("verity-off", set_verity_enabled_state_service,

reinterpret_cast(1));

} else if (!strcmp(name, "reconnect")) {

ret = create_service_thread("reconnect", reconnect_service, transport);

#endif

}

if (ret >= 0) {

close_on_exec(ret);

}

return ret;

}

可以看到各种各样的命令是如何执行的,这里命令分为两类,一类是adbd本身可以处理的,例如adb reboot 、adb remount、adb root等等;一类是adbd不能处理的,需要通过调用StartSubprocess函数来执行其他二进制程序或者脚本来完成,例如adb logcat 、adb shell等。

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

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

相关文章

python生成json_如何将Python数组转为Json格式数据并存储?

在Python中将数组转为Json数据存储时需要用到将json模块中的json.dumps()或者json.dump()方法。 json.dumps()方法用法 使用json.dumps( )方法将Python数组转为json格式数据 # 导入json模块 import json # 定义Python数组 py_list [{JavaEE: "http://java.itheima.com&qu…

vscode angular智能提示_【线下活动】手把手教你玩转 VS Code 插件开发

感谢 Google Developer Group 的邀请&#xff0c;3 月 30 号下午&#xff0c;韩老师将手把手带你玩转 VS Code 插件开发。 Angular 使用了 TypeScript&#xff0c;VS Code 使用了 Chromium。感谢这个开放与包容的时代&#xff0c;技术无界&#xff0c;正是大家对技术有着执着的…

ext js如何动态更改xtype_K8S ConfigMap 用于动态应用程序的实践

编辑&#xff1a;小君君技术校对&#xff1a;星空下的文仔、bot在 Kubernetes 中&#xff0c;ConfigMap 是允许管理员将配置组件与镜像内容解耦&#xff0c;使容器化应用程序产生可移植性的一种资源。ConfigMap 可以与 Kubernetes Pod 一起使用&#xff0c;用于动态添加或更改容…

python教材答案第六章_python第六章{输入和输出}

输出 用print加上字符串&#xff0c;就可以向屏幕上输出指定的文字。比如输出hello, world&#xff0c;用代码实现如下&#xff1a; >>>print hello, world print语句也可以跟上多个字符串&#xff0c;用逗号“,”隔开&#xff0c;就可以连成一串输出&#xff1a; >…

字长16位的计算机表示最大整数_废话不多说跪送计算机选择8前十题

1.字长是CPU的主要性能指标之一,它表示(a)a.CPU—一次能处理二进制数据的位数b.最长的十进制整数的位数c.最大的有效数字位数d.计算结果的有效数字长度答案解析【解析】字长是指计算机运算部件一次能同时处理的二进制数据的位数。2.字长为7位的无符号二进制整数能表示的十进制整…

节点name在graph中无法展示_图节点分类与消息传递

Message passing and node classification本文主要解决的问题&#xff1a;给定一个网络&#xff0c; 其中部分节点有label&#xff0c; 如何能将其他的节点分配对应的节点label呢&#xff1f; &#xff08;在生活中有很多这样的例子&#xff0c; 比如通过交互行为来判断用户是否…

华为荣耀v20是android10,荣耀V20和荣耀V10买哪个好

随着华为NOVA 4的发布&#xff0c;接下来要期待的就是荣耀V20了。从目前曝光的信息来看&#xff0c;这款手机确实憋了不少大招&#xff0c;而且比华为NOVA 4更好的一点是它搭载的是麒麟980处理器&#xff0c;至于它的价格应该会与华为NOVA 4相近。虽然荣耀V20很诱人&#xff0c…

git配置全局用户名和密码_还在手动打包,手动传jar包?那你确实应该学一下jekins配置了...

本文为小编原创文章&#xff0c;首发于Java识堂微信公众号&#xff0c;一个高原创&#xff0c;高收藏的公众号&#xff0c;转载请联系作者先说jekins能干啥&#xff0c;你把代码放到git上&#xff0c;jekins就能帮你编译代码&#xff0c;并且把jar包放到相应的服务器上&#xf…

python图片横向合并_[宜配屋]听图阁

起因&#xff1a; 有一批数据需要每个月进行分析&#xff0c;数据存储在excel中&#xff0c;行标题一致&#xff0c;需要横向合并进行分析。 数据示意&#xff1a;具有多个代码&#xff1a; # -*- coding: utf-8 -*- """ Created on Sun Nov 12 11:19:03 2017 a…

vue项目android,Android与Vue项目交互

1. Android代码class MainActivity : AppCompatActivity() {private lateinit var callJSBtn: Buttonprivate lateinit var webView: WebViewprivate var ajObject: AjObject AjObject()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceSt…

updatebyprimarykeyselective返回什么是成功_嫦娥五号发射升空成功!!!

嫦娥五号探测器发射成功1聚焦发射当时2020年11月24日4时30分&#xff0c;我国在中国文昌航天发射场&#xff0c;用长征五号遥五运载火箭成功发射探月工程嫦娥五号探测器&#xff0c;火箭飞行约2200秒后&#xff0c;顺利将探测器送入预定轨道&#xff0c;开启我国首次地外天体采…

荣耀v40搭载鸿蒙吗,荣耀V40照常发布,将更换操作系统,同nova8搭载鸿蒙2.0发布...

荣耀系列手机的发布节奏有一定程度的变动&#xff0c;例如荣耀Magic3发布推迟&#xff0c;但是根据消息称&#xff0c;荣耀V40还会照常发布。根据目前华为内部关于麒麟9000的分配来看&#xff0c;主要还是对Mate40和P40使用&#xff0c;其他型号中&#xff1a;nova8 Pro 、荣耀…

chrome java插件_Java程序员喜欢的10款软件里有你在用的吗?

作为一名Java程序员&#xff0c;日常开发的过程中&#xff0c;我们需要借助很多工具来进行编码。好的工具可以极大的提升程序员的工作效率&#xff0c;今天我们来认识下大多数程序员喜欢的10款软件。持不同意见或有想要补充的小伙伴&#xff0c;欢迎评论区交流哦~VSCODE不管你是…

transmac使用方法_Mac苹果电脑降级方法?

最近Mac OS升级到最新系统(Catalina 10.15 19A583)后&#xff0c;出现了各种奇葩问题&#xff0c;更关键的是&#xff0c;很多常用(专业)软件不能使用了&#xff0c;这让大家真的是焦头烂额。如果你笔记本电脑能够正常联网其实&#xff0c;Mac系统本身是提供了一个快速恢复系统…

android 富文本框架_当微擎框架遇上uniapp,以一当十同时开发十个平台项目

随着各类平台异军突起&#xff0c;流量也越来越分散。为了适应时代的发展&#xff0c;不少公司在做产品项目的时候&#xff0c;需要例如网站、公众号、H5、微信小程序、抖音小程序、支付宝小程序、百度小程序、360小程序、快应用、安卓app、苹果app的需求。这么多平台&#xff…

html5写入唯一标识,id - 唯一标识符 - html5全局属性

idid全局属性定义唯一标识符(ID)&#xff0c;该标识符在整个文档中必须是唯一的。其目的是在链接(使用片段标识符)&#xff0c;脚本或样式(使用CSS)时标识元素。示例A normal, boring paragraph. Try not to fall asleep.The most exciting paragraph on the page. One of a ki…

python 文本相似度_【机器学习】使用gensim 的 doc2vec 实现文本相似度检测

环境 Python3&#xff0c; gensim&#xff0c;jieba&#xff0c;numpy &#xff0c;pandas 原理&#xff1a;文章转成向量&#xff0c;然后在计算两个向量的余弦值。 Gensim gensim是一个python的自然语言处理库&#xff0c;能够将文档根据TF-IDF, LDA, LSI 等模型转化成向量模…

ubuntu tomcat上传目录权限_等了 3 年,Ubuntu Studio 终于有权限上传更新包

Ubuntu 的衍生版本 Ubuntu Studio 终于选出了两位具有上传更新包权限的开发者。此前就有媒体报导过&#xff0c;Ubuntu Studio 19.04 版本可能会流产&#xff0c;而原因很让人诧异&#xff1a;社区里没有任何一位开发者具有上传更新包的权限。随后 Ubuntu Studio 委员会主席 Er…

计算机主机硬件图片,电脑主机内部有哪些硬件

电脑主机内部有哪些硬件电脑主机里的硬件都有哪些呢&#xff0c;你知道吗?下面将由小编带大家来解答这个疑问吧&#xff0c;希望对大家有所收获!从主机外部看硬件从外边看&#xff0c;主机就是一个扁扁方方的铁盒子&#xff0c;但很多朋友确没有真正了解过它内部到底是什么样子…

springboot profile_SpringBoot简单配置

使用Spring Boot&#xff0c;配置工作将会变得更加简单&#xff0c;我们只需要在application.properties中定义属性&#xff0c;然后在代码中直接使用Value注入即可。 如下&#xff1a;book.authorxxxbook.nameyyy book.pinyin测试这里专门设置了中文&#xff0c;因为中文不做特…