Android Runtime编译优化深度解析(90)

Android Runtime编译优化深度解析

一、Android Runtime概述

Android Runtime(ART)是Android系统中用于执行应用程序字节码的运行环境,在Android 5.0(Lollipop)版本开始取代Dalvik成为默认运行时。ART采用AOT(Ahead-of-Time)和JIT(Just-in-Time)相结合的编译策略,相比Dalvik的JIT模式,显著提升了应用启动速度和执行效率。

从源码角度看,ART的核心代码位于Android源码的art目录下。art/runtime目录包含了运行时的核心实现,如对象管理、垃圾回收、线程模型等;art/compiler目录则是编译优化的关键所在,其中定义了编译器的各个阶段和优化策略。ART的设计目标是在保证兼容性的前提下,最大限度地提升性能,同时降低内存占用和功耗。

ART的基本工作流程是:在应用安装时,通过AOT编译将字节码编译成机器码存储在设备中;在运行时,JIT编译器动态监控热点代码,对其进行即时编译优化。此外,ART还引入了Profile-guided compilation(基于配置文件的编译),通过收集应用运行时的行为数据,指导编译器进行更精准的优化。

二、ART编译架构

ART的编译架构采用模块化设计,主要分为前端(Front End)、优化阶段(Optimization Passes)和后端(Back End)三个部分。

2.1 前端

前端负责将Dalvik字节码或Java字节码解析为中间表示(Intermediate Representation,IR)。在art/compiler/optimizing/ir_builder.cc文件中,可以看到IR构建的核心代码。例如,IrBuilder::BuildInstruction函数负责将字节码指令转换为对应的IR节点:

cpp 复制代码
void IrBuilder::BuildInstruction(const Instruction* insn) {
  // 根据指令类型创建对应的IR节点
  switch (insn->op_) {
    case Instruction::kInvokeVirtual:
      // 处理虚方法调用指令
      BuildInvoke(insn, true /* is_virtual */);
      break;
    case Instruction::kReturn:
      // 处理返回指令
      BuildReturn(insn);
      break;
    // 其他指令类型的处理
    // ...
  }
}

前端还负责进行一些基础的语义检查和类型推导,确保生成的IR准确反映字节码的语义。

2.2 优化阶段

优化阶段是ART编译的核心,包含一系列优化Pass,如常量折叠、死代码消除、循环优化等。以常量折叠为例,在art/compiler/optimizing/constant_folding_pass.cc中实现了ConstantFoldingPass类:

cpp 复制代码
class ConstantFoldingPass : public HOptimizationPass {
public:
  ConstantFoldingPass(CompilerDriver* driver,
                      OptimizingCompilerStats* stats)
      : HOptimizationPass(driver, stats, "constant-folding") {}

  void Run(HGraph* graph) override {
    // 遍历IR图中的所有节点
    for (HInstructionIterator it(graph); !it.Done(); it.Advance()) {
      HInstruction* insn = it.Current();
      if (insn->IsConstantFoldable()) {
        // 如果节点可进行常量折叠
        HConstant* result = insn->ConstantFold();
        if (result != nullptr) {
          // 用折叠后的常量替换原节点
          insn->ReplaceWith(result);
        }
      }
    }
  }
};

这些优化Pass会反复执行,逐步简化和优化IR,提升代码执行效率。

2.3 后端

后端负责将优化后的IR转换为目标平台的机器码。在art/compiler/backend目录下,可以找到不同架构(如ARM、x86)的后端实现。以ARM后端为例,arm64_compiler.cc中定义了Arm64Compiler类,其核心函数GenerateCode负责生成机器码:

cpp 复制代码
void Arm64Compiler::GenerateCode(HGraph* graph,
                                 CompilationUnit* unit,
                                 Code* code) {
  // 初始化代码生成器
  Arm64InstructionBuilder builder(unit, code);
  // 遍历IR图,生成对应的机器码指令
  for (HInstructionIterator it(graph); !it.Done(); it.Advance()) {
    HInstruction* insn = it.Current();
    insn->GenerateCode(&builder);
  }
  // 完成代码生成并进行收尾工作
  builder.Finish();
}

后端还会根据目标架构的特性进行针对性优化,如利用SIMD指令提升数据处理速度。

三、AOT编译原理

AOT编译是ART在应用安装阶段执行的编译过程,目的是将字节码提前编译成机器码,减少运行时的编译开销,加快应用启动速度。

3.1 AOT编译触发机制

在Android系统中,AOT编译由dex2oat工具触发。dex2oat工具的入口函数位于art/tools/dex2oat/dex2oat.cc

cpp 复制代码
int main(int argc, char** argv) {
  // 解析命令行参数
  CommandLineFlags::SetFlagsFromCommandLine(&argc, argv, true /* remove_flags */);
  // 执行AOT编译
  if (!Dex2Oat().Run()) {
    return 1;
  }
  return 0;
}

Dex2Oat::Run函数会读取APK中的DEX文件,调用ART的编译接口进行编译。在编译过程中,会根据设备的CPU架构选择对应的后端进行机器码生成。

3.2 AOT编译流程

AOT编译的主要流程包括:

  1. DEX文件解析 :在art/dex/dex_file.cc中,DexFile::Open函数负责打开DEX文件并解析其内容,将字节码加载到内存中。
  2. IR构建:与前端流程相同,将字节码转换为IR,为后续优化做准备。
  3. 优化与代码生成 :执行一系列优化Pass,并通过后端生成机器码。生成的机器码会存储在设备的/data/dalvik-cache目录下,以.odex文件形式存在。

3.3 AOT编译的优缺点

优点:

  • 加快应用启动速度,减少首次运行时的编译延迟。
  • 降低运行时的CPU占用,提升整体性能。

缺点:

  • 增加应用安装时间和存储空间占用。
  • 由于提前编译,无法针对运行时的具体情况进行动态优化。

四、JIT编译原理

JIT编译是在应用运行时动态进行的编译过程,主要针对热点代码(频繁执行的代码段)进行即时优化,弥补AOT编译的不足。

4.1 热点代码检测

ART通过CounterTable来实现热点代码检测。CounterTable记录了每个方法的调用次数、循环执行次数等信息。在art/runtime/counters.cc中,CounterTable::UpdateCounter函数负责更新计数器:

cpp 复制代码
void CounterTable::UpdateCounter(Thread* self,
                                 const DexFile::CodeItem* code_item,
                                 CounterId counter_id,
                                 int32_t increment) {
  // 获取对应的计数器
  Counter* counter = GetCounter(self, code_item, counter_id);
  // 更新计数器值
  counter->Add(increment);
  // 检查是否达到热点阈值
  if (counter->Value() >= kHotnessThreshold) {
    // 触发JIT编译
    JitCompile(self, code_item);
  }
}

当某个方法或代码段的计数器达到预设的热点阈值时,会触发JIT编译。

4.2 JIT编译流程

JIT编译的流程与AOT编译类似,但更注重快速响应:

  1. 代码片段提取:从运行时环境中提取热点代码对应的字节码。
  2. IR构建与优化:生成IR并执行部分关键优化Pass,如内联优化、常量传播等。
  3. 快速代码生成:后端生成机器码,并替换原解释执行的代码片段。

4.3 JIT编译的优势

JIT编译能够根据应用运行时的实际情况,对热点代码进行动态优化,弥补AOT编译无法适应运行时变化的缺陷。同时,JIT编译仅针对热点代码,避免了全量AOT编译带来的存储和时间开销。

五、内联优化

内联优化是ART编译优化中的重要手段,通过将被调用方法的代码直接嵌入到调用点,减少方法调用的开销。

5.1 内联决策

art/compiler/optimizing/inline_pass.cc中,InlinePass类负责内联决策。ShouldInline函数定义了内联的判断条件:

cpp 复制代码
bool InlinePass::ShouldInline(HInvoke* invoke,
                              const DexFile::MethodId* callee_method_id) {
  // 检查方法是否可内联(如非虚方法、方法体大小限制等)
  if (!invoke->IsStatic() &&!invoke->IsPrivate()) {
    return false;
  }
  if (GetMethodBytecodeSize(callee_method_id) > kMaxInlineSize) {
    return false;
  }
  return true;
}

除了方法的可见性和大小限制外,还会考虑方法的调用频率、是否为热点方法等因素。

5.2 内联过程

当决定内联时,InlinePass::Inline函数负责将被调用方法的代码嵌入到调用点:

cpp 复制代码
void InlinePass::Inline(HInvoke* invoke,
                        HGraph* graph,
                        HBasicBlock* block) {
  // 获取被调用方法的IR图
  HGraph* callee_graph = GetCalleeGraph(invoke);
  // 将被调用方法的代码插入到调用点
  for (HInstructionIterator it(callee_graph->GetEntryBlock());!it.Done(); it.Advance()) {
    HInstruction* insn = it.Current();
    HInstruction* cloned_insn = insn->Clone();
    block->InsertInstruction(cloned_insn);
  }
  // 处理参数传递和返回值
  // ...
  // 删除原调用指令
  invoke->Remove();
}

内联优化后,减少了方法调用的栈操作和跳转开销,提升了代码执行效率。

六、循环优化

循环是程序中常见的性能瓶颈,ART针对循环进行了多种优化,如循环展开、循环不变代码外提等。

6.1 循环不变代码外提

art/compiler/optimizing/loop_optimizations_pass.cc中,LoopInvariantCodeMotionPass类实现了循环不变代码外提:

cpp 复制代码
class LoopInvariantCodeMotionPass : public HOptimizationPass {
public:
  LoopInvariantCodeMotionPass(CompilerDriver* driver,
                              OptimizingCompilerStats* stats)
      : HOptimizationPass(driver, stats, "loop-invariant-code-motion") {}

  void Run(HGraph* graph) override {
    // 遍历所有循环
    for (HLoopInfo* loop_info : graph->GetLoopInfos()) {
      HBasicBlock* preheader = loop_info->GetPreheader();
      // 查找循环不变代码
      for (HInstructionIterator it(loop_info->GetLoopHeader());!it.Done(); it.Advance()) {
        HInstruction* insn = it.Current();
        if (insn->IsLoopInvariant()) {
          // 将不变代码移动到循环前置块
          preheader->InsertInstruction(insn);
        }
      }
    }
  }
};

通过将循环不变的计算移到循环外部,减少了重复计算,提升了循环执行效率。

6.2 循环展开

循环展开通过增加每次迭代处理的数据量,减少循环控制指令的开销。在art/compiler/optimizing/loop_unrolling_pass.cc中,LoopUnrollingPass类实现循环展开:

cpp 复制代码
class LoopUnrollingPass : public HOptimizationPass {
public:
  LoopUnrollingPass(CompilerDriver* driver,
                    OptimizingCompilerStats* stats)
      : HOptimizationPass(driver, stats, "loop-unrolling") {}

  void Run(HGraph* graph) override {
    // 遍历所有循环
    for (HLoopInfo* loop_info : graph->GetLoopInfos()) {
      // 计算展开因子
      int unroll_factor = CalculateUnrollFactor(loop_info);
      // 展开循环体
      UnrollLoop(loop_info, unroll_factor);
    }
  }
};

循环展开需要在减少控制开销和增加代码体积之间进行平衡,避免过度展开导致性能下降。

七、寄存器分配

寄存器分配是将IR中的临时变量映射到目标平台寄存器的过程,合理的寄存器分配可以减少内存访问开销,提升性能。

7.1 寄存器分配算法

ART采用图着色(Graph Coloring)算法进行寄存器分配。在art/compiler/backend/register_allocator.cc中,RegisterAllocator类实现了寄存器分配逻辑:

cpp 复制代码
class RegisterAllocator {
public:
  RegisterAllocator(InstructionSelector* selector,
                    RegisterSet* available_registers)
      : selector_(selector),
        available_registers_(available_registers) {}

  void AllocateRegisters() {
    // 构建冲突图(Conflict Graph)
    BuildConflictGraph();
    // 使用图着色算法分配寄存器
    ColorGraph();
    // 将分配结果应用到IR
    AssignRegistersToInstructions();
  }

private:
  // 构建冲突图
  void BuildConflictGraph() {
    // 遍历所有IR节点,确定变量的生命周期和冲突关系
    // ...
  }

  // 图着色算法
  void ColorGraph() {
    // 尝试为每个变量分配寄存器,解决冲突
    // ...
  }

  // 应用分配结果
  void AssignRegistersToInstructions() {
    // 将分配的寄存器写入IR节点
    // ...
  }
};

图着色算法通过为冲突图中的节点分配不同颜色(代表不同寄存器),确保在同一时间不会有两个冲突的变量使用相同的寄存器。

7.2 寄存器分配优化

为了进一步优化寄存器分配,ART还引入了寄存器溢出(Register Spilling)机制。当寄存器资源不足时,将部分变量临时存储到内存中,释放寄存器供其他变量使用。在art/compiler/backend/register_allocator.cc中,SpillRegisters函数负责处理寄存器溢出:

cpp 复制代码
void RegisterAllocator::SpillRegisters() {
  // 选择需要溢出的变量
  std::vector<HValue*> variables_to_spill = SelectVariablesToSpill();
  // 生成内存存储指令
  for (HValue* variable : variables_to_spill) {
    GenerateSpillCode(variable);
  }
}

寄存器分配和溢出策略的合理设计,对代码的执行效率有着重要影响。

八、垃圾回收与编译优化

ART的垃圾回收(Garbage Collection,GC)机制与编译优化紧密结合,通过减少不必要的内存管理开销,提升整体性能。

8.1 垃圾回收算法

ART采用分代垃圾回收算法,将对象分为年轻代(Young Generation)和老年代(Old Generation),分别采用不同的回收策略。在art/runtime/gc/heap.cc中,Heap类实现了垃圾回收的核心逻辑:

cpp 复制代码
class Heap {
public:
  void CollectGarbage(GcCause gc_cause) {
    // 根据垃圾回收原因选择回收策略
    if (gc_cause == kGcCauseAlloc || gc_cause == kGcCauseBackground) {
      // 进行年轻代回收
      CollectYoungGeneration();
    } else {
      // 进行全量回收
      CollectAllGenerations();
    }
  }

private:
  void CollectYoungGeneration() {
    // 使用复制算法(Copying Algorithm)回收年轻代
    // ...
  }

  void CollectAllGenerations() {
    // 使用标记-清除-整理(Mark-Sweep-Compact)算法回收老年代
    // ...
  }
};

分代回收算法能够根据对象的生命周期特性,高效地回收不再使用的对象,减少内存碎片化。

8.2 编译优化对垃圾回收的影响

编译优化可以通过减少对象创建、优化对象生命周期管理等方式,降低垃圾回收的频率和开销。例如,内联优化可以减少方法调用时临时对象的创建;循环优化可以通过减少循环体内的对象分配,降低年轻代的压力。

同时,ART的编译器会在生成代码时插入必要的垃圾回收安全点(Safe Points),确保在垃圾回收时能够正确暂停和恢复线程,保证内存状态的一致性。在art/runtime/interpreter/interpreter_common.cc中,解释器会在合适的位置插入安全点检查:

cpp 复制代码
void Interpreter::Execute() {
  // 执行字节码指令
  while (!IsDone()) {
    Instruction* insn = FetchInstruction();
    // 检查是否到达安全点
    if (IsSafePoint()) {
      CheckForGc();
    }
    ExecuteInstruction(insn);
  }
}

通过编译优化与垃圾回收的协同工作,ART能够在保证内存安全的前提下,提升应用的执行效率。

九、Profile-guided compilation

Profile-guided compilation(基于配置文件的编译)是ART的一项重要优化技术,通过收集应用运行时的行为数据,指导编译器进行更精准的优化。

9.1 数据收集

ART通过Profiler收集应用运行时的各种数据,如方法调用频率、循环执行次数、对象分配位置等。在`art

ART通过Profiler收集应用运行时的各种数据,如方法调用频率、循环执行次数、对象分配位置等。在art/runtime/profiler/profile_saver.cc中,ProfileSaver类负责将收集到的数据持久化存储。例如,在记录方法调用次数时:

cpp 复制代码
void ProfileSaver::RecordMethodInvocation(const DexFile::MethodId* method_id, uint32_t count) {
    // 将方法ID和调用次数关联存储
    method_invocation_counts_[method_id] += count;
}

这些数据会被定期写入设备存储,形成配置文件(Profile)。数据收集过程对应用性能影响较小,采用异步和轻量级的方式,避免给运行时带来过多负担 。

9.2 数据利用

在编译阶段,art/compiler/optimizing/profile_based_compilation_pass.cc中的ProfileBasedCompilationPass类读取配置文件数据,并将其应用到编译优化决策中。比如,根据方法调用频率进行更精准的内联决策:

cpp 复制代码
bool ProfileBasedCompilationPass::ShouldInlineBasedOnProfile(HInvoke* invoke, const DexFile::MethodId* callee_method_id) {
    // 从配置文件获取被调用方法的调用次数
    auto it = method_invocation_counts_.find(callee_method_id);
    if (it != method_invocation_counts_.end() && it->second > kHighInvocationThreshold) {
        // 调用次数高时,倾向于进行内联
        return true;
    }
    return false;
}

此外,还可以根据循环执行次数调整循环展开策略,根据对象分配热点优化内存布局等,使得编译器能针对应用实际运行特性进行优化。

9.3 动态更新与适应

随着应用运行,其行为模式可能发生变化,ART支持配置文件的动态更新。当检测到应用运行状态有显著改变(如进入不同功能模块),Profiler会重新收集数据并更新配置文件,确保编译优化始终贴合应用当前的运行特征,实现持续的性能提升。

十、指令集优化

10.1 不同架构的指令集适配

ART针对多种硬件架构进行优化,如ARM、x86、MIPS等。在art/compiler/backend目录下,不同架构的后端代码独立实现指令集相关优化。以ARM架构为例,在arm64_compiler.cc中,Arm64Compiler类会利用ARM64指令集的特性进行优化:

cpp 复制代码
void Arm64Compiler::GenerateVectorInstructions(HInstruction* insn) {
    // 检查是否可使用NEON指令集进行向量化操作
    if (insn->IsVectorizable()) {
        // 生成NEON指令,并行处理数据
        GenerateNeonInstructions(insn);
    }
}

对于x86架构,在x86_compiler.cc中,X86Compiler会利用SSE、AVX等指令集加速数据处理,通过适配不同架构的指令集,充分发挥硬件性能优势。

10.2 指令选择与调度

在代码生成阶段,art/compiler/backend/instruction_selector.cc中的InstructionSelector类负责从目标架构指令集中选择最合适的指令。例如,在选择算术运算指令时:

cpp 复制代码
void InstructionSelector::SelectArithmeticInstruction(HInstruction* insn) {
    // 根据操作数类型和目标架构特性选择指令
    if (insn->GetOpCode() == HInstruction::kAdd && insn->GetInput(0)->IsInteger() && insn->GetInput(1)->IsInteger()) {
        if (target_arch_ == kArm64) {
            // ARM64架构下选择ADD指令
            EmitArm64AddInstruction(insn);
        } else if (target_arch_ == kX86) {
            // x86架构下选择ADD指令
            EmitX86AddInstruction(insn);
        }
    }
}

同时,指令调度(Instruction Scheduling)会对生成的指令进行重新排序,通过减少指令间的数据依赖和流水线停顿,进一步提升执行效率,如在arm64_instruction_scheduler.cc中,Arm64InstructionScheduler类实现了ARM64架构下的指令调度逻辑。

十一、内存优化相关编译策略

11.1 对象布局优化

ART通过编译优化调整对象在内存中的布局,减少内存碎片化并提升访问效率。在art/runtime/gc/heap.cc中,对象布局的计算和调整涉及到多个因素。例如,对于类的成员变量布局,编译器会考虑数据类型和对齐要求:

cpp 复制代码
size_t Heap::CalculateObjectSize(const Class* clazz) {
    size_t size = kObjectHeaderSize;
    // 遍历类的成员变量
    for (const Field* field : clazz->GetInstanceFields()) {
        size_t field_size = GetFieldSize(field);
        // 计算对齐后的大小
        size = Align(size, GetFieldAlignment(field));
        size += field_size;
    }
    return size;
}

通过合理布局,减少内存空洞,使得对象占用空间更小,同时在访问成员变量时能更快速地定位。

11.2 内存访问优化

编译优化还会针对内存访问指令进行优化,减少缓存不命中(Cache Miss)。例如,在循环访问数组时,编译器会分析访问模式,尝试将连续访问的数据加载到缓存中。在art/compiler/optimizing/memory_access_optimization_pass.cc中的MemoryAccessOptimizationPass类会进行相关优化:

cpp 复制代码
void MemoryAccessOptimizationPass::OptimizeArrayAccess(HInstruction* insn) {
    if (insn->IsArrayLoad() || insn->IsArrayStore()) {
        // 分析数组访问的步长和连续性
        int access_stride = AnalyzeArrayAccessStride(insn);
        if (access_stride == 1) {
            // 连续访问时,添加预取指令
            EmitPrefetchInstruction(insn);
        }
    }
}

预取指令提前将数据加载到缓存,减少后续访问的等待时间,提升内存访问效率。

十二、多线程相关编译优化

12.1 线程同步优化

在多线程环境下,线程同步操作(如锁操作)可能带来性能开销。ART编译器会对同步代码进行优化,在art/runtime/monitor_android.cc中,对于synchronized关键字对应的锁操作,编译器会尝试进行锁粗化(Lock Coarsening)和锁消除(Lock Elimination)。例如,锁粗化逻辑:

cpp 复制代码
void MonitorAndroid::CoarsenLocks(Method* method) {
    // 分析方法中的锁操作序列
    std::vector<MonitorEnterExitPair> lock_pairs = AnalyzeLockPairs(method);
    for (const auto& pair : lock_pairs) {
        if (ShouldCoarsen(pair)) {
            // 将相邻的锁操作合并
            CombineLocks(pair);
        }
    }
}

通过减少锁的获取和释放次数,降低线程同步带来的开销。

12.2 并行执行优化

对于支持并行计算的代码段,ART编译器会生成支持多线程并行执行的代码。例如,在处理大规模数据计算时,编译器会将任务划分成多个子任务,利用多个线程并行处理。在art/compiler/optimizing/parallelization_pass.cc中的ParallelizationPass类负责相关优化:

cpp 复制代码
void ParallelizationPass::ParallelizeLoop(HLoopInfo* loop_info) {
    // 判断循环是否可并行化
    if (IsLoopParallelizable(loop_info)) {
        // 划分循环迭代任务
        PartitionLoopIterations(loop_info);
        // 生成多线程并行执行代码
        GenerateParallelCode(loop_info);
    }
}

通过并行执行,充分利用多核处理器的性能,加速任务处理。

十三、异常处理与编译优化

13.1 异常处理机制实现

ART的异常处理机制在art/runtime/exceptions.cc中有详细实现。当异常抛出时,系统需要快速定位到异常处理代码块。编译器在生成代码时,会插入异常表(Exception Table),记录每个代码块对应的异常处理逻辑:

cpp 复制代码
void Compiler::GenerateExceptionTable(Method* method) {
    // 遍历方法的基本代码块
    for (BasicBlock* block : method->GetBasicBlocks()) {
        for (ExceptionHandler* handler : block->GetExceptionHandlers()) {
            // 将异常处理信息添加到异常表
            exception_table_.push_back({block, handler->GetCatchBlock(), handler->GetExceptionType()});
        }
    }
}

异常表使得在异常发生时,能够高效地找到对应的处理逻辑。

13.2 异常处理相关的编译优化

编译器会对异常处理代码进行优化,减少正常路径(无异常发生时的代码执行路径)的性能开销。例如,在art/compiler/optimizing/exception_handling_optimization_pass.cc中的ExceptionHandlingOptimizationPass类会进行以下优化:

cpp 复制代码
void ExceptionHandlingOptimizationPass::OptimizeExceptionHandling(HGraph* graph) {
    // 分析异常处理代码块的可达性
    AnalyzeExceptionBlockReachability(graph);
    // 移除不可达的异常处理代码
    RemoveUnreachableExceptionHandlers(graph);
    // 合并重复的异常处理逻辑
    MergeDuplicateExceptionHandlers(graph);
}

通过这些优化,在保证异常处理正确性的同时,提升应用正常运行时的性能表现。

相关推荐
天天摸鱼的java工程师1 小时前
面试官说:“设计一个消息中间件你会怎么做?”我当场就不困了 ☕️🚀
java·后端·面试
七七&5562 小时前
Spring全面讲解(无比详细)
android·前端·后端
独立开阀者_FwtCoder2 小时前
Vue 抛弃虚拟 DOM,底层到底换成啥了?怎么更新 DOM?
前端·面试·github
熬了夜的程序员2 小时前
【华为机试】240. 搜索二维矩阵 II
线性代数·算法·华为·面试·矩阵·golang·深度优先
蒟蒻小袁3 小时前
力扣面试150题--搜索二维矩阵
leetcode·面试·矩阵
朱涛的自习室3 小时前
新一代 Agentic AI 智能体,助力 Android 开发 | Google I/O
android·android studio·ai编程
安卓开发者4 小时前
OkHttp 与 Glide 完美结合:打造高效的 Android 图片加载方案
android·okhttp·glide
安卓开发者4 小时前
OkHttp 与 Stetho 结合使用:打造强大的 Android 网络调试工具链
android·okhttp
过期动态4 小时前
MySQL中的排序和分页
android·java·数据库·mysql·adb
Littlewith4 小时前
Node.js:常用工具、GET/POST请求的写法、工具模块
java·服务器·开发语言·c++·面试·node.js