Android马甲包的那些事儿

Posted 张海龙_China

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android马甲包的那些事儿相关的知识,希望对你有一定的参考价值。

制作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中,这样就完成了统一。

iOS 性能优化那些繁杂琐碎的事儿

👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇

简介

这篇文章文章主要介绍iOS性能优化方面的信息,主要从四个方面进行:应用启动时间;页面刷新滚动流畅度;耗电量;安装包的大小

  • 应用启动时间

  • 页面刷新滚动流畅度

  • 耗电量

  • 安装包的大小

1. 应用启动时间

这里的应用启动时间指,应用启动到显示第一个页面展示时的时间。

应用启动有冷启动和热启动,热启动是指应用在后台活着,然后再启动应用。这里只谈冷启动。

启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。

先来看看Xcode9新加的神器,通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments),DYLD_PRINT_STATISTICS设置为1,如果查看更详细的信息可以DYLD_PRINT_STATISTICS_DETAILS设置为1。

然后启动应用,即可查看到以下信息

Total pre-main time: 588.23 milliseconds (100.0%)
         dylib loading time: 264.36 milliseconds (44.9%)
        rebase/binding time:  56.19 milliseconds (9.5%)
            ObjC setup time:  49.84 milliseconds (8.4%)
           initializer time: 217.71 milliseconds (37.0%)
           slowest intializers :
             libSystem.B.dylib :   9.18 milliseconds (1.5%)
    libMainThreadChecker.dylib :  36.42 milliseconds (6.1%)
          libglInterpose.dylib :  82.35 milliseconds (14.0%)
         libMTLInterpose.dylib :  32.51 milliseconds (5.5%)
                         MeeYi :  24.89 milliseconds (4.2%)

可以看到,在执行main函数前,应用准备了执行了4个流程:dylib loading、rebase/binding、ObjC setup、initializer,下面我们将好好分析这几个流程。

  • load dylibs:加载动态库,包括系统的、自己添加的(第三方的),递归一层一层加载所依赖的库。

  • Rebase&Bind:修复指针,mach-o内部的存储逻辑是,信息的存储地址是虚拟内存,不是直接对应物理内存;每一次应用启动的时候,内存的开始地址又是随机的,因此需要对接虚拟内存和物理内存地址。为了安全,防止黑客攻击。

  • Objc:注册类信息到全局Table中

  • Initializers:初始化部分,+load方法初始化,C/C++静态初始化对象和标记__attribute__(constructor)的方法

  • Main() :执行main函数,执行APPDelegate的方法

  • 加载Window+加载RootViewController+初始化操作:主要在didFinishLaunchingWithOptions执行操作,比如初始化第三方库,初始化基础信息,加载RootViewController等

在了解了应用启动流程后,那对应用启动优化的工作就细分到了对每个流程的优化上。

1.1 main()函数之前:

1.1.1 dylibs:加载动态库

启动的第一步是加载动态库,加载系统的动态库是很快的,因为可以缓存,而加载内嵌的动态库速度较慢。

所以,提高这一步的效率的关键是:减少动态库的数量。合并动态库。

比如公司内部由私有Pod建立了如下动态库:XXTableView, XXHUD, XXLabel,强烈建议合并成一个XXUIKit来提高加载速度。

1.1.2 Rebase & Bind & Objective C Runtime

Rebase和Bind都是为了解决指针引用的问题。对于Objective C开发来说,主要的时间消耗在Class/Method的符号加载上,所以常见的优化方案是:

1)减少__DATA段中的指针数量。

2)合并Category和功能类似的类。比如:UIView+Frame,UIView+AutoLayout…合并为一个

删除无用的方法和类。

3)多用Swift Structs,因为Swfit Structs是静态分发的。

1.1.3 Initializers

通常,我们会在+load方法中进行method-swizzling,但这会影响应用启动的时间。

1)用initialize替代load。不少同学喜欢用method-swizzling来实现AOP去做日志统计等内容,强烈建议改为在initialize进行初始化。

2)减少atribute((constructor))的使用,而是在第一次访问的时候才用dispatch_once等方式初始化。

3)不要创建线程

4)使用Swfit重写代码。

1.2 main()函数之后:

优化的核心思想:能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。

我们首先来分析下,从main函数开始执行,到你的第一个界面显示,这期间一般会做哪些事情。

  • 执行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions,applicationDidBecomeActive,

  • 初始化第三方skd

  • 初始化Window,初始化基础的ViewController

  • 获取数据(Local DB/Network),展示给用户。

在这个过程中我们可以借助工具来进行检测

  • 知道这个过程后,可以借助Time Profiler工具查找具体的耗时模块,几点要注意:

    • 分析启动时间,一般只关心主线程

    •  选择Hide System Libraries和Invert Call Tree,这样我们能专注于自己的代码

    •  右侧可以看到详细的调用堆栈信息

  • 另外,也可以借用C语言函数查看模块运行时间:

CFTimeInterval startTime = CACurrentMediaTime();
//执行方法
CFTimeInterval endTime = CACurrentMediaTime();

当检测出耗时的模块时,就可以按照优化的核心思想来进行处理了。即:

能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。

2. 页面刷新滚动流畅度

在优化流程度前需要先了解下iOS页面的成像过程。

2.1 CPU(Central Processing Unit,中央处理器):

对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)

2.2 GPU(Graphics Processing Unit,图形处理器):

纹理的渲染

2.3 成像过程:

CPU计算信息,GPU渲染信息到帧缓存区(iOS是双缓存机制,有前帧缓存、后帧缓存),视频控制器从帧缓存中读取信息显示到屏幕上。

2.4 造成卡顿的原因:

按照60FPS的刷帧率,每隔16ms就会有一次VSync信号,VSync信号来的时候就需要从帧缓存区中取缓存显示到屏幕上,如果每次VSync信号来的时候CPU和GPU没有处理好信息渲染到缓存区,那么就会从缓存中拿之前缓存的显示,就造成了丢帧,丢帧多了就会造成卡顿。

2.5 检测卡顿

平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作,这里检测的有两个方案:

  • Instruments中的coreAnimation工具,查看刷帧率,最理想最高的是60fps

  • 可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的
    这个可以借助第三方框架(github上很多),如:LXDAppFluecyMonitor、JPFPSStatus

2.6 解决卡顿

尽可能减少CPU、GPU资源消耗

2.6.1 优化

  • 优化CPU

    • 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView

    • 不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改

    • 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性

    • Autolayout会比直接设置frame消耗更多的CPU资源

    • 图片的size最好刚好跟UIImageView的size保持一致

    • 控制一下线程的最大并发数量

    • 尽量把耗时的操作放到子线程:如文本处理(尺寸计算、绘制);图片处理(解码、绘制)

  • 优化GPU

    • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示

    • 尽量减少视图数量和层次

    • 减少透明的视图(alpha<1),不透明的就设置opaque为YES

    • 尽量避免出现离屏渲染

  • 避免离屏渲染

在OpenGL中,GPU有2种渲染方式:

    • On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作;

    • Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作

    • 离屏渲染消耗性能的原因

    • 需要创建新的缓冲区

    • 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕

    • 会造成离屏渲染的有:

    • 光栅化,layer.shouldRasterize = YES

    • 遮罩,layer.mask

    • 圆角,同时设置layer.masksToBounds = YES、layer.cornerRadius大于(考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片)

    • 阴影,layer.shadowXXX,如果设置了layer.shadowPath就不会产生离屏渲染

3. 耗电量

3.1 应用耗电的主要来源有:

  • CPU处理,Processing

  • 网络,Networking

  • 定位,Location

  • 图像,Graphics

3.2 耗电优化:

  • 尽可能降低CPU、GPU功耗

  • 少用定时器

  • 优化I/O操作

    • 尽量不要频繁写入小数据,最好批量一次性写入

    • 读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问

    • 数据量比较大的,建议使用数据库(比如SQLite、CoreData)

  • 网络优化

    • 减少、压缩网络数据

    • 如果多次请求的结果是相同的,尽量使用缓存

    • 使用断点续传,否则网络不稳定时可能多次传输相同的内容

    • 网络不可用时,不要尝试执行网络请求

    • 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间

    • 批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载

  • 定位优化

    • 如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成后,会自动让定位硬件断电

    • 如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务

    • 尽量降低定位精度,比如尽量不要使用精度最高的kCLLocationAccuracyBest

    • 需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新

    • 尽量不要使用startMonitoringSignificantLocationChanges,优先考虑startMonitoringForRegion:

  • 硬件检测优化

    • 用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件

4. 安装包瘦身

安装包(IPA)主要由可执行文件、资源组成,因此对于iOS安装包的瘦身也将从这两个方面进行

4.1 资源(图片、音频、视频等)

  • 采取无损压缩

  • 去除没有用到的资源

4.2 可执行文件瘦身

  • 编译器优化

    • Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default设置为YES

    • 去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions设置为NO, Other C Flags添加-fno-exceptions

  • 利用AppCode(www.jetbrains.com/objc/)检测未使用的代码:菜单栏 -> Code -> Inspect Code

  • 编写LLVM插件检测出重复代码、未被调用的代码

  • 生成LinkMap文件,可以查看可执行文件的具体组成,哪些文件偏大

    • 可借助第三方工具解析LinkMap文件:github.com/huanxsd/Lin…

4.3 bitcode

xcode7之后多了这样的一个设置,默认是打开的。打开bitcode设置后,编译出来的包不是最终的二进制包而是bitcode中间码,Apple会根据编译器、应用设备来优化bitcode来给你最终最最优化的二进制应用包。这样避免了苹果更新了编译器或硬件设备时再提交app包到appstore的问题。同时也享受到了编译器改进带来的好处。

但是有个坑的地方,有些第三方库并不支持bitcode,如果要使用对应的第三方库就得关闭这个bitcode。由于时间太久,已经忘了当时是哪些第三方库不支持了,不知道现在有没有支持。

5. 其他:

  • Facebook 和 Pinterest 维护的 ASDK 可对视图的渲染进行优化,具体可参考这篇博客

  • 网络请求优化:

    • 网络请求数据缓存:针对于时效性比较长的可以做缓存,在请求的时候在有效期内直接获取此信息

    • 网络请求次数优化:请求开始、取消、回调之前做限制-------AOP面向切片编程

青山不改,绿水长流,感谢大家支持,希望这篇文章能帮助到你!!

转自:掘金-会飞的金鱼

链接:https://juejin.cn/post/7056447904189251598

-End-

最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

点击👆卡片,关注后回复【面试题】即可获取

在看点这里好文分享给更多人↓↓

以上是关于Android马甲包的那些事儿的主要内容,如果未能解决你的问题,请参考以下文章

android环境搭建中的那些事儿

Android 子线程更新UI那些事儿

Android Handler那些事儿,消息屏障?IdelHandler?ANR?

Android默认头像那些事儿

Android默认头像那些事儿

Android+Handler+Thread 那些事儿