将APK转换为最小APK后如何解析?

How to parse an APK after converting it to a minimal one?

背景

我希望能够解析各种来源或各种类型的 APK 文件。我只想获得有关它们的非常具体的基本信息:

Android 框架只有一个函数来解析 APK 文件 (PackageManager.getPackageArchiveInfo),但它有 2 个缺点:

  1. 它需要一个文件路径。这并不总是可用,因为您可能需要处理 Uri,或者您的 APK 文件位于某个 ZIP 文件中。

  2. 它无法处理拆分的 APK 文件。只有其中一个基础。如果你在拆分 APK 文件上尝试它,你只会得到 null。

因此,我试图找到一个可靠的 Java&Kotlin 解决方案,可以轻松处理它们。

问题

我发现的每个解决方案都有其自身的缺点。有些库我什至找不到如何使用,有些库在没有真实文件路径的情况下无法解析 APK 文件。其中一些只是创建新文件,而不是 returning 你 Java/Kotlin 中的对象。有些甚至是其他编程语言,让我想知道是否可以使用它们。

到目前为止,我发现唯一一个足够好的是 "hsiafan" apk-parser,它只需要 APK 中的 2 个文件:清单文件和资源文件。它有一些问题,但通常它可以为您提供我提到的信息。

事情是,正如我所写,它并不总是有效,所以我想 return 了解基础知识,至少当我注意到它无法解析时,至少在它是普通 APK 的情况下拆分 APK 文件的基础。它不能很好处理的一些情况是:

  1. 有时无法正确获取应用名称 (here)。
  2. 某些资源以某种方式隐藏在某些应用程序中 (here)。即使是 Jadx 等工具也无法显示它们,但其他工具可以。
  3. 自适应图标难以解析,包括可绘制 XML 文件。虽然我成功解析了 VectorDrawable (here),但这是一个很好的解决方法。

还有其他一些。

我试过的

所以,我想再次尝试 Android 框架,但这次有了一个新想法:我可以根据当前的情况只提取它真正需要的内容,而不是处理整个原始 APK观察它。

例如,如果我知道我需要处理的资源,我可以只将它们(以及一些需要的关键文件)复制到一个新的 APK 中,然后丢弃其余的。这可以减少对 copy/download 大量数据的需求,只是为了解析 APK 并获取有关它的信息。例如,如果 APK 文件是 100MB,当我们只需要其中的一小部分时,就没有必要全部获取。

首先,我想看看如何创建一个可以解析的新 APK,所以现在我将原始 APK(当前应用的当前应用)的所有条目复制到一个新文件中,并选择解析它:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (savedInstanceState != null)
            return
        thread {
            val originalApplicationInfo = packageManager.getApplicationInfo(packageName, 0)
            val filePath = originalApplicationInfo.publicSourceDir
            val outputFile = File(cacheDir, "test.apk")
            outputFile.parentFile!!.mkdirs()
            outputFile.delete()
            ZipFile(filePath).use { zipFile ->
                ZipOutputStream(FileOutputStream(outputFile)).use { zipOutputStream ->
                    for (entry in zipFile.entries()) {
                        val name = entry.name
                        zipOutputStream.putNextEntry(ZipEntry(name))
                        zipFile.getInputStream(entry).use { it.copyTo(zipOutputStream.buffered()) }
                        zipOutputStream.closeEntry()
                    }
                }
            }
            val originalLabel = originalApplicationInfo.loadLabel(packageManager)
            val originalIcon: Drawable? = originalApplicationInfo.loadIcon(packageManager)
            Log.d("AppLog", "originalPackageInfo: label:$originalLabel appIcon:${originalIcon?.javaClass?.simpleName}")
            //
            val packageArchiveInfo = packageManager.getPackageArchiveInfo(outputFile.absolutePath, 0)
            val label = packageArchiveInfo?.applicationInfo?.loadLabel(packageManager)?.toString()
            val appIcon = packageArchiveInfo?.applicationInfo?.loadIcon(packageManager)
            Log.d("AppLog", "packageArchiveInfo!=null?${packageArchiveInfo != null} label:$label appIcon:${appIcon?.javaClass?.simpleName}")
        }
    }
}

文件确实生成了,但由于某种原因 Android 框架无法解析它,因为 packageArchiveInfo 为空。

问题

  1. 我做的示例怎么不能用?新APK怎么解析不了?
  2. getPackageArchiveInfo 函数解析 APK 所需的最少文件集是什么,仅用于我上面提到的信息?
  3. 如果这种解决方案不起作用,是否有一个库可以处理所有类型的 APK 文件以及我提到的所有信息,无论来源如何(包括 Uri 和 zip 文件内)?

编辑:按照建议,我可以只复制文件夹 "AndroidManifest.xml"、"resources.arsc"、"res" 中的那些,但它似乎仍然无法正常工作,因为图标不不会总是一样的:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (savedInstanceState != null)
            return
        thread {
            val installedApplications = packageManager.getInstalledPackages(0)
            Log.d("AppLog", "checking ${installedApplications.size} apps")
            for (originalPackageInfo in installedApplications) {
                val originalApplicationInfo = originalPackageInfo.applicationInfo
                val filePath = originalApplicationInfo.publicSourceDir
                val outputFile = File(cacheDir, "test.apk")
                outputFile.parentFile!!.mkdirs()
                outputFile.delete()
                val toExtract = setOf<String>("AndroidManifest.xml", "resources.arsc", "res")
                ZipFile(filePath).use { zipFile ->
                    ZipOutputStream(FileOutputStream(outputFile)).use { zipOutputStream ->
                        for (entry in zipFile.entries()) {
                            val name = entry.name
                            if (toExtract.contains(name.split("/")[0])) {
                                zipOutputStream.putNextEntry(ZipEntry(name))
                                zipFile.getInputStream(entry).use { inStream ->
                                    zipOutputStream.buffered().apply {
                                        inStream.copyTo(this)
                                    }.flush()
                                }
                            }
                        }
                    }
                }
                val packageName = originalApplicationInfo.packageName
                val originalLabel = originalApplicationInfo.loadLabel(packageManager)
                val originalIcon: Drawable? = originalApplicationInfo.loadIcon(packageManager)
                val originalIconBitmap = originalIcon?.toBitmap()
                //
                val packageArchiveInfo = packageManager.getPackageArchiveInfo(outputFile.absolutePath, 0)
                if (packageArchiveInfo == null) {
                    Log.e("AppLog", "$packageName could not parse generated APK")
                    continue
                }
                val label = packageArchiveInfo.applicationInfo.loadLabel(packageManager).toString()
                val appIcon = packageArchiveInfo?.applicationInfo?.loadIcon(packageManager)
                val appIconBitmap = appIcon?.toBitmap()
                when {
                    label != originalLabel ->
                        Log.e("AppLog", "$packageName got wrong label $label vs $originalLabel")
                    packageArchiveInfo.versionName != originalPackageInfo.versionName ->
                        Log.e("AppLog", "$packageName got wrong versionName ${packageArchiveInfo.versionName} vs ${originalPackageInfo.versionName}")
                    packageArchiveInfo.versionCode != originalPackageInfo.versionCode ->
                        Log.e("AppLog", "$packageName got wrong versionCode ${packageArchiveInfo.versionCode} vs ${originalPackageInfo.versionCode}")
                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && packageArchiveInfo.applicationInfo.minSdkVersion != originalApplicationInfo.minSdkVersion ->
                        Log.e("AppLog", "$packageName got wrong minSdkVersion ${packageArchiveInfo.applicationInfo.minSdkVersion} vs ${originalApplicationInfo.minSdkVersion}")
                    appIcon?.javaClass?.name != originalIcon?.javaClass?.name ->
                        Log.e("AppLog", "$packageName got different app icon type: ${appIcon?.javaClass?.simpleName} vs ${originalIcon?.javaClass?.simpleName}")
                    originalIconBitmap != null && appIconBitmap != null && (originalIconBitmap.width != appIconBitmap.width || originalIconBitmap.height != appIconBitmap.height) ->
                        Log.e("AppLog", "$packageName got wrong app icons sizes:${appIconBitmap.width}x${appIconBitmap.height} vs ${originalIconBitmap.width}x${originalIconBitmap.height}")
                    originalIconBitmap != null && appIconBitmap != null && !areBitmapsSame(originalIconBitmap, appIconBitmap) ->
                        Log.e("AppLog", "$packageName got wrong app icons content ")
                    (originalIconBitmap == null && appIconBitmap != null) || (originalIconBitmap != null && appIconBitmap == null) ->
                        Log.e("AppLog", "$packageName null vs non-null app icon: ${appIconBitmap != null} vs ${originalIconBitmap != null}")
                }
            }
            Log.d("AppLog", "done")
        }
    }

    fun areBitmapsSame(bitmap: Bitmap, bitmap2: Bitmap): Boolean {
        if (bitmap.width != bitmap2.width || bitmap.height != bitmap2.height)
            return false
        for (x in 0 until bitmap.width)
            for (y in 0 until bitmap.height)
                if (bitmap.getPixel(x, y) != bitmap2.getPixel(x, y))
                    return false
        return true
    }
}

我认为,由于应用程序图标非常复杂并且依赖于各种资源(甚至可能以奇怪的方式隐藏),所以除了实际将文件放在文件系统上之外别无选择。


编辑:关于获取应用程序图标,我实际上并没有很好地用于 APK 文件。我在我的 own app 上使用了一些不同的东西,它在这里似乎也工作得很好,除了某些情况下图标有点不同(例如 shape/color 可能不同),可能是由于不同配置。但至少当应用程序明显具有非默认应用程序图标时,它不会 return 您使用 Android 的默认应用程序图标。

遗憾的是,在创建最小化的 APK 时,它并不总能获得良好的应用程序图标(有时 return 告诉我 ColorStateListDrawable 或 ColorDrawable,例如),可能是因为有时原始 APK 在非-常规路径。假设您拥有整个 APK,这就是您获取应用程序图标的方法:

在获取之前,使用:

packageArchiveInfo.applicationInfo.publicSourceDir = targetFilePath
packageArchiveInfo.applicationInfo.sourceDir = targetFilePath

然后调用这个函数:

    fun getAppIcon(context: Context, applicationInfo: ApplicationInfo): Drawable? {
        val packageManager = context.packageManager
        try {
            val iconResId = applicationInfo.icon
            if (iconResId != 0) {
                val resources: Resources = packageManager.getResourcesForApplication(applicationInfo)
                val density = context.resources.displayMetrics.densityDpi
                var result = ResourcesCompat.getDrawableForDensity(resources, iconResId, density, null)
                if (result != null)
                    return result
            }
        } catch (e: Exception) {
//            e.printStackTrace()
        }
        try {
            val applicationIcon = packageManager.getApplicationIcon(applicationInfo)
//            Log.d("AppLog", "getApplicationIcon type:${applicationIcon.javaClass.simpleName}")
            return applicationIcon
        } catch (ignored: Exception) {
        }
        return null
    }

要转换为位图,您可以使用:

val appIconBitmap = try {
    appIcon?.toBitmap(appIconSize, appIconSize)
} catch (e: Exception) {
    e.printStackTrace()
    null
}

要获取应用程序图标大小,您可以使用:

    fun getAppIconSize(context: Context): Int {
        val activityManager = context.getSystemService<ActivityManager>()
        val appIconSize = try {
            activityManager.launcherLargeIconSize
        } catch (e: Exception) {
            TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48f, context.resources.displayMetrics).toInt()
        }
        return appIconSize
    }

修改复制代码自

zipFile.getInputStream(entry).use { it.copyTo(zipOutputStream.buffered()) }

zipFile.getInputStream(entry).use {
    val bufStream = zipOutputStream.buffered()
    it.copyTo(bufStream)
    bufStream.flush()
}

或类似的东西以确保所有数据都被写出。

我以为 APK 没有通过证书验证,但事实并非如此。上述内容将为您提供标签和图标。

使用原始代码我看到了以下错误

chunk size is bigger than given data
Failed to load 'resources.arsc' in APK '/data/user/0/com

这向我表明输出出现了问题。通过上述更改,我看到以下内容:

originalPackageInfo: label:APK Copy and Read appIcon:AdaptiveIconDrawable
packageArchiveInfo!=null?true label:APK Copy and Read appIcon:AdaptiveIconDrawable

如果我在 Android Studio 中从设备资源管理器打开创建的 APK,它会很好地解析出来。

至于最低限度,我认为,为了您的目的,您将需要清单、resource.arsc 文件和 res 目录。以下是如何获得仅使用这些元素进行解析的简化 APK:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (savedInstanceState != null)
            return
        thread {
            val originalApplicationInfo = packageManager.getApplicationInfo(packageName, 0)
            val filePath = originalApplicationInfo.publicSourceDir
            val outputFile = File(cacheDir, "test.apk")
            outputFile.parentFile!!.mkdirs()
            outputFile.delete()
            val toExtract = setOf<String>("AndroidManifest.xml", "resources.arsc","res")
            ZipFile(filePath).use { zipFile ->
                ZipOutputStream(FileOutputStream(outputFile)).use { zipOutputStream ->
                    for (entry in zipFile.entries()) {
                        val name = entry.name
                        if (toExtract.contains(name.split("/")[0])) {
                            zipOutputStream.putNextEntry(ZipEntry(name))
                            zipFile.getInputStream(entry).use { inStream ->
                                zipOutputStream.buffered().apply {
                                    inStream.copyTo(this)
                                }.flush()
                            }
                        }
                    }
                }
            }
            val originalLabel = originalApplicationInfo.loadLabel(packageManager)
            val originalIcon: Drawable? = originalApplicationInfo.loadIcon(packageManager)
            Log.d(
                "AppLog",
                "originalPackageInfo: label:$originalLabel appIcon:${originalIcon?.javaClass?.simpleName}"
            )
            //
            val packageArchiveInfo =
                packageManager.getPackageArchiveInfo(outputFile.absolutePath, 0)
            val label = packageArchiveInfo?.applicationInfo?.loadLabel(packageManager)?.toString()
            val appIcon = packageArchiveInfo?.applicationInfo?.loadIcon(packageManager)
            Log.d(
                "AppLog",
                "packageArchiveInfo!=null?${packageArchiveInfo != null} label:$label appIcon:${appIcon?.javaClass?.simpleName}"
            )
        }
    }
}