安徽省住建厅网站官网/seo网站排名优化软件
本文讲解如何编译插桩操纵字节码。 就使用 ASM 来实现简单的编译插桩效果,通过插桩实现在每一个 Activity 打开时输出相应的 log 日志。实现思路
过程主要包含两步:
1、遍历项目中所有的 .class 文件
如何找到项目中编译生成的所有 .class 文件,是我们需要解决的第一个问题。众所周知,Android Studio 使用 Gradle 编译项目中的 .java 文件,并且从 Gradle1.5.0 之后,我们可以自己定义 Transform,来获取所有 .class 文件引用。但是 Transform 的使用需要依赖 Gradle Plugin。因此我们第一步需要创建一个单独的 Gradle Plugin,并在 Gradle Plugin 中使用自定义 Transform 找出所有的 .class 文件。
2、遍历到目标 .class 文件 (Activity)之后,通过 ASM 动态注入需要被插入的字节码
如果第一步进行顺利,我们可以找出所有的 .class 文件。接下来就需要过滤出目标 Activity 文件,并在目标 Activity 文件的 onCreate 方法中,通过 ASM 插入相应的 log 日志字节码。
创建主项目 ASMLifeCycleDemo,当前项目中只有一个 MainActivity,如下:
package com.jscode.asmlifecycledemo;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}
}
创建自定义 Gradle 插件
首先在 ASMLifeCycleDemo 项目中创建一个新的 module,并选择 Android Library 类型,命名为 asm_lifecycle_plugin。将 asm_lifecycle_plugin module 中除了 build.gradle 和 main 文件夹之外的所有内容都删除。然后在 main 目录下分别创建 groovy 和 java 目录,结构如下:
因为 Gradle 插件是使用 groovy 语言编写的,所以需要新建一个 groovy 目录,用来存放插件相关的.groovy类。
但 ASM 是 java 层面的框架,所以在 java 目录里存放 ASM 相关的类。 然后,在 groovy 中创建目录com.jscode.plugin,并在此目录中创建类 LifeCyclePlugin.groovy 文件。
在 LifeCyclePlugin 中重写 apply 方法,实现插件逻辑,因为是 demo 演示,所以我只是简单的打印 log 日志。 目录结构如上图,代码如下
package com.jscode.pluginimport com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Projectpublic class LifeCyclePlugin implements Plugin<Project> {@Overridevoid apply(Project project) {System.out.println("========LifeCyclePlugin========")def android = project.extensions.getByType(AppExtension)LifeCycleTransform lt = new LifeCycleTransform()android.registerTransform(lt)}
}
以看出 LifeCyclePlugin 实现了 gradle api 中的 Plugin 接口。当我们在 app module 的 build.gradle 文件中使用此插件时,其 LifeCyclePlugin 的 apply 方法将会被自动调用。
接下来,将 asm_lifecycle_plugin module 的 build.gradle 中的内容全部删掉,改为如下内容:
plugins {id 'groovy'id 'maven-publish' //本文用到maven本地仓库来管理plugin
}dependencies {implementation gradleApi()implementation localGroovy()implementation("com.android.tools.build:gradle:4.1.3")implementation 'org.ow2.asm:asm:7.1'implementation 'org.ow2.asm:asm-commons:7.1'
}publishing {repositories {maven {// $rootDir 表示你项目的根目录// 这里配置发布到的本地目录url = "$rootDir/repo"}}publications {maven(MavenPublication) {// 插件的组ID,建议设置为插件的包名groupId = 'com.jscode.lifecycleplugin'// 插件的名字,后续在引用时会用到artifactId = 'myplugin'version = '1.0.0'// 组件类型from components.java}}
}
group 和 version 都需要在 app module 引用此插件时使用。 所有的插件都需要被部署到 maven 库中,我们可以选择部署到远程或者本地。
本文只是演示,所以只是将插件部署到本地目录中。具体地址通过 repository 属性配置,如图所示我将其配置在项目根目录下的 asm_lifecycle_repo 目录下。
最后一步,创建 properties 文件。 在 plugin/src/main 目录下新建目录 resources/META-INF/gradle-plguins,然后在此目录下新建一个文件:lifecycleplugin.properties,其中文件名 lifecycleplugin 就是我们自定义插件的名称,稍后我们在 app module 中会使用到此名称。
在 .properties 文件中,需要指定我们自定义的插件类名 LifeCyclePlugin,如下所示:
implementation-class=com.jscode.plugin.LifeCyclePlugin
至此,自定义 Gradle 插件就已经写完,现在可以在 Android Studio 的右边栏找到 Gradle 中点击 publishToMavenLocal,执行 plugin 的部署任务:
可以看到,构建成功之后,在 Project 的根目录下将会出现一个 repo 目录,里面存放的就是我们的插件目标文件。测试 asm_lifecycle_plugin
为了测试自定义的 Gradle 插件是否可用,可以在工程项目中的 build.gradle 中引用此插件。
// Top-level build file where you can add configuration options common to all sub-projects/modules.buildscript {dependencies {classpath 'com.android.tools.build:gradle:4.1.3'classpath 'com.jscode.lifecycleplugin:myplugin:1.0.0'}
}plugins {id 'com.android.application' version '7.2.0' apply falseid 'com.android.library' version '7.2.0' apply false
}task clean(type: Delete) {delete rootProject.buildDir
}
app module 中的 build.gradle
plugins {id 'com.android.application'
}apply plugin: 'lifecycleplugin'android {compileSdk 33defaultConfig {applicationId "com.jscode.asmlifecycledemo"minSdk 23targetSdk 32versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}
}dependencies {implementation 'androidx.appcompat:appcompat:1.6.1'implementation 'com.google.android.material:material:1.9.0'implementation 'androidx.constraintlayout:constraintlayout:2.1.4'testImplementation 'junit:junit:4.13.2'androidTestImplementation 'androidx.test.ext:junit:1.1.5'androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
setting.gradle文件配置如下:
pluginManagement {repositories {gradlePluginPortal()google()mavenCentral()mavenLocal()}
}
dependencyResolutionManagement {repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)repositories {google()mavenCentral()}
}
rootProject.name = "ASMLifeCycleDemo"
include ':app'
include ':asm_lifecycle_plugin'
然后在命令行中使用 gradlew 执行构建命令,如果打印出我们自定义插件里的 log,则说明自定义 Gradle 插件可以使用
自定义 Transform,实现遍历 .class 文件
自定义 Gradle 插件已经写好,接下来就需要实现遍历所有 .class 的逻辑。这部分功能主要依赖 Transform API。什么是 Transform ?
Transform 可以被看作是 Gradle 在编译项目时的一个 task,在 .class 文件转换成 .dex 的流程中会执行这些 task,对所有的 .class 文件(可包括第三方库的 .class)进行转换,转换的逻辑定义在 Transform 的 transform 方法中。实际上平时我们在 build.gradle 中常用的功能都是通过 Transform 实现的,比如混淆(proguard)、分包(multi-dex)、jar 包合并(jarMerge)。自定义 Transform
在com.jscode.plugin目录中,新建 LifeCycleTransform.groovy,并继承 Transform 类
package com.jscode.pluginimport com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import com.jscode.asm.LifecycleClassVisitor
import groovy.io.FileType
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriterpublic class LifeCycleTransform extends Transform {@OverrideString getName() {return "LifeCycleTransform"}@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS}@OverrideSet<? super QualifiedContent.Scope> getScopes() {return TransformManager.PROJECT_ONLY}/*** 表示当前 Transform 是否支持增量编译* @return*/@Overrideboolean isIncremental() {return false}@Overridevoid transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {Collection<TransformInput> inps = transformInvocation.inputsTransformOutputProvider outputProvider = transformInvocation.outputProviderfor (TransformInput ti : inps) {for (DirectoryInput di: ti.directoryInputs) {File dir = di.fileif (dir) {dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File f ->// 对class文件读取解析ClassReader reader = new ClassReader(f.bytes)// 对class文件的写入ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)// 访问class文件的相应内容ClassVisitor visitor = new LifecycleClassVisitor(writer)// 依次调用ClassVisitor接口的各个方法reader.accept(visitor, ClassReader.EXPAND_FRAMES)// toByteArray方法最终修改字节码并以byte数组形式返回byte[] bytes = writer.toByteArray()// 通过文件流写入方式覆盖掉原先的内容,实现class文件的改写FileOutputStream fos = new FileOutputStream(f.path)fos.write(bytes)fos.close()}}def dest = outputProvider.getContentLocation(di.name, di.contentTypes, di.scopes, Format.DIRECTORY)FileUtils.copyDirectory(di.file, dest)}// 由于从Android Gradle插件3.6.0-alpha01开始,不再生成R.java,并且将R片段与其他源分开编译为R.jar// 所以要把R.jar复制过来ti.jarInputs.each { JarInput jarInput ->File file = jarInput.filedef dest = outputProvider.getContentLocation(jarInput.name,jarInput.contentTypes,jarInput.scopes, Format.JAR)FileUtils.copyFile(file, dest)}}}
}
解释说明:Transform 主要作用是检索项目编译过程中的所有文件。通过这几个方法,我们可以对自定义 Transform 设置一些遍历规则,具体如下: getName:设置我们自定义的 Transform 对应的 Task 名称。
Gradle 在编译的时候,会将这个名称显示在控制台上。比如:Task :app:transformClassesWithXXXForDebug。 getInputType:在项目中会有各种各样格式的文件,通过 getInputType 可以设置 LifeCycleTransform 接收的文件类型,此方法返回的类型是 Set<QualifiedContent.ContentType> 集合。 ContentType 有以下 2 种取值。
enum DefaultContentType implements ContentType {/*** The content is compiled Java code. This can be in a Jar file or in a folder. If* in a folder, it is expected to in sub-folders matching package names.*/CLASSES(0x01),/** The content is standard Java resources. */RESOURCES(0x02);private final int value;DefaultContentType(int value) {this.value = value;}@Overridepublic int getValue() {return value;}}
- CLASSES:代表只检索 .class 文件;
- RESOURCES:代表检索 java 标准资源文件。
getScopes()这个方法规定自定义 Transform 检索的范围,具体有以下几种取值:
enum Scope implements ScopeType {/** Only the project (module) content */PROJECT(0x01),/** Only the sub-projects (other modules) */SUB_PROJECTS(0x04),/** Only the external libraries */EXTERNAL_LIBRARIES(0x10),/** Code that is being tested by the current variant, including dependencies */TESTED_CODE(0x20),/** Local or remote dependencies that are provided-only */PROVIDED_ONLY(0x40),/*** Only the project's local dependencies (local jars)** @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}*/@DeprecatedPROJECT_LOCAL_DEPS(0x02),/*** Only the sub-projects's local dependencies (local jars).** @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}*/@DeprecatedSUB_PROJECTS_LOCAL_DEPS(0x08);private final int value;Scope(int value) {this.value = value;}@Overridepublic int getValue() {return value;}}
isIncremental() 表示当前 Transform 是否支持增量编译,我们不需要增量编译,所以直接返回 false 即可。 transform()在 自定义Transform 中最重要的方法就是 transform()。在这个方法中,可以获取到两个数据的流向。
inputs:inputs 中是传过来的输入流,其中有两种格式,一种是 jar 包格式,一种是 directory(目录格式)。
outputProvider:outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做,否则编译会报错。
我们可以实现一个简易 LifeCycleTransform,功能是打印出所有 .class 文件。代码如上LifeCycleTransform所示
解释说明:
- 自定义的 Transform 名称为 LifeCycleTransform;
- 检索项目中 .class 类型的目录或者文件;
- 设置当前 Transform 检索范围为当前项目;
- 设置过滤文件为 .class 文件(去除文件夹类型),并打印文件名称。
将自定义的 LifeCycleTransform 注册到 Gradle 插件中
在 LifeCyclePlugin 中添加如下代码:
public class LifeCyclePlugin implements Plugin<Project> {@Overridevoid apply(Project project) {System.out.println("========LifeCyclePlugin========")def android = project.extensions.getByType(AppExtension)LifeCycleTransform lt = new LifeCycleTransform()android.registerTransform(lt)}
}
再次在命令行中执行 build 命令,可以看到 LifeCycleTransform 检索出的所有 .class 文件。
从图中可以看出,Gradle 编译时多了一个我们自定义的 LifeCycleTransform 类型的任务,并且将所有 .class 文件名打印出来,其中包含了我们需要的目标文件 MainActivity.class。
使用 ASM,插入字节码到 Activity 文件
ASM 是一套开源框架,其中几个常用的 API 如下:
ClassReader:负责解析 .class 文件中的字节码,并将所有字节码传递给 ClassWriter。
ClassVisitor:负责访问 .class 文件中各个元素,还记得上一课时我们介绍的 .class 文件结构吗?ClassVisitor 就是用来解析这些文件结构的,当解析到某些特定结构时(比如类变量、方法),它会自动调用内部相应的 FieldVisitor 或者 MethodVisitor 的方法,进一步解析或者修改 .class 文件内容。
ClassWriter:继承自 ClassVisitor,它是生成字节码的工具类,负责将修改后的字节码输出为 byte 数组。
添加 ASM 依赖
在 asm_lifecycle_plugin 的 build.gradle 中,添加对 ASM 的依赖,如下:
dependencies {implementation gradleApi()implementation localGroovy()implementation("com.android.tools.build:gradle:4.1.3")implementation 'org.ow2.asm:asm:7.1'implementation 'org.ow2.asm:asm-commons:7.1'
}
创建自定义 ASM Visitor 类
在 asm_lifecycle_plugin module 中的 src/main/java 目录下创建包 danny.jiang.asm,并分别创建 LifecycleClassVisitor.java 和 LifecycleMethodVisitor.java。代码如下: LifecycleClassVisitor.java
package com.jscode.asm;import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;public class LifecycleClassVisitor extends ClassVisitor {private String className;private String superName;public LifecycleClassVisitor(ClassVisitor cv) {super(Opcodes.ASM5, cv);}@Overridepublic void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {super.visit(version, access, name, signature, superName, interfaces);this.className = name;this.superName = superName;}@Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);if (superName.equals("androidx/appcompat/app/AppCompatActivity")) {if (name.startsWith("onCreate")) {return new LifecycleMethodVisitor(mv, className, name);}}return mv;}@Overridepublic void visitEnd() {super.visitEnd();}
}
红框中,在 visitMethod 方法中,过滤出继承自 AppCompatActivity 的文件,并在 LifeCycleMethodVisitor.java 中对 onCreate 进行改造。 LifeCycleMethodVisitor.java
package com.jscode.asm;import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;public class LifecycleMethodVisitor extends MethodVisitor {private String className;private String methodName;public LifecycleMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) {super(Opcodes.ASM5, methodVisitor);this.className = className;this.methodName = methodName;}@Overridepublic void visitCode() {super.visitCode();mv.visitLdcInsn("==========>");mv.visitLdcInsn(className + "." + methodName + "()");mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);mv.visitInsn(Opcodes.POP);}
}
上述是真正执行插入字节码的逻辑。可以看出 ASM 都是直接以字节码指令的方式进行操作的,所以如果想使用 ASM,需要程序员对字节码有一定的理解。如果对字节码不是很了解,也可以借助三方工具 ASM Bytecode Outline 来生成想要的字节码。
修改 LifeCycleTransform 的 transform 方法,使用 ASM
各种 Visitor 都定义好之后,我们就可以修改 LifeCycleTransform 的 transform 方法,并将需要插桩的字节码插入到 MainActivity.class 文件中:
@Overridevoid transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {Collection<TransformInput> inps = transformInvocation.inputsTransformOutputProvider outputProvider = transformInvocation.outputProviderfor (TransformInput ti : inps) {for (DirectoryInput di: ti.directoryInputs) {File dir = di.fileif (dir) {dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File f ->System.out.println("find file name = " + f.name)// 对class文件读取解析ClassReader reader = new ClassReader(f.bytes)// 对class文件的写入ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)// 访问class文件的相应内容ClassVisitor visitor = new LifecycleClassVisitor(writer)// 依次调用ClassVisitor接口的各个方法reader.accept(visitor, ClassReader.EXPAND_FRAMES)// toByteArray方法最终修改字节码并以byte数组形式返回byte[] bytes = writer.toByteArray()// 通过文件流写入方式覆盖掉原先的内容,实现class文件的改写FileOutputStream fos = new FileOutputStream(f.path)fos.write(bytes)fos.close()}}def dest = outputProvider.getContentLocation(di.name, di.contentTypes, di.scopes, Format.DIRECTORY)FileUtils.copyDirectory(di.file, dest)}// 由于从Android Gradle插件3.6.0-alpha01开始,不再生成R.java,并且将R片段与其他源分开编译为R.jar// 所以要把R.jar复制过来ti.jarInputs.each { JarInput jarInput ->File file = jarInput.filedef dest = outputProvider.getContentLocation(jarInput.name,jarInput.contentTypes,jarInput.scopes, Format.JAR)FileUtils.copyFile(file, dest)}}}
重新部署自定义 Gradle 插件,并运行主项目
上面几步如果一切执行顺利,那接下来就可以在点击 publishToMavelLocal 重新部署 LifeCyclePlugin。
注意:重新部署时,需要先在 app module 的 build.gradle 中将插件依赖注释,否则报错。
部署成功之后,重新在 app 中依赖自定义插件并运行主项目,当 MainActivity 被打开时,会在 logcat 中看到如下
读到这里你可能会有疑虑,如果在项目中打开了混淆,那注入的字节码还会正常 work 吗? 其实无需担心,因为混淆其实也是一个 Transform,叫作 ProguardTransform,它是在自定义的 Transform 之后执行。
总结
本文详细操作了一遍编译插桩的流程。期间涉及了几个知识点:
- 自定义 Gradle 插件;
- Transform API 的使用;
- ASM 的使用。
相关文章:

Android 编译插桩操纵字节码
本文讲解如何编译插桩操纵字节码。 就使用 ASM 来实现简单的编译插桩效果,通过插桩实现在每一个 Activity 打开时输出相应的 log 日志。实现思路 过程主要包含两步: 1、遍历项目中所有的 .class 文件 如何找到项目中编译生成的所有 .class 文件&#…...

云原生的简单理解
一、何谓云原生? 一种构建和运行应用软件的方法 应用程序从设计之初即考虑到云的环境,原生为云而设计,在云上以最佳姿势运行,充分利用和发挥云平台的弹性分布式优势。 二、包括以下四个要素 采用容器化部署:实现云平…...

AVL Cruise 2020.1 安装教程
文章目录 安装包安装破解 安装包 链接:https://pan.baidu.com/s/1GxbeDj_SyvKFyPeTsstvTQ?pwd6666 提取码:6666 安装 安装文件: 双击setup.exe: 一直netx,中间要修改两次路径,第一次是安装位置…...

数组07-滑动窗口、HashMap
LeetCode——904. 水果成篮 你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。 你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,…...

【C++杂货店】类和对象(上)
【C杂货店】类和对象(上) 一、面向过程和面向对象初步认识二、类的引入三、类的定义四、类的访问限定符及封装4.1 访问限定符4.2 封装 五、类的作用域六、类的实例化七、类对象模型7.1 类对象的存储规则7.2 例题7.3结构体内存对齐规则 八、this指针8.2 t…...

K8S笔记
...

MySQL关于日期函数的使用-笔记
韩老师笔记 select current_time select CURRENT_DATE create table mes ( id int, content VARCHAR(255), send_time DATETIME ) select * from mes; insert into mes values(1,北京,CURRENT_DATE) insert into mes (id,send_time) values(2,CURRENT_TIME) insert into mes v…...

【postgresql 】 ERROR: “name“ is not supported as an alias
org.postgresql.util.PSQLException: ERROR: "name" is not supported as an alias 错误:不支持将“name”作为别名 SELECT real_name name FROM doc_user 加上 在关键词上加上 “” 示例: SELECT real_name "name" FROM do…...

都用HTTPS了,还能被查出浏览记录?
最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥? 实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露…...

vi配置文件.vimrc内容示例
1、.vimrc配置文件介绍 (1).vimrc是vi编辑器的配置文件,里面可以对vi编译器做个性化配置; (2).vimrc在用户目录下,每个用户有一个,类似于.bashrc文件,将下面的配置文件内…...

MacOS上的Pip和Python升级指南
在MacOS系统上,保持Pip和Python版本的最新状态对于顺利进行Python开发至关重要。通过升级Pip和Python,你可以享受到最新的功能、修复的bug以及提升的开发效率。本文将为你提供在MacOS上升级Pip和Python的详细指南,助你打造更强大的开发环境。…...

VB6.0实现修改EXE程序的图标
当你给一家公司做技术支持的时候,需求各种各样的,其中今天遇到就是要修改某个程序的图标,代码实现如下。 // q1016058890 群 214016721 //注 意:这个方法貌似只对有些EXE文件有效,这不是万能的方法,此…...

Python 编程基础 | 第二章-基础语法 | 2.3、for 语句
一、for 语句 1、循环语句 for循环的语法格式如下: for iterating_var in sequence:statements(s)例如: for ch in "hello world":print(ch)fruits ["banana", "apple", "mango"] for fruit in fruits:print(…...

linux下解决tomcat错误问题
错误一: Linux下Tomcat启动报错:Neither the JAVA_HOME nor the JRE_HOME environment variable is defined 原因:可能是Linux环境变了,需要在catalina.sh文件里指定JDK路径 解决方式: 在/bin/catalina.sh配置文件中加…...

PMP证书的价值如何?
2022年开始,PMP考试启用了新考纲,不光考试内容进行了大刀阔斧的改革,出题方式也进行了更新。除原有的PMBOK6和PMBOK7主考教材外,还增加了一本《敏捷实践指南》。 别小看新加的这本书,它虽然与PMBOK代表的预测法属于完…...

linux上mysql数据备份(全量备份策略+增量备份策略)
执行备份策略前,先做好scp命令的准备 解决思路: 生成SSH公钥/私钥后,您需要将公钥添加到服务器上,从而使服务器可以使用该公钥来验证您的身份。 生成SSH公钥/私钥的命令为 ssh-keygen -t rsa -b 4096什么都不用输入,…...

PHP实现DFA算法,查找关键词
# 添加关键词 到全局字典dict里面 protected function addWord($strWord) {$len mb_strlen($strWord,UTF-8);$curNode &$this->dict;for ($index 0; $index < $len; $index) {$word mb_substr($strWord, $index, 1, UTF-8);if (!isset($curNode[$word])) {$curNo…...

JTS:08 JTS图形相交
这里写目录标题 版本JTS disjoint intersects俩个图形不相交俩个图形 边相交俩个图形 内部相交俩个图形 点相交 版本 org.locationtech.jts:jts-core:1.19.0 链接: github JTS disjoint intersects 不相交的 九交模型FF*FF**** 相交的 九交模型 [T********] [*T*******] [**…...

深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官
目录 1. ThreadLocal 的主要功能? 2. ThreadLocal 代码举例 3. ThreadLocal 源码分析 3.1 ThreadLocal 的 get 方法源码解析 3.2 ThreadLocal 的 set 方法源码解析 3.3 ThreadLocal 的 createMap 方法源码解析 3.4 ThreadLocal 的 set 方法总结 4. 为什么En…...

sort()排序函数(c++)
文章目录 sort()排序函数(c)一、原理二、使用方法(一)头文件(二)使用语法1.方式一(默认)2.方式二:定义升序或降序3.方式三:自定义 sort()排序函数(…...

如何评估测试用例的优先级?
评估测试用例的优先级,有助于我们及早发现和解决可能对系统稳定性和功能完整性产生重大影响的问题,助于提高测试质量,提高用户满意度。 如果没有做好测试用例的优先级评估,往往容易造成对系统关键功能和高风险场景测试的忽略&…...

510758-28-8,用于标记蛋白质和酶的配体TBTA
产品简介:Tris(benzyltriazolylmethyl)amine (TBTA)是一种配体,能作为生化工具用于标记蛋白质和酶。 CAS号:510758-28-8 中文名:三[(1-苄基-1H-1,2,3-三唑-4-基)甲基]胺 英文名:TBTA 化学式:C30H30N10…...

Jtti:云服务器ftp不能访问端口如何解决
如果您的云服务器上的FTP服务无法访问端口,可能有多种原因导致这种情况。以下是一些可能的解决方法: 检查FTP服务状态: 首先,请确保您的FTP服务器正在运行。您可以使用以下命令来检查FTP服务器的状态,具体命令可能因FT…...

云服务器租用价格表概览_阿里云腾讯云华为云
云服务器租用价格多少钱一年?阿腾云分享阿里云、腾讯云和华为云的云服务器租用价格表:阿里云2核2G服务器108元一年起、腾讯云2核2G3M带宽轻量服务器95元一年、华为云2核2G3M云耀L实例89元一年起,阿腾云分享更多关于云服务器租用价格明细&…...

E. Iva Pav -前缀和 + 二分 +位运算
题面 分析: 赛时一直纠结于与运算前缀和不可逆,导致没有思路,但是发现行不通并没有及时思考别的解决办法导致一条路走到黑,阻碍了自己的思维,在今年的网络赛赛时也是一样,行不通的时候就没心思去重新想其…...

新手学习:ArcGIS对shp文件裁剪
新手学习:ArcGIS对SHP文件裁剪 新手学习 记录每个步骤,因为有很多控件可能刚开始还不熟悉,根本不知道在哪里,所以写的比较详细。 1.添加要裁剪的shp文件 2.查看shp文件的地理坐标系 双击shp文件,就可以查看shp文件的…...

Java 设计模式——抽象工厂模式
目录 1.概念2.结构3.实现4.优缺点5.使用场景6.模式扩展7.JDK源码解析——Collection.iterator方法 1.概念 (1)Java 设计模式——工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机等。这些工厂只生产同种类产品…...

如何使用ChatGPT构建一个Web应用程序?
围绕ChatGPT的最大卖点之一是它可以成为一种有效的编程工具。其想法是这样的:你用自然语言描述需求,该聊天机器人生成满足该需求的代码。但是ChatGPT在这方面到底有多好呢? 还有什么比亲自测试一下更好的方法呢?我们让ChatGPT从头…...

关闭手机广告的步骤
关闭手机广告的步骤 小米 1.设置→小米账号→声明与条款→系统广告→系统工具广告→关闭 2.设置→应用设置→应用管理→右上角三个点→设置→关闭“应用升级提醒”&“资源推荐” 3.桌面左滑打开负一屏→划到底部→设置→服务管理→选择关闭项目 4.桌面→打开任意文件夹…...

【Verilog 教程】6.6Verilog 仿真激励
关键词:testbench,仿真,文件读写 Verilog 代码设计完成后,还需要进行重要的步骤,即逻辑功能仿真。仿真激励文件称之为 testbench,放在各设计模块的顶层,以便对模块进行系统性的例化调用进行仿真…...