优化APK体积_android apk 大小优化-程序员宅基地

技术标签: java  性能优化  android  android studio  

该篇文章主要来介绍如何减少APK体积,以帮助用户更快地下载App,并加速安装/更新过程。

APK内容结构一瞥

要查看APK文件中都包含哪些内容,有两种方式。第一种通过Android Studio的Analyze APK功能查看,该工具不仅可以还原XML类型代码的原始内容和各类资源文件,而且连Dex文件也能还原,比起第二种方式手动解压查看要简单直观得多;第二种则是直接将APK文件解压。则两种方式解压同一个app都是一样的文件结构,如下所示。
在这里插入图片描述
apk的内容结构根据App功能和打包时的具体操作差异,文件结构可能会有所区别,但大体相当。
无论采用上述哪种方式,我们都可以发现,这个APK文件内部主要是由5个文件和文件夹组成。

  • AndroidManifest.xml:该文件对应源代码中的同名文件,它保存着一个App的名字、版本信息、所需权限、自定义数据、Activity配置信息等。唯一不同的是,解压后的文件是经过处理的,直接使用文本查看器是无法看到原始数据的。有两种方法可以解决这个问题:一种是使用RE浏览器,当然,使用该方法只能在手机上查看,但好处是无须解压APK;另一种方法是在计算机上使用AXMLPrinter2工具还原AndroidManifest.xml文件,具体通过执行:

java -jar AXMLPrinter2.jar AndroidManifest.xml

需要特别注意的是,该工具不仅能还原AndroidManifest.xml,还能还原layout中的XML布局文件以及drawable中的XML代码文件,它几乎对所有XML文件都适用。

  • classes.dex:想要允许Java代码,需要先将其编译,生成class文件。在class1中保存了字节码,再由jvm执行它们。在android中则做了进一步优化,即将java字节码转换成Dalvik字节码,有Dalvik虚拟机运行它。classes.dex文件保存了Dalvik字节码。
  • META—INF:该目录中存放签名文件,用于校验APK文件的合法性。它通常包含4个文件:CERT.RSA、CERT.DSA、CERT.SF以及MANIFEST.MF。其中,CERT.RSA是打包release版本时,开发者利用私钥对APK进行签名的文件;CERT.SF以及MANIFEST.MF文件描述了SHA-1哈希值。
  • res:该目录存放各种资源文件,包括图片、文本等。它和resource.arsc文件配合使用,访问时要通过resource.arsc中记录的ID和资源的映射关系找到对应的资源文件。
  • resource.arsc:该文件时编译后的二进制文件、通常包含ID和具体资源文件之间的映射关系以及源码中res/values的内容。

最后需要特别说明的是,由于不同APK在打包方式上有少许差异,文件结构可能与上述内容有所区别,但大多数只限于文件名或目录名的不同,其作用和结构仍保持一致。

多渠道打包

如果我们想要尽可能让程序在尽可能多的设备上正常收到推送消息,通常的做法是集成各个厂商独立的推送库。但这也带来了负面作用——各库之间是否有冲突、多个推送服务同时运行,拖慢app的运行速度、apk的体积增大。
面对这种现象,理想的做法是相应版本只集成相应厂商的推送服务,比如发布到华为应用市场的APK只集成华为的推送服务,发布到小米应用市场的APK只集成小米的推送服务。最后,在做一个集成通用推送服务的版本,比如极光推送,用来发布到通用的应用市场上,比如应用宝、百度应用市场等。

多渠道打包原理

减少APK体积的一种方法是分渠道打包,其主要思想是只保留该渠道所用的资源,去除其他渠道专用的资源。这里的资源包括程序代码、资源文件以及第三方库。下面我们介绍下多渠道打包的重要知识点。

  • buildTypes参数
    涉及多渠道打包的核心难点是build.gradle文件以及不同版本的代码、库、资源等文件的存放技巧。我们先来看build.gradle。
    要实现多渠道打包,主要通过build.gradle文件android节点下的配置参数进行定义。在默认新创建的工程中,可以在android节点下找到buildTypes子节点,该子节点又包含release和debug子节点。
buildTypes {
    
        debug {
    
            signingConfig signingConfigs.debug
        }
        release {
    
            signingConfig signingConfigs.debug
            minifyEnabled enableProguardInReleaseBuilds
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        }
    }

下面列举该节点下比较常用的配置参数。

参数名称 说明
Debuggable 定义该版本是否为可调试
minifyEnabled 定义是否需要自动移除没有用的java代码
multiDexEnabled 定义是否可以拆分多个dex包
signingConfig 指定签名配置文件
versionNameSuffix 定义VersionName的后缀
zipAlignEnabled 定义是否启用zipAlign优化APk
shrinkResources 当其值为true时,编译相应版本时会省略没有用到的素材

除了上述完全自定义版本参数的方法外,还可以继承某个版本,对某些参数进行自定义,类似java中的Override。举个例子,现要定义名为custom的版本,当我们想集成Debug版本时,可以按如下方式配置:

custom.initWith(Debug)
custom{
    
   zipAlignEnabled true
}

这样配置后,编译出的custom版本就是启用了zipAlign优化后的Debug版本,其他未在此配置的参数则与Debug版本的配置参数保持一致。

  • productFlavors参数
    除了buildTypes外,还可以在android节点下添加productFlavors字段,该字段定义了“分发渠道”。所谓“分发渠道”,即前文中所述专门发布到华为应用市场和小米应用市场的版本。该字段在默认配置中并不存在,需要我们手动声明它。
productFlavors {
    
        huawei {
    
        }
        xiaomi {
    
            
        }
    }

下面列举该节点下比较常用的配置参数。

参数名称 说明
ApplicationId 完整的程序包名
ApplicationIdSuffix 包名的后缀
versionName 定义了该渠道版本的VersionName的值
versionCode 定义了该渠道版本的VersionCode的值
dimension 指维度,所有的Flavor都必须包含dimension定义
实例解析

接下来,我们用一个实例来演示如何分渠道打包 APK。 项目需求如下:
为了统计用户对地图类 App的喜好,需要分不同渠道进行分发。渠道A 集成百度地图,在X应用市场上架:渠道B集成高德地图,在Y应用市场上架。

  1. 分渠道打包
    创建一个新工程,并命名为 AndroidMultiApkDemo.
    按照实际需求,我们需要编译两个版本,分别是集成百度地图版和集成高德地图版。因此,我们添加 productFlavors 节点并声明这两个渠道。具体代码如下:
productFlavors{
    
  baidu{
    
    dimension "default"
  }
  amap{
    
    dimension "default"
  }
}

buildTypes 不做处理,Sync 成功后,打开 Build Variants 视图,可以看到一共可产出 4 个版本。
然后,打开Project 视图,并以Project 方式查看项目文件。依次展开 AndroidMultiApkDemo->app->src,对于新建的工程,src目录中通常包含 main、 test 和 androidTest 三个子日录。我们在src 中新建amap 和 baidu 两个新目录:将main 目录下的内容完整地复制到这两个新目录中。
之所以创建 amap 和 baidu 两个目录,目的在于分别编写两个渠道版本的代码。在编译时,系统将根据 productFlavors 中的版本名称与 src中的目录名自动匹配,从而将不同渠道的代码分开处理。而这两个目录中的内容与main 中的代码是继承关系,这就解释了为何我们无论选择哪种 BuildVariants, main 目录下的内容都为启用状态,且图标样式不变:
接下来,按照百度地图和高德地图的官方文档分别进行集成。完成后,在Build Variants 中选择相应的版本,再执行 Build APK 即可编译出对应版本。如果读者偏爱使用命令行,也可在工程根目录下执行

./gradlew assemble [渠道版本名]
例如: ./gradlew assembleamapDebug

即可生产出集成高德地图的 Debug版本。至此,项目需求已经可以完整地实现,可以分发了。似还有哪里不对劲,因为这样做会集成高德和百度地图的so库和jar包,我们需要将其分开来。

  1. 分治第三方库
    其实要剔除其他渠道的jar和so库很简单,核心思想就是将它们分路径存放,然后在具体声明。
    对于本例,笔者的处理方式是将百度地图的 JAR 包放在 app 目录下的libs baidu子目录下;高德地图的 JAR 包放在app 目录下的libs armap 子目录,而不是将它们一起放在libs 中。对于so库,笔者将其放在丁 src目录下amap 子目录和baida 子目录各自的 jniLibs目录中。
dependencies{
    
baiduImplementation files ('libs_baidu/BaidulBS Android.jar')
amapImplementation files ('libs_amap/...')
}

由于修改后的 jnilLibs 目录在编译时 可以自动识别,因此无须显示指定。
最后,分别再次编译这两个版本,可以看到APRK文件有明显的缩小。

  1. 注意事顼
    为了讲述多渠道打包的方法,我们以集成不同地图为例,且整个 App 仅包含单一的地图显示功能。但在实际开发中几乎不存在这样的场景,真实的需求很可能只是整个 App 中的某个小功能点在不同的渠道版本之间有所差异。就比如前面提到的推送服务,实际上仅仅是推送服务单个模块的实现不同,其他的功能点都是一样的。
    因此,在实际操作时,我们仅需单独实现有差异的功能模块即可,不要将所有的代码都按照渠道不同在不同的代码目录中都放置一份,而是把相同的代码放到main 目录下即可。否则虽然能做到多渠道分发,但维护起来和同时维护多个Project 的工作量相差无几。

优化资源文件

针对某些开发场景,应用多渠道打包技术已经可以减少APk的体积了。但这还不够,我们还可以从资源文件入手,进一步地缩小APK的文件大小。

图片格式的选择
  • 使用webp替代传统图片格式
    需要注意的是,webp并非全能,一下几点需要留意:
  1. 对于GIF格式,转换后的webp只能保留其静止状态。也就是说,如果GIF本身是动态图片,不要对其转换。
  2. App图标可能在上架时要求必须是采用png格式。
  3. webp格式在android3.2及以上版本受支持,无损、透明的webp格式在android4.3及以上版本受支持。如果你的app运行在较早版本的系统上,那么谨慎使用webp格式。
  4. webp格式与经过9-patch处理的图片不兼容。

排除了上述例外情况后,就可以放心地对原有图片进行转换了。Android Studio中提供了快速方便地转换工具,可以对单个文件进行转换,也可以对整个目录下的图片进行转换。并且可以根据情况做出如下排除:

  1. 有损和相关质量参数以及无损编码选择
  2. 跳过9-Patch处理的图片(必选)
  3. 跳过转换后比转换前体积更大的图片(可选)
  4. 跳过带有透明度的原始图片(可选,当API Level不符合要求时,此项为必选)
  • 压缩PNG/JPG图片
    推荐使用TinyPNG网站对图片进行压缩,除此之外,对于png图片aapt工具在项目编译过程中还将通过无损压缩的方式优化位于res/drawable中的图片资源文件。但是有可能压缩后的文件反而更大,为了避免aapt帮倒忙,需要在gradle配置文件中添加以下代码来规避此风险:
aaptOptions{
    
   cruncherEnabled = flase
}
  • 复用相似的图片
    这个想必就不必多说了吧。对于一些图片可以通过旋转等变换得到所需图片的就不需要美术再出一张图片了。

  • 应对多尺寸的9Patch处理方案
    这里罗列一些注意事项:

    1. 9Patch处理的图片,原图应是PNG格式而不是JPG格式。
    2. 在使用9Patch工具描绘边缘时,四边都要画,且尽量对称。
    3. 经过9Patch处理后的图片应放在drawable目录下,不要放在mipmap目录下。
合理使用矢量图

众所周知,矢量图是经过矢量计算绘制出来的图形,它的特点是无论怎样放大和缩小,图像都不会失真。随之而来的另一个优点是节省空间。分辨率为100x100的矢量图和分辨率为1000x1000的图片都可以使用同一个文件,而无论是JPG、PNG还是WebP格式,对于相同内容的图片而言,文件尺寸的分辨率往往成正相关的关系。另外,虽然矢量图绘制路径可由开发人员完全自定义,但是实际上很少有人去改动它,因为它不是很直观,一般的人还真是改不了。

资源文件后加载技术

如果你正在开发的App确实有很多图片显示需求,而且都是GIF图,无法应用WebP转换,也无法压缩,那该怎么办?典型的例子就是聊天软件中的动画表情,它们大部分是GIF动图,而且数量十分庞大。解决方案就是资源后加载。
首先,在APK内部以ZIP格式压缩一些常用的动画表情,放到assets目录中,随APK分发。然后,在适当的时机完成其他动画表情的下载。这样一来,既保证了用户体验,又达到了给APK瘦身的目的。

使用代码混淆

代码混淆可以帮助我们很好地隐藏代码,防止被反编译。同时,还可以达到缩减发布安装包大小的目的。在早期版本(3.4版本前)的Android Studio中,代码混淆时由ProGuard来完成的。现在,Android官方采用R8编译器来处理代码混淆,且与ProGuard规则配置文件相兼容。

R8编译器的优化原理

在使用 Android Gradle Pluein 3.4.0 及以上版本迸行编译时,R8编译器将会参与到构建流程中。当然,就提是我们启用了它,默认情况下是没有启用的。启用后,R8编译器将自动执行下列任务:

  • 代码压缩:该项任务将检测开发者自己编写的代码以及依赖库中没有使用到的类、变量,方法和宇段等,并合理地移除它们,达到减少代码量的目的。
    举个例子:开发者自己编写的代码分别用到了某个依赖库中名为A.class的X()、Y()方法,但完整的A.class不仅包舍这两个方法,还有Z()方法。但由于Z()方法并没有发生过调用,因此它将被移除。当然,如果Z()方法被依赖库调用,而我们又不十分确定的时候,可以查问依赖库的文档。文档中会对代码混淆做出详细说明,如果某个类中的方法不应移除,就需要使用-keep规则忽略这个类。还有,当我们通过反射的方式使用某段代码时,相应的代码可能会被认为是无用的代码,也应添加-keep规则,否则将可能导致App运行崩溃。
    另外,对于64KB引用限制,若进行代码压缩后可以避免,则无须再启用多dex的方式编译。
  • 代码混淆:该项任务通过缩短类和成员名称达到防止反编译和减少代码量的目的,进行过代码混淆的类、方法、成员名通常人们轻易无法理解和理清其调用关系。
  • 代码优化:该项任务将通过优化代码的逻辑结构达到减少代码量的目的,比如移除空的eise分支等。需要注意的是,使用R8编泽器后,之前的-optimizations 和-optimizationpasses可能会失效。这是因为RB编译器会忽略影响代码优化的混淆规则,它不允许用户修改优化行为。但是,你可以完全停用代码优化任务。
  • 资源压缩:参看上面部分。
启用代码混淆

前文中己经讲过,默认情况下代码混淆并不生效,需要开发者手动启用它。这是因为如果默认启用,每次编评都会耗费大量的时间。另一方面,如果开发者并没有制定哪些类需要排除,极易触发运行崩溃。
启用代码混淆的方法在之前已经大致介绍过,主要是编辑Project中的build.gradle 文件,示例如下:

buildTypes {
    
        release {
    
            // Caution! In production, you need to generate your own keystore file.
            // see https://reactnative.dev/docs/signed-apk-android.
            signingConfig signingConfigs.debug
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        }
    }

上述代码对release版本进行了混淆方式定义。

  • minifyEnabled是代码混淆的“总开关”;默认值为false。
  • shrinkResources是缩减资源文件的开关,默认值为false,作用是清理没有用到的资源素材文件。
  • proguardFilcs用于指定代码混清的规则,getDetaultProguardFile将返回SDK目录中tools目录下的proguard路径。在该目录中存在名为proguard-android-optimize.txt的文件,后面的proguard-rules.pro存在于每个module中。一般情况下,自定义的代码混淆规则会在此文件中配置。
    例如上述代码中的写法可将混滑配置文件结合到一起,我们可以在该字段添加多个混淆配置文件。需要注意的是,当项目中集成了第三方库且库中存在混淆配置文件时,则其中的混淆规则会追加应用到整个项目中。
添加混淆例外项的两种方式

(1)在Proguard配置文件中 (通常是module 中的proguard-rules.pro)使用-keep来添加例外。比如,要对名为 MainActivity java 的类原样保留,通常的写法为:

-keep public class Mainactivity

(2)在要添加例外的类中通过@Keep 注解的万式漆加例外。当@Keep 注解作用与类时,整个类的内容特被保留:当@Keep 注解作用于 某个方法或变量时,相应的方法或变量将被保留。该方法只有迁移到 Androidx 后可用,否则请按照方式(1)来添加例外。

提醒读者:如果一个 App在混淆后反复崩溃,但无论从代码逻辑还是资源文件都查不出问题,那么可以关闭混淆后打包试试。
如果在没有启用混淆的情况 下程序可以正常运行,问题就出在混淆的步骤之中。通常的做法是看崩溃发生在哪个类,然后尝试将其添加到例外项中最后再次尝试运行。以上操作可以解决大部分由混淆带来的异常问题。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_36828822/article/details/127142611

智能推荐

MYSQL, mybatis 如何使用自增主键_mybatis auto_increment-程序员宅基地

文章浏览阅读2.5k次。通常我们在应用中对mysql执行了insert操作后,需要获取插入记录的自增主键。本文将介绍java环境下的4种方法获取insert后的记录主键auto_increment的值:通过JDBC2.0提供的insertRow()方式 通过JDBC3.0提供的getGeneratedKeys()方式 通过SQL select LAST_INSERT_ID()函数 通过SQL @@IDENTITY 变量1.通过JDBC2.0提供的insertRow()方式自jdbc2.0以来,可以通过下面的方式执._mybatis auto_increment

Python中 tf.placeholder()函数解释_python placeholder-程序员宅基地

文章浏览阅读6.4k次,点赞6次,收藏11次。此函数可以理解为形参,用于定义过程,在执行的时候再赋具体的值。不必指定初始值,可在运行时,通过 Session.run 的函数的 feed_dict 参数指定。这也是其命名的原因所在,仅仅作为一种占位符。tf.placeholder( dtype, shape=None, name=None)参数:dtype:数据类型。常用的是tf.float32,tf.float64等数值类型shape:数据形状。默认是None,就是一维值,也可以多维,比如:[None,3],表示_python placeholder

RK平台,芯片rtl8821cs,重启wifi概率性无法打开_rk重新上电概率打不开wifi蓝牙-程序员宅基地

文章浏览阅读3.4k次。文章目录重启wifi概率性无法打开发现问题问题分析解决方法重启wifi概率性无法打开发现问题  最近在调试A100项目,建立在RK平台上的一个医疗随行包+智能音箱;在调试的过程中发现了一个bug:通过reboot命令重启的时候会概率性的出现WIFI打不开的情况;问题分析  根据查看kernel log,发现在sdio去探测设备的过程中,sdio报错了,导致无法探测到设备,以致于驱动..._rk重新上电概率打不开wifi蓝牙

文件系统的类型简介_系统用认识媒介类型是文件的什么-程序员宅基地

文章浏览阅读4.2k次。文件系统的类型简介Linux支持多种文件系统类型,包括ext2、ext3、vfat、jffs、romfs和nfs等,为了对各类文件系统进行统一管理,Linux引入了虚拟文件系统VFS(Virtual File System),为各类文件系统提供一个统一的应用编程接口。根据存储设备的硬件特性、系统需求,不同的文件系统类型有不同的应用场合。在嵌入式Linux应用中,主要的存储设备为_系统用认识媒介类型是文件的什么

魅族u20怎么刷Android,魅族魅蓝U20/U10一键Root权限获取+USB驱动安装-程序员宅基地

文章浏览阅读1.5k次。魅族新的手机型号为魅蓝U20发布了,,售价特公布了为千元级别手机,那么meizuU20手机配置如何呢?我们看看吧,,屏幕尺寸为5.5英寸,分辨率为1920*1080高清,系统是基于安卓的Flyme5系统,兼容安卓系统的APK格式文件安装和使用。处理器为HelioP101.8GHz(八核心)摄像头为1300万像素,前置为500万,。可运行内存为2GB,机身存储空间为16GB这个手机目前售价为..._1920*1080手机可以root的

陕西省计算机二级mysql报名_转发教育部考试中心关于全国计算机等级考试(NCRE)体系调整的通知...-程序员宅基地

文章浏览阅读112次。附件全国计算机等级考试调整方案2015年,考试中心组织召开了第六届全国计算机等级考试(NCRE)考委会会议,会议完成NCRE考委会换届选举,并确定了下一步改革目标。在新的历史时期,NCRE将在保持自身特色、稳定发展的基础上进一步考试改革。从2018年3月开始,将实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代码:17)..._二级mysql报名

随便推点

APP安全测试工具_QARK初探-程序员宅基地

文章浏览阅读9.7k次。1、简介检测android应用程序安全漏洞,可以用于已打包但是未加固的app或者源代码。https://github.com/linkedin/qark2、安装要求Tested on Python 2.7.13 and 3.6 Tested on OSX, Linux, and Windows现有win10安装pip install qark安装成功后可以使用一下命令查看qark --help安装反编译工具_jadx:https:._qark

校验码——奇偶校验码详解,码距,例题_奇偶校验题目-程序员宅基地

文章浏览阅读1.1w次,点赞7次,收藏18次。相关文章: 校验码——码距 校验码——海明码及码距 校验码——CRC循环冗余校验码 一、码距二、奇偶校验码 奇偶校验码是一种增加二进制传输系统最小距离的简单和广泛采用的方法。例如,单个的奇偶校验将使码的最小距离由一增加到二。 一个二进制码字,如果它的码元有奇数个1,就称为具有奇性。例如,码字“10110101”有五个1,因此,这个码字具有奇性。同样,偶性码字具有偶数个1。注意奇性检测等效于所有码元的模二加,..._奇偶校验题目

25.请编写一个函数fun,它的功能是:比较两个字符串的长度,(不得调用C语言提供的求字符串长度的函数),函数返回较长的字符串。若两个字符串长度相同,则返回第一个字 符串。_3、(串比较):编写一个函数fun,功能是对两个字符串进行比较;在主函数中输入两个字 符串,调用fu-程序员宅基地

文章浏览阅读4k次,点赞9次,收藏10次。25.请编写一个函数fun,它的功能是:比较两个字符串的长度,(不得调用C语言提供的求字符串长度的函数),函数返回较长的字符串。若两个字符串长度相同,则返回第一个字符串。例如,输入:beijing shanghai(为回车键),函数将返回shanghai。#include <stdio.h>char *fun(char *s1,char *s2){//考察传递字符串 char *p=s1; char *q=s2; int m=0; int n=0; while(*p){ _3、(串比较):编写一个函数fun,功能是对两个字符串进行比较;在主函数中输入两个字 符串,调用fun函数完成串比较,在主函数中输出这两个字符串的比较结果。要求用指针完成fun函数,不得使用strcmp库函数。

pycharm使用日志_pycharm r日志详情-程序员宅基地

文章浏览阅读4.5k次。这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入欢迎使用Ma..._pycharm r日志详情

Universal-Image-Loader源码阅读(3)-utils/IoUtils_universal_utils-程序员宅基地

文章浏览阅读176次。该类从名字看就是IO工具类。同样类声明为final,构造为private,方法都是static。这些是工具类的标配呀!源码:/** * Provides I/O operations * * @author Sergey Tarasevich (nostra13[at]gmail[dot]com) * @since 1.0.0 */public final _universal_utils

淘宝代购系统;海外代购系统;代购程序,代购系统源码PHP前端源码演示-程序员宅基地

文章浏览阅读549次。本帖只展示部分演示站 需了解更多请移步注册http://console.open.onebound.cn/console/?i=Rookie代购业务近年兴起的一种购物模式,是帮国外客户购买中国商品。主要通过外贸代购模式,把淘宝、天猫等电商平台的全站商品通过API接入到你的网站上,瞬间就可以架设一个有数亿产品的大型网上商城,而且可以把这些中文的商品全部自动翻译成各国语言,能让国外客户看懂,直接在网站上下单,然后网站运营方代为购买再邮寄给客户,收取商品差价以及代购费和运费,利润可观,市场巨大。目前跨境

推荐文章

热门文章

相关标签