伍中联(Vanda)的博客

当一切理所当然就没有你什么事了 当你说:“这个好麻烦,那么不要犹豫,主动出击”

0%

背景

这个是源于项目中为了减少DNS的解析时间,同时又能够使用Chromium的net库。

源码分析

Cronet本身是编译了net库,同时提供了一个已经封装好的Java Api,不得不说Google考虑还是很周到的,但Cronet本身提供的Api接口比较少,通过json的配置又不能动态的去更新,所以就需要在dns解析的时候来获取应用层的已经解析的dns列表。

所以,通过java的源码,CronetUrlRequestContext.Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@UsedByReflection("CronetEngine.java")
public CronetUrlRequestContext(final CronetEngineBuilderImpl builder) {
mNetworkQualityEstimatorEnabled = builder.networkQualityEstimatorEnabled();
CronetLibraryLoader.ensureInitialized(builder.getContext(), builder);
if (!IntegratedModeState.INTEGRATED_MODE_ENABLED) {
nativeSetMinLogLevel(getLoggingLevel());
}
if (builder.httpCacheMode() == HttpCacheType.DISK) {
mInUseStoragePath = builder.storagePath();
synchronized (sInUseStoragePaths) {
if (!sInUseStoragePaths.add(mInUseStoragePath)) {
throw new IllegalStateException("Disk cache storage path already in use");
}
}
} else {
mInUseStoragePath = null;
}
synchronized (mLock) {
mUrlRequestContextAdapter =
nativeCreateRequestContextAdapter(createNativeUrlRequestContextConfig(builder));
if (mUrlRequestContextAdapter == 0) {
throw new NullPointerException("Context Adapter creation failed.");
}
}

// Init native Chromium URLRequestContext on init thread.
CronetLibraryLoader.postToInitThread(new Runnable() {
@Override
public void run() {
CronetLibraryLoader.ensureInitializedOnInitThread();
synchronized (mLock) {
// mUrlRequestContextAdapter is guaranteed to exist until
// initialization on init and network threads completes and
// initNetworkThread is called back on network thread.
nativeInitRequestContextOnInitThread(mUrlRequestContextAdapter);
}
}
});
}

Api层通过CronetEngineBuilder 来构造所提供的参数,然后设置到Jni层。

1
CronetLibraryLoader.ensureInitialized(builder.getContext(), builder);

这个先确定库的初始化。 然后在create native 的Config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
synchronized (mLock) {
mUrlRequestContextAdapter =
nativeCreateRequestContextAdapter(createNativeUrlRequestContextConfig(builder));
if (mUrlRequestContextAdapter == 0) {
throw new NullPointerException("Context Adapter creation failed.");
}
}

@VisibleForTesting
public static long createNativeUrlRequestContextConfig(CronetEngineBuilderImpl builder) {
final long urlRequestContextConfig = nativeCreateRequestContextConfig(
builder.getUserAgent(), builder.storagePath(), builder.quicEnabled(),
builder.getDefaultQuicUserAgentId(), builder.http2Enabled(),
builder.brotliEnabled(), builder.cacheDisabled(), builder.httpCacheMode(),
builder.httpCacheMaxSize(), builder.experimentalOptions(),
builder.mockCertVerifier(), builder.networkQualityEstimatorEnabled(),
builder.publicKeyPinningBypassForLocalTrustAnchorsEnabled(),
builder.threadPriority(Process.THREAD_PRIORITY_BACKGROUND));
for (CronetEngineBuilderImpl.QuicHint quicHint : builder.quicHints()) {
nativeAddQuicHint(urlRequestContextConfig, quicHint.mHost, quicHint.mPort,
quicHint.mAlternatePort);
}
for (CronetEngineBuilderImpl.Pkp pkp : builder.publicKeyPins()) {
nativeAddPkp(urlRequestContextConfig, pkp.mHost, pkp.mHashes, pkp.mIncludeSubdomains,
pkp.mExpirationDate.getTime());
}
return urlRequestContextConfig;
}

优先是先创建 nativeCreateRequestContextConfig 创建配置,我们就可以延着这个native
调用栈来查看native处理流程,SDK有一行注释代码

1
2
3
4
5
6
7
// Native methods are implemented in cronet_url_request_context_adapter.cc.
private static native long nativeCreateRequestContextConfig(String userAgent,
String storagePath, boolean quicEnabled, String quicUserAgentId, boolean http2Enabled,
boolean brotliEnabled, boolean disableCache, int httpCacheMode, long httpCacheMaxSize,
String experimentalOptions, long mockCertVerifier,
boolean enableNetworkQualityEstimator,
boolean bypassPublicKeyPinningForLocalTrustAnchors, int networkThreadPriority);

cronet_url_request_context_adapter.cc 就是入口了,我们查看下这个类,找到如下函数:

1
// Create a URLRequestContextConfig from the given parameters.
static jlong JNI_CronetUrlRequestContext_CreateRequestContextConfig(
    JNIEnv* env,
    const JavaParamRef<jstring>& juser_agent,
    const JavaParamRef<jstring>& jstorage_path,
    jboolean jquic_enabled,
    const JavaParamRef<jstring>& jquic_default_user_agent_id,
    jboolean jhttp2_enabled,
    jboolean jbrotli_enabled,
    jboolean jdisable_cache,
    jint jhttp_cache_mode,
    jlong jhttp_cache_max_size,
    const JavaParamRef<jstring>& jexperimental_quic_connection_options,
    jlong jmock_cert_verifier,
    jboolean jenable_network_quality_estimator,
    jboolean jbypass_public_key_pinning_for_local_trust_anchors,
    jint jnetwork_thread_priority) {
  return reinterpret_cast<jlong>(new URLRequestContextConfig(
      jquic_enabled,
      ConvertNullableJavaStringToUTF8(env, jquic_default_user_agent_id),
      jhttp2_enabled, jbrotli_enabled,
      static_cast<URLRequestContextConfig::HttpCacheType>(jhttp_cache_mode),
      jhttp_cache_max_size, jdisable_cache,
      ConvertNullableJavaStringToUTF8(env, jstorage_path),
      /* accept_languages */ std::string(),
      ConvertNullableJavaStringToUTF8(env, juser_agent),
      ConvertNullableJavaStringToUTF8(env,
                                      jexperimental_quic_connection_options),
      base::WrapUnique(
          reinterpret_cast<net::CertVerifier*>(jmock_cert_verifier)),
      jenable_network_quality_estimator,
      jbypass_public_key_pinning_for_local_trust_anchors,
      jnetwork_thread_priority >= -20 && jnetwork_thread_priority <= 19
          ? base::Optional<double>(jnetwork_thread_priority)
          : base::Optional<double>()));
}

这个是一个静态的函数,把Java端的参数传递到到URLRequestContextConfig这个类里面。 在Java层我们注意到这行代码:

1
nativeCreateRequestContextAdapter(createNativeUrlRequestContextConfig(builder));

也就是说创建的配置项会传递给创建出来的CreateRequestContextAdapter类,我们看看jni的CreateRequestContextAdapter类的构造函数:

1
CronetURLRequestContextAdapter::CronetURLRequestContextAdapter(
    std::unique_ptr<URLRequestContextConfig> context_config) {
  // Create context and pass ownership of |this| (self) to the context.
  std::unique_ptr<CronetURLRequestContextAdapter> self(this);
#if BUILDFLAG(INTEGRATED_MODE)
  // Create CronetURLRequestContext running in integrated network task runner.
  context_ =
      new CronetURLRequestContext(std::move(context_config), std::move(self),
                                  GetIntegratedModeNetworkTaskRunner());
#else
  context_ =
      new CronetURLRequestContext(std::move(context_config), std::move(self));
#endif
}

从CronetURLRequestContextAdapter的构造函数中我们能过看到参数是URLRequestContextConfig,通过参数创建出CronetURLRequestContext,然后通过context_的上下文来进行Start等操作,所以进行参数解析的是在CronetURLRequestContext中,我们就看看CronetURLRequestContext做了什么事情,入口是构造函数:

1
CronetURLRequestContext::CronetURLRequestContext(
    std::unique_ptr<URLRequestContextConfig> context_config,
    std::unique_ptr<Callback> callback,
    scoped_refptr<base::SingleThreadTaskRunner> network_task_runner)
    : default_load_flags_(
          net::LOAD_NORMAL |
          (context_config->load_disable_cache ? net::LOAD_DISABLE_CACHE : 0)),
      network_tasks_(
          new NetworkTasks(std::move(context_config), std::move(callback))),
      network_task_runner_(network_task_runner) {
  if (!network_task_runner_) {
    network_thread_ = std::make_unique<base::Thread>("network");
    base::Thread::Options options;
    options.message_loop_type = base::MessageLoop::TYPE_IO;
    network_thread_->StartWithOptions(options);
    network_task_runner_ = network_thread_->task_runner();
  }
}

context_config 作为NetworkTasks的参数,我们看看NetworkTasks,不难发现NetworkTasks中正是进行参数转换的地方

1
void CronetURLRequestContext::NetworkTasks::Initialize(
    scoped_refptr<base::SingleThreadTaskRunner> network_task_runner,
    scoped_refptr<base::SequencedTaskRunner> file_task_runner,
    std::unique_ptr<net::ProxyConfigService> proxy_config_service) {
  DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_);
  DCHECK(!is_context_initialized_);

  std::unique_ptr<URLRequestContextConfig> config(std::move(context_config_));
  network_task_runner_ = network_task_runner;
  if (config->network_thread_priority)
    SetNetworkThreadPriorityOnNetworkThread(
        config->network_thread_priority.value());
  base::DisallowBlocking();
  net::URLRequestContextBuilder context_builder;
  context_builder.set_network_delegate(
      std::make_unique<BasicNetworkDelegate>());
  context_builder.set_net_log(g_net_log.Get().net_log());

  context_builder.set_proxy_resolution_service(
      cronet::CreateProxyResolutionService(std::move(proxy_config_service),
                                           g_net_log.Get().net_log()));

  config->ConfigureURLRequestContextBuilder(&context_builder,
                                            g_net_log.Get().net_log());                                            

通过config来构建出 net::URLRequestContextBuilder,我们看看net::URLRequestContextBuilder的组成,其中有一行:

1
// By default host_resolver is constructed with CreateDefaultResolver.
  void set_host_resolver(std::unique_ptr<HostResolver> host_resolver);

到这里我们就知道,其实CronetURLRequestContext 通过URLRequestContextConfig 来构建自己的URLRequestContextBuilder,URLRequestContextBuilder中包含了设置的所有参数。

在回到我们的问题,我们是要解决自定义dns解析,而Java层没有接口来提供可选的设置项。在Java层可以通过设置ExperimentalOptions来配置不同的参数来开启功能,但这个一旦设置了不能动态更改。在url_request_context_config.cc 的 ParseAndSetExperimentalOptions 中有一段代码如下:

1
if (async_dns_enable || stale_dns_enable || host_resolver_rules_enable ||
      disable_ipv6_on_wifi) {
    CHECK(net_log) << "All DNS-related experiments require NetLog.";
    std::unique_ptr<net::HostResolver> host_resolver;
    if (stale_dns_enable) {
      DCHECK(!disable_ipv6_on_wifi);
      host_resolver.reset(new StaleHostResolver(
          net::HostResolver::CreateDefaultResolverImpl(net_log),
          stale_dns_options));
    } else {
      host_resolver = net::HostResolver::CreateDefaultResolver(net_log);
    }
    if (disable_ipv6_on_wifi)
      host_resolver->SetNoIPv6OnWifi(true);
    if (async_dns_enable)
      host_resolver->SetDnsClientEnabled(true);
    if (host_resolver_rules_enable) {
      std::unique_ptr<net::MappedHostResolver> remapped_resolver(
          new net::MappedHostResolver(std::move(host_resolver)));
      remapped_resolver->SetRulesFromString(host_resolver_rules_string);
      host_resolver = std::move(remapped_resolver);
    }
    context_builder->set_host_resolver(std::move(host_resolver));
  }

以上代码的作用就是在开启不同的值时可以提供域名对应的值,但这个只是局限于首次加载配置选项,后面无法更改。

HostResolver域名解析

HostResolver是域名解析的服务的接口,我们先看看这个接口做了什么事情。

1
virtual int Resolve(const RequestInfo& info,
                      RequestPriority priority,
                      AddressList* addresses,
                      CompletionOnceCallback callback,
                      std::unique_ptr<Request>* out_req,
                      const NetLogWithSource& net_log) = 0;                                      

这个方法的目的是将解析的ip存储到addresses中,所以,我们就能过在这个地方加入Java端的dns解析

##前言

由于项目中需要使用验证QUIC协议,针对目前的QUIC协议的客户端支持,唯一的选择就是Chromium的Net库,所以方便后续的功能定制开发,就花了大部分时间来进行Net模块的编译和开发。

一些坑的感受

Chromium作为一个庞大的工程,其文档和工具应该都非常完善,所以需要开发Chromium最好的就是Chromium本身提供的说明文档。

Chromium工程和依赖非常大,我这边去到了25GB之多,这还是不包括系统本身的依赖。

Chromium本身不支持Mac下的编译,所以我选择了使用Ubuntu来进行编译开发

Chromium

站点:Chromium

代码搜索功能:代码搜索

代码的搜索功能是非常有用的功能,能够快速的找到相关的代码引用,当然如果自己去搭建也是可以的(目前我已经编译出Ubuntu的Chromium和Android的Chromium,能够顺利的进行debug,后续文章介绍如果操作)

针对Android的代码获取

这里面有详细的操作流程

获取代码

获取代码需要能够访问Google的网络,由于不具备续传,所以断了就GG了。

Install depot_tools

获取depot_tools repository:

1
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

添加depot_tools到PATH

1
export PATH="$PATH:/path/to/depot_tools"

获取代码

1
2
mkdir ~/chromium && cd ~/chromium
fetch --no-history android

这样会快一点,另外这样的是最新的版本,如果需要切换到指定的版本,

1
git fetch origin tag 73.0.3657.0

这个步骤会比较漫长,耐心等待吧。。。

之后就参考官方的说明就可以了。

编译Cronet模块

Chromium 采用 gn + ninja来构建的,通过一些配置选项来支持构建的。
如果clone下代码后并且已经安装好依赖后,Cronet模块位于

1
src/components/cronet

这个是针对Android和iOS提供了net模块的支持,android文件夹里面有jni模块和Java层的api方法,jni不需要自己手动去编译,Chromium是通过注解来自动生成的。

需要编译这个也很简单,在这个目录中有一个编译介绍的文档,我这边贴出来, 很详细了吧

Cronet build instructions

[TOC]

Checking out the code

Follow all the
Get the Code
instructions for your target platform up to and including running hooks.

Building Cronet for development and debugging

To build Cronet for development and debugging purposes:

First, gn is used to create ninja files targeting the intended platform, then
ninja executes the ninja files to run the build.

Android / iOS builds

$ ./components/cronet/tools/cr_cronet.py gn --out_dir=out/Cronet

If the build host is Linux, Android binaries will be built. If the build host is
macOS, iOS binaries will be built.

Note: these commands clobber output of previously executed gn commands in
out/Cronet. If --out_dir is left out, the output directory defaults to
out/Debug for debug builds and out/Release for release builds (see below).

If --x86 option is specified, then a native library is built for Intel x86
architecture, and the output directory defaults to out/Debug-x86 if
unspecified. This can be useful for running on mobile emulators.

Desktop builds (targets the current OS)

TODO(caraitto): Specify how to target Chrome OS and Fuchsia.

gn gen out/Cronet

Running the ninja files

Now, use the generated ninja files to execute the build against the
cronet_package build target:

$ ninja -C out/Cronet cronet_package

Building Cronet mobile for releases

To build Cronet with optimizations and with debug information stripped out:

$ ./components/cronet/tools/cr_cronet.py gn --release
$ ninja -C out/Release cronet_package

Note: these commands clobber output of previously executed gn commands in
out/Release.

Building for other architectures

By default ARMv7 32-bit executables are generated. To generate executables
targeting other architectures modify cr_cronet.py‘s
gn_args variable to include:

  • For ARMv8 64-bit: target_cpu="arm64"
  • For x86 32-bit: target_cpu="x86"
  • For x86 64-bit: target_cpu="x64"

Alternatively you can run gn args {out_dir} and modify arguments in the editor
that comes up. This has advantage of not changing cr_cronet.py.

时间线

6.15 正式从给我成长、对我帮助最大的阿里巴巴手淘离职,从UC浏览器到夸克浏览器再到总部的手淘,经历了不同部门不同风格的团队。非常感恩我遇到的这几个部门的老大,让我至今仍然热爱、回味在阿里的这段时光。

为什么离开

在很多人眼中,进入阿里巴巴集团总部淘宝技术部是很多人的梦想,说真的我也非常高兴能够去到总部,那时起我也没有想过要离开,我甚至把社保公积金都全部转移到杭州,确实是打算在杭州定居。
去到杭州的这段时间,遇到很多事情,把宝宝接到身边、购买了人生中的第一辆轿车、遇到了一群牛逼轰轰的人、当然我也遇到很难过的事情,17年是我最努力的一年,以3.75的S1绩效转岗到手淘,但年终绩效的回报却是我在阿里最不理想的一次(最终绩效并不是手淘评的),但17年的努力让我快速成长,在功能设计、SDK、抽象、内聚、架构等等都有很大的提高,非常感谢志明老大,充分的信任和指导。那时候发了一个朋友圈,然后很多猎头朋友找,我也就试试看的态度,我也慢慢的在思考自己方向。

常问问自己想要什么

我们往往不知道自己想要什么,或者不知道想要成为什么样的人,所以我们就会迷惘。
离开时景宝老大约我一起吃了个饭,意味深长的和我说有没有想清楚自己想要什么。

非常感谢景宝老大,教会我思考问题的方式。人啊,其实就是追逐着自己想要什么。

新的机会 – 面向国外市场的游戏社交

经历了过亿级的UC浏览器和手淘,从0-1参与夸克浏览器的开发,独立完成夸克浏览的核心功能,这些过程经历让我成长,从充满压力到满满的收获,最后才会发现自己原来也能够做好以及能够做的更好。

相对印尼的这些国家的情况,我觉得游戏社交是一个蛮不错的东西,自己也爱玩游戏,早些年自己也爱玩一些小游戏,加上社交的属性,我个人比较看好这个,所以就过来这边做技术相关的工作

转变

以前都喜欢去专研技术,但现在担任了技术负责人,慢慢的发现自己没有那么多的精力去亲力亲为,所以我这个时候就会花时间帮助组内同学提高技术、用自己的经验和数据来驱动、找技术应用场景来实施落地,多和组内同学沟通,听取他们的观点和想法。

功能应用周期

在这段时间里,我慢慢理解,不同的同学特性都是不一样的,没必要要求每个人都一样,相互学习和借鉴都需要一个过程,好的东西大家慢慢适应使用就会知道这些东西好在哪里。

优化后的成果

总体来说效果还是不错的

  1. 全量编译从原来的2-3分钟,优化后再1分钟以内
  2. 增量编译从原来的1分钟,优化后在13s, 甚至更快
优化前的clean后的全量编译时间(2m40s):

优化前的增量编译时间(49s):

代码已经执行

优化后的clean后的全量编译时间(47s):

优化后的增量编译时间(6s):

代码已经执行

优化手段

优化的目的并不是改造原有的编译流程,而是遵循Google的本身的构建流程,方便Google的后续功能优化的升级我们能够快速的相应

耗时点分布

不同App使用的工具不同,也可能存在分化,大体的解决思路是一致的,在遵循Google的原则上,我们需要针对本身App的特性进行优化。保持AS原本的功能不变的原则。

我们可以通过执行命令来分析不同的task的耗时情况

1
2
./gradlew assembleDebug --profile

通过这个可以查看任务的耗时情况,其实AS的build中也能够查看相关的任务耗时信息。

工程耗时原因:

  1. 注解使用,编译时动态生成和插入代码,导致使用的地方每次都会进行代码插入,从而导致进行Multidex 消耗大量时间
  2. 工程中model数据激增,目前有31个model
  3. 优化配置没有进行合理的配置,比如针对本地开发可以简化一些配置

升级Gradle

目前我们gradle 升级到了3.2.1

注解进行增量

Gradle 4.7 已经支持了注解的增量,目前需要注解框架已经在陆续支持增量,所有找到问题,去解决这个并不是难事。

优化配置

目前工程中都是遵循Gradle的本身的编译原则,为了解决本地编译耗时的问题,我们本地编译和线上打包编译是不同的规则,线上打包就是标准的Gradle的流程。本地编译进行了相关的改造

AS本身的配置,把内存使用等各个功能都配置上,比如:

1
2
3
4
5
6
7
8
9
10
# 编译时使用守护进程
org.gradle.daemon=true
#JVM最大允许分配的堆内存,按需分配
org.gradle.jvmargs=-Xmx8192m -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#使用并行编译
org.gradle.parallel=true
org.gradle.configureondemand=true
# 缓存的是javacompiler编译生成的 class,没有cache transform插入的代码,暂时先关闭 cache,后续再研究优化
org.gradle.caching=true

本地编译时,主工程的build.gradle 增加一个api21的Flavors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (isLocalBuild()) {
aaptOptions {
cruncherEnabled = false
}
}

dexOptions {
javaMaxHeapSize "2g"
jumboMode true
incremental true
preDexLibraries true
}

if (isLocalBuild()) {
flavorDimensions "api"
productFlavors {
minApi21 {
minSdkVersion '21'
dimension "api"
}
}
}

本地动态的aar工程

基本思路是,将本地的model全部通过脚本来生成aar供本地编译来使用,减少重复编译aar的时间。工程中只保留一个main工程,不同的业务开发同学在动态生成的配置文件中配置需要开发开启的model

编译

目前将项目工程中只保留了main的代码工程模块,其他的model都以本地Maven的形式存在

首次运行工程

  1. 本地编译默认是采用本地maven仓库的形式,需要执行项目脚本生成 本地arr文件,执行 ./bmaList.sh
  2. 针对自身所在的model进行业务开发,这个需要开启对应的业务开发model,在文件 aar_configs.properties 中的
1
2
open_model_name = framework-base,framework-ui  

填写自身对应的model,注意没有双引号

将model发布成对应的本地maven arr的形式,操作步骤

  1. 首先需要在settings.gradle 中 projectList 中的,移动到 aarList
  2. 在需要发布的model的build.gradle 中添加maven插件
1
apply from: rootProject.getProjectDir().getAbsolutePath() + '/publish.gradle'
  1. 需要在bmaList.sh 中添加,注意添加的顺序,需要在本身model依赖的其他aar对应的model的后面进行添加

单独model的aar发布

项目中提供了单独的model的arr的发布脚本 bma.sh

  1. 执行 ./bma.sh model_name

发布完成后 进行gradle同步

开启所有的model

目前为了方便大家的开发,存在需要开启所有model的情况,这种情况我们进行了简易的支持

在 aar_configs.properties 中配置 open_model_name = –all

然后同步工程

总结

本次的编译速度的优化,是一次尝试。在实际操作过程中,很多同学不习惯去使用,比如开启对应的model,要生成aar等操作相对会繁琐一些。

所以后续我们提供了开启所有的model的配置,尽量做到最大化的适应,对于目前不习惯的同学可以开启所有的model,相应的就是增量编译速度慢一些。

对于熟悉、已经适应的同学可以开启相应的model来开发,提高自己的开发效率。

总结下在kotlin常使用的一些东西和含义

val与var

1
2
3
val 是常量
var 是变量

我们经常能够遇到这个,kotlin提供了很好的编译检查功能,其中 ? 有一些常用的地方

1
private var name: String? = null

这个表示 变量 name 能够为空值

1
2
val video = Video.create()
video?.play()

这个说明如果video 不为空,那么就会调用play方法

1
2
fun name(name:String?) {
}

这里面告诉使用者,name有可能是一个空值

1
2
fun name(name:String?):String? {
}

这里面告诉使用者,name方法返回可能是一个空值

!!

使用的过程中也经常遇到

1
2
val video = Video.create()
video!!.play()

我们可以写 video!! ,这会返回一个非空的video b 值 或者如果 video 为空,就会抛出一个 NPE 异常

::

1
startActivity(Intent(this@KotlinActivity, MainActivity::class.java))

得到类的Class对象

?:

一般存在2中写法,

1
val a: Int = if (b != null) b.length else -1 

在kotlin中 条件表达式具备返回值,上面的写法我们可以用Elvis操作符(?:)来简写

1
val a: Int = b?.length ?: -1 

这样是不是简单了很多呢

== 与 ===

== 判断值是否相等。
=== 判断值及引用是否完全相等。

1
2
3
4
val num = 128
val a:Int? = num
val b:Int? = num
println("a == b = ${a == b} \n a === b = ${a === b}")

a == b = true
a === b = false

a 和 b 都是一个常量,引用的地址当然是不一样的

_

这个一般是选择忽略返回值

1
2
3
data class Book(var id: Int, var name: String)
val book = Book(0, "vanda")
val (id, _) = book

VandaDownloader开源

基于独立设计的浏览器下载原型的特性,重新设计、重构代码结构,使用Kotlin重新编写,开源地址:

VandaDownloader

简易流程图

Demo

常规下载 & 读写分离

常见的文件下载手段其中一项是读写分离,也就是说利用空间换时间的方式来增加效率。

常规下载

一般来说,常见的文件下载方式如下:

这种文件的下载方式也是我们常见的简单的文件下载,其中存在一些不足,

  1. 单线程进行文件的下载
  2. 网络IO和文件IO处于同一线程中,进行网络数据读取时,文件IO就处于block状态,进行文件IO时网络IO处于block状态,时间利用不高效
  3. 下载状态的回调也不应该有频繁的线程切换,回调不应该是文件下载线程直接进行回调
读写分离

读写分离的大致模型如下:

读写分离的主要目的有几点:

  1. 网络IO与文件IO相互不影响,不会block,当前线程负责网络数据的读取,没有文件IO的操作,当前线程网络数据读取就是最高效的
  2. 文件IO全局采用单个线程,避免了系统资源的浪费,同时避免了多个线程同时进行IO是操作系统调度带来的资源竞争。
  3. 每次读取segment时可以提供任务进度回调,而这个时候并没有进行文件IO操作,我们把数据保存在内存中
    segment是一个双向链式结构,并且提供SegmentPool来缓存segment,避免系统GC和申请byte时的zero-fill。,这些数据块使用链表进行管理,这可以仅通过移动“指针”就进行数据的管理,而不用真正去处理数据,而且对扩容来说也十分方便。它对数据进行了分块处理,这样在大数据IO的时候可以以块为单位进行IO,这可以提高IO的吞吐率。

多Type需求

产品原型

1
最近在完成一个类型信息流产品,涉及到很多type类型

技术支撑

1
在技术层面上,通过RecyclerViewViewHolder,抽象业务逻辑,结合不同的type来注册不同的ViewHolder,然后各自ViewHolder处理对应样式的逻辑和业务

抽象业务

主要有两部分,一部分是支持多type本身的Adapter的抽象,一部分是由数据驱动列表的数据解析,同时包含后端的数据构建。

数据原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"data": {
"list": [
{
"cardType": "cardType_1",
"description": "",
"pic": "",
"title": "",
"id": "14499",
"count": "1"
},
{
"cardType": "cardType_2",
"video": "",
"videoUrl": "",
"musicList": [
],
"price": "",
"salePrice": "120"
},
{
"cardType": "cardType_3",
"advertising": "",
"advertisingTitle": "",
"advertisingUrl": [
],
"advertisingPrice": ""
}
]
}
}

典型的信息流中包含的cardtype样式会有很多种,每种都会相应的对应一种数据,然后把不同的数据生成一个json的列表,每个item是不同的对象和字段,这样我们很难使用json工具来反射这个list,所以需要自己去解析list元素所属卡片的数据格式。

Adapter抽象

正常的写法是,我们需要自己去继承Adapter,然后编写一个对应的ViewHolder,然后写逻辑,这样代码耦合度高,往往需要写很多代码,包括处理业务逻辑。

抽象思想:

1
2
一个列表中包含了不同的类型的卡片,每个卡片对应自己的数据对象,每个卡片都需要绑定自己的数据对象和逻辑,也就是说ViewHolder + Data.class 组成一个个体

上图中已经标注出相应的模式。

理想中的使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GridLayoutManager layoutManager = new GridLayoutManager(getContext(), SPAN_COUNT);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (items.size() > 0) {
Object object = items.get(position);
return (object instanceof MusicData ? SPAN_COUNT : 1;
}
return 1;
}
});

mRecyclerView.setLayoutManager(layoutManager);

adapter = new MultiAdapter();
adapter.register(MusicData.class, new MusicViewBinder());
adapter.register(VideoData.class, new VideoViewBinder());
adapter.register(MovVideoData.class, new MovViewBinder());
adapter.setItems(items);
mRecyclerView.setAdapter(adapter);

数据解析模块:

1

关于Android 的fitsSystemWindows

今天在写一个嵌套滚动的时候将fitsSystemWindows属性设置了下,大概如下

1
fitsSystemWindows = true

这个就是说明留出一个状态栏的高度

默认是false

这个对一些放置在状态栏下面的布局有点作用

产品需求

应用背景的高斯模糊

bitmap matrix RenderScript

自己实现,bitmap matrx

粗略的Google了下,Android系统提供了一套实现,调用和简单,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static Bitmap apply(Context context, Bitmap sentBitmap, int radius) {
final Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
final RenderScript rs = RenderScript.create(context);
final Allocation input = Allocation.createFromBitmap(rs, sentBitmap, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
final Allocation output = Allocation.createTyped(rs, input.getType());
final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));

script.setRadius(radius);
script.setInput(input);
script.forEach(output);
output.copyTo(bitmap);

sentBitmap.recycle();
rs.destroy();
input.destroy();
output.destroy();
script.destroy();

return bitmap;
}

其中radius最大值是25,模糊度一般。

处理如下图耗时66ms

自己实现

不限制radius,这里设置25,耗时48ms,效果图:

将radius设置为60,耗时101ms,达到设计要求,效果如图: