Iterator 元素的删除

0x81 问题情景

有一个链式列表LinkedList,我们需要对其中的元素进行遍历,如果遇到符合条件的元素,则从LinkedList中删除该元素,为了方便遍历我们通常使用forE的方式进行遍历,满足条件再remove掉元素:

1
2
3
4
5
6
for (RequirementBean requirementBean : requirementBeanList) {
if (TextUtils.equals(requirementBean.getUuid(),
(String) cardView.getTag())) {
requirementBeanList.remove(requirementBean);
}
}

但是执行这段代码很不幸的触发了异常ConcurrentModificationException。

异常结果

0x82 异常原因

首先我们要明白forE的遍历机制,其实它的内部实现就是通过迭代器Iterator实现的,也就是说它本身也是Collection.iterator()后使用iterator.next()的方式遍历元素。关于Iterator网上的解释是Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。

Read More

Android O Font Support

0x81 早期的字体支持

在平时的开发当中,我们通常不会去手动干预应用的字体,因为使用系统的字体时,如果系统的字体发生改变,相应的我们的APP也会跟着改变。可是有的时候产品会有特殊的需求,我们需要保持整体的界面风格或者在某些位置我们需要使用特殊风格的艺术字体,更有甚者使用iconfont作为app的图标来源,在这种状况下我们就需要修改我们应用内的字体从而达到我们想要的效果。

在Android O之前的平台,字体一直是作为普通资源存在的,它不像Drawable中的图片拥有自己的resId从而通过R文件的方式引用它。Google推荐我们将字体放入Android的静态资源目录assets下,然后通过Typeface提供的方法createFromAsset获取Typeface对象并设置给TextView:

1
2
Typeface typeFace = Typeface.createFromAsset(context.getAssets(), "number.otf");
setTypeface(typeFace);

这种方式是在Java带马当中进行设置,在xml中通常是没有效果的,xml的typeface和fontFamily的支持也有限,我们有时候会使用自定义属性来自定义字体的处理。

所幸,在Android O之后,Google终于又将字体支持提上了台面。

0x82 Android Studio 3.0 对字体的支持

Android Studio已经可以直接预览字体文件,并且提供了对font资源的支持,我们只需要将字体文件放入res/font资源文件夹,AS会为字体文件生成资源ID,但是这个字体文件还不推荐直接使用,我们需要创建一个xml资源描述文件来描述一个fontFamily才能在程序中使用它:

1
2
3
4
5
6
7
8
9
10
11
12
<font-family xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<font
android:font="@font/number"
android:fontStyle="normal"
android:fontWeight="400"
app:font="@font/number"
app:fontStyle="normal"
app:fontWeight="400"
tools:targetApi="o" />
</font-family>

上面描述了一个字重400普通风格的number字体,其中app命名空间是为了向后兼容,其中对于字重的信息可以去看维基百科。

对于Java代码中字体资源的使Google提供了新的ResourceCompat类来帮助我们返回Typeface:

1
2
Typeface typeFace = ResourcesCompat.getFont(context, R.font.number_font);
setTypeface(typeFace);

Read More

PopupWindow中使用Fragment的坑

0x81 产品需求

项目中存在一个弹出窗口,其内部有很多筛选条件用于筛选,后来这个筛选条件分了一个大组,变成了具体到组内的筛选条件,这样就必然要进行View结构上的更改。因为之前是用PopupWindow做的,新需求又是要分页切换筛选,我首先想到干脆扔个ViewPager加个FragmentPagerAdapter,把SupportFragmentManager传进去就可以了,这样原来的筛选View复用,省不少事。后来发现我果然想得太简单了,遇到了十分熟悉的一个异常java.lang.IllegalArgumentException。这个异常通常指代参数非法,比如使用了一个根本不存在的参数,而具体到错误描述,居然是No view found for id 0x7f0d0136(com.fionera.base:id/vp_filter_container) for Fragment...,意思是Android根据id并没有找到对应的View。

0x82 问题原因

根据描述No view found for id 0x7f0d0136(com.fionera.base:id/vp_filter_container) for Fragment...,源码中异常抛出的部分如下:

1
2
3
4
5
6
7
ViewGroup container = null;  
if (f.mContainerId != 0) {
container = (ViewGroup)mContainer.onFindViewById(f.mContainerId);
if (container == null && !f.mRestored) {
throwException(new IllegalArgumentException("No view found for id 0x" + Integer.toHexString(f.mContainerId) + " (" + f.getResources().getResourceName(f.mContainerId) + ") for fragment " + f));
}
}

我们可以发现,在通过FragmentManager为Fragment寻找Container的时候发生了container变量为空,从而触发了异常,而这个f.mContainerId就是ViewPager的id。由此可以猜测,使用FragmentManager的Transition应该也会触发这个错误(待验证)。现在核心的问题就在于FragmentManager的mContainer.onFindViewById(f.mContainerId)返回null,这意味着mContainer中没有这个id的控件,根据平时开发的理解,mContainer通常是Activity或Fragment中指定的(我们通过AppCompatActivity#getSupportFragmentManager或v4.Fragment#getChildFragmentManager对Fragment进行管理),而PopupWindow是一个非常独特的东西,它的ContentView通常是独立的,虽然它使用了Activity的Window,但是在Activity中无法获取PopupWindow中的View(指的是没办法在Activity的根View中通过findViewById找到,Dialog也一样且Dialog有自己的Window),所以FragmentManager中是拿不到View的,自然会抛出异常。也就是说,在所有没有直接attach在RootView上的自定义View中使用Fragment都会出现这个问题,因为它附加Container是我们没办法控制的。

0x83 解决方案

  1. 使用PagerAdapter

    我们可以不使用Fragemnt*PagerAdapter等涉及到FragmentManager的Adapter,而是使用最原始的PagerAdapter,这样我们便可以自己控制View,将原本的Fragment切换成有状态保护的自定义View即可。

  2. 使用DialogFragment

    其实Dialog如果不做处理也会出现这种问题,Google提供了DialogFragment这一方案,它比Dialog更灵活,还具有Fragment的特性,还能通过tag让FragmentManager进行缓存。但是有一点需要自行处理,就是DialogFragment的展示行为形同Dialog,诸如PopupWindow的showAsDropDown可能要自己进行具体的实现。

这个问题遇到的人可能不多,因为很少会有这种情况,StackOverFlow上有关的讨论也不多,支持较多的都是替换为DialogFragment。说句题外话,我觉得BottomSheet更适合实现这种需求,但是原型设计的事情我们说的往往不算:(

使用 RxLifecycle

[2017-07-21修改]

0x80 “解除订阅”

经过进一步的使用和Github上README的解释,Rxlifecycle实际上并没有真正的解除订阅关系,而只是终止了事件流,如若需要显式地解除订阅(比如doOnUnsubscribe/doOnDispose的操作)可能仍然需要手动解除。另一方面,如果事件流已经停止并且没有强引用的对象存在,当发生GC时变为DISPOSED状态的Disposable最终会被回收。

0x81 响应式编程

RxJava 为编程带来的便利性毋庸置疑,它把我这种编码猴子从复杂的多线程编程和回调地狱中解救出来。
流式的业务处理让原本复杂的多线程逻辑变得符合人类思维,事件发起者(被关注者)Observable 可以立即或随时的发送事件,订阅者接受事件进行处理。
一旦订阅者与被订阅的者建立联系Subscribtion,如果此时不想接受事件的订阅,还可以进行解除订阅。
在Android 编程当中,很多元素都有生命周期的概念,一个个钩子都与元素挂钩,而一个元素要实现某些功能通常都是在固定的生命周期当中,这便引出了生命周期与订阅的关系。
当然,我们可以手动处理这种关系,但是这种模板式的关系约束,完全可以抽象出来,自动管理Observable 的订阅与“解除订阅”,这便是RxLifecycle。

0x82 RxLifecycle

RxLifecycle 是一个基于RxJava 开发的生命周期管理库,它能按配置自动管理Observable的订阅和解除。
要使用它只需要在build.gradle中添加依赖:

1
2
3
4
// RxLifecycle
compile 'com.trello:rxlifecycle:1.0' [1]
compile 'com.trello:rxlifecycle-android:1.0' [2]
compile 'com.trello:rxlifecycle-components:1.0' [3]

其中[1]是基础库,[2]是对Android支持,[3]是RxActivity、RxFragment等组件。

Read More

利用Build Variant启用Stetho

0x81 Stetho

之前项目中使用了OkHttp作为网络请求工具,为了方便跟踪调试,我在项目中使用了Stetho这个调试工具来抓包。Stetho很强大,由Facebook出品,它构建了一个Debug Bridge让Chrome Developer Tools附到了Android应用上,这样便可以使用CDT对App的网络请求进行抓包并跟踪。著名的移动数据库Realm也提供了Stetho的插件方便开发者观察Realm数据库数据。

Stetho的集成方式很简单:

1
2
3
4
dependencies {
implemetation 'com.facebook.stetho:stetho:1.5.0'
implemetation 'com.facebook.stetho:stetho-okhttp3:1.5.0'
}

之后在Application中初始化:

1
2
3
Stetho.initializeWithDefaults(this);
// OkHttp3 Helper
new OkHttpClient.Builder().addNetworkInterceptor(new StethoInterceptor()).build();

0x82 只在Debug模式下启用Stetho

我们都知道,开发时的辅助工具我们在发布版本时都是要去除的,冗余代码不说,所有切入式的调试工具都会影响App的性能。因此我们要保证只在Debug模式下编译代码到App并启用,而发布模式需要排除代码并去除初始化的Api调用。

最直接的办法,就是在发包时手动去除代码,移除依赖。当然Build Variant的支持下,我们可以使用Gradle强大的依赖管理来完成这件事情:

1
2
3
4
dependencies {
debugImplemetation 'com.facebook.stetho:stetho:1.5.0'
debugImplemetation 'com.facebook.stetho:stetho-okhttp3:1.5.0'
}

通过这种方式我们就能保证只有在debug下才会依赖Stetho,然后利用Build Variant分离代码的特点,在Debug的文件下添加初始化Api的调用,而Release不写就可以达到目的,我们便不需要再手动去管理打包的依赖问题。

分离式的工程结构

本地aar通过module进行依赖

0x81 不支持本地aar的插件

Android Studio 3.0.0 canary 之后的更新使用了新的Gradle API,对原本的DSL配置作出了诸多改变,之前的升级到gradle-plugin 3.0.0也参照Google的官方文档讲述了如何迁移到Plugin 3.0.0。在Android Studio 进行一次次更新之后,到了canery5这个版本,该插件的行为再次发生了很大的变化,flavorSelection不能在外层定义以及Local Jars In Libraries带来的变更。这其中最坑的一个问题就是Local AAR 文件将不能被正确依赖:While using this plugin with Android Studio, dependencies on local AAR files are not yet supported.

0x82 曲线救国——aar module

很多人在gradle的repo下提了issue,也表示会出现类似cannot resolve *@xxx dependencies的错误Gradle 4.1 M1 break Android local aar import,有人提出了一个临时的解决办法,将aar作为module提供依赖支持,通过Android Studio 的添加新module的方法,选择到入aar文件,即可根据aar文件生成一个library,该module的build.gradle文件内容如下:

1
2
configurations.maybeCreate("default")
artifacts.add("default", file('ptr_lib-1.0.0.aar'))

目录结构

这样配置过后,移除原本的flatLibs{}配置并将implementation换成library便能成功处理依赖了。

配置webpack-dev-server

0x81 webpack配置文件

webpack作为一个模块化工具,在提供打包功能时我们通常不会直接调用webpack命令去生成目标文件。因为开发过程中代码是频繁变化的,如果我们手动去处理将会增加非常多的无意义的体力劳动,因此虽然webpack提供的默认配置文件webpack.config.js文件已经帮我们省了很多事了,但这仍然是不够的。对于这种情况webpack2官方由一个构建脚本模板,模板没有特殊需求可以直接拿来用,它启动一个基于express的小型server来观察文件变化自动编译并热交换,这样我们就能在开发当中“所见即所得”。

0x82 webpack-dev-server

在最开始学习使用webpack的时候,官方推荐了辅助开发工具,其实这个工具就是express的一个小封装,与webpack的配置相辅相成,webpack-dev-server会主动调用webpack.config.js作为webpack配置文件,而webpack本身也提供devServer选项用于配置webpack-dev-server。

Read More

webpack3集成vux的坑

0x81 使用Vux

最近一段时间又忙的要死,每每年中就是重灾期,感觉自己忙的手忙脚乱。值得庆幸的是在忙碌之中自己仍旧在坚持在学习道路上,由于公司起了一个比较大的合作项目,并打算投入使用Vue作为javascript框架并最终选用了Vux作为组件库简化开发,由于刚从webpack1转到webpack3,我这两天在手动集成vux上废了不少时间。

官方关于如何使用vux提供了两种方案——初始化模板和手动集成。

  1. 模板工程
    这种比较适合以Vux新起的项目,它的内部使用webpack2作为模板,已经处理好了Vux的配置,可以直接开始使用。

  2. 手动集成
    这种适合需要使用自己webpack配置的工程,通过按照文档的描述完成配置来引入Vux组件库。

0x82 rules和loaders对vux-loader的影响

webpack从1到2很大的一个变化就是对于加载器的支持方式,webpack1中的loaders被更换为更加灵活的rules,而vux-loader就以module中是否存在rules来判断开发者使用的webpack版本:

1
2
3
4
5
6
7
8
9
10
// check webpack version by module.loaders
let isWebpack2

if (typeof vuxConfig.options.isWebpack2 !== 'undefined') {
isWebpack2 = vuxConfig.options.isWebpack2
} else if (oldConfig.module && oldConfig.module.rules) {
isWebpack2 = true
} else if (oldConfig.module && oldConfig.module.loaders) {
isWebpack2 = false
}

当然这只是判断条件之一,后面还根据package.json配置文件进一步判断。

上面这种判断方式导致会出现一个问题,就是webpack2其实为了兼容性仍旧保留了loaders语法,如果不是使用webpack2推荐的方式进行配置,就可能导致vux-loader认为开发者使用的是webpack1从而配置失败。

Read More

从webpack1迁移到webpack2

0x81 webpack 3.0.0的发布

webpack是一个模块化构建工具,它与一般的构建任务工具gulp不同,它不仅能完成构建任务,webpack还可以完成项目的模块化功能,从而更方便的处理项目中的依赖关系,并且在构建打包时可以根据依赖图高效的增量构建。JS的依赖管理其实有非常多种实现方式,如Node.js的CommonJS方式、AMD/CMD等等,但是这些依赖管理方式要么过于繁琐要么不支持异步,因此ECMAScript6规范了import语法来像python一样管理依赖。虽然官方规范很好,但是浏览器的支持程度是一步步推进的,在平常的开发当中我们仍然使用es5语法,但是webapck默认实现了import/export的依赖管理方式,如果项目要使用其他es6语法,则需要babel等转换工具的支持,而webpack有相应的loader来做无缝的转换。

webpack 3.0.0于几天前发布,其实它相对于webpack2的变化不是很大,更多的是内部的更新,对开发者而言broken api较少,不需要做webpack1向webpack2那样大的迁移变动。由于我从接触webpack开始就是1.x,并且用的比较简单也没遇到什么问题,所以年初发布了2.x之后很意外的没有立即向webpack2迁移(虽然官方给出了迁移文档),如今3.0.0已经发布,我觉得是时候开始用webpack3了,所以这篇文章就是根据官方的文档进行迁移。

0x82 迁移过程

  1. resolve的变更
    在webpack1中我的输出配置是这么写的:

    1
    2
    3
    4
    output: {
    - path: 'dist'
    + path: path.resolve(__dirname, 'dist')
    }

    意为webpack打包后输出到相对目录dist下,更新后提示path不能使用相对路径,需修改成绝对路径。根据官方文档的说法,resolve.root, resolve.fallback, resolve.modulesDirectoriesresolve.modules取代:

    1
    2
    3
    4
    5
    resolve: {
    - root: path.join(__dirname, "src")
    + modules: [
    + path.join(__dirname, "src"),
    }
  2. loaders的变更
    loader是webpack如此强大的一个很重要原因,webpack本身只能依赖和打包js文件,但是有了各种各样的loader支持,我们将可以处理任何类型的文件。

    webpack2使用了新的rules系统,原本的module.loaders依然可用来保证兼容性,但是官方推荐使用新的命名约定。

    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
    - preLoaders: [
    - {
    - test: /\.js$/,
    - loader: 'eslint',
    - exclude: /node_modules/
    - }
    - ],
    - loaders: [
    + rules: [
    + {
    + test: /\.js$/,
    + loader: 'eslint',
    + enforce: 'pre',
    + exclude: /node_modules/
    + },
    {
    test: /\.js$/,
    - loader: 'babel',
    + loader: 'babel-loader',
    exclude: /node_modules/
    },
    {
    test: /\.css$/,
    - loader: 'style-loader!css-loader'
    + use: ['style-loader', 'css-loader']
    },
    {
    test: /\.vue(\?[^?]+)?$/,
    - loaders: []
    + use: []
    },
    {
    test: /\.gif$/,
    - loader: 'file',
    + loader: 'file-loader',
    exclude: /node_modules/
    }
    ]

    loader系统的变更可以说的非常大的变更了,虽然webpack2保留了兼容语法,但是我们不知道什么时候会被移除。上面的例子中一些重要的变更基本上都有了,其中很重要的一点preLoaders和postLoaders被移除使用loaders属性替代,还有一点要注意就是loader不会再自动添加’-loader’后缀了,官方这么做是为了避免混淆。
    PS:json-loader已经默认存在于webpack当中,不再需要手动添加。

  3. BannerPlugin的变更
    BannerPlugin不再支持双参数banner和options:

    1
    2
    3
    4
    plugins: [
    - new webpack.BannerPlugin('Banner', {raw: true, entryOnly: true});
    + new webpack.BannerPlugin({banner: 'Banner', raw: true, entryOnly: true});
    ]
  4. 其他变更
    主要是内置插件的变更以及对Promise异步的支持,更多的参阅官方文档。

迁移的过程就这些,比向AndroidGradlePlugin3的迁移容易得多。

读书笔记:ELF文件结构

0x81 ELF文件的构成

Linux最常见的文件就是ELF文件了,无论是目标文件、动态链接库文件还是可执行文件,都是ELF文件,它与微软Windows的PE文件类似,都是COFF的一种实现,按照某种规则存储程序代码和数据。ELF文件本身的构成简单又复杂,在之前的文章中已经简单的陈述了ELF文件可能包含的结构,在这里我们略去复杂繁琐的部分,把其中最重要的部分提取出来:ELF文件头、各个段及段表、重定位表、字符串表以及符号表等等。

0x82 ELF文件头

要分析ELF文件,就需要使用我们之前提到的readelf工具,它能帮我们分析出文件中的信息。其中ELF最开始的部分就是ELF文件头,它包括了一部分基本信息如ELF版本号和程序入口地址等等。我们使用readelf -h {filename}命令查看一下ELF文件头信息:

SimpleSection.o的文件头信息

从上面的信息我们可以很容易的看出ELF文件的魔数和其基本信息,这些信息当中的所有数据结构都定义在/usr/include/elf.h文件当中。对于Magic Number,这是一个固定长度为16个字节的部分,对应结构体Elf32_Ehdr中的e_ident成员,其中第1个字节0x7f为ELF文件标记,紧接着三个字节0x454c46对应’E’’L’’F’三个字母,第5个字节代表ELF字长(0x00无效0x01为32位0x02为64位),第6个字节为字节序(0x00无效0x01小端0x02大端),然后一个字节是ELF版本号基本为1,其余9个字节保留补0。

Read More