Android自动化测试框架实现

2023-11-16

背景介绍

        最近打算梳理一下不同产品领域的自动化测试实现方案,如:Android终端、Web、服务端、智能硬件等,就先从Android终端产品开始梳理吧。本文主要介绍UI自动化测试的实现,因为这类测试解决方案比较通用,Android系统层、内核层的自动化测试解决方案可能要根据公司的具体业务来定了。

开源工具

        目前已经有比较不错的一些开源工具,如:Appnium、Python-UiAutomator,功能基本差不多,实现原理也是一样的,这里不做过多描述,有兴趣的同学可以查阅相关资料学习。

自研工具详述

        既然已经有开源的工具了,为什么还要自研?

        1、根据公司业务需要,满足定制化需求

        2、结合公司自动化框架,打造更加归一化、集成化的平台

        3、降低维护成本、提升易用性

一、实现原理概述

        我们先讲一下PC怎么跟Android终端通信,进而实现对它的控制,方式有两种:有线方式和无线方式。

有线方式:PC跟终端通过USB线连接,这种方式其实是通过adb作为中间媒介来实现通信的,如下图所示:

上图提到了Adb Client、Adb Server、Adb Daemon三个关键要素,下面简单讲一下三者的作用:

Adb Client:这部分就是需要我们自研开发的socket客户端

Adb Server:其实就是通过adb start-server命令在PC上启动的一个socket服务

Adb Daemon:是在终端上后台运行的一个守护进程,开机自启动,而且即使被杀掉,系统也会重新启动该进程,主要作用是跟Adb Server进行连接通信

注意:Adb Client与Adb Server通信遵循adb协议,具体协议内容可以百度查一下,相关资料很多

        通过以上内容大家应该清楚了PC怎么连接终端并且跟终端进行通信,那么重点来了,想要自研UI自动化测试工具就必须开发一个应用软件实现对uiautomator的封装,并在终端上作为Server运行起来,那么Adb Client跟我们开发的终端应用软件是如何进行通信的呢?如下图所示:

         本质上就是通过adb forward进行端口转发,关于这部分的内容不在这里做重点介绍,大家可以去查询adb协议。

无线方式:PC跟终端都连接到了同一个网络,通过网络进行通信,如下图所示:

         可以看出,相比有线方式,无线方式更加简单直接,PC作为客户端、应用软件作为Server端,二者通过建立socket通信来实现数据交互,从而实现PC对终端的控制。

 总结:不管哪种通信方式,要实现自研工具,有两块要进行开发(PC侧的客户端和终端侧的应用软件)。

二、Adb Client代码示例

Adb Client代码示例:
 

# -*- coding:utf-8 -*-

import os
import subprocess
import socket
import whichcraft
from collections import namedtuple
from components.device.android.adb.errors import AdbError

_OKAY = "OKAY"
_FAIL = "FAIL"
_DENT = "DENT"  # Directory Entity
_DONE = "DONE"
ForwardItem = namedtuple("ForwardItem", ["serial", "local", "remote"])


def where_adb():
    adb_path = whichcraft.which('adb')
    if adb_path is None:
        raise EnvironmentError("Can't find adb,please install adb first.")
    return adb_path


class _AdbStreamConnect(object):

    """
            连接adb服务
            即通过adb start-server在PC侧ANDROID_ADB_SERVER_PORT端口起的服务
            具体可以发送哪些命令需要查看adb协议
    """

    def __init__(self, host=None, port=None):
        self.__host = host
        self.__port = port
        self.__conn = None

        self._connect()

    def _create_socket(self):
        adb_host = self.__host or os.environ.get('ANDROID_ADB_SERVER_HOST', '127.0.0.1')
        adb_port = self.__port or int(os.environ.get('ANDROID_ADB_SERVER_PORT', 7305))
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        try:
            s.connect((adb_host, adb_port))
            return s
        except:
            s.close()
            raise

    @property
    def conn(self):
        return self.__conn

    def _connect(self):
        try:
            self.__conn = self._create_socket()
        except Exception as e:
            subprocess.Popen('adb kill-server')
            subprocess.Popen('adb start-server')
            self.__conn = self._create_socket()

    def close(self):
        self.conn.close()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    def send(self, cmd):
        # cmd: str表示期望cmd是字符串类型
        if not isinstance(cmd, str):
            cmd = str(cmd)
        self.conn.send("{:04x}{}".format(len(cmd), cmd).encode("utf-8"))

    def read(self, n):
        # -> str表示接口返回值为字符串
        return self.conn.recv(n).decode()

    def read_string(self):
        size = int(self.read(4), 16)
        return self.read(size)

    def read_until_close(self):
        content = ""
        while True:
            chunk = self.read(4096)
            if not chunk:
                break
            content += chunk
        return content

    def check_okay(self):
        # 前四位是状态码
        data = self.read(4)
        if data == _FAIL:
            raise AdbError(self.read_string())
        elif data == _OKAY:
            return
        raise AdbError("Unknown data: %s" % data)


class AdbClient(object):

    def __init__(self, host=None, port=None):
        self.__host = host
        self.__port = port

    def server_version(self):
        """
        @summary:获取adb版本号
        @return :版本号1.0.41中的41
        """
        with self._connect() as c:
            c.send("host:version")
            c.check_okay()
            return int(c.read_string(), 16)

    def _connect(self):
        return _AdbStreamConnect(self.__host, self.__port)

    def forward(self, serial, local, remote, norebind=False):
        """
        @summary:给adb服务端发送host-serial:<sn>:forward:tcp:<pc_port>;tcp:<phone_port>进行端口转发
        @param serial:手机sn号,sn为None时按默认连接一部手机处理
        @param local:PC侧socket客户端端口
        @param remote:手机侧socket服务端端口
        @param norebind:fail if already forwarded when set to true
        @attention :PC跟手机通过USB方式通信
        """
        with self._connect() as c:
            cmds = ["host", "forward"]
            if serial:
                cmds = ["host-serial", serial, "forward"]
            if norebind:
                cmds.append("norebind")
            cmds.append("tcp:%s;tcp:%s" % (local, remote))
            print(cmds)
            c.send(":".join(cmds))
            c.check_okay()

    def forward_list(self, serial=None):
        """
        @summary:查看端口转发是否成功
        @param serial:手机sn号
        @attention :PC跟手机通过USB方式通信
        """
        with self._connect() as c:
            list_cmd = "host:list-forward"
            if serial:
                list_cmd = "host-serial:{}:list-forward".format(serial)
            c.send(list_cmd)
            c.check_okay()
            content = c.read_string()
            for line in content.splitlines():
                parts = line.split()
                if len(parts) != 3:
                    continue
                if serial and parts[0] != serial:
                    continue
                yield ForwardItem(*parts)

    def shell(self, serial, command):
        """
        @summary:执行shell命令
        @param serial:手机sn号
        @param command:要执行的命令
        @attention :只能执行adb shell命令
        """
        with self._connect() as c:
            c.send("host:transport:" + serial)
            c.check_okay()
            c.send("shell:" + command)
            c.check_okay()
            return c.read_until_close()

    def device_list(self):
        """
        @summary:获取手机列表
        @attention :
        """
        device_list = []
        with self._connect() as c:
            c.send("host:devices")
            c.check_okay()
            output = c.read_string()
            for line in output.splitlines():
                parts = line.strip().split("\t")
                if len(parts) != 2:
                    continue
                if parts[1] == 'device':
                    device_list.append(parts[0])
        return device_list

    def must_one_device(self, serial):
        device_list = self.device_list()
        if len(device_list) == 0:
            raise RuntimeError(
                "Can't find any android device/emulator"
            )
        elif serial is None and len(device_list) > 1:
            raise RuntimeError(
                "more than one device/emulator, please specify the serial number"
            )

uiautomator.py代码实现:

# -*- coding: utf-8 -*-
# @Time    : 2023/2/5 20:24
# @Author  : 十年
# @Site    : https://gitee.com/chshao/aiplotest
# @CSDN    : https://blog.csdn.net/m0_37576542?type=blog
# @File    : uiautomator.py
# @Description  : 对这个模块文件的总体描述
import re
import time
import json
import socket
from typing import Union
from aitest.buildin.AiDecorator import singleton
from aitest.components.android.adb import AdbClient

TAG_UI = "UiAutomator"
TAG_WIFI = "WifiManager"
TAG_STUB = "Stub"

PORT10086 = 10086
PORT12306 = 12306
ip_pattern = re.compile('\d+.\d+.\d+.\d+')


@singleton
class UiClient(object):
    gSocket = None

    def connect(self, addr: Union[None, str, tuple]):
        """
        @summary:PC连接手机
        @param addr:手机sn号或者(ip,port)
        @attention :
        """
        if isinstance(addr, tuple):
            self._connect_wifi(addr[0], addr[1])
        self._connect_usb(addr)

    def _connect_usb(self, serial):
        adb_client = AdbClient()
        adb_client.must_one_device(serial)
        self._check_and_forward(adb_client, serial)

    def _check_and_forward(self, adb_client: AdbClient, serial=None):
        forward_list = adb_client.forward_list(serial)
        for forwardItem in forward_list:
            if forwardItem.serial == serial:
                port = forwardItem.local.split(':')[-1]
                self._connect_wifi('127.0.0.1', port)
        # 通过adb在pc上创建一个server监听10086端口,并将10086端口收到的数据转发给手机server的12306端口
        adb_client.forward(serial, PORT10086, PORT12306)
        # 创建一个socket客户端连接10086端口
        self._connect_wifi('127.0.0.1', PORT10086)

    def _connect_wifi(self, host, port):
        self.gSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.gSocket.connect((host, port))
        self.gSocket.settimeout(2)

    def send_cmd(self, cmd):
        cmd_bytes = json.dumps(cmd).encode('utf-8')
        cmd_len = len(cmd_bytes).to_bytes(4, "little")
        self.gSocket.send(cmd_len + cmd_bytes)

    def recv_ack(self, timeout=10):
        endTime = time.time() + timeout
        while time.time() < endTime:
            # 取前4位包头,为数据内容长度
            dataLenBytes = self.gSocket.recv(4)
            if dataLenBytes != b"":
                dataLen = int.from_bytes(dataLenBytes, "little")
                # 取出数据内容并返回
                return self.gSocket.recvData(dataLen)
        return b""

    def close(self):
        if self.gSocket:
            self.gSocket.close()


class Device(object):

    def __init__(self, addr=None):
        """
        @param addr: None/手机sn/IP
        """
        self.ui = UiClient()
        if addr is not None:
            if re.match(ip_pattern, addr):
                addr = (addr, PORT12306)
        self.ui.connect(addr)

    def command(self, clsName, method, args=None):
        cmd = {"class": clsName,
               "method": method,
               "args": args,
               "requestId": "request_" + str(round(time.time(), 3))}
        return cmd

    def executeCmd(self, clsName, method, args=None):
        self.ui.send_cmd(self.command(clsName, method, args))
        ack = self.ui.recv_ack()
        if ack:
            return json.loads(ack.decode('utf-8')).get("result")
        return ""

    def __checkResult(self, ack, expect):
        if ack == expect:
            return True
        return False

    def click(self, x, y):
        args = [{"k": "int", "v": x}, {"k": "int", "v": y}]
        ack = self.executeCmd(TAG_UI, "click", args)
        return self.__checkResult(ack, 'true')

    def clickById(self, id):
        args = [{"k": "string", "v": id}]
        ack = self.executeCmd(TAG_UI, "clickById", args)
        return self.__checkResult(ack, 'true')

    def clickByText(self, text):
        args = [{"k": "string", "v": text}]
        ack = self.executeCmd(TAG_UI, "clickByText", args)
        return self.__checkResult(ack, 'true')

    def clickByImage(self, image):
        pass

    def isScreenOn(self):
        ack = self.executeCmd(TAG_UI, "isScreenOn", [])
        if ack == "":
            return "unknown"
        return self.__checkResult(ack, 'true')

    def screenOn(self):
        self.executeCmd(TAG_UI, "wakeUp", [])
        return self.isScreenOn()

    def screenOff(self):
        self.executeCmd(TAG_UI, "sleep", [])
        return not self.isScreenOn()

    def goBack(self):
        ack = self.executeCmd(TAG_UI, "pressBack", [])
        return self.__checkResult(ack, 'true')

    def goHome(self):
        ack = self.executeCmd(TAG_UI, "pressHome", [])
        return self.__checkResult(ack, 'true')

    def getTextById(self, id):
        args = [{"k": "string", "v": id}]
        return self.executeCmd(TAG_UI, "getTextById", args)

三、应用软件(uiautomator封装)

        首先,打开Androdi Studio新建一个Android工程,然后在app文件夹下的build.gradle文件添加依赖库,如下图:

    androidTestImplementation 'androidx.test:runner:1.3.0'
    androidTestImplementation 'androidx.test:rules:1.3.0'
    androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
    androidTestImplementation 'androidx.core:core:1.3.0'
    androidTestImplementation 'androidx.annotation:annotation:1.1.0'

AndroidManifest.xml中添加如下权限:

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />

开发完成后,需要打包编译两个APP:app-debug.apk、app-debug-androidTest.apk

app-debug.apk为待测apk,实际上没什么作用,但是需要安装,只要保证能正常运行就行

app-debug-androidTest.apk为测试apk,uiautomator的封装实现就在这个apk里面

启动测试服务:adb shell am instrument -w -r -e debug false -e class com.aiplot.wetest.Stub com.aiplot.wetest.test/androidx.test.runner.AndroidJUnitRunner

adb install命令参数如下

-t 允许测试包
-l 锁定该应用程序
-s 把应用程序安装到sd卡上
-g 为应用程序授予所有运行时的权限
-r 替换已存在的应用程序,也就是说强制安装
-d 允许进行将见状,也就是安装的比手机上带的版本低 

Android源码地址

链接:http://aospxref.com/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android自动化测试框架实现 的相关文章

  • 了解joda时间PeriodFormatter

    我以为我明白了 但显然我不明白 你能帮我通过这些单元测试吗 Test public void second assertEquals 00 00 01 OurDateTimeFormatter format 1000 Test public
  • Werkzeug 中的线程和本地代理。用法

    首先 我想确保我正确理解了功能的分配 分配本地代理功能以通过线程内的模块 包 共享变量 对象 我对吗 其次 用法对我来说仍然不清楚 也许是因为我误解了作业 我用烧瓶 如果我有两个 或更多 模块 A B 我想将对象C从模块A导入到模块B 但我
  • GWT 2.3 开发模式 - 托管模式 JSP 编译似乎不使用 java 1.5 兼容性

    无法编译 JSP 类 生成的 servlet 错误 DefaultMessage 上次更新 0 日期 中 0 时间 HH mm ss z 语法 错误 注释仅在源级别为 1 5 时可用 在尝试以开发模式在 Web 浏览器中打开我的 gwt 模
  • Java实现累加器类,提供Collector

    A Collector具有三种通用类型 public interface Collector
  • 在 Spyder 的变量资源管理器中查看局部变量

    我是 python 新手 正在使用 Spyder 的 IDE 我欣赏它的一项功能是它的变量资源管理器 然而 根据一些研究 我发现它只显示全局变量 我找到的解决方法是使用检查模块 import inspect local vars def m
  • HashMap 值需要不可变吗?

    我知道 HashMap 中的键需要是不可变的 或者至少确保它们的哈希码 hashCode 不会改变或与另一个具有不同状态的对象发生冲突 但是 HashMap中存储的值是否需要与上面相同 为什么或者为什么不 这个想法是能够改变值 例如在其上调
  • 在 Android 中上传文件出现内存不足错误

    我的上传代码如下 String end r n String twoHyphens String boundary try URL url new URL ActionUrl HttpURLConnection con HttpURLCon
  • 在 SWT/JFace RCP 应用程序中填充巨大的表

    您将如何在 SWT 表中显示大量行 巨大是指超过 20K 行 20 列的东西 不要问我为什么需要展示那么多数据 这不是重点 关键是如何让它尽可能快地工作 这样最终用户就不会厌倦等待 每行显示某个对象的实例 列是其属性 一些 我想使用 JFa
  • 返回 Java 8 中的通用函数接口

    我想写一种函数工厂 它应该是一个函数 以不同的策略作为参数调用一次 它应该返回一个函数 该函数根据参数选择其中一种策略 该参数将由谓词实现 嗯 最好看看condition3为了更好的理解 问题是 它没有编译 我认为因为编译器无法弄清楚函数式
  • 使用布尔值进行冒泡排序以确定数组是否已排序

    我有以下用于冒泡排序的代码 但它根本不排序 如果我删除布尔值那么它工作正常 我知道 由于我的 a 0 小于所有其他元素 因此没有执行交换 任何人都可以帮助我解决这个问题 package com sample public class Bub
  • 带有 RotatingFileHandler 的 Python 3 记录器超出 maxBytes 限制

    我使用以下代码来限制日志文件的大小 最小示例 import logging from logging handlers import RotatingFileHandler Set up logfile and message loggin
  • PIL - 需要抖动,但限制调色板会导致问题

    我是 Python 新手 正在尝试使用 PIL 来执行 Arduino 项目所需的解析任务 这个问题涉及到Image convert 方法以及调色板 抖动等选项 我有一些硬件能够一次仅显示 16 种颜色的图像 但它们可以指定为 RGB 三元
  • 附加两个具有相同列、不同顺序的数据框

    我有两个熊猫数据框 noclickDF DataFrame 0 123 321 0 1543 432 columns click id location clickDF DataFrame 1 123 421 1 1543 436 colu
  • 更改Android菜单的背景颜色[重复]

    这个问题在这里已经有答案了 我正在尝试将标准浅灰色更改为浅绿色 似乎没有一个简单的方法可以做到这一点 例如 通过 Android 主题 但我找到了一个解决方法 如本页所述 http tinyurl com 342dgn3 http tiny
  • 从列表python的单个列表中删除子列表

    我已经经历过从列表列表中删除子列表 https stackoverflow com questions 47209786 removing sublists from a list of lists 但当我为我的数据集扩展它时 它不适用于我
  • Java Swing:需要一个高质量的带有复选框的开发 JTree

    我一直在寻找一个 Tree 实现 其中包含复选框 其中 当您选择一个节点时 树中的所有后继节点都会被自动选择 当您取消选择一个节点时 树中其所有后继节点都会自动取消选择 当已经选择了父节点 并且从其后继之一中删除了选择时 节点颜色将发生变化
  • Resteasy 可以查看 JAX-RS 方法的参数类型吗?

    我们使用 Resteasy 3 0 9 作为 JAX RS Web 服务 最近切换到 3 0 19 我们开始看到很多RESTEASY002142 Multiple resource methods match request警告 例如 我们
  • python 日志记录会刷新每个日志吗?

    当我使用标准模块将日志写入文件时logging 每个日志会分别刷新到磁盘吗 例如 下面的代码会将日志刷新 10 次吗 logging basicConfig level logging DEBUG filename debug log fo
  • 泛型、数组和 ClassCastException

    我想这里一定发生了一些我不知道的微妙事情 考虑以下 public class Foo
  • putFragment() - 片段 x 当前不在 FragmentManager 中

    上面的标题被问了很多次 但答案似乎与FragmentStatePagerAdapter这与我的问题无关 我正在使用该方法putFragment Bundle String Fragment 直接地 The 安卓文档 http develop

随机推荐

  • Unity的 Input.GetAxis使用

    使用GetAxis可获得很多常用的设备输入 鼠标左右键 滚轮 鼠标移动增量 空格跳跃 WSAD 可用这些输入增量进行操作控制 比如物体的前后左右移动 镜头前进后退 缩放观察 物体拖拽旋转等 转载于 https www cnblogs com
  • postman 执行下载接口时闪退问题

    下载内容过多时容易导致postman闪退 在试验接口正确性时不要着急 可以对下载日志大小进行一个缩减
  • vmware14安装黑苹果max ox x 10.13懒人版教程

    准备材料 vmware 14 0 链接 https pan baidu com s 1 fjAngjUZ9HihzboBR0eJA 提取码 wwnn vmware文件名后面有永久可用的序列号 14 0目前最高支持mac10 13 故使用这个
  • UVA 10970 - Big Chocolate

    Root AOAPC I Beginning Algorithm Contests Training Guide Rujia Liu UVA 10970 Big Chocolate 题意 计算将一块n m的巧克力切成n m块所需的次数 方法
  • Android studio报错:e: org.jetbrains.kotlin.codegen.CompilationException: Back-end (JVM) Internal error

    Android studio运行时报错 e org jetbrains kotlin codegen CompilationException Back end JVM Internal error wrong bytecode gener
  • mtu设置--解决部分网站打不开的问题

    资料一 一 常见问题介绍 1 什么情况下需要改MTU 如果您的动态域名网站不能被正常访问 很难连接 连接上也非常慢 请试试把DirectSend设为 总是关闭 如果关闭后可以正常访问 这种情况就需要修改MTU 如果您的网站连接正常 只是下载
  • 达尔文商品体系

    一 背景介绍 最初是由天猫发起的 针对天猫品牌混乱 原始商品信息错误和商家重复铺货 商家的宝贝 库存价格等信息杂乱无章 商家发布商品流程冗长 管理商品非常不便 消费者也很难找到确定的商品相关信息 前台的搜索应用困难等问题 旨在规范商品信息确
  • 批量爬虫采集大数据的技巧和策略分享

    作为一名专业的爬虫程序员 今天主要要和大家分享一些技巧和策略 帮助你在批量爬虫采集大数据时更高效 更顺利 批量爬虫采集大数据可能会遇到一些挑战 但只要我们掌握一些技巧 制定一些有效的策略 我们就能在数据采集的道路上一帆风顺 1 设立合理的请
  • 服务器虚拟机无法连接msk,无法连接mks【图文详解】

    喜欢使用电脑的小伙伴们一般都会遇到win7系统无法连接mks的问题 突然遇到win7系统无法连接mks的问题就不知道该怎么办了 其实win7系统无法连接mks的解决方法非常简单 按照1 首先检查了下 windows的防火墙设置 可以看到Vm
  • QT笔记——QProcess学习

    我们常常想通过某一个类 来启动一个外部进程 本文将讲解如何通过QProcess来进行启动外部进程 一 了解QProcess QProcess是Qt框架提供的一个类 用于在应用程序中执行外部进程 它提供了一系列函数来启动 控制和与外部进程进行
  • axios请求超时

    axios请求超时 设置重新请求的完美解决方法 自从使用Vue2之后 就使用官方推荐的axios的插件来调用API 在使用过程中 如果服务器或者网络不稳定掉包了 你们该如何处理呢 下面我给你们分享一下我的经历 具体原因 最近公司在做一个项目
  • Spring Cloud Gateway学习

    文章大纲 为什么需要网关 传统的单体架构只有一个服务开放给客户端调用 但是在微服务架构体系中是将一个系统拆分成多个微服务 那么作为客户端如何去调用这些微服务呢 如果没有网关的存在 就只能在本地记录每个微服务的调用地址 无网关的微服务架构存在
  • C++之MFC学习

    问题1 stdafx h是怎么引入进来的 define h与stdafx h之间的关系 为什么在MuisicPlayer cpp中引入stdafx h 问题2 enum class的使用 问题3 列表初始化 int window trans
  • linux重做系统分区,搜索所有硬盘分区上的linux系统,重新安装grub的方法

    电脑硬盘上 非vmware 安装了Ubuntu Fedora Debian openSUSE linuxMint Mageia elementaryOS PearOS ZorinOS Bodhi Manjaro Sparky linuxDe
  • 【星球精选】如何高效构建 Roam 与 theBrain 间细粒度双向链接?

    Roam Research 作为卡片盒很好用 只是目前缺乏中观网络可视化能力 老牌知识管理应用TheBrian 可以很好补充上这个短板 因此我希望将二者结合起来使用 只是这个过程 有些曲折 在 Obsidian 中的 excalibrain
  • redis可视工具AnotherRedisDesktopManager的使用

    redis可视工具AnotherRedisDesktopManager的使用 系列文章 macm1安装redis过程 springboot整合redis及set map list key value和实体类操作 redis可视工具Anoth
  • 龙书11_chapter_6 一:一般绘制流程

    先看BoxDemo的前几节 1 vertex input Layout 2 vertexBuffer 3 IndexBuffer 4 vertexShader 5 constant Buffer 6 pixelShader 7 render
  • Hbase存储及元数据meta

    Hbase的列族式存储 列族就是多个数据列的组合 列族式可以说是表的schema的一部分 而列不是 Hbase可以说是列簇数据库 在创建表的时候要指定列族 而不需要指定具体的列 Hbase Table组成 Table rowkey fami
  • Sqli-labs之Less-25和Less-25a

    Less 25 GET 基于错误 您所有的OR AND都属于我们 字符串单引号 Notice Undefined variable hint in C phpStudy WWW sqli Less 25 index php on line
  • Android自动化测试框架实现

    背景介绍 最近打算梳理一下不同产品领域的自动化测试实现方案 如 Android终端 Web 服务端 智能硬件等 就先从Android终端产品开始梳理吧 本文主要介绍UI自动化测试的实现 因为这类测试解决方案比较通用 Android系统层 内