恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Android Transform API:字节码增强的黄金窗口与构建控制权

  • 首页
  • 资讯中心
  • /
  • Android Transform API:字节码增强的黄金窗口与构建控制权

相关资讯

用PyTango开发Tango控制系统自定义客户端:从通信模型到实战踩坑 2026/10/17 1:34:12
指纹解锁亮屏时间优化:从链路拆解到ftrace量化分析 2026/10/17 1:34:12
MP2642GR-Z双向均衡原理与PIC18F4585实时控制实战 2026/10/17 1:34:12

最新资讯

RBF_BP神经网络自校正PID控制器Matlab仿真详解
大模型——MCP Server 开发实战指南(Python版):从零搭建到 TaoToken 统一 Key 接入
机器视觉:工业镜头——从光学成像到镜头选型
机器视觉:光学成像基础——从物体到传感器的光学映射
算力依然不足?用 TaoToken 统一 Key 打通 Cline MCP 与 Windsurf BYOK 的配置思路
DeepSeek-R1接入实战:API调用与本地部署全流程指南

今日推荐

Qwen3.5-122B-A10B-singprobe幻觉检测实战:AUC 0.7806,如何实时揪出大模型一本正经的胡话?
NetSpot 实战:用信道分析与频宽优化解决 Wi-Fi 卡顿
C++ split() 字符串分割的五种实现与性能实测

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Android Transform API:字节码增强的黄金窗口与构建控制权

发布时间:2026/10/17 1:34:12
Android Transform API:字节码增强的黄金窗口与构建控制权 1. 这不是“过时API”的怀旧笔记而是Android构建链路上的底层控制权争夺战“Android Transform API”这七个字对很多刚接触Gradle插件开发或深度定制构建流程的开发者来说像一本被塞进抽屉角落、封面泛黄的技术手册。它常被误读为“AS 3.0时代遗留的过渡方案”甚至被直接等同于“已被废弃的旧接口”。但真实情况恰恰相反——它从未真正退出历史舞台而是在AGPAndroid Gradle Plugin持续演进中悄然完成了从“显性工具”到“隐性骨架”的身份转换。我带过的几个团队里有做SDK分发的团队靠它在APK打包前注入统一埋点字节码有做合规加固的团队用它拦截所有网络请求类并重写SSL握手逻辑还有做跨端容器的团队借它把Flutter引擎so库按ABI自动拆包重签名。这些都不是Demo级尝试而是已上线千万级DAU产品的核心构建环节。它的价值不在于“还能不能用”而在于“你是否真正理解了Android构建的‘可编程边界’在哪里”。Transform API本质是AGP在class文件生成后、Dex合并前这个黄金窗口期向开发者开放的一把精密手术刀——它不碰源码不改资源只在字节码层面做可控干预。这种能力在R8全量混淆、D8强制脱糖、DEX分包策略日益复杂的今天反而比以往任何时候都更关键。如果你还在用“加个Keep注解”来保活类或者靠“在build.gradle里堆一堆proguard-rules.pro”来兜底那说明你还没真正拿到构建流程的控制台权限。这篇文章不讲API签名怎么写也不罗列官方文档里能查到的参数列表而是带你回到AGP源码的调用栈深处看Transform是如何被注册、如何被调度、如何与DexArchiveMerger形成协同以及为什么在AGP 8.0中它虽被“隐藏”却通过TransformManager和Artifacts API以更稳定的方式继续服役。适合正在做代码扫描、安全加固、AOP切面、编译期性能监控或者想搞懂“为什么我的自定义Task总在Dexing之后才执行”的中高级Android工程师。2. 核心设计逻辑为什么AGP必须保留这个“看似多余”的中间层2.1 构建流水线中的不可替代性定位要理解Transform API的设计初衷得先看清Android构建的完整数据流。从Java/Kotlin源码开始经历javac/kotlinc编译生成.class再到D8将.class转为.dex最后由ZipMergingTask打包进APK。表面看这是一个单向流水线但实际存在三个关键矛盾点字节码修改时机不可控javac输出.class后若直接用ASM修改会破坏后续D8的类型推断若等D8输出.dex后再改又失去对Java语法结构的感知能力.dex无方法签名泛型信息多模块依赖链的合并盲区App模块依赖LibA和LibB两者都含同名类D8默认按依赖顺序合并但若LibA需对LibB的某个工具类做增强传统方式无法在合并前介入增量构建的原子性挑战修改一个.java文件理论上只需重编译该文件对应的.class但若增强逻辑涉及跨类调用就必须确保所有相关类的字节码同步更新否则增量构建会产出不一致的DEX。Transform API正是为解决这三重矛盾而生。它被设计在Javac/Kotlinc Task完成之后、D8 Task启动之前且以模块粒度隔离执行。每个Transform实例接收本模块产出的所有.class文件包括其依赖传递进来的jar/aar中的classes处理完毕后输出新的.class集合再交由D8消费。这个位置就像工厂流水线上的质检工位上游送来半成品.class工位内可进行打磨、钻孔、喷漆字节码增强但绝不改变零件的基本尺寸类结构兼容性最终送往下一道工序D8。AGP 4.0引入Variant API后Transform被纳入Variant的TransformManager统一管理每个Variant如debug、release拥有独立的Transform链确保不同构建变体可配置差异化处理逻辑。这种设计让“在release包中插入性能监控字节码而在debug包中跳过”成为可能而非全局开关。2.2 与Task机制的本质差异状态感知 vs 流程驱动很多开发者初学时会困惑“既然Gradle本身就有Task为什么还要单独搞个Transform” 这触及了AGP构建模型的核心哲学。Gradle Task是流程驱动的定义输入inputs、输出outputs、动作doLast由Gradle根据输入输出时间戳决定是否执行。而Transform是状态感知的它不关心文件何时生成只关注“当前构建上下文的状态”。例如一个用于统计方法调用次数的Transform其行为取决于当前Variant是否启用了isMinifyEnabled true。当启用混淆时它需读取mapping.txt反查原始方法名未启用时则直接使用原名。这种决策无法在Task的inputs/outputs声明中体现因为mapping.txt是ProGuard/R8 Task的输出而Transform需在其之前执行。AGP通过TransformManager将Transform注册为Variant的“生命周期钩子”在D8 Task执行前由Manager遍历所有注册的Transform按顺序调用其transform()方法并传入包含当前Variant全部构建信息的TransformInvocation对象。该对象内含context含variantName、isIncremental等、inputs分directoryInputs和jarInputs两类、outputProvider指定输出路径。这种设计使Transform天然具备对构建上下文的感知能力这是纯Gradle Task无法替代的。2.3 AGP版本演进中的“隐身”逻辑从显式API到隐式契约AGP 7.0起官方文档开始弱化Transform APIAGP 8.0中android.registerTransform()方法被标记为Deprecated。但这并非功能删除而是架构升级。新版本中Transform的注册被收归ApplicationPlugin和LibraryPlugin内部通过TransformManager统一管理。开发者不再手动调用registerTransform()而是通过project.android.applicationVariants.all { variant - ... }获取variant后调用variant.transformClassesWith(YourTransform::class.java).after(preBuild)。这种变化背后是AGP对构建稳定性的强化旧版Transform允许任意顺序注册易导致依赖冲突如A Transform需在B Transform输出后才能处理新版通过after()/before()明确声明执行序且AGP会在调度前做拓扑排序确保无环依赖。更重要的是AGP 8.0引入了ArtifactsAPI作为替代方案它将构建产物抽象为ArtifactType如CLASSES_JAR、PROCESSED_JAVA_RES通过androidComponentsDSL注册FinalizeBundle或PrepareLibraryBundle等回调。但Artifacts API目前仅覆盖部分场景对需要深度字节码操作如方法内联、异常捕获重写的复杂需求Transform仍是唯一选择。我实测过AGP 8.2中一个用于替换OkHttp拦截器链的Transform它在Artifacts API下无法获取到完整的class hierarchy因某些依赖库的class在CLASSES_JAR中被压缩而Transform的directoryInputs能提供解压后的文件树。这印证了一个经验当你的需求涉及跨类分析、动态代理生成或运行时类加载器适配时Transform仍是不可绕过的底层通道。3. 实战核心环节从零实现一个防反射调用的字节码加固Transform3.1 需求拆解与技术选型依据我们以一个真实业务场景为例某金融类App需防止攻击者通过反射调用关键支付类如PaymentProcessor的私有方法绕过风控校验。常规方案是在方法内加if (Debug.isDebuggerConnected()) throw new SecurityException()但这只能防调试无法阻止反射。更彻底的方案是在编译期对所有被SecureMethod注解标记的方法重写字节码使其在反射调用时抛出定制异常。此需求需满足精准识别仅处理被SecureMethod注解的方法且该注解需保留在运行时Retention(RetentionPolicy.RUNTIME)字节码重写在目标方法第一行插入if (sun.reflect.Reflection.getCallerClass() java.lang.Class.class) throw new IllegalAccessException(...)兼容混淆即使方法名被R8重命名仍能正确注入故需在D8之前处理避免混淆后方法名丢失增量友好仅重写被修改的类不触发全量重建。技术选型上我们放弃Javassist反射API开销大、不支持Kotlin协程挂起点和Byte Buddy学习成本高、与AGP集成复杂选用ASM 9.4。理由ASM是AGP自身使用的字节码框架与D8兼容性最佳其Visitor模式天然契合Transform的流式处理且AGP 8.0内置ASM 9.x无需额外依赖。注意必须使用ASM_API_VERSION Opcodes.ASM9否则处理Kotlin内联函数时会抛UnsupportedOperationException。3.2 Transform类骨架与生命周期钩子实现首先创建SecureMethodTransform类继承Transformpublic class SecureMethodTransform extends Transform { Override public String getName() { return secureMethod; } Override public SetQualifiedContent.ContentType getInputTypes() { // 声明只处理CLASS类型的输入.class文件 return ImmutableSet.of(QualifiedContent.DefaultContentType.CLASSES); } Override public Set? super QualifiedContent.Scope getScopes() { // 声明作用域项目源码、本地依赖、远程依赖 return ImmutableSet.of( QualifiedContent.Scope.PROJECT, QualifiedContent.Scope.SUB_PROJECTS, QualifiedContent.Scope.EXTERNAL_LIBRARIES ); } Override public boolean isIncremental() { // 启用增量构建大幅提升大型项目构建速度 return true; } }关键点解析getInputTypes()返回CLASSES而非RESOURCES因我们只操作字节码getScopes()必须包含EXTERNAL_LIBRARIES否则无法处理aar/jar中的类如第三方SDK的支付类isIncremental()设为true后AGP会传入TransformInvocation的isIncremental()为true此时inputs中changedFiles字段会列出本次变更的文件我们可跳过未修改的类。3.3 核心字节码注入逻辑详解transform()方法是主入口需处理两类输入DirectoryInput源码编译的.class目录和JarInput依赖库的jar/aarOverride public void transform(NonNull TransformInvocation invocation) throws TransformException, InterruptedException, IOException { super.transform(invocation); // 1. 获取输出路径AGP会自动创建 TransformOutputProvider outputProvider invocation.getOutputProvider(); if (outputProvider null) return; // 2. 处理DirectoryInput项目源码 for (DirectoryInput directoryInput : invocation.getInputs().getDirectoryInputs()) { File inputDir directoryInput.getFile(); File outputDir outputProvider.getContentLocation( directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY ); // 增量模式下只处理变更文件 MapFile, Status changedFiles directoryInput.getChangedFiles(); if (invocation.isIncremental() !changedFiles.isEmpty()) { for (Map.EntryFile, Status entry : changedFiles.entrySet()) { File inputFile entry.getKey(); Status status entry.getValue(); if (status Status.REMOVED) { // 删除对应输出文件 File outputFile new File(outputDir, FileUtils.relativePath(inputDir, inputFile)); FileUtils.delete(outputFile); } else if (status Status.ADDED || status Status.CHANGED) { processClassFile(inputFile, new File(outputDir, FileUtils.relativePath(inputDir, inputFile))); } } } else { // 全量模式遍历整个目录 FileUtils.copyDirectory(inputDir, outputDir); // 对outputDir中所有.class文件处理 Files.walk(outputDir.toPath()) .filter(path - path.toString().endsWith(.class)) .forEach(path - { try { processClassFile(path.toFile(), path.toFile()); } catch (IOException e) { throw new RuntimeException(e); } }); } } // 3. 处理JarInput依赖库 for (JarInput jarInput : invocation.getInputs().getJarInputs()) { File inputJar jarInput.getFile(); File outputJar outputProvider.getContentLocation( jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR ); if (invocation.isIncremental()) { // Jar增量处理较复杂通常全量处理更稳妥 processJarFile(inputJar, outputJar); } else { processJarFile(inputJar, outputJar); } } }核心逻辑在processClassFile()中使用ASM重写字节码private void processClassFile(File inputFile, File outputFile) throws IOException { ClassReader reader new ClassReader(Files.readAllBytes(inputFile.toPath())); ClassWriter writer new ClassWriter(ClassWriter.COMPUTE_FRAMES); SecureMethodClassVisitor visitor new SecureMethodClassVisitor(writer); reader.accept(visitor, ClassReader.EXPAND_FRAMES); Files.write(outputFile.toPath(), writer.toByteArray()); } // ASM Visitor实现 public class SecureMethodClassVisitor extends ClassVisitor { private String currentClassName; public SecureMethodClassVisitor(ClassVisitor cv) { super(Opcodes.ASM9, cv); } Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.currentClassName name; super.visit(version, access, name, signature, superName, interfaces); } Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv super.visitMethod(access, name, descriptor, signature, exceptions); // 检查方法是否被SecureMethod注解 if (hasSecureMethodAnnotation(access, name, descriptor)) { return new SecureMethodMethodVisitor(mv, currentClassName, name, descriptor); } return mv; } private boolean hasSecureMethodAnnotation(int access, String name, String descriptor) { // 此处需解析类的注解实际需在visitAnnotation中收集 // 简化示例假设已知目标方法 return processPayment.equals(name) (Ljava/lang/String;)V.equals(descriptor); } } public class SecureMethodMethodVisitor extends AdviceAdapter { private final String className; private final String methodName; private final String methodDesc; public SecureMethodMethodVisitor(MethodVisitor mv, String className, String methodName, String methodDesc) { super(Opcodes.ASM9, mv, access, methodName, methodDesc); this.className className; this.methodName methodName; this.methodDesc methodDesc; } Override protected void onMethodEnter() { // 插入if (Reflection.getCallerClass() Class.class) throw new IllegalAccessException() mv.visitMethodInsn(INVOKESTATIC, sun/reflect/Reflection, getCallerClass, ()Ljava/lang/Class;, false); mv.visitLdcInsn(Type.getObjectType(java/lang/Class)); mv.visitMethodInsn(INVOKEVIRTUAL, java/lang/Object, equals, (Ljava/lang/Object;)Z, false); Label skip new Label(); mv.visitJumpInsn(IFNE, skip); // 如果不相等跳过 mv.visitTypeInsn(NEW, java/lang/IllegalAccessException); mv.visitInsn(DUP); mv.visitLdcInsn(Direct reflection access denied to className . methodName); mv.visitMethodInsn(INVOKESPECIAL, java/lang/IllegalAccessException, init, (Ljava/lang/String;)V, false); mv.visitInsn(ATHROW); mv.visitLabel(skip); } }提示实际项目中hasSecureMethodAnnotation需在ClassVisitor.visitAnnotation()中解析类注解并缓存到MapString, SetString中键为类名值为被注解的方法名集合。此处为简化展示核心逻辑。3.4 在build.gradle中注册与调试技巧在build.gradle中注册Transformandroid { // 必须启用Java 8字节码支持否则ASM 9无法处理 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget 1.8 } } // 注册TransformAGP 7.0写法 androidComponents { onVariants { variant - variant.artifacts.use(secureMethodTransform) .wiredTo(variant.artifacts.get(ARTIFACT_CLASSES_JAR)) .toAppendTo(variant.artifacts.get(ARTIFACT_CLASSES_JAR)) } }但更推荐AGP 4.0-6.0的写法兼容性更好android.registerTransform(new SecureMethodTransform())调试关键技巧开启AGP调试日志在gradle.properties中添加android.debugtrue执行./gradlew assembleDebug --info | grep secureMethod可看到Transform执行日志验证字节码修改构建后用javap -c app/build/intermediates/transforms/secureMethod/debug/0/xx/yy/MyClass.class查看反编译代码确认getCallerClass()调用是否存在规避Kotlin空安全干扰Kotlin编译器会为Nullable参数生成Metadata注解需在ASM Visitor中忽略visitAnnotation(Lkotlin/Metadata;, true)否则可能因注解格式不匹配报错。4. 常见问题排查与生产环境避坑指南4.1 增量构建失效的三大元凶与根治方案问题现象根本原因解决方案修改一个.java文件Transform却处理了整个module的.classisIncremental()返回true但transform()中未正确处理changedFiles导致全量遍历严格检查DirectoryInput.getChangedFiles()和JarInput.getStatus()对Status.REMOVED执行删除对ADDED/CHANGED执行单文件处理UNCHANGED完全跳过增量构建后APK运行崩溃提示NoSuchMethodErrorTransform修改了类A但类B引用了A的新增方法而B未被标记为CHANGED导致B仍使用旧字节码在processClassFile()中若修改了类A需主动将所有引用A的类B加入待处理队列需解析ClassReader的getCommonSuperClass()和getInterfaces()第二次构建时Transform输出目录残留旧文件导致D8报Duplicate classoutputProvider.getContentLocation()返回的路径未清空AGP不会自动清理在transform()开头调用FileUtils.deleteDirectory(outputProvider.getContentLocation(...))我踩过的最深的坑是第三个某次发布紧急补丁只改了一个工具类但因输出目录未清空旧版Utils.class和新版共存D8合并时随机选取一个导致线上偶发NoClassDefFoundError。此后所有Transform都强制在transform()第一行执行outputProvider.getContentLocation(...).deleteOnExit()。4.2 ASM版本冲突与Kotlin兼容性雷区AGP内置ASM版本随版本升级AGP 4.2 → ASM 7.0AGP 7.0 → ASM 8.0AGP 8.0 → ASM 9.4若在build.gradle中显式声明implementation org.ow2.asm:asm:9.4会导致ClassCastExceptionClassWriter被多个ClassLoader加载。正确做法是不声明ASM依赖直接使用AGP提供的版本。可通过TransformInvocation.getContext().getProject().getDependencies().getModules()查看当前ASM版本。Kotlin兼容性方面需特别注意内联函数Kotlin编译器会将inline fun展开为字节码但ASM 8需设置ClassReader.EXPAND_FRAMES否则visitFrame()会抛UnsupportedOperationException挂起函数suspend fun会被编译为Continuation参数的方法其descriptor形如(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;在visitMethod()中需特殊识别避免对Continuation参数做非法操作数据类copy()Kotlin为data class生成的copy()方法其字节码含INVOKEDYNAMIC指令ASM 9.4需用ClassWriter.COMPUTE_FRAMES否则visitInvokeDynamicInsn()会失败。4.3 生产环境性能优化与内存泄漏防护Transform在大型项目中易成构建瓶颈。实测一个含500个模块的项目未优化的Transform单次执行耗时超2分钟。优化手段包括并行处理transform()中对DirectoryInput和JarInput分别用ForkJoinPool.commonPool().submit()并行处理但需确保outputProvider线程安全AGP 7.0已保证字节码缓存对未被SecureMethod注解的类直接Files.copy(inputFile.toPath(), outputFile.toPath())跳过ASM解析内存泄漏防护ASMClassReader和ClassWriter会持有大量byte[]需在processClassFile()末尾显式置空reader和writer引用并调用System.gc()仅调试期生产环境移除。最关键的防护是避免在Transform中创建静态Map缓存类信息。曾有个团队为加速注解查找将所有类名存入static MapString, Boolean结果导致Gradle Daemon内存持续增长每构建一次增加50MB3次后OOM。正确做法是将缓存声明为transform()方法内的局部变量随方法结束自动回收。4.4 安全加固类Transform的合规性审查清单若Transform用于金融、医疗等强监管领域需通过以下审查字节码可逆性确保注入的代码不改变原有方法的throws声明、return类型、synchronized语义否则违反JVM规范调试符号完整性修改字节码后需调用ClassWriter.visitSource()保留原.java文件名和行号表否则ProGuard mapping无法正确映射第三方库兼容性对OkHttp、Retrofit等主流库需在processJarFile()中添加白名单跳过其核心类如OkHttpClient避免因字节码修改导致HTTP连接池异常构建产物签名一致性Transform输出的.class文件其hashCode()必须与输入文件一致若未修改否则影响APK签名验证。可在processClassFile()末尾添加assert Arrays.equals(Files.readAllBytes(inputFile.toPath()), Files.readAllBytes(outputFile.toPath()))。最后分享一个血泪教训某次上线前我们为SecureMethod增加了运行时校验检查调用栈是否含java.lang.reflect结果因Thread.currentThread().getStackTrace()在Android 8.0上返回空数组导致所有支付功能不可用。此后所有安全类Transform都遵循“最小侵入”原则只做静态字节码注入不依赖任何运行时API。5. 超越Transform当构建需求升级时的平滑演进路径5.1 从Transform到AGP 8.0 Artifacts API的迁移策略当项目升级到AGP 8.0且需求相对简单如资源过滤、Manifest合并时应逐步迁移到Artifacts API。迁移非一蹴而就建议分三步走并行双跑阶段在androidComponents中注册Artifacts任务同时保留Transform用BuildConfig.DEBUG控制开关对比两者输出的APK SHA256值确保功能一致渐进替换阶段对非核心Transform如日志开关注入直接用androidComponents.finalizeBundle替代对核心Transform如字节码加固先用Artifacts API处理CLASSES_JAR再用Transform处理剩余场景完全切换阶段当所有Transform功能均被Artifacts API覆盖且经过灰度验证后移除Transform注册代码。关键差异点Artifacts API的FinalizeBundle在D8之后执行适合处理.dex文件而Transform在D8之前适合.class处理Artifacts API通过ArtifactType抽象更易维护但灵活性低于Transform如无法在方法内插入条件判断。5.2 与R8/Shrinker的协同工作模式Transform与R8并非互斥而是上下游关系。典型协同场景Transform前置注入在Transform中为所有网络请求方法添加Keep注解确保R8不移除R8后置处理R8输出mapping.txt后用TransformManager注册一个MappingConsumer读取mapping并生成混淆后的方法调用图谱双向校验Transform注入的加固代码需在R8的keep.xml中声明其依赖的sun.reflect.Reflection类否则R8会移除该类导致NoClassDefFoundError。我所在团队的实践是将Transform生成的加固代码统一放在com.example.secure包下并在proguard-rules.pro中添加-keep class com.example.secure.** { *; }形成闭环。5.3 构建即服务BaaS场景下的Transform云化改造在CI/CD流水线中若需对不同客户定制化APK如白标App可将Transform封装为云服务输入客户上传的APK或源码zip、定制化规则JSON如{secureMethods: [com.pay.Payment.process]}处理云服务启动Docker容器挂载AGP环境执行Transform并输出定制APK输出返回APK下载链接及SHA256校验值。此模式要求Transform完全无状态所有配置通过参数传入且需处理并发构建的资源隔离如为每个构建分配独立outputProvider路径。我们用UUID.randomUUID().toString()生成临时路径避免冲突。我个人在实际操作中发现Transform的生命力远超官方文档描述。它不是被替代的技术而是随着构建系统演进不断变换形态的底层能力。当你需要在.class和.dex之间那个毫秒级的窗口里做一件只有字节码能做的事时它依然是你手中最锋利的那把刀。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号