学习Android(十三)JAR和AAR

1. 简介

在 Android 开发中,JAR(Java Archive)和 AAR(Android Archive)是两种常见的打包格式,用于封装和复用代码库。

2. JAR 与 AAR 的区别与用途

  • JAR (Java Archive)

    • 用途:适用于纯 Java/Kotlin 代码库的打包。

    • 内容:包含编译后的 .class 文件和可选的 META-INF / MANIFEST.MF 文件。

    • 特点:

      • 不包含 Android 资源文件(如布局、图片等)。

      • 不能包含 AndroidManifes.xml

      • 适用于平台无关的通用逻辑库。

    • 使用场景:

      • 封装通用的工具类(如加密、网络请求封装等)。

      • 在多个 Java 或 Android 项目中共享通用逻辑。

  • AAR (Android Archive)

    • 用途:专为 Android 平台设计,适用于包含资源和组件的库。

    • 内容:包含编译后的 .class 文件、AndroidManifest.xml 、资源文件(如布局、图片—)等。

    • 特点:

      • 可以包含 Android 资源和清单文件。

      • 支持包含本地库(.so 文件)。

      • 适用于封装具有 UI 组件的库。

    • 使用场景:

      • 封装自定义控件、布局、主题等可视化组件。

      • 在多个 Android 项目中共享完整的功能模块。

特性JAR包AAR包
包含内容仅包含.class文件包含.class、资源文件、清单等
是否包含资源文件
是否包含清单文件是(AndroidManifest.xml
使用场景通用工具类库Android UI 组件、资源库
适用平台Java 平台Android 平台

简单来说,工具类相关的封装到 JAR 包中,自定义 View、布局相关的封装到的 AAR 包中。

3. 创建 JAR 包

  • 创建 Java or Kotlin Library 模块

    • 在 Andorid Studio 中,选择 File -> New -> New Module

      在这里插入图片描述

    • 选择 Java or Kotlin Library ,填写模块名称等信息,点击 Finish

      在这里插入图片描述

  • 编写代码

    我们在创建的 utils 模块中创建我们的工具类代码,例如我们写一个对文件处理的通用工具类

    import java.io.*
    import java.nio.charset.Charset
    
    /**
     * 文件操作工具类
     */
    object FileUtils {
    
        // ======================== 文件/目录检查与创建 ========================
    
        /**
         * 检查文件是否存在
         * @param filePath 文件绝对路径
         * @return Boolean 是否存在
         */
        fun isFileExist(filePath: String): Boolean = File(filePath).exists()
    
        /**
         * 创建目录(若不存在)
         * @param dirPath 目录路径
         * @return Boolean 是否成功创建或已存在
         */
        fun createDirIfNotExists(dirPath: String): Boolean {
            val dir = File(dirPath)
            return if (!dir.exists()) dir.mkdirs() else true
        }
    
        /**
         * 创建空文件(若不存在)
         * @param filePath 文件路径
         * @return Boolean 是否成功创建或已存在
         */
        fun createFileIfNotExists(filePath: String): Boolean {
            val file = File(filePath)
            if (file.exists()) return true
            return try {
                file.parentFile?.mkdirs() // 创建父目录
                file.createNewFile()
            } catch (e: IOException) {
                e.printStackTrace()
                false
            }
        }
    
        // ======================== 文件读写 ========================
    
        /**
         * 将字符串写入文件(覆盖模式)
         * @param filePath 文件路径
         * @param content 字符串内容
         * @param charset 字符集(默认UTF-8)
         * @return Boolean 是否成功
         */
        @JvmOverloads
        fun writeStringToFile(
            filePath: String,
            content: String,
            charset: Charset = Charsets.UTF_8
        ): Boolean {
            return try {
                File(filePath).writeText(content, charset)
                true
            } catch (e: IOException) {
                e.printStackTrace()
                false
            }
        }
    
        /**
         * 从文件读取字符串内容
         * @param filePath 文件路径
         * @param charset 字符集(默认UTF-8)
         * @return String? 内容或null
         */
        @JvmOverloads
        fun readFileToString(filePath: String, charset: Charset = Charsets.UTF_8): String? {
            return try {
                File(filePath).readText(charset)
            } catch (e: IOException) {
                e.printStackTrace()
                null
            }
        }
    
        // ======================== 文件操作(复制、删除、移动) ========================
    
        /**
         * 复制文件(使用NIO高效复制)
         * @param srcPath 源文件路径
         * @param destPath 目标文件路径
         * @return Boolean 是否成功
         */
        fun copyFile(srcPath: String, destPath: String): Boolean {
            return try {
                FileInputStream(File(srcPath)).channel.use { srcChannel ->
                    FileOutputStream(File(destPath)).channel.use { destChannel ->
                        srcChannel.transferTo(0, srcChannel.size(), destChannel)
                        true
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
                false
            }
        }
    
        /**
         * 删除文件或目录(递归删除)
         * @param path 文件/目录路径
         * @return Boolean 是否成功
         */
        fun deleteRecursively(path: String): Boolean {
            val file = File(path)
            return if (file.isDirectory) {
                file.listFiles()?.forEach { deleteRecursively(it.absolutePath) }
                file.delete()
            } else {
                file.delete()
            }
        }
    
        /**
         * 重命名文件/目录
         * @param srcPath 原路径
         * @param newName 新名称(不含路径)
         * @return Boolean 是否成功
         */
        fun renameFile(srcPath: String, newName: String): Boolean {
            val srcFile = File(srcPath)
            val destFile = File(srcFile.parent, newName)
            return srcFile.renameTo(destFile)
        }
    
        // ======================== 文件属性获取 ========================
    
        /**
         * 获取文件大小(字节)
         * @param filePath 文件路径
         * @return Long 文件大小(-1表示失败)
         */
        fun getFileSize(filePath: String): Long {
            return try {
                File(filePath).length()
            } catch (e: SecurityException) {
                -1
            }
        }
    
    }
    
  • 构建 JAR 包

    • 我们点击打开右侧的 Gradle 工具栏,打开我们的 JAR 模块,展开 Tasks > build,双击 build 任务。

      在这里插入图片描述

      注意: 如果我们右侧的 Gradle 中没有 build,我们要在 Settings -> Experimental 中开启

      在这里插入图片描述

      然后再重新执行 Sync Project with Gradle File ,就会发现我们右侧的 Gradle 中就出现了 build。

    • 构建完成后,JAR 文件位于:

      • <project_root>/<module_name>/build/libs/

        在这里插入图片描述

  • 使用 JAR 包

    • 我们在自己的 app 模块中,创建一个 libs 文件夹,将我们构建 JAR 包放入其中

      在这里插入图片描述

    • 在项目级的 build.gradle.kts 中添加本地 JAR 包依赖

      implementation(files("/libs/utils.jar"))
      

      然后我们就可以在这个模块中使用我们 JAR 包提供的工具类了。

4. 创建 AAR 包

  • 创建 Android Library 模块

    • 在 Andorid Studio 中,选择 File -> New -> New Module

    • 选择 Android Library,填写模块名称等信息,点击 Finish

      在这里插入图片描述

  • 编写代码

    我们在创建的 components 模块中创建我们自定义的组件,例如我们写一个圆形 ImageView

    • 创建 res -> values -> attrs.xml

      <?xml version="1.0" encoding="utf-8"?>
            <resources>
                <declare-styleable name="CircleImageView">
                    <attr name="borderWidth" format="dimension"/>
                    <attr name="borderColor" format="color"/>
                    <attr name="forceSquare" format="boolean"/>
                </declare-styleable>
            </resources>
      
    • 创建 CircleImageView

      import android.content.Context
      import android.graphics.*
      import android.util.AttributeSet
      import androidx.appcompat.widget.AppCompatImageView
      import androidx.core.graphics.drawable.toBitmap
      
      class CircleImageView @JvmOverloads constructor(
          context: Context,
          attrs: AttributeSet? = null,
          defStyleAttr: Int = 0
      ) : AppCompatImageView(context, attrs, defStyleAttr) {
      
          // 画笔配置
          private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
          private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG)
          private val shaderMatrix = Matrix()
      
          // 绘制区域
          private var drawRect = RectF()
          private var borderRect = RectF()
      
          // 属性配置
          private var borderWidth = 0f
          private var borderColor = Color.WHITE
          private var isSquare = false // 是否强制为正方形
      
          init {
              // 从XML属性读取配置
              val ta = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView)
              borderWidth = ta.getDimension(R.styleable.CircleImageView_borderWidth, 0f)
              borderColor = ta.getColor(R.styleable.CircleImageView_borderColor, Color.WHITE)
              isSquare = ta.getBoolean(R.styleable.CircleImageView_forceSquare, false)
              ta.recycle()
      
              // 初始化画笔
              borderPaint.style = Paint.Style.STROKE
              borderPaint.color = borderColor
              borderPaint.strokeWidth = borderWidth
      
              // 默认缩放类型为居中裁剪
              scaleType = ScaleType.CENTER_CROP
          }
      
          override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
              super.onSizeChanged(w, h, oldw, oldh)
              updateDimensions()
          }
      
          private fun updateDimensions() {
              val width = if (isSquare) width.coerceAtMost(height) else width
              val height = if (isSquare) height.coerceAtMost(width) else height
      
              val size = width.coerceAtMost(height) - borderWidth * 2
              val halfBorder = borderWidth / 2
      
              // 图片绘制区域
              drawRect.set(
                  halfBorder,
                  halfBorder,
                  size + halfBorder,
                  size + halfBorder
              )
      
              // 边框绘制区域
              borderRect.set(
                  halfBorder,
                  halfBorder,
                  size + halfBorder,
                  size + halfBorder
              )
      
              // 更新着色器矩阵
              updateShaderMatrix()
          }
      
          private fun updateShaderMatrix() {
              val bitmap = (drawable?.toBitmap()) ?: return
              val scale: Float
              var dx = 0f
              var dy = 0f
      
              val bitmapWidth = bitmap.width.toFloat()
              val bitmapHeight = bitmap.height.toFloat()
      
              when (scaleType) {
                  ScaleType.CENTER_CROP -> {
                      val widthScale = drawRect.width() / bitmapWidth
                      val heightScale = drawRect.height() / bitmapHeight
                      scale = widthScale.coerceAtLeast(heightScale)
      
                      dx = (drawRect.width() - bitmapWidth * scale) * 0.5f
                      dy = (drawRect.height() - bitmapHeight * scale) * 0.5f
                  }
                  // 其他缩放类型可根据需要添加
                  else -> {
                      scale = 1f
                  }
              }
      
              shaderMatrix.setScale(scale, scale)
              shaderMatrix.postTranslate(dx + drawRect.left, dy + drawRect.top)
      
              val shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
              shader.setLocalMatrix(shaderMatrix)
              paint.shader = shader
          }
      
          override fun onDraw(canvas: Canvas) {
              if (drawable == null) {
                  super.onDraw(canvas)
                  return
              }
      
              // 绘制圆形图片
              canvas.drawOval(drawRect, paint)
      
              // 绘制边框
              if (borderWidth > 0) {
                  canvas.drawOval(borderRect, borderPaint)
              }
          }
      
          // 属性设置方法
          fun setBorderWidth(width: Float) {
              borderWidth = width
              borderPaint.strokeWidth = width
              updateDimensions()
              invalidate()
          }
      
          fun setBorderColor(color: Int) {
              borderColor = color
              borderPaint.color = color
              invalidate()
          }
      }
      
  • 构建 AAR 包

    • 在右侧的 Gradle 面板中,找到 components 模块。

    • 展开 Tasks > build,双击 build 任务。

    • 构建完成后,AAR 文件位于:

      • <project_root>/<module_name>/build/outputs/aar/

        在这里插入图片描述

  • 使用 AAR 包

    • 将我们构建的 AAR 包放到项目的 libs 下

    • 在项目级的 build.gradle.kts 中添加本地 AAR 包依赖

       implementation(files("/libs/components-debug.aar"))
      
    • 在 xml 中直接使用,效果如下

      在这里插入图片描述

  • 将 AAR 包配置在本地 Maven 仓库

    • 在 AAR 模块中的 build.gradle.kts 中进行

      plugins {
          alias(libs.plugins.android.library)
          alias(libs.plugins.kotlin.android)
          id("maven-publish") // 引入用于发布到 Maven 仓库的插件
      }
      
      configure<PublishingExtension> {
          repositories {
              mavenLocal() // 指定发布目标为本地 Maven 仓库 (~/.m2/repository)
          }
      }
      
      afterEvaluate {
          extensions.configure<PublishingExtension>("publishing") {
              publications {
                  create<MavenPublication>("release") {  // 创建名为 release 的发布配置(与 release 构建变体关联)
                      groupId = "com.example" // 组织 ID,发布后的依赖路径一部分
                      artifactId = "components" // 组件 ID,依赖路径一部分
                      version = "v1.0.0" // 版本号
      
                      from(components["release"]) // 从 release 构建变体中发布 AAR 包
                  }
              }
          }
      }
      
      android {
          namespace = "com.example.components"
          compileSdk = 35
      
          defaultConfig {
              minSdk = 21
      
              testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
              consumerProguardFiles("consumer-rules.pro")
          }
      
          buildTypes {
              release {
                  isMinifyEnabled = false
                  proguardFiles(
                      getDefaultProguardFile("proguard-android-optimize.txt"),
                      "proguard-rules.pro"
                  )
              }
          }
          compileOptions {
              sourceCompatibility = JavaVersion.VERSION_11
              targetCompatibility = JavaVersion.VERSION_11
          }
          kotlinOptions {
              jvmTarget = "11"
          }
      }
      
      dependencies {
      
          implementation(libs.androidx.core.ktx)
          implementation(libs.androidx.appcompat)
          implementation(libs.material)
          testImplementation(libs.junit)
          androidTestImplementation(libs.androidx.junit)
          androidTestImplementation(libs.androidx.espresso.core)
      }
      

      编译之后,我们在

      • 在 macOS 上:/Users/<用户名>/.m2/repository

      • 在 Linux 上:/home/<用户名>/.m2/repository

      • 在 Windows 上:C:\Users<用户名>.m2\repository

      查看是否生成成功,

      接着我们在全局的 settings.gradle.kts 中添加 mavenLocal() ,然后我们需要使用的项目中添加依赖

      implementation("com.example:components:v1.0.0")
      

      运行结果和添加 AAR 包一样。

5. JAR 和 AAR 的面试题

  • JAR 和 AAR 的主要区别是什么?

    答:

    • JAR(Java Archive):主要用于打包 Java 类文件(.class)、资源文件(如 .properties)和 MANIFEST.MF 文件,不包含 Android 资源(如布局、图片等)或 AndroidManifest.xml

    • AAR(Android Archive):专为 Android 开发设计,除了包含 .class 文件外,还包括资源文件(如 .xml.png)、AndroidManifest.xmlresources.arsc 和可能的本地库(.so 文件)。

    总结: JAR 适用于纯 Java 库,而 AAR 适用于包含 Android 组件和资源的库。

  • 在什么情况下选择使用 JAR 或AAR ?

    答:

    • 使用 JAR 的情况:

      • 库仅包含 Java 代码,无需 Android 资源或组件。

      • 需要跨平台使用,如在 Java SE 或其他非 Android 环境中。

    • 使用 AAR 的情况:

      • 库包含 Android 资源(如布局、图片)或组件(如自定义 View、Service)。

      • 需要在 Android 项目中完整集成库的功能。

  • 如何在 Android Studio 中生成 JAR 和 AAR 文件?

    答:

    • 生成 JAR 文件:

      1. build.gradle 中应用 java-library 插件。

      2. 运行 ./gradlew build,生成的 JAR 文件位于 build/libs/ 目录下。

    • 生成 AAR 文件:

      1. build.gradle 中应用 com.android.library 插件。

      2. 运行 ./gradlew assembleRelease,生成的 AAR 文件位于 build/outputs/aar/ 目录下。

  • 如何在项目中引入 JAR 和 AAR 文件?

    答:

    • 引入 JAR 文件:

      • 将 JAR 文件放入项目的 libs/ 目录。

      • build.gradle 中添加依赖

    • 引入 AAR 文件:

      • 将 AAR 文件放入项目的 libs/ 目录。

      • build.gradle 中添加依赖

  • AAR 文件包含哪些内容?

    答:AAR 文件通常包含:

    • classes.jar:编译后的 Java 类文件。

    • AndroidManifest.xml:库的清单文件。

    • 资源文件夹(如 res/):布局、图片等资源。

    • resources.arsc:编译后的资源索引。

    • 本地库(如 jni/):.so 文件。

  • 使用 JAR 和 AAR 各有哪些优缺点?

    答:

    • JAR 的优点:

      • 结构简单,易于管理。

      • 适用于纯 Java 项目或跨平台使用。

    • JAR 的缺点:

      • 不支持 Android 资源和组件。

      • 无法包含 AndroidManifest.xml

    • AAR 的优点:

      • 支持完整的 Android 资源和组件。

      • 适用于模块化的 Android 项目。

    • AAR 的缺点:

      • 结构复杂,体积较大。

      • 仅适用于 Android 项目。

  • 如何将 AAR 文件发布到 Maven 仓库?

    答:可以使用 Gradle 的 maven-publish 插件,将 AAR 文件发布到本地或远程 Maven 仓库。配置示例:

    plugins {
        alias(libs.plugins.android.library)
        alias(libs.plugins.kotlin.android)
        id("maven-publish") // 引入用于发布到 Maven 仓库的插件
    }
    
    configure<PublishingExtension> {
        repositories {
            mavenLocal() // 指定发布目标为本地 Maven 仓库 (~/.m2/repository)
        }
    }
    
    afterEvaluate {
        extensions.configure<PublishingExtension>("publishing") {
            publications {
                create<MavenPublication>("release") {  // 创建名为 release 的发布配置(与 release 构建变体关联)
                    groupId = "com.example" // 组织 ID,发布后的依赖路径一部分
                    artifactId = "components" // 组件 ID,依赖路径一部分
                    version = "v1.0.0" // 版本号
    
                    from(components["release"]) // 从 release 构建变体中发布 AAR 包
                }
            }
        }
    }
    
  • 如何在多模块项目中管理 JAR 和 AAR 依赖?

    答:

    • 将公共模块打包为 AAR,供其他模块引用。

    • 使用 Gradle 的 implementationapi 关键字管理依赖关系。

    • 通过设置统一的版本号和依赖路径,确保依赖的一致性。

  • JAR 和 AAR 的兼容性如何?

    答:

    • JAR 文件可在任何支持 Java 的环境中使用,包括 Android。

    • AAR 文件仅适用于 Android 项目,因其包含 Android 特有的资源和组件。

  • 如何查看 AAR 文件的内容?

    答:AAR 文件本质上是一个 ZIP 压缩包,可以通过解压查看其内容:

    解压后,可以看到 classes.jarAndroidManifest.xmlres/ 等文件夹和文件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值