Completing the Android Skill Tree-Fun with Gradle (1) | Booklet Learn for Free

Completing the Android Skill Tree-Fun with Gradle (1) | Booklet Learn for Free

Basically, Android developers have indirect or direct contact with Gradle. After all, using AS to guide projects is this problem: stuck

Gradle:Build Running

Half an hour has passed, and you are still Building Running, and you can only fuck, what is this thing TM doing? ? ?

After learning Gradle, it may help you understand the compilation process and make some optimizations for compilation speed~

Of course, the benefits are not limited to this. For example, you can learn about the specific APP packaging process through Gradle, and you can reduce the APK size in the form of custom tasks or writing Gradle plug-ins, such as resource obfuscation tools

AndResGuard
, There are also multi-channel packaging, compilation speed, etc., anyway, it is not a loss for learning.

Coincides with the catch activities Nuggets free brochure to learn , into a wave of "Mastering Gradle" , Gradle on the system under study it, we want to want to learn Gradle friends benefit.

0x1, first getting to know Gradle

1. Automated build tools

**

Gradle
** is defined as follows:

Written in pure Java, open source based on the concepts of Ant and Maven

Automated build tools
, Focus on flexibility and performance,
Build script
Abandon the cumbersome configuration based on XML, adopt
Groovy
or
Kotlin
of
Domain Specific Language (DSL)
To write.

The word splitting begins:

  • Build The process of generating executable programs from source code;
  • Automation replace some manual work with machines;

The manifestation of the build in Android is: compile source code generate .apk executable file , there is such a build flow chart on the Android official website :

Briefly analyze the following process:

  • IDE converts the source code into dex, and other content into compiled resources;
  • APK packager integrates dex and compiled resources into a single apk;
  • The packager uses the zipalign tool to optimize the application and sign the apk;

Of course, this is a highly abstracted process, and the actual packaging process is much more complicated:

  • aapt command generate R.java file;
  • aidl command generate the java file corresponding to aidl;
  • javac command compile java source file to generate class file;
  • dx.bat class is converted to class.dex file
  • ...Wait

Think about if every time you package an apk, you have to manually use various commands and tools to package in order. How low efficiency is. When you encounter a dozen or more channel packages, the package boy will die directly!

Tips : Different application markets may have different statistical requirements, and an installation package needs to be released for each application market. This is the channel package.

People are fallible , especially in this manual intervention repetitive tasks , have occurred to mess with the wrong package channels to identify such things.

This kind of pipelined repetitive construction can be written into a script (

Build script
), every time you pack, execute this script
automation
Just package it, so that developers can write functional codes without distraction, thereby improving development efficiency.

The content of the build script is to execute commands and call tools in sequence according to the build process, and finally output the generated executable file to a specific directory.

Speaking of scripts, some children's shoes are ready to prepare Python and Bash shuttles right away. In fact, it is not necessary.

Open source automated build tool
It's very fragrant, there is no need to repeat the wheel, you have to step on the wave pit~


2. The difference between Ant, Maven and Gradle

When Android used Eclipse as IDE in the early days, the automated build tool used

Apache Ant
, Written in Java, platform-independent, based on the idea of task chain, using XML as the build script, the file is build.xml by default, and the basic configuration template is as follows:

<?xml version="1.0" encoding="UTF-8" ?> < project name = "HelloWorld" default = "run" basedir = "." > < property name = "src" value = "src"/> < property name = "dest" value = "classes"/> < property name = "jarfile" value = "hello.jar"/> <target name = "init" > < mkdir dir = "${dest}"/> </target > < target name = "compile" depends = "init" > < javac srcdir = "${src}" destdir = "${dest}"/> </target > < target name = "build" depends = "compile" > < jar jarfile = "${jarfile}" basedir = "${dest}"/> </target > <target name = "test" depends = "build" > < java classname = "test.ant.HelloWorld" classpath = "${hello_jar}"/> </target > < target name = "clean" > < delete dir = " {dest} $ "/> < Delete File = " $ {hello_jar} "/> </target > </Project > copy the code

You can clearly see that there are five Targets defined in the script: init, compile, build, test, clean, and Target.

They also define dependencies through depends to form the order of execution. Compile is executed before build Target is executed, and init is required before compile is executed.

Ant also supports custom Target, but there is a problem:

No way to manage dependencies
, The project depends on a third-party library, you need to manually
Correct version
The package is copied to the lib directory, and the scene of finding various jar packages in the early days is still vivid.

Therefore, with dependent library management

Apache Maven
Here, you only need to indicate the required package and version in the project management file (pom.xml), and Maven will automatically package it into the project during construction. An example is as follows:

<?xml version="1.0" encoding="utf-8"?> < project ...xmlns... > < groupId > cn.coderpig </groupId > < artifactId > Test </artifactId > < version > 1.0. 0-SNAPSHOT </version > < Dependencies > < dependency > < the groupId > com.squareup.okhttp3 </the groupId > < the artifactId > okhttp </the artifactId > < Version > 4.9.1 </Version > </dependency > </Dependencies > </Project > copy the code

Defined its own package: cn.coderpig:Test:1.0.0-SNAPSHOT, the project depends on: com.squareup.okhttp3:okhttp:4.9.1, Maven will automatically enter the okhttp package, if not, it will automatically go online Download (remote warehouse, can be specified through the repository tag).

Maven abandoned the practice of defining tasks through Target in Ant and adopted

The idea of convention over configuration
, Abstract a set of
Build the life cycle
Clean default site, require users
In a given life cycle
Use plug-ins to complete the build work.

Good standardization is good, but it also brings a problem:

Implementation of custom build requirements is troublesome
, There is no way to be as flexible as Ant.

Another point is that both Ant and Maven use XML to define build scripts, which are less readable and extensible, and are weak in task execution (does not support loops, conditional judgments are troublesome, etc.).

So, combining the strengths of Ant and Maven, more powerful

Gradle
coming:

  • Abandon cumbersome XML and use Grovvy DSL or Kotlin DSL to write build scripts;
  • The jar package can be downloaded automatically like Maven, that is, dependency management;
  • There is a default standard build life cycle, and flexible custom tasks are also supported;
  • The build level supports the gradual migration from Ant and Maven;

Tips : DSL (Domain Specific Language), domain-specific language, focusing on specific problem areas of computer languages, such as SQL supports only database-related operations, regular expression support only retrieve the replacement string. Expressed in a concise form, intuitive and easy to understand, so that the cost of reading and adjusting the code can be reduced, that is, refinement but not broadness.

A brief summary of the three differences:

Ant supports automated packaging logic. Maven has more than it automatically downloads jar packages, standardizing the packaging logic, but it is not easy to customize. Gradle can not only download jar packages automatically, but also write scripts by itself, and script writing is cooler than Ant.


3. Gradle download configuration

Gradle is based on JVM and requires a Java environment (version 1.8 and above). Download the corresponding version of Gradle on the Gradle official website . Here, taking 6.1.1 as an example, there are four types of packages available for download:

Both bin and all can be downloaded. I am used to the latter. Sometimes I need to read the source code and documentation. After downloading the zip package, unzip it and configure the environment variable GRADLE_HOME . The Windows environment configuration example is as follows:

New Path environment variables:

After configuration, open the terminal and type: gradle -v to verify whether it works.


4. Gradle Wrapper

Android Studio uses by default

Gradle Wrapper
Instead of directly using
Gradle
, The command is also used
gradlew
Instead of
gradle
.

This Gradle Wrapper is a layer of encapsulation for Gradle, so that developers do not need to care about the version changes of the project Gradle.

To create a new directory, type the following command:

gradle wrappercopy code

The generated file directory structure is as follows:

.gradle gradle wrapperwrapper | gradle-wrapper.jar//Used to download the required Gradle; | gradle-wrapper.properties//configuration file; gradlew//executable script under Linux; gradlew.bat//executable script under Windows; Copy code

Then type: gradlew build compile, check that the corresponding version of Gradle in the configuration file is not available locally, it will start

wrapper process
After downloading and configuring Gradle, this process will be automatically closed.

gradle-wrapper.properties
The content of the configuration file is as follows:

Point to the C:\Users\Username.gradle directory under Windows , open it and you can see the download of each version of gradle:

There is another advantage of encapsulating one layer: Gradle can also be used to build projects on machines without Gradle installed, but there is one thing to note:

Each Gradle version corresponds to a Daemon process, starting at 512M. If the computer configuration is not good , you should try to avoid running multiple versions of Gradle at the same time. Suggestion: Manage Gradle yourself, that is, use the local to create Gradle environment , the configuration method of AS is as follows:


5. Initial experience of building scripts

Type the following command to create a new build.gradle file and compile it:

touch build.gradle echo println( "Hello Gradle!" ); >> build.gradle gradlew Copy code

The output is as follows:

carried out

gradle
Will look for the name from the current directory
build.gradle
or
build.gradle.kts
File and execute its contents.

In addition to manual creation, you can also use

gradle init
Automatically initialize different types of projects:

Take the Kotlin application project as an example, open the directory to view the created files:

You can see that some dependencies have been added to build.gradle, type

gradlew build
Compile the following project:


6. Where did all the bags go?

Here comes the question: Where are the downloaded third-party dependent libraries ?

Answer: ~/.gradle/cache/dodules-2/files-2.1/package name/library name/version number/hash string/ , the example is as follows:

If you don t want to download gradle related to ~/.gradle, you can add environment variables yourself

GRADLE_USER_HOME
,Such as:

Later, the things downloaded by gradle will be placed in this directory:

The above changes may not take effect in Android Studio, and sometimes you need to configure it yourself:


0x2, Gradle's execution architecture

When I wanted to delete the above C:\Test directory, I found that it couldn't be deleted:

There is a process occupying this folder. What kind of process is that? answer:

daemon process
, You can type the following command
gradle --status
View a wave:

The process id is 10276 the process is idle (BUSY means the task is being built) additional information: 6.1.1, open the task manager to locate this process:

We all know that java code is compiled into class bytecode and then run on the JVM, then use jdk's own

jvisualvm.exe
View a wave of specific information:

Okay, that's it

daemon
The daemon process is named GradleDaemon, so why let a daemon process stay in the background?

This has to mention Maven first:

When Maven is building, it will start a Maven JVM process. After the build is completed, this process will be closed. It must be started every time a Maven build is used. Loading the required jar file is a time-consuming process.

After Gradle 3.0, Daemon mode is used by default:

Start a very lightweight client JVM process, which is only used to communicate with the background deamon JVM process. After the client process is built, the client process is closed, but the deamon process remains (in the IDLE idle state). When the next build is required, the deamon process is directly enabled to reduce the time-consuming wait for the build. The deamon process is reserved in the background for three hours by default, and it is closed if it is not started during this time period.

0x3, Gradle configuration

There are three places where Gradle is configured, and the parameter priorities are as follows:

Command line parameters
>
~/.gradle/gradle.properties
>
Project root directory/gradle.properties

List the more commonly used command-line options, you can get an impression after a while, and then check it when you use it (for more details, please refer to the official document )

# Command structure gradle [taskName...] [--option-name...] # Incremental compilation: In the same project, the same task will not be executed multiple times without meaning unless necessary; # Cache: Whether in the same project or not, as long as the Task input is unchanged, the cached result will be reused, no need Really execute the task; # TasksExecute gradle myTask # Execute a task gradle :my-subproject:taskName # Execute the task in the subproject gradle my-subproject:taskName # Same as above, if no subproject is specified, this task of all subprojects will be executed, such as gradle clean Gradle task1 task2 # Run multiple tasks gradle dist --exclude-task test # Exclude a task from execution gradle dist -x test # Same as above gradle test --rerun-tasks # Mandatory execution of UP-TO-DATE tasks , That is, do not use incremental compilation, perform full compilation; gradle test - continue # By default, once the Task fails, the build will fail, and execution can continue through this parameter; # Common tasks (Task conventions between plugins) gradle build gradle run gradle check gradle clean # delete the build directory # Build details gradle projects # List all sub-projects gradle tasks # List all Tasks (Tasks assigned to a task group) gradle tasks --group = "build setup" # List Tasks of a specific task group gradle tasks --all # List all Tasks gradle -q help --task libs # View the detailed information of a task gradle myTask --scan # Generate a visual compilation report gradle dependencies # List project dependencies gradle -q project:properties # List the list of project properties # Debugging options -?, -h, - help # Help information -v , --version # Version information -s, --stacktrace # Print out exception stack trace information; -S, --full-stacktrace # More than above complete information; # Performance related --build-cache # Reuse cache --no-build-cache # Do not reuse cache, default --max-workers # Maximum number of processors --parallel # Generate projects in parallel --no-parallel # No Parallel project generation --priority # Gradle start process priority --profile # Generate performance report # Daemon --daemon # deamon process of building use --no-daemon # do not use deamon process of building --foreground # foreground process started deamon process --status # deamon process and see how it works in recent stop; --stop # stop All deamon processes of the same version; # Log option -q, --quiet # Only log errors -w, --warn -i, --info -d, --debug --console=(auto,plain,rich,verbose) # Specify the output type --warning-mode=(all,fail,none,summary) # Specify the warning level # Execution option --include-build # Compound build --offline # Offline build --refresh-dependencies # Forced to clear the dependency cache --dry-run # Look at the task execution order without actually executing the task --no-rebuild # Do not build project dependencies repeatedly # Environment option -b, --build-file # Specify the build file -c, --settings-file # Specify the settings file -g, --gradle-user-home # Specify the default. Gradle directory -p, --project- dir # Specify the starting directory of Gradle --project-cache-dir # Specify the cache directory, default. gradle -D, --system-prop # Set JVM system properties -I, --init-script # Specify the initialization script -P, --project-prop # Specify the project properties of the root project; copy the code

Tips : Some properties can be configured in the gradle.properties file, so you don t need to enter the command line every time. For more information, see: System properties

0x4, Gradle basics

1. Gradle build life cycle

Gradle build is divided into three stages:

2. Life Cycle Monitoring (HOOK)

3. Grovvy Basic Grammar Crash Course

Grovvy is on the JVM

Scripting language
, Based on Java extension
Dynamic language
In addition to being compatible with Java, new functions such as closures have also been added.

Gradle will compile the **.gradle** Groovy script into a .class java bytecode file and run it on the JVM.

Gradle is an automated build tool, a program running on the JVM, Groovy is a language based on the JVM, the relationship between the two is the same as Android and Java.

The syntax involved in Groovy in Gradle is just relatively simple. After learning and interested in Groovy, you can move to the official website to learn ( groovy API ).

The Android project is built with Gradle and used by default

Groovy DSL
Script construction, starting from Gradle 4.0, officially supported
Kotlin DSL
Script construction, the two can coexist, this section is based on
Groovy DSL
To explain. (Personally, I feel that his two syntaxs are too similar. I will migrate from Kotlin to Groovy without pressure~)

Basic rules

  • The comments are consistent with Java, support://or/**/
  • Does not end with a semicolon;
  • Single-quoted strings will not escape the $ sign, double-quoted strings can use string templates, and triple-quoted strings are formatted strings;
  • Method brackets can be omitted, and return can be omitted, and the last line of code will be returned by default;
  • Code blocks can be passed as parameters

Definition (using the def keyword definition)

//Define variables: Groovy supports dynamic types, you don t need to specify the type when you define it, it will deduce it by itself def a = 1 //Define integer type, Groovy compiler will pack all basic types into object type def b = "String: ${a}" //Define string template de double c = 1.0 //Define Double Type copy code

Declare variables

//Local variables, visible only in the scope where they are declared def dest = "dest" task copy( type: Copy) { from "source" into dest } //ext additional attributes, all enhanced objects in the Gradle domain model can accommodate additional user-defined attributes ext { springVersion = "3.1.0.RELEASE" emailNotification = "build@master.org" } task printProperties { doLast { println springVersion println emailNotification } } //Variables declared with type modifiers are visible in closures, but not in methods String localScope1 = 'localScope1' println localScope1 closure = { println localScope1 } def method() { try { localScope1 } catch (MissingPropertyException e) { the println 'localScope1NotAvailable' } } closure.call() method() //output: //localScope1 //localScope1 //localScope1NotAvailable copy the code

function

//A function without a return value needs to use the def keyword, and the execution result of the last line of code is the return value. //A parameterless function def fun1() {} //Parameter function def fun2(def1, def2) {} //If the function return type is specified, the def keyword can be omitted String fun3() { return "return value" } //Simplified, the effect is the same as fun3 String fun4() { "Return value" } //Function call can be without parentheses println fun3 Copy code

cycle

//b before i, output 5 tests for (i = 0 ; i < 5 ; i++) { println( "Test" ) } //Output 6 tests for (i in 0. .5 ) { println( "Test" ) } //If you want to output 5, you can change it to: for (i in 0 .. < 5 ) //Number of cycles, from 0 to 4 4. times{ println( "Test: ${it}" ) } Copy code

Ternary operator, judgment

//Consistent with Java, the blank can also be abbreviated as this: def name = 'd' def result = name?: "abc" //Still useful? Empty, same as Kotlin, person is not empty Data attribute is not empty only then print name println person?.Data?.name //asType is type conversion def s3 = s1.asType(Integer) Copy code

Closure

The essence of closure is

Code block
, Functions that can be passed as variables at runtime, and retain access to the scope of the variables that define them.

//Define the closure def clouser = {String param1, int param2 -> //The front of the arrow is the parameter definition, followed by the code println "code part" } //call closure clouser.call() clouser() //If the closure does not define parameters, it implies an it parameter, which is similar to this def noParamClosure = {it-> true } //The last parameter of the function is a closure, the parentheses can be omitted, similar to the usage of the callback function task CoderPig { doLast ({ println "Test" }) } task CoderPig { doLast { println "Test" } } //The key variables in the closure, when there is no nested closure, they all point to the same one. When there is a closure: //this: the class where the closure is defined; //owner, delegate: which closure object is closest to him //Closure delegation: Each closure has a delegate object, which Groovy uses to find //variables and method references that are not local variables or parameters of the closure, which is the proxy mode class Info { int id; String code; def log() { println( "code:${code};id:${id}" ) } } def info(Closure<Info> closure) { Info p = new Info() closure.delegate = p //Delegation mode takes precedence closure.setResolveStrategy(Closure.DELEGATE_FIRST) closure(p) } task configClosure { doLast { info { code = "cix" id = 1 log() } } } //Output: the Task: configClosure //code: CIX; ID:. 1 Copy Code

collection

//Array, the definition method is expanded as follows, others are similar to Java def array1 = [ 1 , 2 , 3 , 4 , 5 ] as int [] int [] array2 = [ 1 , 2 , 3 , 4 , 5 ] /* List: Linked list, corresponding to ArrayList, the variable is defined by [], and the element can be any object. */ //Define the list def testList = [ 1 , 2 , 3 ] //Add elements, shift left to add new elements testList << 300 ; testList.add( 6 ) testList.leftShift( 7 ) //delete testList.remove( 7 ) testList.removeAt( 7 ) testList.removeElement( 6 ) testList.removeAll { return it% 2 == 0 } //custom rule //Find int result = testList.find { return it% 2 == 0 } def result2 = testList.findAll { return it% 2 != 0 } def result3 = testList.any { return it% 2 != 0 } def result4 = testList.every { return it% 2 == 0 } //Get the minimum, maximum, and number of conditions list.min() list.max( return Math.abs(it)) def num = findList.count { return it >= 2 } //Sort testList.sort() sortList.sort {a, b -> a == b? 0 : Math.abs(a) <Math.abs(b)? 1 : -1 } /* Map: key-value table, corresponding to LinkedHashMap, use: colon definition, key must be a string, it can be wrapped without quotation marks*/ //access aMap.keyName aMap[ 'keyName' ] aMap.anotherkey = "i am map" aMap.anotherkey = [ a: 1 , b: 2 ] //Traverse def result = "" [ a: 1 , b: 2 ].each {key, value -> result += "$key$value" } //Traversal with index (counter starting from 0, Map.Entry object passed when two parameters) [ a: 1 , b: 2 ].eachWithIndex {entry, index, -> result += "$entry$index" } [ a: 1 , b: 2 ].eachWithIndex {key, value, index, -> result += "$key$value$index" } //Group def group = students.groupBy { def student -> return student.value.score >= 60 ? 'Pass' : 'Fail' } /* Range, range, an extension of List*/ def range = 1. .5 println(range) //output: [1, 2, 3, 4, 5] range.size() //length range. iterator() //iterator def s1 = range.get( 1 ) //get the element with label 1 range.contains( 5 ) //whether it contains the element 5 range.last() //the last element range.remove( 1 ) //remove the element labeled 1 range.clear() //clear the list println( "first data: " +range.from) //first data println( "last data:" +range .to) //The last data copy code

4. Project

The Gradle build consists of one or more Projects. An example of printing Project information is as follows (build.gradle). Two sub-modules are created:

//configuration information description("a test item") version("v0.0.1") group("Test") //Define a method to print the project def printProject(pj) { println("===============") println("Get project information:") println("Project name:" + pj.name) println("Path:" + pj.path) println("Project description:" + pj.description) println("The directory containing the build script:" + pj.projectDir) println("Generate file directory:" + pj.buildDir) println("Which group belongs:" + pj.group) println("Version:" + pj.version) } //Get all project information def getAllProjectInfo() { //Root project printProject(project) //All subprojects, call getAllprojects() to get all projects, that is, include the root project this.getSubprojects().eachWithIndex {Project entry, int i -> printProject(entry) } } //call method getAllProjectInfo() Copy code

The results of the operation are as follows:

You can also configure a Project, such as:

project('module1') { description("Submodule No. 1") version("v0.0.1") group("Test") } Copy code

More APIs can be viewed in the Project class:

5. Task

Create task

/* === Create a Task named customTask1, use gradlew customTask1 to execute this Task === */ def customTask1 = task("customTask1") customTask1.doFirst { println "Task[${name}] before execution" } customTask1.doLast { println "Task[${name}] after execution" } //Output:> Task :customTask1 //Task customTask1 Before execution //Task customTask1 After execution /* ========== Map mode is limited to related configuration, such as: specify group when creating ====== */ def customTask2 = task(group:'test', "customTask2") customTask2.doLast { println("Task:" + name + "belongs to group:" + customTask2.group) } //Output:> Task :customTask2 //Task: customTask2 belongs to the group: test /* ================= Realization of closure method =================== */ task customTask3 { group("test") description("Task name + Closure creation task") doLast { println("Task:" + name + "belongs to group:" + customTask2.group) } } //Output:> Task :customTask3 //Task: customTask3 belongs to the group: test /* ======================= Created by TaskContainer ==================== */ tasks.create("customTask4") { group("test") description("Created by TaskContainer") doLast { println("Task:" + name + "belongs to group:" + customTask2.group) } } //Output:> Task :customTask4 //Task: customTask4 belongs to the group: test /* ==================== Custom Task =================== */ class CustomTask extends DefaultTask { //The TaskAction annotation indicates the method to be executed by the Task itself @TaskAction def doSomeThing() { println("Perform some operations") } } task(type: CustomTask, "customTask5") { doFirst {println "1 before task execution"} doLast {println "After the task is executed"} doFirst {println "Before task execution 2"} } //Output:> Task :customTask5 //Before task execution 2 //Before task execution 1 //perform some operations //After the task is executed /* ===================== Attachment: Available configuration in Map =================== */ //type Create based on an existing Task, similar to class inheritance, the default value is DefaultTask //overwrite Whether to replace the existing Task, generally used in conjunction with type, the default value is false //dependsOn Configure the dependencies of the current task, the default value[] //action an Action or a closure added to the task, the default value is null //description task description, the default value is null //group task group, the default value is null Copy code

Visit task

//Through the attribute name access, the created task will be an attribute of the Project, and the attribute name is the task name customTask3.name //Access through the TaskContainer collection tasks['customTask1'].name //Access through TaskContainer's find or get method //The former returns null if not found, the latter throws UnknownTaskException if not found tasks.findByPath(":customTask1") tasks.findByName("customTask1") tasks.getByPath(":customTask1") tasks.getByName("customTask1") //Access by task name customTask1.name Copy code

Task execution

During Gradle task execution, you can use doFirst and doLast to configure tasks before or after the task is executed. For examples, see Custom Task above, in addition, you can also pass

getActions()
Get all executable Actions.

Sequence of tasks

accessible

shoundRunAfter
with
mustRunAfter
Method to control who performs the two tasks first;

Enable and disable tasks

Modify Task

enabled
The attribute is enough, the default is true, which means it is enabled, such as: customTask1.enable = true

onlyif assertion task

Assertions are conditional expressions, and the Task object has a

onlyIf
The method, this method can receive a closure as a parameter, the closure returns true, then the task is executed, so that the task's assertion can be used to control which tasks need to be executed. Such as: package channel packages on demand.

Task rules

The created tasks are in TaskContain, you can pass it

addRule
Method to add response task rules.

Task input and output

Task provided

inputs
with
outputs
Attributes.

Mount the custom Task to the build process

During the execution of Task, call another task

execute()
Method.

Tips : You can probably have an impression of the API, and I will explain it in depth after studying the principles of Script and plug-ins~


references

This article is participating in the "Nuggets Booklet Free to Learn!" activity, click to view activity details