Android马甲包的那些事儿

news2024/12/22 22:10:06

制作Android马甲包最简单的方式就是使用 productFlavors 机制。

本文就是在productFlavors机制的基础上制作的马甲包,每个马甲只需要

  1. 在build.gradle文件中配置一下包名、各种key、签名文件

  1. 配置启动页、logo、app名等资源

  1. 配置服务器域名、微信分享回调Activity等代码

此外,代码、资源文件等全部都天然支持差异化功能

1. 原理

如下面代码所示,我们在build.gradle中使用productFlavors机制可以创建两个flavor——hdd以及jinyouzi,这样在Build Variant中就可以通过hddDebug、hddRelease、jinyouziDebug、jinyouziRelease来编译对应马甲的debug、release包。

注意,在此文章中hdd是基线包,jinyouzi是马甲包。

android {
    defaultConfig {
        applicationId "com.xxx.xxxxxxx.app"

        flavorDimensions "product"
    }

    productFlavors{
        hdd {
            dimension "product"
        }

        jinyouzi {
            dimension "product"
        }
    }
}

配置了flavor之后,我们在app/src下面可以创建与main目录同级的hdd、jinyouzi目录。这两个目录中的资源文件、代码在编译对应的flavor时可以加入编译。也就是说hdd = ['src/main', 'src/hdd'],jinyouzi = ['src/main', 'src/jinyouzi']。

  • 对于资源文件来说,flavor下的资源会“覆盖”main下面的资源,也就是flavor的优先级高——不知道官方怎么称呼,我借用Android系统开发中的名词,称之为overlay机制。

其实这点与apk的编译流程有关,在 Shrink, obfuscate, and optimize your app - Merge duplicate resources中有提到:
Gradle merges duplicate resources in the following cascading priority order:
Gradle 会按以下级联优先顺序合并重复资源:
Dependencies → Main → Build flavor → Build type
依赖项 → 主资源 → 构建flavor → 构建类型
For example, if a duplicate resource appears in both your main resources and a build flavor, Gradle selects the one in the build flavor.
例如,如果某个重复资源同时出现在主资源和构建flavor中,Gradle 会选择构建flavor中的重复资源。
  • 对于代码文件来说,如果flavor和main下有代码文件名称一样,编译时会报错。所以需要把各个flavor有差异的文件放到各个flavor下,而不是main下。

这就是马甲包的资源、代码管理的关键点。 这段关键点一头雾水没关系,后面具体配置的时候就会体会到。

此外,各个flavor原本就能配置不同的applicationId、版本号、友盟统计分享等key以及签名文件等,具体代码在后面会谈到。

2. 具体需求

我们先下面会从以下几个方面说明实际需求需要修改的位置:

  1. applicationId、版本号

  1. 资源文件

  1. 各种key的配置

  1. 代码文件

  1. 签名配置

2.1 applicationId、版本号

applicationId、版本号可以在flavors中直接进行配置:

build.gradle:

android {
    ...
    productFlavors{
        hdd {
            dimension "product"
            applicationId "com.xxx.xxxxxxx.app"
            versionCode 100080
            versionName "1.0.8"
        }

        jinyouzi {
            dimension "product"
            applicationId "com.xxx.flavor.app"
            versionCode 101030
            versionName "1.1.3"
        }
    }
}

applicationId在AndroidManifest.xml中也需要使用到,这个在第2.3小节中一起介绍。

2.2 资源文件

利用productFlavors机制,可以为每个flavor创建不同的文件目录。

各个flavor的logo、启动页、app_name等可以放到对应flavor的文件目录中。这样就达到了马甲包的UI效果——换个皮肤。

在文本中,由于hdd是基线,jinyouzi是基于hdd的马甲,因此只需要在jinyouzi中放置需要更改的hdd中对应文件就可以起到覆盖基线资源的效果。

对于drawable、mipmap资源而言,文件会替换基线的文件。

对于values里面的资源而言,资源不是简单粗暴的文件覆盖,而是每一项具体资源的覆盖。我们只需要在jinyouzi中新增对应的strings、color就可以了。

比如jinyouzi中的 colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimaryDark">#F1964A</color>
    <color name="colorTextPrimary">#ffffff</color>
    <color name="colorTextSecond">#ffffff</color>
    <color name="colorControlNormal">#FFFFFF</color>
    <color name="colorTabIndicatorLightBackground">@color/fffd850a</color>
    <color name="colorTabIndicatorDarkBackground">@color/white</color>
    <color name="colorTabSelected">#FFFFFF</color>
    <color name="colorTabNormal">#ffdddddd</color>
</resources>

jinyouzi中的 strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">jinyouxi</string>

    <string name="we_chat_name">jinyouzi_wechat_name</string>
    <string name="we_chat_id" translatable="false">jinyouzi_wechat_id</string>
</resources>

2.3 各种key的配置

这里的key配置包括友盟统计、微信分享等key的传统意义上的key配置,还包括AndroidManifest上的客制化配置。

此处的配置主要体现在build.gradle以及Androidmanifest.xml文件中。

先上一段配置完全的build.gradle文件,其中私密信息使用xxx代替:

build.gradle

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    defaultConfig {
        applicationId "com.xxx.xxxxxxx.app"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion

        flavorDimensions "product"

        multiDexEnabled true
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    signingConfigs {
        hdd {
            keyAlias 'xxxx'
            keyPassword 'xxxxxxx'
            storeFile file('../hdd.jks')
            storePassword 'xxxxxxx'
        }

        flavor {
            keyAlias 'xxxxx'
            keyPassword 'xxxxxxx'
            storeFile file('../flavor.jks')
            storePassword 'xxxxxxx'
        }
    }

    productFlavors{
        hdd {
            dimension "product"
            applicationId "com.xxx.xxxxxxx.app"
            versionCode 100080
            versionName "1.0.8"
            def qq_id = 1000xxxxxx
            buildConfigField('String', 'BUGLY_ID', '"xxxxxxx"')
            buildConfigField('String', 'UMCONFIGURE_ID', '"xxxxxxx"')
            buildConfigField('String', 'QQ_SHARE_ID', "\"$qq_id\"")
            buildConfigField('String', 'QQ_SHARE_SECRET', '"xxxxxxx"')
            buildConfigField('String', 'WX_SHARE_ID', '"xxxxxxx"')
            buildConfigField('String', 'WX_SHARE_SECRET', '"xxxxxxx"')
            manifestPlaceholders = [
                    schema : "hdd",
                    qq_id : qq_id
            ]
            signingConfig signingConfigs.hdd
        }

        jinyouzi {
            dimension "product"
            applicationId "com.xxx.flavor.app"
            versionCode 101030
            versionName "1.1.3"
            def qq_id = 1000xxxxxx
            buildConfigField('String', 'BUGLY_ID', '"xxxxxxx"')
            buildConfigField('String', 'UMCONFIGURE_ID', '"xxxxxxx"')
            buildConfigField('String', 'QQ_SHARE_ID', "\"$qq_id\"")
            buildConfigField('String', 'QQ_SHARE_SECRET', '"xxxxxxx"')
            buildConfigField('String', 'WX_SHARE_ID', '"xxxxxxx"')
            buildConfigField('String', 'WX_SHARE_SECRET', '"xxxxxxx"')
            manifestPlaceholders = [
                    schema : "jinyouzi",
                    qq_id : qq_id
            ]
            signingConfig signingConfigs.flavor
        }
    }

    buildTypes {
        debug {
            zipAlignEnabled false
            shrinkResources false
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

            signingConfig release.signingConfig
        }
        release {
            zipAlignEnabled true
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

在上面的配置中,我们为各个flavor定义了不同的

  • applicationId

  • 版本号

  • Bugly ID

  • 友盟ID

  • QQ分享Key

  • 微信分享Key

  • 应用scheme

  • 签名文件

对于配置中的Bugly ID、友盟ID、QQ分享Key、微信分享Key等,使用了buildConfigField来定义,这样编译的时候会在BuildConfig.java文件中生成对应的配置:

BuildConfig.java

/**
 * Automatically generated file. DO NOT MODIFY
 */
package com.hdd.android.app;

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.xxx.flavor.app";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "jinyouzi";
  public static final int VERSION_CODE = 101030;
  public static final String VERSION_NAME = "1.1.3";
  // Fields from product flavor: jinyouzi
  public static final String BUGLY_ID = "xxxxxxx";
  public static final String QQ_SHARE_ID = "xxxxxxx";
  public static final String QQ_SHARE_SECRET = "xxxxxxx";
  public static final String UMCONFIGURE_ID = "xxxxxxx";
  public static final String WX_SHARE_ID = "xxxxxxx";
  public static final String WX_SHARE_SECRET = "xxxxxxx";
}

在代码中就可以这样直接使用了:

HddApplication.kt

class HddApplication : Application() {

    init {
        PlatformConfig.setWeixin(BuildConfig.WX_SHARE_ID, BuildConfig.WX_SHARE_SECRET)
        PlatformConfig.setQQZone(BuildConfig.QQ_SHARE_ID, BuildConfig.QQ_SHARE_SECRET)
    }

    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(base)
        MultiDex.install(base)
        Beta.installTinker()
    }

    override fun onCreate() {
        super.onCreate()
        initConfig()
    }

    private fun initConfig() {
        application = this

        Bugly.init(this, BuildConfig.BUGLY_ID, BuildConfig.DEBUG)

        //友盟    参数5:Push推送业务的secret,否则传空。
        UMConfigure.setLogEnabled(BuildConfig.DEBUG)
        UMConfigure.init(
            application,
            BuildConfig.UMCONFIGURE_ID,
            null,
            UMConfigure.DEVICE_TYPE_PHONE,
            null
        )
    }

    companion object {
        lateinit var application: Application
            private set
    }
}

还可以通过resValue、meta-data方式来实现上面功能。

resValue编译时会产生对应的资源文件。

meta-data方式通过动态替换AndriodManifest中的meta-data,然后在程序中获取实现。

另外因为QQ分享Key以及应用scheme需要在AndroidManifest.xml中配置对应的值,所以这里使用了manifestPlaceholders。

manifestPlaceholders = [
        schema : "hdd",
        qq_id : qq_id
]

在这配置的值可以在AndroidManifest.xml中直接使用。此外applicationId也天生支持在AndroidManifest.xml使用。

我们看看如何在AndroidManifest.xml中进行相关配置:

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.xxx.xxxxxxx.app">

    <application
        android:name=".HddApplication"...>

        <activity
            android:name=".core.splash.SplashActivity"
            android:screenOrientation="portrait"
            android:theme="@style/SplashTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <!-- 配置scheme -->
                <data android:scheme="${schema}" />
            </intent-filter>
        </activity>

        <!-- 微信分享 -->
        <activity
            android:name="${applicationId}.wxapi.WXEntryActivity"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:exported="true"
            android:theme="@android:style/Theme.Translucent.NoTitleBar" />
        <!-- QQ分享 -->
        <activity
            android:name="com.tencent.tauth.AuthActivity"
            android:launchMode="singleTask"
            android:noHistory="true" >
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <!-- 配置qq_id -->
                <data android:scheme="tencent${qq_id}" />
            </intent-filter>
        </activity>

        <!-- 配置FileProvider -->
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

    </application>

</manifest>

总结一下上面的AndroidManifest.xml代码:

  • applicationId在微信分享回调页面、FileProvider两处位置要配置。

  • manifestPlaceholders中scheme配置到SplashActivity上,qq_id配置到QQ分享AuthActivity上

QQ分享配置需要注意,qq_id定义的是int类型。所以QQ_SHARE_ID配置为"\"$qq_id\""。且AndroidManifest中对应的scheme也将为正确的tencent1000xxxxxx。

微信分享回调Activity必须是应用实际包名目录下的wxapi子目录中的WXEntryActivity文件,任意更改目录都不会收到微信分享回调。

比如在在hdd马甲下配置微信分享回调,需要在com.xxx.xxxxxxx.app.wxapi下创建WXEntryActivity文件。

jinyouzi马甲下配置,则需要在com.xxx.flavor.app.wxapi下创建。

这部分代码写到对应flavor目录下。

当然,合理利用activity-alias能更漂亮的完成微信回调WXEntryActivity的配置,比如说:

<!-- 微信分享 -->
<activity
    android:name="anydir.WXEntryActivity"
    android:configChanges="keyboardHidden|orientation|screenSize"
    android:exported="true"
    android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity-alias
    android:name="${applicationId}.wxapi.WXEntryActivity"
    android:exported="true"
    android:launchMode="singleTask"
    android:targetActivity="anydir.WXEntryActivity"
    android:taskAffinity="com.tencent.mm" />

2.4 代码文件

代码文件处理方式就多样了,可以通过2.2小节类似的原理,还可以使用静态工厂方法根据包名构造出不同的类。我们还是说前者吧。

这里拿域名来距离,由于基线的域名是配置在代码中的常量。为了尽可能不修改代码,同时满足马甲包不同域名的要求,所以马甲包也是配置在代码中的,且配置文件所在的包、配置文件的类名以及其包含的public字段名、方法名都必须保持一致。

基线域名配置:

app/src/hdd/java/com/xxx/xxxxxxx/app/http/HttpConfig.kt

package com.xxx.xxxxxxx.app.http

import com.xxx.xxxxxxx.app.BuildConfig

object HttpConfig {
    const val DOMAIN_SIT = "https://xxxxxx.xxxxx.com/"
    const val DOMAIN_UAT = "http://xxxxxx.test.xxxxx.com/"
    val DOMAIN = if (BuildConfig.DEBUG) DOMAIN_UAT else DOMAIN_SIT

    const val DOMAIN_H5_SIT = "https://xxxxxx.xxxxxx.com/"
    const val DOMAIN_H5_UAT = "http://xxxxxx.test.xxxxxx.com/"
    val DOMAIN_H5 = if (BuildConfig.DEBUG) DOMAIN_H5_UAT else DOMAIN_H5_SIT
}

马甲包域名配置:

app/src/jinyouzi/java/com/xxx/xxxxxxx/app/http/HttpConfig.kt

package com.xxx.xxxxxxx.app.http

import com.xxx.xxxxxxx.app.BuildConfig

object HttpConfig {
  const val DOMAIN_SIT = "https://yyyyyy.yyyyy.com/"
  const val DOMAIN_UAT = "http://yyyyyy.test.yyyyy.com/"
  val DOMAIN = if (BuildConfig.DEBUG) DOMAIN_UAT else DOMAIN_SIT

  const val DOMAIN_H5_SIT = "https://yyyyyy.yyyyyy.com/"
  const val DOMAIN_H5_UAT = "http://yyyyyy.test.yyyyyy.com/"
  val DOMAIN_H5 = if (BuildConfig.DEBUG) DOMAIN_H5_UAT else DOMAIN_H5_SIT
}

Note: 由于其他代码使用HttpConfig时会通过基线包名import,所以马甲的HttpConfig文件package以及其他可供外部代码使用的域、方法等入口需要与基线保持一致,以免编译报错。

除入口外,各个马甲内部可以自由扩展,但与基线代码交互时一定要走入口,避免直接交互。

2.5 签名配置

其实在2.3配置中的build.gradle中已经贴出了该部分代码。下面说明一下。

我们知道可以给每个flavor单独配置signingConfig,但是这种配置在debug包时会用Android默认的debug签名。大部分情况OK,除了测试环境微信分享。

不能忍,所以我们解决一下,让各个马甲的debug、release签名保持一致。

关键代码如下,具体可以查看最上面的build.gradle代码:

buildTypes {
    debug {
        ...
        signingConfig release.signingConfig
    }
}

将debug的签名配置显示指定为release的配置,而release的配置在各个flavor中,这样就完成了统一。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows上tensorflow的GPU死活引用不了(tensorflow 2.11无法调用GPU)

tensorflow对于gpu的支持只到2.10&#xff0c;如果你装了最新的tf(2.11)&#xff0c;需要先卸载2.11。 安装代码&#xff1a; pip install tensorflow2.10 -i https://pypi.tuna.tsinghua.edu.cn/simple/解决过程&#xff1a; 查看CUDA与cuDNN配套版本&#xff1a; https:/…

解决ModuleNotFoundError: No module named ‘pygame‘问题

一、问题描述在开发环境运行Python的源码游戏时&#xff0c;游戏不能正常运行&#xff0c;且提示&#xff08;ModuleNotFoundError: No module named pygame【没有发现模块错误&#xff1a;没有发现名为pygame的模块】&#xff09;如下图所示&#xff1a;二、问题分析通过查看提…

OpenCV实战——基于均值漂移算法检测图像内容

OpenCV实战——基于均值漂移算法检测图像内容0. 前言1. 均值漂移算法2. 检测图像内容3. 完整代码相关链接0. 前言 直方图反投影的结果是一个概率图&#xff0c;表示在特定图像位置找到给定图像内容的概率。假设我们现在知道一个物体在图像中的大概位置&#xff1b;概率图可用于…

在 Navicat Monitor for MySQL/MariaDB 中配置实例

Navicat Monitor for MySQL/MariaDB 是一个无代理的远程服务器监控工具&#xff0c;它包含的功能可以使监控数据库&#xff08;DB&#xff09;实例发挥最大效用和更轻松。此外&#xff0c;基于服务器的架构使其可以通过网页浏览器从任何地方访问&#xff0c;从而为你提供无障碍…

DaVinci 项目设置:图像缩放调整

项目设置/图像缩放调整Project Settings/Image Scaling图像缩放调整 Image Scaling选项卡可用于设置片段在输入、输出时的缩放及相应的插值算法。图像缩放调整Image Scaling主要用于选择缩放处理的插值方法&#xff0c;也可用于载入输入、输出缩放调整的预设。缩放过滤器Resize…

特别提醒|2023年考PMP需关注的5大问题

目前知道的是2023年考试时间为3月、5月、8月、11月&#xff0c;但是3月不给新报名&#xff0c;需要报名的话&#xff0c;就是报5月的考试了。当然有的伙伴会有一些小问题&#xff0c;这里给大家整理了一些基本的问题给大家回答一下&#xff0c;大家如果还有其他的问题可以评论提…

[Flink] 容错机制与状态一致性机制

文章目录1.状态一致性1.1 状态一致性分类2.一致性检查点 checkpoint3.端到端&#xff08;end-to-end&#xff09;状态一致性4. 端到端的精确一次&#xff08;exactly-once&#xff09;保证4.1 幂等写入4.2 事务写入5.FlinkKafka 端到端状态一致性的保证5.1 Exactly-once 两阶段…

常见智力题汇总(建议收藏)

&#x1f680;write in front&#x1f680; &#x1f4dc;所属专栏&#xff1a; 智力题 &#x1f6f0;️博客主页&#xff1a;睿睿的博客主页 &#x1f6f0;️代码仓库&#xff1a;&#x1f389;VS2022_C语言仓库 &#x1f3a1;您的点赞、关注、收藏、评论&#xff0c;是对我最…

Web 前端开发技术 —— JavaScript

Web 前端开发技术 —— JavaScript 总结 JavaScript 内容&#xff01; 文章目录Web 前端开发技术 —— JavaScript一、js 的引用方式与执行顺序1、引用方式在标签中直接写 js 代码复用 js 代码通过 import 方式2、执行顺序3、html、css、js 三者之间的关系二、变量与运算符变量…

C++之函数重载

文章目录前言一、函数重载二、如何支持函数重载&#xff08;C支持函数重载的原理--名字修饰(name Mangling)&#xff09;三、参数有什么区别才能构成函数重载1.参数个数不同2.参数类型不同3.参数顺序不同四、返回值类型不同是否可以构成函数重载总结前言 我们知道在使用C语言进…

jmh的一些作用

目录说明说明 jmh可以用来java基准测试&#xff0c;性能测试用这个测比较标准&#xff0c;可以设置预热、迭代次数&#xff0c;对某块代码精准测试&#xff0c;耗时时间单位有毫秒、纳秒等。 就先说到这\color{#008B8B}{ 就先说到这}就先说到这 在下Apollo\color{#008B8B}{在下…

AcWing 323. 战略游戏(树形DP + 状态机DP)

AcWing 323. 战略游戏&#xff08;树形DP 状态机DP&#xff09;一、问题二、分析1、思路分析2、状态表示3、状态转移4、循环设计5、初末状态三、代码一、问题 二、分析 1、思路分析 这道题最后问的其实就是&#xff0c;在一棵树中&#xff0c;每个边至少选择一个端点的条件下…

【FLASH存储器系列十五】NAND Flash究竟能不能随机读写到某个字节的数据?

网上有很多文章写道&#xff0c;nand flash的读写操作是以page为单位&#xff0c;还有文章说些nand flash时必须按page0、page1、page2…的顺序写&#xff0c;必须先写完前面的page才能写后面的page。难道nandflash就不能随机读到某个字节吗&#xff1f;只能一次性读一页&#…

区区几行代码,就能全面实现 Python 自动探索性数据分析

探索性数据分析是数据科学模型开发和数据集研究的重要组成部分之一。在拿到一个新数据集时首先就需要花费大量时间进行EDA来研究数据集中内在的信息。自动化的EDA Python包可以用几行Python代码执行EDA。 在本文中整理了10个可以自动执行EDA并生成有关数据的见解的Python包&am…

C语言——二分查找与猜数字游戏

文章目录二分查找二分查找的思想二分查找的条件二分查找的实现过程代码举例猜数字游戏游戏说明猜数字游戏思想代码实现打印菜单打印主函数打印游戏函数整体代码演示二分查找 题目&#xff1a; 在一个有序数组中查找具体的某个数字n。 首先我们先定义一个110的数组 &#xff0c;…

immersive-translate(沉浸式双语网页翻译扩展),解决谷歌翻译无法使用问题

前言 谷歌停止了大陆的谷歌翻译服务&#xff0c;所以找到了immersive-translate 插件解决翻译问题。当然 最直接就是 换个浏览器比如 Edge\Firefox等等。 主要特性 智能识别网页主内容区&#xff0c;区别于同类插件翻译网页所有的区域&#xff0c;这可以极大增强译文的阅读…

【C++11】右值引用与移动构造、万能引用与完美转发

目录 一、右值引用 1.1 左值引用和右值引用 1.2 左值引用与右值引用比较 1.3 右值引用的使用场景和意义 二、移动构造 2.1 移动构造的实现 2.2 移动赋值 2.3 默认成员函数 2.4 default关键字 2.5 delete 关键字 2.6 STL中的移动构造 二、完美转发 2.1 模板中的万能…

利用剪枝降低bfs算法的时空复杂度(一道OJ题目)

作者&#xff1a;非妃是公主 专栏&#xff1a;《算法》《刷题笔记》 个性签&#xff1a;顺境不惰&#xff0c;逆境不馁&#xff0c;以心制境&#xff0c;万事可成。——曾国藩 《算法》专栏系列文章 算法设计与分析复习01&#xff1a;主方法求递归算法时间复杂度 算法设计与分析…

我写了一个脚本,实现了图片分类问题的全自动化训练

众所周知,图片分类问题属于计算机视觉中比较容易解决的问题之一 但 这几天被数据集的问题搞得焦头烂额, 照理说分类问题的数据集应该比较好制作 但 如果之前没有现成的数据集 也会变得比较麻烦 直到我偶然发现了一个HuggingFace的图片搜索API 无限次调用 而且不需要身份验证 真…

【手撕面试题】HTML+CSS(高频知识点一)

目录 面试官&#xff1a;给定一个元素&#xff0c;如何实现水平垂直居中&#xff1f; 面试官&#xff1a;padding与margin有什么不同&#xff1f; 面试官&#xff1a;vw和百分比有什么区别&#xff1f; 面试官&#xff1a;行内元素与块级元素有什么区别&#xff1f; 面试官…