从开发一个插件看,安卓gradle插件适配AGP8.0

transform API没学会?不用学了,AsmClassVisitorFactory更简单

    • 前言
    • 从零开始,构建一个兼容AGP8.0的插件
    • 插件发布
    • 为什么适配AGP8.0没用8.0.0版本?
    • 同一插件如何注册多个转换任务/顺序执行多个转换任务
    • InstrumentationParameters,插件配置参数
    • 总结
    • 实例代码
    • 参考链接

前言

相信很多小伙伴项目还没有升级AGP7.0,可是最新的AGP已经到8.2了,适配AGP8.0也要提上日程了,尤其是一些插件项目,因为8.0删除了transform API,所以需要提前做好适配工作。
如果你是一个插件小白,本篇可以教你从0开始在AGP7.0以上如何开发插件。
如果你是一个插件开发者,相信本篇也可以给你适配AGP8.0带来一些帮助。

从零开始,构建一个兼容AGP8.0的插件

首先我们新建一个空项目,然后在项目中开始添加模块。
由于as没有创建插件模块的选项,所以这里我们选择手动添加。
第一步:在app同级目录创建如下文件
创建插件文件夹
然后在setting.gradle配置文件中引入插件

include ':app'
include ':plugin'

接着我们在插件目录的build.gradle文件中添加一些必要的依赖:

plugins {id 'java'id 'groovy'id 'kotlin'
}dependencies {//gradle sdkimplementation gradleApi()//groovy sdkimplementation localGroovy()implementation 'com.android.tools.build:gradle:7.4.2'implementation 'com.android.tools.build:gradle-api:7.4.2'implementation 'org.ow2.asm:asm:9.1'implementation 'org.ow2.asm:asm-util:9.1'implementation 'org.ow2.asm:asm-commons:9.1'
}

大家可能注意到了,这里我们依赖的gradle版本并非8.0版本,而是gradle7.4.2版本,为啥不用8.0.0版本呢,这个稍后再解释,我们继续插件的创建。
接着我们开始添加插件的源文件:
添加源文件
在TestPlugin.properties配置中指定插件入口类,同时该配置文件的名称xxx.properties的xxx即为插件的名称,也就是后期我们应用引入该插件时的名称

implementation-class=com.cs.plugin.TestPlugin

这里还需要注意一点,就是创建META-INF.gradle-plugins的文件夹时,一定要创建两个文件夹,千万不要这样创建
在这里插入图片描述
在这里插入图片描述
接下来开始真正的插件代码逻辑了
TestPlugin中添加如下代码:

class TestPlugin  : Plugin<Project> {override fun apply(project: Project) {//这里appExtension获取方式与原transform api不同,可自行对比val appExtension = project.extensions.getByType(AndroidComponentsExtension::class.java)//这里通过transformClassesWith替换了原registerTransform来注册字节码转换操作appExtension.onVariants { variant ->//可以通过variant来获取当前编译环境的一些信息,最重要的是可以 variant.name 来区分是debug模式还是release模式编译variant.instrumentation.transformClassesWith(TimeCostTransform::class.java, InstrumentationScope.ALL) {}//InstrumentationScope.ALL 配合 FramesComputationMode.COPY_FRAMES可以指定该字节码转换器在全局生效,包括第三方libvariant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)}}}

这里我们注册一个TimeCostTransform的字节码转换功能,用来统计方法执行的时长。TimeCostTransform需要实现AsmClassVisitorFactory这个接口,该接口正是用于替换原Transform的API,新API中只需要关注ASM操作的实现即ClassVisitor,大大简化了插件开发的工作。
TimeCostTransform中添加如下代码

abstract class TimeCostTransform : AsmClassVisitorFactory<InstrumentationParameters.None> {override fun createClassVisitor(classContext: ClassContext,nextClassVisitor: ClassVisitor): ClassVisitor {//指定真正的ASM转换器return TimeCostClassVisitor(nextClassVisitor)}//通过classData中的当前类的信息,用来过滤哪些类需要执行字节码转换,这里支持通过类名,包名,注解,接口,父类等属性来组合判断override fun isInstrumentable(classData: ClassData): Boolean {//指定包名执行return classData.className.startsWith("com.cs.supportagp80")}
}

接着我们创建一个TimeCostClassVisitor的字节码转换器,用来执行在方法开始时及结束时分别插入代码来统计方法耗时,并且打印出来的逻辑

class TimeCostClassVisitor(nextVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM5, nextVisitor) {override fun visitMethod(access: Int,name: String?,descriptor: String?,signature: String?,exceptions: Array<out String>?): MethodVisitor {val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)if (name == "<clinit>" || name == "<init>") {return methodVisitor}val newMethodVisitor =object : AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, descriptor) {private var startTimeLocal = -1 // 保存 startTime 的局部变量索引override fun visitInsn(opcode: Int) {super.visitInsn(opcode)}@Overrideoverride fun onMethodEnter() {super.onMethodEnter();// 在onMethodEnter中插入代码 val startTime = System.currentTimeMillis()mv.visitMethodInsn(Opcodes.INVOKESTATIC,"java/lang/System","currentTimeMillis","()J",false)startTimeLocal = newLocal(Type.LONG_TYPE) // 创建一个新的局部变量来保存 startTimemv.visitVarInsn(Opcodes.LSTORE, startTimeLocal)}@Overrideoverride fun onMethodExit(opcode: Int) {// 在onMethodExit中插入代码 Log.e("tag", "Method: $name, timecost: " + (System.currentTimeMillis() - startTime))mv.visitTypeInsn(Opcodes.NEW,"java/lang/StringBuilder");mv.visitInsn(Opcodes.DUP);mv.visitLdcInsn("Method: $name, timecost: ");mv.visitMethodInsn(Opcodes.INVOKESPECIAL,"java/lang/StringBuilder","<init>","(Ljava/lang/String;)V",false);mv.visitMethodInsn(Opcodes.INVOKESTATIC,"java/lang/System","currentTimeMillis","()J",false);mv.visitVarInsn(Opcodes.LLOAD, startTimeLocal);mv.visitInsn(Opcodes.LSUB);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append","(J)Ljava/lang/StringBuilder;",false);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/lang/StringBuilder","toString","()Ljava/lang/String;",false);mv.visitLdcInsn("plugin")mv.visitInsn(Opcodes.SWAP)mv.visitMethodInsn(Opcodes.INVOKESTATIC,"android/util/Log","e","(Ljava/lang/String;Ljava/lang/String;)I",false)mv.visitInsn(POP)super.onMethodExit(opcode);}}return newMethodVisitor}
}

由于本篇的重点是插件的逻辑,所以字节码转换部分这里不做过多解释。到这里基本就完成了一个简单的插件的开发了。

插件发布

接下来发布插件,由于maven插件在AGP7.0已经废弃了,所以需要使用maven-publish插件来发布我们的插件代码到仓库,这里我们直接发布到本地仓库,修改后插件build.gradle代码如下:

plugins {id 'java'id 'groovy'id 'kotlin'id 'maven-publish'
}dependencies {//gradle sdkimplementation gradleApi()//groovy sdkimplementation localGroovy()implementation 'com.android.tools.build:gradle:7.4.2'implementation 'com.android.tools.build:gradle-api:7.4.2'implementation 'org.ow2.asm:asm:9.1'implementation 'org.ow2.asm:asm-util:9.1'implementation 'org.ow2.asm:asm-commons:9.1'
}publishing {repositories { RepositoryHandler handler ->handler.maven { MavenArtifactRepository mavenArtifactRepository -> //正式仓库url '..\\localmaven'allowInsecureProtocol = trueif (url.toString().startsWith("http")) {credentials {username = ''password = ''}}}}publications { PublicationContainer publication ->maven(MavenPublication) {version '0.0.1'artifactId 'Plugin'groupId 'com.cs.testplugin'from components.java}}
}

发布到仓库有两种方式
一种是,在as中添加一个gradle执行任务
添加gradle任务
创建gradle任务
点击执行即可发布插件到maven仓库
执行任务
第二种即是直接使用gradle命令发布。
首先需要在项目的gradle.properties配置文件中配置本地jdk路径(仅命令行操作需要)

org.gradle.java.home=D\:\\Android Studio\\jre

在项目文件夹下执行命令.\gradlew publish即可发布

.\gradlew publish

为什么适配AGP8.0没用8.0.0版本?

以上代码,如果将gradle版本替换为8.0.0也完全没有问题,但是这里有一个坑,那就是如果插件使用了8.0以上版本,那就必须使用jdk17来编译。而在应用中引用插件的时候,也必须使用jdk17才可以编译。这样就造成了如果要使用8.0编译的插件,还得把应用升级到使用jdk17,而对于大多数项目可能才刚刚升级到gradle7.0,因为gradle7.0或AS新版本的关系,才使用了jdk11。所以目前来说jdk17的应用普及率还比较低,这样的要求暂时还不太合适。
因此目前市面上已经兼容AGP8.0的插件几乎都是使用AGP7.x的版本来编译的。

到这里我相信大家对使用AsmClassVisitorFactory来替换transform API还有一些疑问,比如使用transform 时,可以在一个插件中注册多个转换任务,现在应该怎么做呢?
AsmClassVisitorFactory接口携带的类型是干嘛的?
接下来一一为大家解答:

同一插件如何注册多个转换任务/顺序执行多个转换任务

这个问题目前官方文档目前没有做任何说明,我目前也没有找到其他相关文章。抱着试一试的想法我问了下chatGPT
在这里插入图片描述
大家都知道chatGPT有时候喜欢一本正经的胡说八道,所以我也只能亲自尝试一下。
我copy了TimeCostTransform和TimeCostClassVisitor,改名为MethodTimeTransform和MethodTimeClassVisitor,接着修改了MethodTimeClassVisitor的打印log以区分,最终注册两个transform:

appExtension.onVariants { variant ->//可以通过variant来获取当前编译环境的一些信息,最重要的是可以 variant.name 来区分是debug模式还是release模式编译variant.instrumentation.transformClassesWith(TimeCostTransform::class.java, InstrumentationScope.ALL) {}variant.instrumentation.transformClassesWith(MethodTimeTransform::class.java, InstrumentationScope.ALL) {}//InstrumentationScope.ALL 配合 FramesComputationMode.COPY_FRAMES可以指定该字节码转换器在全局生效,包括第三方libvariant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)}

接着maven发布,在空项目应用跑起来看一下
在这里插入图片描述
好家伙,还真行,替换注册顺序,也没毛病
在这里插入图片描述

在这里插入图片描述

InstrumentationParameters,插件配置参数

实现AsmClassVisitorFactory接口需要携带一个InstrumentationParameters类型,看这个类型的描述是参数的意思。也就是说插件运行可以携带一些配置参数。另外我们可以看到注册转换任务时的方法,第三个参数instrumentationParamsConfig也是用作初始化参数配置的
在这里插入图片描述
接下来我详细和大家介绍一下如何使用,以及和传统的方式有何区别。
首先我们创建配置文件ConfigExtension和ConfigExtensionNew,内容完全相同

open class ConfigExtension {public var logTag: String = "cs"public var includePackages: Array<String> = arrayOf()public var includeMethods: Array<String> = arrayOf()
}

创建PluginHelper,传统方式,使用单例存储配置

object PluginHelper {var extension: ConfigExtension? = null
}

创建TimeCostConfig,新api的方式,存储配置

interface TimeCostConfig : InstrumentationParameters {@get:Inputval packageNames: ListProperty<String>@get:Inputval methodNames: ListProperty<String>@get:Inputval logTag: Property<String>
}

TestPlugin中添加配置文件相关逻辑

class TestPlugin  : Plugin<Project> {override fun apply(project: Project) {//这里appExtension获取方式与原transform api不同,可自行对比val appExtension = project.extensions.getByType(AndroidComponentsExtension::class.java)//读取配置文件project.extensions.create("TestPlugin", ConfigExtension::class.java)project.extensions.create("TestPluginNew", ConfigExtensionNew::class.java)//这里通过transformClassesWith替换了原registerTransform来注册字节码转换操作appExtension.onVariants { variant ->//传统方式,配置获取后直接使用单例存储,使用时读取PluginHelper.extension = project.extensions.getByType(ConfigExtension::class.java)val extensionNew = project.extensions.getByType(ConfigExtensionNew::class.java)//可以通过variant来获取当前编译环境的一些信息,最重要的是可以 variant.name 来区分是debug模式还是release模式编译variant.instrumentation.transformClassesWith(MethodTimeTransform::class.java, InstrumentationScope.ALL) {}variant.instrumentation.transformClassesWith(TimeCostTransform::class.java, InstrumentationScope.ALL) {//配置通过指定配置的类,携带到TimeCostTransform中it.packageNames.set(extensionNew.includePackages.toList())it.methodNames.set(extensionNew.includeMethods.toList())it.logTag.set(extensionNew.logTag)}//InstrumentationScope.ALL 配合 FramesComputationMode.COPY_FRAMES可以指定该字节码转换器在全局生效,包括第三方libvariant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)}}
}

在应用中可以设置两个配置代码块,分别对应TestPlugin和TestPluginNew
接下来修改transform任务,添加通过配置执行不同的asm操作
TimeCostTransform中

abstract class TimeCostTransform() : AsmClassVisitorFactory<TimeCostConfig> {override fun createClassVisitor(classContext: ClassContext,nextClassVisitor: ClassVisitor): ClassVisitor {//指定真正的ASM转换器,传入配置return TimeCostClassVisitor(nextClassVisitor, parameters.get())}//通过classData中的当前类的信息,用来过滤哪些类需要执行字节码转换,这里支持通过类名,包名,注解,接口,父类等属性来组合判断override fun isInstrumentable(classData: ClassData): Boolean {//指定包名执行//通过parameters.get()来获取传递的配置val packageConfig = parameters.get().packageNames.get()if (packageConfig.isNotEmpty()) {return packageConfig.any { classData.className.contains(it) }}return true}
}

TimeCostClassVisitor中增加读取配置,过滤配置中的方法名,指定log打印配置读取的tag

class TimeCostClassVisitor(nextVisitor: ClassVisitor,val config: TimeCostConfig) : ClassVisitor(Opcodes.ASM5, nextVisitor) {override fun visitMethod(access: Int,name: String?,descriptor: String?,signature: String?,exceptions: Array<out String>?): MethodVisitor {val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)if (name == "<clinit>" || name == "<init>") {return methodVisitor}//如果不在配置的方法名列表中,不执行val methodNameConfig = config.methodNames.get()if (methodNameConfig.isNotEmpty()) {if (methodNameConfig.none { name == it }) {return methodVisitor}}//从配置中读取tagval tag = config.logTag.get()val newMethodVisitor =object : AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, descriptor) {private var startTimeLocal = -1 // 保存 startTime 的局部变量索引override fun visitInsn(opcode: Int) {super.visitInsn(opcode)}@Overrideoverride fun onMethodEnter() {super.onMethodEnter();// 在onMethodEnter中插入代码 val startTime = System.currentTimeMillis()mv.visitMethodInsn(Opcodes.INVOKESTATIC,"java/lang/System","currentTimeMillis","()J",false)startTimeLocal = newLocal(Type.LONG_TYPE) // 创建一个新的局部变量来保存 startTimemv.visitVarInsn(Opcodes.LSTORE, startTimeLocal)}@Overrideoverride fun onMethodExit(opcode: Int) {// 在onMethodExit中插入代码 Log.e("tag", "Method: $name, timecost: " + (System.currentTimeMillis() - startTime))mv.visitTypeInsn(Opcodes.NEW,"java/lang/StringBuilder");mv.visitInsn(Opcodes.DUP);mv.visitLdcInsn("Method: $name, timecost: ");mv.visitMethodInsn(Opcodes.INVOKESPECIAL,"java/lang/StringBuilder","<init>","(Ljava/lang/String;)V",false);mv.visitMethodInsn(Opcodes.INVOKESTATIC,"java/lang/System","currentTimeMillis","()J",false);mv.visitVarInsn(Opcodes.LLOAD, startTimeLocal);mv.visitInsn(Opcodes.LSUB);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append","(J)Ljava/lang/StringBuilder;",false);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/lang/StringBuilder","toString","()Ljava/lang/String;",false);//从配置中读取tagmv.visitLdcInsn(tag)mv.visitInsn(Opcodes.SWAP)mv.visitMethodInsn(Opcodes.INVOKESTATIC,"android/util/Log","e","(Ljava/lang/String;Ljava/lang/String;)I",false)mv.visitInsn(POP)super.onMethodExit(opcode);}}return newMethodVisitor}
}

MethodTimeTransform中和MethodTimeClassVisitor中大同小异,只是将配置读取变为了从PluginConfingHelper中读取,这里就不贴代码了。
接下来我们发布仓库,然后在应用中添加配置文件:

TestPlugin {includePackages = ['com.cs.supportagp80']includeMethods = ["test","onCreate"]logTag = 'Plugin'
}
TestPluginNew {includePackages = ['com.cs.supportagp80']includeMethods = ["test","onCreate"]logTag = 'Plugin'
}

在MainActivity中添加一个测试方法

class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)test()}private fun test(){print("test")}
}

运行代码,查看日志
在这里插入图片描述
很好,两个配置块都生效了,看到这里肯定有人说,这两种配置不是都能生效吗?为什么要有新的配置方式。别急,马上我们就可以看到他们之间的区别。
接下来我们修改TestPluginNew 配置块中的配置为:

TestPluginNew {includePackages = ['com.cs.supportagp80']includeMethods = ["test"]logTag = 'Plugin-new'
}

然后重新执行代码,日志打印如下:
在这里插入图片描述
我们可以看到,配置生效了,tag变成了Plugin-new,且只打印了test方法的执行时间。
然后我们再修改TestPlugin配置块中的配置为:

TestPlugin {includePackages = ['com.cs.supportagp80']includeMethods = ["test"]logTag = 'Plugin-old'
}

然后执行代码,日志打印如下:
在这里插入图片描述
我们可以看到,TestPlugin 的配置块修改并没有生效,依旧是上一次的配置。
到这里我相信大家应该已经看明白了两种配置文件的配置方式到底有何区别,就是新方式设置配置,在修改后可以及时生效,老方式却不能。
最终经过我多次试验,总结得到的一个不太严谨的结论是,老方式的配置,只有在对应的类文件发生变化,需要重新编译时才会生效。

总结

以上就是如何开发兼容AGP8.0插件的全部内容了。
本文详细介绍了如何使用gradle7.4.2版本开发一个兼容gradle8.0的插件,并且分析了如何使用transformClassesWith注册多个转换任务,顺序执行。最后分析了新API下如何配置插件的配置参数,与原有方式配置参数有何区别。

实例代码

本篇全部代码可见:supportAGP8.0

参考链接

Transform 被废弃,ASM 如何适配?
现在准备好告别Transform了吗? | 拥抱AGP7.0
神策数据官方 Android 埋点插件

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

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

相关文章

Android开发:kotlin封装 Intent 跳转Activity,报ActivityNotFoundException 问题

Android开发&#xff1a;kotlin封装 Intent 跳转Activity&#xff0c;报ActivityNotFoundException 问题 前言起因问题解决方法一&#xff1a;方法二&#xff1a; 总结 前言 近期用kotlin进行项目开发&#xff0c;写了挺多次跳转Activity页面代码&#xff0c;发现和Java有一点…

安卓APP源码和设计报告——运动健身教学

实 验 报 告 课程名称 实验名称 指导教师 专业 班级 学号 姓名 目 录 一、设计背景31. 需求分析32. 课题研究的目的和意义3二、系统需求分析与开发环境31. 系统功能需求32.系统界面需求43.开发环境4三、系统设计4四、系统测试51.脑模拟器测试6五、总结与展望6六、重要…

安卓APP源码和设计报告——仿淘宝水果商城

项目名称 仿淘宝水果商城项目概述 随着互联网技术地高速发展&#xff0c;计算机进入到每一个人的生活里&#xff0c;从人们的生活方式到整个社会的运转都产生了巨大的变革&#xff0c;而在信息技术发达的今天&#xff0c;互联网的各种娱乐方式都在渗透到人们的生活方式之中&am…

对标ChatGPT3.5,支持手机电脑网页使用,无需魔法

说到 Claude 是什么&#xff0c;大家可能没听说过。 但是说到 OpenAI&#xff0c;说到 ChatGPT&#xff0c;相信大家一定听说过&#xff0c;玩过。 PS&#xff1a;关于 Claude 网页版的注册教程&#xff0c;我之前已经写过文章了&#xff0c;现在额外介绍如何使用手机App和电脑…

安卓调试|一文归纳总结adb调试工具常规命令

欢迎关注「全栈工程师修炼指南」公众号 点击 &#x1f447; 下方卡片 即可关注我哟! 设为「星标⭐」每天带你 基础入门 到 进阶实践 再到 放弃学习&#xff01; “ 花开堪折直须折&#xff0c;莫待无花空折枝。 ” 作者主页&#xff1a;[ https://www.weiyigeek.top ] 博客&…

安卓APP源码和报告——学生信息管理系统

学生信息管理系统APP演示视频 《移动开发技术II》实践考核方案 适用网络工程&#xff08;网络软件开发&#xff09;2018级 一、考核内容&#xff1a; 环境配置及移动开发生命周期、控件的使用、用户界面设计、数据存储与访问、广播、服务、网络编程、蓝牙应用等知识点。 二…

hnust 湖南科技大学 2023 安卓 期中考试 复习资料

前言 ★&#xff1a;录音中提到的✦&#xff1a;推测考点致谢&#xff1a;hwl&#xff0c;lqx&#xff0c;ly&#xff0c;sw重点来源&#xff1a;7-8班 PPT和录音内容来源&#xff1a;PPT知识点大多很抽象&#xff0c;需要联系实际代码来理解多做1-9章课后习题&#xff0c;编程…

chatgpt API key 获取及延续

目录 问题描述API key 获取API key 延续注册虚拟卡虚拟卡绑定openAI 账户虚拟卡注销参考链接 问题描述 chatgpt目前已被很多人作为辅助工具&#xff0c;使用openai开放的api进行请求与应用chatgpt成为一种十分便利的操作 API key 获取 网址&#xff1a;https://openai.com/p…

安卓期末大作业——图书信息管理系统

前言 随着信息技术的高速发展&#xff0c;科技逐渐走进各行各业&#xff0c;帮助人们快速、便利地完成一些工作。BMS系统是基于Android移动设备的应用软件&#xff0c;该系统能够帮助用户在家里通过手机查看相应图书馆的馆藏情况&#xff0c;而不用到图书馆中查找。同时该系统…

安卓APP源码和设计报告——麻雀笔记

目录 一 安卓应用程序开发背景3 1.1开发背景3 1.2开发环境4 二 安卓应用程序开发理论与方法4 三 记事本应用程序的设计与实现5 3.1 拟解决的问题及目标5 3.2 总体设计6 3.3 详细设计与编码实现6 四 总结23 一 安卓应用程序开发背景 1.1开发背景 1.智能手机的市场 …

安卓APP源码和设计报告——好再来点餐

大作业文档 项目名称&#xff1a;好再来点餐专业&#xff1a;班级&#xff1a;学号&#xff1a;姓名&#xff1a; 目 录 一、项目功能介绍3 二、项目运行环境3 1、开发环境3 2、运行环境3 3、是否需要联网3 三、项目配置文件及工程结构3 1、工程配置文件3 2、工程结构…

来打卡!吴恩达3门AI新课;我用AI出版97本书;如何在创业小厂做技术领导;手把手教你用SD写好提示词 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f916; 「讯飞听见会写」开放内测申请&#xff0c;基于AI的文件内容处理 「讯飞听见会写」是讯飞「星火认知大模型」的个人示范应用产品&…

安卓APP源码和设计报告——基于Android的垃圾分类系统

《移动应用开发》大作业报告 题 目基于Android的垃圾分类系统系 部班 级学 生 姓 名学 号指 导 教 师时 间 1、项目名称 垃圾分类系统 2、项目概述 近些年&#xff0c;由于人民生活水平是的提高&#xff0c;生活方式与生活节奏的加快&#xff0c;使我国的垃圾生产数量已远…

安卓期末大作业——购物商城(源码+18页报告)

Android系统原理及应用报告 题 目&#xff1a; 学 号&#xff1a; 班 级&#xff1a; 姓 名&#xff1a; 完成时间 报告要求须知 项目报告按照实践开发实际情况编写&#xff0c;着重工程项目的需求分析、系统功能分析及模块图、数据库及E-…

安卓APP源码和报告——音乐播放器

课 程 设 计 报 告 院 系&#xff1a;专 业&#xff1a;题 目&#xff1a;科 目&#xff1a;学 生&#xff1a;指导教师&#xff1a;完成时间&#xff1a; 目 录 1. 引言1 1.1 目的1 1.2 背景1 2. 需求分析1 3. 系统设计1 3.1总体设计1 3.2功能设计1 4. 系统开发2 4.1…

【AIGC使用教程】Notion AI 从注册到体验:如何免费使用

欢迎关注【AIGC使用教程】 专栏 【AIGC使用教程】SciSpace 论文阅读神器 【AIGC使用教程】Microsoft Edge/Bing Chat 注册使用完全指南 【AIGC使用教程】GitHub Copilot 免费注册及在 VS Code 中的安装使用 【AIGC使用教程】GitHub Copilot 免费注册及在 PyCharm 中的安装使用 …

体验不了ChatGPT?来试试POE桌面版!

POE Poe App目前备受欢迎&#xff0c;许多用户已开始使用加入ChatGPT API后引入的聊天机器人。最早在App Store推出&#xff0c;目前Poe App还没有推出针对Android用户的版本&#xff0c;但今天poe.com推出了桌面版&#xff0c;Android用户也可以通过桌面浏览器使用ChatGPT。需…

推特、微博对手Threads软件的下载、注册、使用最新超详细教程

经过马斯克不断折腾&#xff0c;推特面临用户大量流失的风险&#xff0c;尤其近期限制推文阅读量&#xff0c;更是导致大量用户出走。 于是乎&#xff0c;Meta公司7月6日正式发布对标推特的新社交平台 Threads&#xff0c;当前Threads只能在 iOS、Android 平台上安装 APP 使用&…

【ChatGPT+MindShow高效生成PPT,保姆级安装教程】

&#x1f680; AI破局先行者 &#x1f680; &#x1f332; AI工具、AI绘图、AI专栏 &#x1f340; &#x1f332; 如果你想学到最前沿、最火爆的技术&#xff0c;赶快加入吧✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;CSDN-Java领域优质创作者&#x1f3c6;&am…

安卓期末大作业——日记APP

2022/2023 学年 第 一 学期 课程设计 实验报告 模 块 名 称 Android课程设计 专 业 通信工程&#xff08;嵌入式培养&#xff09; 学 生 班 级 学 生 学 号 学 生 姓 名 指 导 教 师 设计题目熟悉adt-bundle-windows-x86或android-studio-ide应用开发环境&#xff1a;安…