Android Weekly Notes Issue #224

Android Weekly Issue #224

September 25th, 2016
Android Weekly Issue #224html

本期内容包括: Google Play的pre-launch报告; Wear的Complications API; Android Handler解析; RxAndroid; 测量性能的库: Pury; 方法数限制; APK内容分析; Redux for Android; 一种view形成的泄露; 注解处理; 更好的Adapter; Intro屏等等.前端

ARTICLES & TUTORIALS

Apk的pre-launch报告 Awesome pre-launch reports for Alpha/Beta APK's

Google Play team在I/O 2016的时候宣布了不少新features, 其中有一个pre-launch report.java

这个report是干什么的呢, 它会报告在一些设备上测试你的应用的时候发现的issues.android

要生成这种报告, 你应该在Developer console上enable它. 而后上传alpha/beta apk. 上传到beta channel以后, 5-10分钟就会生成报告.git

报告主要包括三个部分:github

  • Crashes
  • Screenshots
  • Security

官方文档: pre-launch数据库

Wear Complications API

在钟表的定义里, complications是指表上除了小时和分钟指示以外其余的东西.apache

在Android Wear里面咱们已经有一些complications的例子, 好比向用户显示计步器, 天气预报, 下一个会议时间等等.redux

可是以前有一个很大的限制就是每个小应用都必须实现本身的逻辑来取数据, 好比有两个应用都取了今天的天气预报信息, 将会有两套机制取一样的数据, 这明显是一种浪费.c#

Android Wear 2.0推出了Complications API解决了这个问题.

通讯主要是Data providersWatch faces之间的, 前者包含取数据的逻辑, 后者负责显示.

Complications API定义了一些Complications Types, 见官方文档.

做者在他朋友的开源应用里用了新的API: Memento-Namedays, 这个应用是生日或者日期提醒类的.

首先, 做者用Wearable Data Layer API同步了手机和手表的数据. 而后在Wear module里继承ComplicationProviderService建立了complication data provider, 这里就提供了onComplicationActivated, onComplicationDeactivated, onComplicationUpdate等回调.

用户也能够点击Complications, 能够用setTapAction()指定点击后要启动的Activity.

能够指定ComplicationProviderService的更新频率, 是在manifest里用这个key:
android.support.wearable.complications.UPDATE_PERIOD_SECONDS.

更新得太频繁会比较费电.
须要注意的是这并非一个常量, 由于系统也会根据手机的情况进行一些调节, 没必要要的时候就不须要频繁更新.

本文做者采用的方式是用ProviderUpdateRequester. 在manifest里面设置0.

ComponentName providerComponentName = new ComponentName(
    context,
    MyComplicationProviderService.class
);
ProviderUpdateRequester providerUpdateRequester = new
    ProviderUpdateRequester(context, providerComponentName);
providerUpdateRequester.requestUpdateAll();

最后, 这里是官网文档:
Complications.

这里是做者PR: PR

Android Handler Internals

首先, 做者举了一个简单的例子, 用两种方法, 用Handler来实现下载图片并显示到ImageView上的过程.

主要是由于网络请求须要在非UI线程, 而View操做须要在UI线程. Handler就用来在这两种线程之间切换调度.

Handler的组成

  • Handler
  • Message
  • Message Queue
  • Looper

Handler

Handler是线程间消息传递的直接接口, 生产者和消费者线程都是经过调用下面的操做和Handler交互:

  • creating, inserting, removing Messages from Message Queue.
  • processing Messages on the consumer thread.

每个Handler都是和一个Looper和一个Message Queue关联的. 有两种方法来建立一个Handler:

  • 用默认构造器, 将会使用当前线程的Looper.
  • 显式地指明要用的Looper.

Handler不能没有Looper, 若是构造时没有指明Looper, 当前线程也没有Looper, 那么将会抛出异常.

由于Handler须要Looper中的消息队列.

一个线程上的多个Handler共享同一个消息队列, 由于它们共享同一个Looper.

Message

Message是一个包含任意数据的容器, 它包含的数据信息是callback, data bundle和obj/arg1/arg2, 还有三个附加数据what, time和target.

能够调用Handler的obtainMessage()方法来建立Message, 这样message是从message pool中取出的, target会自动设置成Handler本身. 因此直接能够在后面调用sendToTarget()方法.

Message pool是一个最大尺寸为50的LinkedList. 当消息被处理完以后, 会放回pool, 而且重置全部字段.

当咱们使用Handler来post(Runnable)的时候, 其实是隐式地建立一个Message, 它的callback存这个Runnable.

Message Queue

Message Queue 是一个无边界的LinkedList, 元素是Message对象. 它按照时间顺序来插入Message, 因此timestamp最小的最早分发.

MessageQueue中有一个dispatch barrier表示当前时间, 当message的timestamp小于当前时间时, 被分发和处理.

Handler提供了一些方法在发message的时候设置不一样的时间戳:

sendMessageDelayed(): 当前时间 + delay时间.

sendMessageAtFrontOfQueue(): 把时间戳设为0, 不建议使用.

sendMessageAtTime().

Handler常常须要和UI交互, 可能会引用Activity, 因此也常常会引发内存泄漏.
做者举了两个例子, 略.

须要注意:
非静态内部类会持有外部类实例引用.
Message会持有Handler引用, 主线程的Looper和MessageQueue在程序运行期间是一直存在的.

建议的是, 内部类用static修饰, 另用WeakReference.

Debug Tips
显示Looper中dispatched的Messages:

final Looper looper = getMainLooper();
looper.setMessageLogging(new LogPrinter(Log.DEBUG, "Looper"));

显示MessageQueue中和handler相关的pending messages:

handler.dump(new LogPrinter(Log.DEBUG, "Handler"), "");

Looper

Looper 从消息队列中读取消息, 而后分发给target handler. 每当一个Message穿过了dispatch barrier, 它就能够在下一个消息循环中被Looper读.

一个线程只能关联一个Looper. 由于Looper类中有一个静态的ThreadLocal对象保证了只有一个Looper和线程关联, 企图再加一个就会抛出异常.

调用Looper.quit()会当即终止Looper, 丢弃全部消息.
Looper.quitSafely()会将已经经过dispatch barrier的消息处理了, 只丢弃pending的消息.

Looper是在Thread的run()方法里setup的, Looper.prepare()会检查是否以前存在一个Looper和这个线程关联, 若是有则抛异常, 没有则创建一个新的Looper对象, 建立一个新的MessageQueue. 见代码.

如今Handler能够接收或者发送消息到MessageQueue了. 执行Looper.loop()方法将会开始从队列读出消息. 每个loop迭代都会取出下一个消息.

Crunching RxAndroid - Part 10 细细咀嚼RxAndroid

做者这个是个系列文章, 本文是part 10.

Android的listener不少, 咱们能够经过RxJava把listener都变成发射信息的源, 而后咱们subscribe.

本文举例讲了Observable.fromCallable()Observable.fromAsync()方法的用法.

Pury a new way to profile your Android application

在作任何优化以前咱们都应该先定位问题. 首先是收集性能数据, 若是收集到的信息超过了能够接受的阈值, 咱们再进一步深究, 找到引发问题的方法或者API.

幸运的是, 有一些工具能够帮咱们profiling:

  • Hugo@DebugLog注解来标记方法, 而后参数, 返回值, 执行时间都会log出来.
  • Android Studio toolset. 好比System Trace, 很是准确, 提供了不少信息, 可是须要你花时间来收集和分析数据.
  • 后台解决方案, 好比JMeter, 它们提供了不少功能, 须要花时间来学习如何使用, 第二就是高并发profile也不是常见的需求.

Missing tool

关于咱们关心的应用的速度问题, 大多数能够分为两种:

  • 特定方法和API的执行时间, 这个能够被Hugo cover.
  • 两个事件之间的时间, 这多是独立的两段代码, 可是在逻辑上关联. Android Studio toolset能够cover这种, 可是你须要花不少时间来作profile.

做者意识到下面的需求没有被知足:

  • 开始和结束profiling应该是被两个独立的事件触发的, 这样才能够知足咱们灵活性的需求.
  • 若是咱们想监控performance, 仅仅开始和结束事件是不够的. 有时候咱们须要知道这之间发生了什么, 这些阶段信息应该被放在一个报告里, 让咱们更容易明白和分享数据.
  • 有时候咱们须要作重复操做, 好比loading RecyclerView的下一页, 那么一个回合的操做显然是不够的, 咱们须要进行屡次操做, 而后显示统计数据, 好比平均值, 最小最大值.

基于上面的需求, 做者建立了Pury.

Introduction to Pury

Pury是一个profiling的库, 用于测量多个独立事件之间的时间.
事件能够经过注解或者方法调用来触发, 一个scenario的全部事件被放在同一个报告里.

而后做者举了两个例子, 一个用来测量启动时间, 另外一个用来测量loading pages.

Inner structure and limitations

性能测量是Profilers作的, 每个Profiler包含一个list, 里面是Runs. 多个Profilers能够并行运行, 可是每一个Profiler中只有一个Run是active的.

Profiling with Pury

Pury能够测量多个独立事件之间的时间, 事件能够用注解或者方法调用触发.
基本的注解有: @StartProfiling, @StopProfiling, @MethodProfiling

方法:

Pury.startProfiling();

Pury.stopProfiling();

最后做者介绍了一些使用细节.
项目地址: Pury

处理方法数限制问题 Dealing With the 65K Methods limit on Android

做为Android开发, 你可能会看到过这种信息:

Too many field references: 88974; max is 65536.
You may try using –multi-dex option.

首先, 为何会存在65k的方法数限制呢?

Android应用是放在APK文件里的, 这里面包含了可执行的二进制码文件(DEX - Dalvik Executable), 里面包含了让app工做的代码.

DEX规范限制了单个的DEX文件中的方法总数最大为65535, 包括了Android framework方法, library方法, 还有你本身代码中的方法. 若是超过了这个限制你将不得不配置你的app来生成多个DEX文件(multidex configuration).

可是开启了multidex配置以后有一些随机性的兼容问题, 因此咱们在决定开启multidex以前, 首先采起的第一步是减小方法数来避免这个问题.

在咱们开始改动以前, 先提出了这些问题:

  • 咱们有多少方法?
  • 这些方法都是从哪里来?
  • 主要的方法来源是谁?
  • 咱们真的须要全部这些方法吗?

在搜寻这些问题的答案的过程当中, 咱们发现了一些有用的工具和tips:

MethodsCount.com 将会告诉你一个库有多少方法, 还提供了每一个方法的依赖.

JakeWharton/dex-method-list utility 能够显示.apk, .aar, .dex, .jar或.class文件中的全部方法引用. 这能够用来发现一个库中到底有多少方法是被你的app使用了.

mihaip/dex-method-counts 这个工具能够按包来输出方法, 计算出一个DEX文件中的方法数而后按包来分组输出. 这有利于咱们明白哪些库是方法数的主要来源.

Gradle build system 提供了关于项目结构颇有价值的信息. 一个有用的task是dependencies, 让你看到库的依赖树, 这样你就能够看到重复的依赖, 进而删除它们来减小方法数.

Classyshark 是一个Android可执行文件的浏览器. 用这个工具你能够打开Android的可执行文件(.jar, .class, .apk, .dex, .so, .aar, 和Android XML)来分析它的内容.

apk-method-count 这是一个工具, 用来快速地查apk中的方法数, 拖拽apk以后就会获得结果.

What's in the APK APK中有什么

APK: Android application package 是Android系统的一种文件格式, 其实是一种压缩文件, 若是把.apk重命名为.zip, 就能够取出其内容.

可是此时咱们直接在文本编辑器打开AndroidManifest.xml的时候看到的全是机器码.

固然是有工具来帮咱们分析这些东西的, 这个工具从一开始就有, 那就是aapt, 它是Android Build Tool的一部分.

aapt - Android Asset Packaging Tool 这个工具能够用来查看和增删apk中的文件, 打包资源, 研究PNG文件等等.

它的位置在: <path_to_android_sdk>/build-tools/<build_tool_version_such_as_24.0.2>/aapt.

aapt能作的事情, 从man能够看出:

  • aapt list - Listing contents of a ZIP, JAR or APK file.
  • aapt dump - Dumping specific information from an APK file.
  • aapt package - Packaging Android resources.
  • aapt remove - Removing files from a ZIP, JAR or APK file.
  • aapt add - Adding files to a ZIP, JAR or APK file.
  • aapt crunch - Crunching PNG files.

用这个工具来分析咱们的apk:

输出基本信息:
aapt dump badging app-debug.apk

输出声明的权限:
aapt dump permissions app-debug.apk

输出配置:
aapt dump configurations app-debug.apk

还有其余这些:

# Print the resource table from the APK.
aapt dump resources app-debug.apk

# Print the compiled xmls in the given assets.
aapt dump xmltree app-debug.apk

# Print the strings of the given compiled xml assets.
aapt dump xmlstrings app-debug.apk

# List contents of Zip-compatible archive.
aapt list -v -a  app-debug.apk

Reductor - Redux for Android

Redux是一个当前JavaScript中很火的构架模式. Reductor把它的概念借鉴到了Java和Android中.

关于状态管理到底有什么好方法呢, 做者想到了前端开发中的SPA(Single-page application), 和Android应用很像, 有没有什么可借鉴的呢? 答案是有.

Redux 是一个JavaScript应用的可预测的状态容器, 能够用下面三个基本原则来描述:

  • 单一的真相来源
  • 状态只读
  • 变化是纯函数形成的

Redux的灵感来源有FluxElm Architecture.
强烈建议阅读一下它的文档.

Reductor是做者用Java又实现了一次Redux.

做者用了一个Todo app的例子来讲明如何使用, 以及它的好处.

做者先写了一个naive的实现, 而后不断地举出它的缺点, 而后改进它.

其中做者用到了pcollection来实现persistent/immutable的集合.

最后还把代码改成对测试友好的.

Android leak pattern: subscriptions in views

开始做者举了一个例子, 一个自定义View, subscribe了Authenticator单例的username变化事件, 从而更新UI.

public class HeaderView extends FrameLayout {
  private final Authenticator authenticator;

  public HeaderView(Context context, AttributeSet attrs) {...}

  @Override protected void onFinishInflate() {
    final TextView usernameView = (TextView) findViewById(R.id.username);
    authenticator.username().subscribe(new Action1<String>() {
      @Override public void call(String username) {
        usernameView.setText(username);
      }
    });
  }
}

可是代码存在一个主要的问题: 咱们历来没有unsubscribe. 这样匿名内部类对象就持有外部类对象, 整个view hierarchy就泄露了, 不能被GC.

为了解决这个问题, 在View的onDetachedFromWindow()回调里调用unsubscribe().

做者觉得这样解决了问题, 可是并无, 仍是检测出了泄露, 而且做者发现View的onAttachedToWindow()onDetachedFromWindow()都没有被调用.

做者研究了onAttachedToWindow()的调用时机:

  • When a view is added to a parent view with a window, onAttachedToWindow() is called immediately, from addView().
  • When a view is added to a parent view with no window, onAttachedToWindow() will be called when that parent is attached to a window.

而做者的布局是在Activity的onCreate()里面setContentView()设置的.
这时候每个View都收到了View.onFinishInflate()回调, 却没有调View.onAttachedToWindow().

View.onAttachedToWindow() is called on the first view traversal, sometime after Activity.onStart().

onStart()方法是否是每次都会调用呢? 不是的, 若是咱们在onCreate()里面调用了finish(), onDestroy()会当即执行, 而不通过其中的其余生命周期回调.

明白了这个原理以后, 做者的改进是把订阅放在了View.onAttachedToWindow()里, 这样就不会泄露了. 对称老是好的.

Annotation Processing in Android Studio 注解和其处理器

做者用例子说明了如何自定义注解和其处理器, 让被标记的类自动成为Parcelable的.
看了这个有助于理解各类依赖和了解相关的目录结构.

建议使用: android-apt.

Parcelable.
相关库代码: aitorvs/auto-parcel.

Writing Better Adapters 写出更好的Adapter

在Android应用中, 常常须要展现List, 那就须要一个Adapter来持有数据.

RecyclerView的基本操做是: 建立一个view, 而后这个ViewHolder显示view数据; 把这个ViewHolder和adapter持有的数据绑定, 一般是一个model classes的list.

当数据类型只有一种时, 实现很简单, 不容易出错. 可是当要显示的数据有不少种时, 就变得复杂起来.

首先你须要覆写:

override fun getItemViewType(position: Int) : Int

默认是返回0, 实现之后把不一样的type转换为不一样的整型值.

而后你须要覆写:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder

为每一种type建立一个ViewHolder.

第三步是:

override fun onBindViewHolder(holder: ViewHolder, position: Int): Any

这里没有type参数.

The Uglyness
好像看起来没有什么问题?
让咱们从新看getItemViewType()这个方法. 系统须要给每个position都对应一个type, 因此你可能会写出这样的代码:

if (things.get(position) is Duck) {
    return TYPE_DUCK
} else if (things.get(position) is Mouse) {
    return TYPE_MOUSE
}

这很丑不是吗?

若是你的ViewHolder没有一个共同的基类, 在binding的时候也是这么丑:

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    val thing = things.get(position)
    if (thing is Animal) {
        (holder as AnimalViewHolder).bind(thing as Animal)
    } else if (thing is Car) {
        (holder as CarViewHolder).bind(thing as Car)
    }
...
}

不少的instance-of和强制类型转换, 它们都是code smells. 违反了不少软件设计的原则, 而且当咱们想要新添一种类型时, 须要改动不少方法. 咱们的目标是添加新类型的时候不用更改Adapter以前的代码.
开闭原则: Open for Extension, Closed for Modification.

Let's Fix It
用一个map来查询? 很差.
把type放在model里? 很差.

解决问题的一种办法是: 加入ViewModel, 做为中间层.

可是若是你不想建立不少的ViewModel类, 还有其余的办法: Visitor模式

interface Visitable {
    fun type(typeFactory: TypeFactory) : Int
}

interface Animal : Visitable
interface Car : Visitable

class Mouse: Animal {
    override fun type(typeFactory: TypeFactory)
        = typeFactory.type(this)
}

工厂:

interface TypeFactory {
    fun type(duck: Duck): Int
    fun type(mouse: Mouse): Int
    fun type(dog: Dog): Int
    fun type(car: Car): Int
}

返回对应的id:

class TypeFactoryForList : TypeFactory {
    override fun type(duck: Duck) = R.layout.duck
    override fun type(mouse: Mouse) = R.layout.mouse
    override fun type(dog: Dog) = R.layout.dog
    override fun type(car: Car) = R.layout.car

Material Intro Screen for Android Apps

如今有两个主流的libraries为Android 应用提供了好看的intro screens, 可是感受并非很好用, 因此做者他们发布了一个新的欢迎界面的库TangoAgency/material-intro-screen, 好用易扩展.

Testing Legacy Code: Hidden Dependencies

本文讨论God Object, Blob, 这种很大的类和方法, 作了不少事情. 若是你想要重构, 先加点测试, 也发现很难, 由于它的依赖太多了, 作了太多事情.

首先, 实例化:
加set方法, 让数据库依赖抽离出来, 这样测试的时候能够传一个Fake的进去.

第二, 更多依赖:
把UserManger和网络请求等依赖也抽为成员变量, 加上set方法或者构造参数, 这样在测试的时候易于把mock的东西传进去.

第三, 清理: 要牢记单一职能原则, 进行职能拆分.

最后, 现实: 清理是一个持续化的过程, 得一步一步来, 有时候小步的改动会帮助你发现另外须要改动的地方.

LIBRARIES & CODE

EncryptedPreferences

AES-256加密的SharedPreferences.

Pury

报告多个不一样事件之间的时间, 可用于性能测量.

Floating-Navigation-View

Floating Action Button, 展开后是一个NavigationView.

Material Intro Screen

易用易扩展的欢迎界面.

SPECIALS

Huge list of useful resources for Android development

资源分享, 包括博客论坛Video社区等等.

相关文章
相关标签/搜索