Blog


一次活见鬼的调试经历

本篇文章没有什么干货,仅是我的一次调试经历。

昨天在调试FIDO项目,运行时发现某个变量的值和我设置的不对。调试进去,几番折腾,吓呆了我:

img1
img2

仅仅经过一个赋值,就把赋值和被赋值的变量都改变了?我的所有知识都无法解释这个问题。

没办法,现实摆在眼前。只能通过一系列的实验排除问题。

  1. 我怀疑是外部变量的问题。于是我传入常量,问题依然存在。

  2. 我在赋值前进行clone(),入参不再改变。根据1和2,我觉得应该是该对象的变量出的问题。

  3. 我不再进行入参的赋值,而是在构造器直接赋值this.attestationType = new short[1]; this.attestationType[0] = 1;。结果:第一句赋值成功,第二句没有效果。

  4. 我不再在构造函数中赋值,而是另开一个方法单独赋值。问题依然存在。
    img3

  5. 我开始怀疑是不是注解的问题。@TagMember是一个RUNTIME注解,功能类似GSON,用于进行对象和特定数据格式(TLV)之间的转换。我把注解注释掉,问题不再复现!
    img4

  6. 我怀疑是注解里面的变量有问题。于是我把supportedExtensionIDs的注解复制到attestationType上。问题依然不再复现。

  7. 这就奇怪了,为什么注解里的值会影响变量本身呢?这和注解的原理和功能并不一致。为了防止“改动完又改回去,突然就可以了”这样的怪圈,我回归了一下原代码,问题重新出现。
    img5

  8. 接下来更奇怪的事情出现了。我把supportedExtensionIDs的值从null改回到{"1", "2"},问题不再出现!这个问题我在步骤1就已经测试过了,这是真的出现了“改动完又改回去,突然就可以了”的致命事件。。我被打懵了。。
    img6

  9. 我再改回null,问题又回来了。
    img7

  10. 我试着精简我的问题模型。我把其他变量都删掉了,只留下attestationType。问题依然存在。

  11. 继续关注刚刚注释注解的线索。我的其他变量都有注解,却偏偏只有这个变量出现了问题。由于步骤7的现象,我把attestationTypesupportedExtensionIDs的位置交换,并把注解的值也交换了。问题依然存在。

  12. 我把attestationType的类型修改为byte[]和int[]。赋值问题不再出现!于是我现在得到的结论是:被注解的short[]类型会出现赋值问题

  13. 为什么偏偏是short[]类型呢?我想的有点偏,我认为是注解的short tag()这个方法导致的问题。但修改TagMember这个tag又会影响太多代码,于是我拷贝了@TagMember类,并定义为@TagMember2。将attestationType的注解改为@TagMember2,并改为int tag()。问题消失了。我自豪的猜想:难道我遇到了JAVA编译器的问题?

  14. 就都麻袋,还不能太早下结论。我把@TagMember2改回short tag()。现在两个注解除了类名都一样了。运行,没有赋值问题。意思就是:和short tag()没有关系。

  15. 一样的注解,但是却不一样的结果?只有@TagMember才会出现问题,@TagMember2没有问题,但他们明明是一样的。@TagMember到底有什么特别之处呢?

  16. 哦,我想起来了。我在父类里面会对其进行反射调用,取值构造新数据结构(步骤4有提到)。而这个方法会通过toString()执行。

  17. caocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocao!!!!!!!!!!!一下子都想通了。

  18. 验证一下,我把父类的继承去掉,让该类继承Object。赋值问题消失。

  19. 我检查了一下toString()中我对short[]类型的处理,发现我在赋值的时候仅做了浅拷贝(也就是直接赋值),然后做了修改值的操作。而在debug过程中每一个单步调试IDE都会调用对象的toString()方法,而toString()方法会修改short[]类型的变量,所以出现了一开始的赋值问题。

  20. 关于步骤7里面的线索(supportedExtensionIDs会影响attestationType的赋值成功):是因为我的toString()方法里没有对String[]类型的处理分支。而在null的时候,我的处理会忽略这个变量,则流程继续走下去,把short[]的处理分支执行了。而在非null的时候,因为toString()无法处理,会直接报错,则没有走到处理short[]的分支。所以supportedExtensionIDs在非null的时候,attestationType的赋值不受影响。

  21. 关于步骤7和步骤1的同样环境不同结果的问题:最后我无法复现步骤1的情况,恐怕是调试期间我不小心改了哪里吧。。我认为不会是很值得研究的原因,解决了主要疑惑和次要疑惑,有时候该放手就放手吧。

  22. 我在整理文章的时候想通了步骤21的疑惑。是断点位置的问题。在后面的调试中,我把断点设在了构造函数末尾,则supportedExtensionIDs的值会影响到attestationType(因为在那个时候IDE才开始调用toString())。而如果是一步一步调试,则supportedExtensionIDs被赋值前attestationType已经被修改了。

  23. 步骤6的问题,是因为我刚好把步骤22所说的断电给移动了,导致了这个现象,其实和值并没有关系。

所以整个“活见鬼”的原因是这样的:

  1. 该类的父类重写了toString,toString里面会反射调用各个成员的值,组成一个新的数据格式。
  2. toString()中我对short[]类型的处理有问题,我没考虑浅拷贝的问题,在里面做一个修改值的行为,把原变量修改了。导致变量本身的值出现的变化。
  3. 在debug的每一次都会调用该变量的toString(),所以值在点击下一步的时候,就会被改变。
  4. 该问题一开始暴露在运行时,是因为运行时后面代码也有调toString(),所以造成了假象。如果我只是用打印的方法查询该变量值而不是debug,在运行时的构造函数中该值不会被改变。
  5. 所以表面看起来只是单步调试了一个赋值语句,而值就被变化了。深层结果是调试器调用了toString()导致的修改值行为。

本次经历给我的经验是:

  1. 对数组的赋值和操作,尤其是在构造函数时,应当进行拷贝而不是直接引用(数组直接clone()就可以了)。之前没因为浅拷贝遇到问题,所以一直没有在意。
  2. 一些莫名其妙的表象其实背后可能是清晰可推理的故障多米诺。我们要做的是,先找出复现和修复规律,然后慢慢精简问题模型,再通过控制变量法找到问题所在。而不是困与一时无法解释花哨表象,无法前进。其实想通之后,原来乱七八糟的bug现象都可以解释了。
  3. debug阶段可能会因为toString()方法影响真实运行时结果。

版权所有,转载请注明出处:
http://sickworm.com/?p=184




Android工程使用org.apache.commons.codec(commons-codec)库,运行时提示Base64.encodeBase64URLSafeString“java.lang.NoSuchMethodError”解决方法

最近工作是开发FIDO UAF项目。FIDO UAF是一个旨在提供身份验证通用方案,以代替繁杂密码记忆的一个方案,Google,阿里这些大头都是核心成员,感觉是用来未来代替密码的1号方案。可能是定义的太通用了,UAF目前应用面还不广。

UAF还有个兄弟叫U2F,是个更精简的方案。U2F是一个用硬件外设(token)代替密码的方案,有点像u盾。外国已经有相关设备,Google登录已经可以通过U2F设备进行验证。使用很方便:你在电脑插入一个U2F设备(初次需要绑定账户),再打开网页,Google就自动为你登录。腾讯也有做一个叫QKey的,很快就会面市了。U2F实用性比较强。

回到正题,我在开发过程中,同事找到了eBay的UAF实现开源代码。我稍微看了一下,看到他们的Base64编码是使用了Base64.encodeBase64URLSafeString这个方法。我试了一下,和普通的Base64编码不同的是把编码末尾的”=“号去掉了。这样的话Http的Get参数就不会出现问题。虽然UAF要求使用POST发送请求,但我看到大佬都这么做,我就跟着用吧。

这个方法在org.apache.commons.codec(commons-codec)中定义。我也下了这个库,放在我自己的项目中,并替换了原来的Base64实现。而稍后在我进行单元测试的时候出问题了。Android单元测试提示我找不到“Base64.encodeBase64URLSafeString”这个方法。

这就奇怪了,编译期没有报错,运行时报错?我一下子没想明白。我反编译commons-codec库,里面是有这个方法的。我在运行时用反射打印出来,没有这个方法。然后我用Java单元测试试了一下,Java单元测试通过。

那看来是Android运行环境的问题了。我上网找了一下,居然有和我一样问题的人:
java.lang.NoSuchMethodError: org.apache.commons.codec.binary.Base64.encodeBase64

里面写的很清楚,Android的framework引用了一个旧版的commons-codec(而这个旧版本没有这个方法)。这会导致后加载的同一名称的包无效(无法加载并覆盖)。所以我在运行时就找不到这个方法了。

解决方法

没办法。Java的类加载安全机制决定了我们没办法使用后加载的类。我想起在“码农翻身”公众号里有看过相关的文章。我找了一下,节选如下:

我感到前途未卜, 但也不能坐以待毙, 一定得多了解信息。
“大哥, 你叫什么名字” , 我看小个子还算和气。
“我就是大名鼎鼎的文件验证器了, 能管很多事”
“那刚才他为啥还得请示上级呢” , 我用眼神指了一下开车的ClassLoader
文件验证器的声音一下子就压低了:
“你不知道,说来话长, 我们之前出现过事故,有个黑客写了个类java.lang.String, 和我们老板手下有一个干活最卖力的员工名字一模一样,只是这个黑客类里边竟然有格式化硬盘的代码,我们的小兵Classloader 不明就里,就把 这个黑客类给先装载了,也执行了, 最后的结果,唉,很惨的… ”

“那后来怎么办?”
“后来我们老板就定下了规矩:他的骨干员工像String, ArrayList等只能由他自己的心腹去装载, 我听说老板的心腹都是分层级的,像传销一样, 每个都有上线, 最顶层的叫Bootstrap Classloader , 下一次级叫Extension Classloader, 现在开车的这位其实叫App Classloader,位于最底层, 咱这位Classloader 在装载一个类之前,一定要问一问这几位权利极高的大爷,请他们先装载,这几位爷装载不了,才由我们这些小兵来出马。“

其实只要先加载了就没办法再重新加载了。我们普通app环境做不到上述黑客的环境,上面的文章只是给大家加深记忆。

不过如果你的类是你自己用Classloader 加载的话,还是可以重新加载的,相关文章:
JAVA动态重新加载Class机制

有办法。其实你把里面的实现拷出来,换个包名也可以。但是类之间的互相依赖可能会有点多。

但我突然发现,

为什么eBay的代码却用了这个库呢????他们是不是没有实际跑过呢???【黑人问号】

总结

Android工程使用org.apache.commons.codec(commons-codec)库,运行时提示“java.lang.NoSuchMethodError”的原因是:Android内部已经加载过同名的旧类库,导致项目中引用的库无法加载,而旧库的Base64类又没有这个方法。

解决方法是:
1. 换一个库,使用Android自带的Base64方法,并加上Base64.URL_SAFE标记位。(最好是Base64.NO_WARP | Base64.NO_PADDING | Base64.URL_SAFE)
2. 把实现拷出来(依赖过多,不现实)

版权所有,转载请注明出处:
https://sickworm.com/?p=175




linux中运行zipalign/aapt提示:No such file or directory解决办法

最近在用Docker+Jenkins做持续集成(CI),中间有个步骤需要调用zipalign对齐jar包,但我运行zipalign的时候却提示:
No such file or directory。

这就奇怪了,找不到这个二进制文件???百思不得其解。。此时我在Mac和Windows上都是可以运行对应平台的zipalign。

首先,
我怀疑是我下的adt有问题。于是我下了3个版本的adt,都报这个错,排除。

然后,
我怀疑我的Docker虚拟机有问题,我把zipalign扔上阿里云运行,报同样的错误。

接着,
我猜测可能zipalign依赖同个文件夹里面的其他运行文件或so库。于是我整个adt拷过去,问题依然存在。

再然后,
求助谷歌,翻到若干的解决方案:
Add the tools to your PATH, or use the full path to zipalign.
copy the Zipalign file from sdk/build-tools/android-4.4W folder to sdk/tools/
以上皆无法解决。

后来我搜索“Linux no such file or directory原因” 找到:
Linux执行可执行文件提示No such file or directory的解决方法
原来是库不匹配的原因。
再搜索“Linux zipalign no such file or directory” 找到:
sudo apt-get install libc6:i386 libstdc++6:i386 zlib1g:i386

先apt-get update,再安装上面的库,提示找不到。(后来了解到要用dpkg拉i386的软件仓库才可以下载:i386的库。其实64位软件库也有32位的库)
我试着碰运气,直接安装libc++6,173M。(依赖大量32位库。后期实验,只需要安装lib32stdc++6和libc6-i386即可)
安装完成后,提示另一个错误:
error while loading shared libraries: libz.so.1: cannot open shared object file: No such file or directory
继续google,找到:
sudo apt-get install lib32z1
安装,解决。

所以zipalign:No such file or directory的原因是找不到匹配的32位库。
提示这个错误可能是二进制文件错误处理信息不够完善。

版权所有,转载请注明出处:
http://sickworm.com/?p=197




python TypeError: not all arguments converted during string formatting解决办法

新浪云想钱想疯了,开始收mysql租金,一个应用一年175元,一天免费10W条读写。此外账户还收取年费36.5元。于是把新浪云的SAE迁移到阿里云ECS上。迁移一个python应用的时候出现这个问题:

TypeError: not all arguments converted during string formatting

对应代码是

sql = '''select * from strategy where user_id=%s'''
param = (user_id)
cursor.execute(sql, param) # TypeError: not all arguments converted during string formatting

网上搜了一下,发现是param没有被识别为元组。原文解释如下:

% 操作符的第二个操作对象如果不是一个元组,那么它就只格式化一个值,否则它尝试把元组里每个值都单独格式化。你不可以同样是给它一个元组,有些时候要它当成多个值来格式化,另一些时候又把它当成单一的要格式化的值。实在不习惯可以用 str 的 .format 方法代替。另外,(b) 不是只包含一个元素 b 的元组,那个括号只是用于提升表达式的优先级,就像 (1 * 2) + 3 和 (2,) + 3 不一样类似。

也就是把param改为(user_id, )即可。

But,

在新浪云上这个写法是可以的,也许库版本不同吧。(python版本SAE 2.7.9,阿里云2.7.6)

版权所有,转载请注明出处:
http://sickworm.com/?p=136




阿里云ECS python pip安装组件失败解决办法

最近使用阿里云写一些python服务,pip安装的时候经常出现下面情况:

Downloading/unpacking uwsgi
  Cannot fetch index base URL https://pypi.python.org/simple/
  Could not find any downloads that satisfy the requirement uwsgi
Cleaning up...
No distributions at all found for uwsgi
Storing debug log for failure in /home/sickworm/.pip/pip.log

解决办法是:

多试几次。

连续执行两次,第一次失败,第二次成功。或你可以试试删除个人目录下的.pip文件夹。

版权所有,转载请注明出处:

http://sickworm.com/?p=130




Worpress中使用JetPack的MarkDown功能测试

  • 2016年3月22日
  • 杂项

内容为Mou软件示例,Mou是一款Mac的MarkDown软件,支持即时预览,Mou预览版免费。JetPack不支持即时预览,建议使用Mou编辑完成后再上传到WordPress。(注意Web版应在“文本”中编辑,而不是在“可视化”中编辑,否则会有格式转换的问题,如代码缩进)

MarkDown和Crayon协同工作参考此处

Mou

Mou icon

Overview

Mou, the missing Markdown editor for web developers.

Syntax

Strong and Emphasize

strong or strong ( Cmd + B )

emphasize or emphasize ( Cmd + I )

Sometimes I want a lot of text to be bold.
Like, seriously, a LOT of text

Blockquotes

Right angle brackets > are used for block quotes.

Links and Email

An email example@example.com link.

Simple inline link http://chenluois.com, another inline link Smaller, one more inline link with title Resize.

A reference style link. Input id, then anywhere in the doc, define the link with corresponding id:

Titles ( or called tool tips ) in the links are optional.

Images

An inline image Smaller icon, title is optional.

A Resize icon reference style image.

Inline code and Block code

Inline code are surround by backtick key. To create a block code:

Indent each line by at least 1 tab, or 4 spaces.
var Mou = exactlyTheAppIwant; 

Ordered Lists

Ordered lists are created using “1.” + Space:

  1. Ordered list item
  2. Ordered list item
  3. Ordered list item

Unordered Lists

Unordered list are created using “*” + Space:

  • Unordered list item
  • Unordered list item
  • Unordered list item

Or using “-” + Space:

  • Unordered list item
  • Unordered list item
  • Unordered list item

Hard Linebreak

End a line with two or more spaces will create a hard linebreak, called <br /> in HTML. ( Control + Return )
Above line ended with 2 spaces.

Horizontal Rules

Three or more asterisks or dashes:




Headers

Setext-style:

This is H1

This is H2

atx-style:

This is H1

This is H2

This is H3

This is H4

This is H5
This is H6

Extra Syntax

Footnotes

Footnotes work mostly like reference-style links. A footnote is made of two things: a marker in the text that will become a superscript number; a footnote definition that will be placed in a list of footnotes at the end of the document. A footnote looks like this:

That’s some text with a footnote.1

Strikethrough

Wrap with 2 tilde characters:

~~Strikethrough~~

Fenced Code Blocks

Start with a line containing 3 or more backticks, and ends with the first line with the same number of backticks:

Fenced code blocks are like Stardard Markdown’s regular code
blocks, except that they’re not indented and instead rely on
a start and end fence lines to delimit the code block.

Tables

A simple table looks like this:

First Header Second Header Third Header
Content Cell Content Cell Content Cell
Content Cell Content Cell Content Cell

If you wish, you can add a leading and tailing pipe to each line of the table:

First Header Second Header Third Header
Content Cell Content Cell Content Cell
Content Cell Content Cell Content Cell

Specify alignment for each column by adding colons to separator lines:

First Header Second Header Third Header
Left Center Right
Left Center Right

Shortcuts

View

  • Toggle live preview: Shift + Cmd + I
  • Toggle Words Counter: Shift + Cmd + W
  • Toggle Transparent: Shift + Cmd + T
  • Toggle Floating: Shift + Cmd + F
  • Left/Right = 1/1: Cmd + 0
  • Left/Right = 3/1: Cmd + +
  • Left/Right = 1/3: Cmd + –
  • Toggle Writing orientation: Cmd + L
  • Toggle fullscreen: Control + Cmd + F

Actions

  • Copy HTML: Option + Cmd + C
  • Strong: Select text, Cmd + B
  • Emphasize: Select text, Cmd + I
  • Inline Code: Select text, Cmd + K
  • Strikethrough: Select text, Cmd + U
  • Link: Select text, Control + Shift + L
  • Image: Select text, Control + Shift + I
  • Select Word: Control + Option + W
  • Select Line: Shift + Cmd + L
  • Select All: Cmd + A
  • Deselect All: Cmd + D
  • Convert to Uppercase: Select text, Control + U
  • Convert to Lowercase: Select text, Control + Shift + U
  • Convert to Titlecase: Select text, Control + Option + U
  • Convert to List: Select lines, Control + L
  • Convert to Blockquote: Select lines, Control + Q
  • Convert to H1: Cmd + 1
  • Convert to H2: Cmd + 2
  • Convert to H3: Cmd + 3
  • Convert to H4: Cmd + 4
  • Convert to H5: Cmd + 5
  • Convert to H6: Cmd + 6
  • Convert Spaces to Tabs: Control + [
  • Convert Tabs to Spaces: Control + ]
  • Insert Current Date: Control + Shift + 1
  • Insert Current Time: Control + Shift + 2
  • Insert entity <: Control + Shift + ,
  • Insert entity >: Control + Shift + .
  • Insert entity &: Control + Shift + 7
  • Insert entity Space: Control + Shift + Space
  • Insert Scriptogr.am Header: Control + Shift + G
  • Shift Line Left: Select lines, Cmd + [
  • Shift Line Right: Select lines, Cmd + ]
  • New Line: Cmd + Return
  • Comment: Cmd + /
  • Hard Linebreak: Control + Return

Edit

  • Auto complete current word: Esc
  • Find: Cmd + F
  • Close find bar: Esc

Post

  • Post on Scriptogr.am: Control + Shift + S
  • Post on Tumblr: Control + Shift + T

Export

  • Export HTML: Option + Cmd + E
  • Export PDF: Option + Cmd + P

And more?

Don’t forget to check Preferences, lots of useful options are there.

Follow @Mou on Twitter for the latest news.

For feedback, use the menu HelpSend Feedback

Crayon高亮测试:

Crayon内置插件

@app.route('/' + test_entry_key, methods = ['GET'])
def test_entry():
    cmd = request.args.get('cmd')
    if not cmd:
        abort(401)

    if cmd == '1':
        if funda.update_funda() != ERROR_OK:
            return make_response('error')
        prased_funda_list, update_time = funda.get_funda()
        return make_response(str(update_time) + '<br>' + str(prased_funda_list))
    elif cmd == '2':
        strategy.do_strategy_and_push()
        return make_response(strategy.details)
    elif cmd == '3':
        mysql.connect()
        ret, user_sql = mysql.select_user(user_id = 'C4737E88-DE87-4D90-9C34-1A6725187254')
        mysql.disconnect()
        server_sync_time = user_sql[0][6]
        return make_response(str(server_sync_time))
    else:
        abort(401)

Mardown

@app.route('/' + test_entry_key, methods = ['GET'])
def test_entry():
    cmd = request.args.get('cmd')
    if not cmd:
        abort(401)

    if cmd == '1':
        if funda.update_funda() != ERROR_OK:
            return make_response('error')
        prased_funda_list, update_time = funda.get_funda()
        return make_response(str(update_time) + '<br>' + str(prased_funda_list))
    elif cmd == '2':
        strategy.do_strategy_and_push()
        return make_response(strategy.details)
    elif cmd == '3':
        mysql.connect()
        ret, user_sql = mysql.select_user(user_id = 'C4737E88-DE87-4D90-9C34-1A6725187254')
        mysql.disconnect()
        server_sync_time = user_sql[0][6]
        return make_response(str(server_sync_time))
    else:
        abort(401)

此处如果把内容复制到“可视化”中,缩进会消失。Markdown文本应在“文本”中编辑。

版权所有,转载请注明出处:
http://sickworm.com/?p=93


  1. And that’s the footnote. 



在ToolBar上创建菜单

好久没写过界面了,最近用最新的支援库,官方建议AppBar+ToolBar代替原ActionBar。然后翻了一下怎么往ToolBar上创建按钮,翻了半天没一个可以。。。最后发现把onCreateOptionsMenu丢了(网上的大爷们这么默契,全部都把这部分代码忽略掉真的好吗)。附上ToolBar上创建菜单的方法。

Activity中写入:

(XML方式)

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu_check, menu);
    return true;
}

 

menu_check.xml:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".CheckActivity">

    <item android:id="@+id/action_controller"
        android:title="@string/start"
        android:orderInCategory="90"
        app:showAsAction="always|withText" />
</menu>
或:
(代码方式)
public boolean onCreateOptionsMenu(Menu menu) {
    menu.add(Menu.NONE, Menu.FIRST + 1, 1, "设置")
        .setIcon( android.R.drawable.ic_menu_help)
        .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
    return true;
}

菜单会显示在ToolBar右上角。setShowAsAction相当xml中的app:showAsAction。withText作用是把菜单的文字显示出来,如果想仅显示文字,不设置icon即可。

然后在onOptionsItemSelected中处理点击事件:

    @Override  
    public boolean onOptionsItemSelected(MenuItem item) {
        switch(item.getItemId()){  
           case R.id.action_controller: // XML方式
               Toast.makeText(MainActivity.this, "设置1", Toast.LENGTH_SHORT).show();  
               break;  
           case Menu.FIRST + 1: // Java代码方式,仅作示例,两种方法不应混用
               Toast.makeText(MainActivity.this, "设置2", Toast.LENGTH_SHORT).show();  
               break;  
           } 

        return super.onOptionsItemSelected(item);  
    }  

版权所有,转载请注明出处:

http://sickworm.com/?p=76




同时指定layout_margin和layout_marginTop,layout_margin生效,layout_marginTop不生效

layout_margin的优先级要高一些。之前写Android XML转Java代码的小工具优先级写错了。

 

 

版权所有,转载请注明出处:

http://sickworm.com/?p=72




gradle task执行顺序

  • 2016年3月3日
  • gradle

刚开始用gradle,临急抱佛脚,走了很多弯路。

task 执行顺序

下面有一个task hello:

task hello {
}

hello {
    println 'Hello nothing1'
}

hello.doFirst {
    println 'Hello first1'
}

hello << {
    println 'Hello <<1'
}

hello.doLast {
    println 'Hello last1'
}

hello << {
    println 'Hello <<2'
}


hello {
    println 'Hello nothing2'
}

hello.doFirst {
    println 'Hello first2'
}

hello << {
    println 'Hello <<3'
}

hello.doLast {
    println 'Hello last2'
}

hello << {
    println 'Hello <<4'
}

他的执行结果是:

Hello nothing1
Hello nothing2
Hello first2
Hello first1
Hello <<1
Hello <<2
Hello last1
Hello <<3
Hello <<4
Hello last2
Hello <<5

由此可以得出:

task内顺序:

  • 什么都不写是init块,最先执行

  • doFirst块,且后定义的先执行

  • doLast块(<<等同于doLast,属于简写),且先定义的先执行

task间顺序

这里我写了一个task bye,并dependsOn hello,其余和hello一样。

结果是:

Hello nothing1
Hello nothing2
Bye nothing1
Bye nothing2
Hello first2
Hello first1
Hello <<1
Hello <<2
Hello last1
Hello <<3
Hello <<4
Hello last2
Hello <<5
Bye first2
Bye first1
Bye <<1
Bye <<2
Bye last1
Bye <<3
Bye <<4
Bye last2
Bye <<5

结果是:

先执行各个task的init块,再根据task之间依赖关系顺序执行

版权所有,转载请注明出处:
https://sickworm.com/?p=66




二维码扫描开源库ZXing定制化

最近在用ZXing这个开源库做二维码的扫描模块,开发过程的一些代码修改和裁剪的经验和大家分享一下。

建议:

如果需要集成到自己的app上,而不是做一个demo,不推荐用ZXing的Android外围开发模块,只用核心的core目录的代码就好了。android和android-core的代码设计的不好,扩展性太差了(我在后期开发新需求的时候改修改了很多CameraManager的逻辑)。只使用core目录的集成方法很简单,参考:

https://github.com/zxing/zxing/blob/master/android/src/com/google/zxing/client/android/DecodeHandler.java

中的decode函数,把摄像头数据转换成二值化图像,然后传入MultiFormatReader解码。

 

以下是正文:

我的代码库:(基于官方3.2.0)

https://github.com/SickWorm/ZXingDialog

代码没有在github维护,所以没有log。但是所有修改的地方我都加上了“@ch”的注释,以方便定位

官方源码:

https://github.com/zxing/zxing

 

实现功能:

1、功能裁剪(只保留QRCode二维码扫描功能,去掉条形码等其他码扫描功能)

2、移除资源依赖,提供Dialog形式的扫码功能

3、API兼容(源码只兼容4.0以上,现兼容至2.1)

4、转换为竖屏(源码为横屏)

5、扫码速度优化(主要分三点,现只完成了一点)

6、设备兼容(针对低分辨率设备)

本文还会提到:

7、自定义界面

8、优化调试方法

 

1、建立工程

ZXing源码并没有提供一个完整的实例工程给我们使用,构建一个工程我们需要源码下的三个文件夹的文件:

core/

android-core/

android/

大概步骤如下:

1、创建一个新工程

2、把android目录下的所有文件覆盖到新工程(内含有资源文件和AndroidManifest.xml等构建app所需的文件)

3、把android-core所有Java文件拷入到src目录下(注意!android-core中的src文件夹需要进行一些改动,原来路径是android-coresrcmainjavacomgooglezxingclientandroidcamera,我们要把中间的mainjava两层文件夹去掉,不然在Eclipse中无法识别包路径)

4、把core目录下的所有Java文件拷入到src目录下(注意!和步骤3一样需要去掉mainjava两层文件夹)。这样ZXing已经可以运行了,我的src目录是这样的:

可以直接运行,效果还不错。如果你遇到一些错误,有可能是编译的JDK版本低于1.7导致的。源码里使用了ArrayList<>这样的写法,1.7以前是不支持的。你可以选择修改源码或者提高编译JDK版本。

但你可能不满足于这个界面,扫描框太大了,而且是横屏全屏的,还要求API 15(Android 4.0.3)。下面我们会对这些需求进行修改。

2、代码优化

1、功能裁剪(只保留QRCode二维码扫描功能,去掉条形码等其他码扫描功能)

我的目标是只保留二维码识别,不需要其他多余的功能。这一部分的步骤我不打算详细说明,因为我已经不记得了。。大家可以直接看我的代码的结果。

可以直接删掉的是:

com.google.zxing.aztec.**  aztec格式的二维码

com.google.zxing.client.android.book.*  Google 图书相关的功能

com.google.zxing.client.android.clickboard.*  不清楚,复制黏贴?

com.google.zxing.client.android.encode.*  用于生成各种码

com.google.zxing.client.android.history.*  保存扫码记录

com.google.zxing.client.android.result.**  扫码应用功能相关的功能性代码

com.google.zxing.client.android.share.*  分享功能

com.google.zxing.client.android.wifi.*  WiFi相关,不清楚具体用途

com.google.zxing.datamatrix.**  datamatrix格式二维码

com.google.zxing.maxicode.**  maxicode格式二维码

com.google.zxing.multi.**  貌似是用于多格式支持的?我没有用到这个包,如果有了解的麻烦告知

com.google.zxing.oned.**  one dimension一维码,也就是条形码(你去百度搜oned会发现奇怪的东西。。)

com.google.zxing.pdf417.**  PDF417格式条形码

需要修改的是:

com.google.zxing.client.android.CaptureActivity:去掉其他功能的相关代码,只保留核心功能,即扫码功能。界面为一个FrameLayout里面包含一个SurfaceView。代码移除就不详细说了,直接看上传的代码吧,这个文件我参考了http://www.cnblogs.com/keyindex/archive/2011/06/08/2074900.html这个链接里的CaptureActivity的修改。

com.google.zxing.MultiFormatReader:这个是指定支持解码的格式,需要把除QR_CODE以外的格式全部去掉,否则会因为删掉了解码包而报错。具体也请看上传的代码。

com.google.zxing.client.result.ProductResultParser:parse函数中,同上。

com.google.zxing.client.android.DecodeThread:构造器中,同上。

另外:

com.google.zxing.client.result 我没有删减这个包的代码,应该也是能优化的

2、移除资源依赖,提供Dialog形式的扫码功能

经过了第1步的精简,其实只剩下了2个地方需要修改:

1.扫描界面

2.扫描成功时播放的beep声音文件

 

1:去除了其余功能后,对于核心功能我们只需要一个SurfaceView和一个画界面的View就可以了。代码如下:

/** * use Java code to build layout instead of xml file * @ch */
private void buildLayout() {
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    FrameLayout layout = new FrameLayout(this); 
    LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 
    layout.setLayoutParams(params); 
    surfaceView = new SurfaceView(this); 
    surfaceView.setLayoutParams(params); 
    layout.addView(surfaceView); 
    viewfinderView = new ViewfinderView(this, null); 
    layout.addView(viewfinderView); 
    setContentView(layout); 
}

ViewfinderView是ZXing自带的View,如果要修改界面,直接修改它就可以了,我们第7点会提到。

2:由于我最终的目的是能打包成jar包,所以beep文件不能放在res里,而是放在assets里。

//待补充

 

3、API兼容(源码只兼容4.0以上,现兼容至2.1)

这部分修改在源码中标记为//@ch api compatible。

CaptureActivity.java:

if (VERSION.SDK_INT < 11) {
    //surfaceview will push buffer automatically
    surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); } 
    //API 11之前需要手动设置,否则会无法显示。API11之后为默认设置。
    CameraConfigurationManager.java: //@ch api compatible
    if (VERSION.SDK_INT < 13) { 
        theScreenResolution.x = display.getWidth(); 
        theScreenResolution.y = display.getHeight(); } else { display.getSize(theScreenResolution); 
    } 
    //getSize是API 13之后的新API,之前需要用getWidth和getHeight。

AutoFocusManager.java:

if (VERSION.SDK_INT > 11) { 
    newTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 
} else { 
    newTask.execute(); 
} 

// 源码中这里设置了多线程的模式,THREAD_POOL_EXECUTOR表示这个task最多只有5个线程同时运行,超过5个的就要等待。在低于API 11的版本中,此为默认选项。其实这里只有单线程,所以随便执行吧。

OpenCameraInterface.java:

if (VERSION.SDK_INT < 9) { 
    return openWithLowApi(); 
} ...... /** * for lower than API 9 * @ch api compatible */
    
public static Camera openWithLowApi() { 
    //If the device does not have a back-facing camera, this returns null
    Camera camera = Camera.open(); 
    return camera; 
} 
//源码的打开摄像头是能区分前后摄像头的,然而API 9之前并没有前置摄像头这个概念,所以做了一下处理

就几个地方,不过也找了我个把小时了。

 

4、转换为竖屏(源码为横屏)

ZXing默认是横屏,但是我们一般的APP都会做成竖屏,如果扫码的时候强制切换成横屏那样体验就不好了。在修改ZXing的竖屏的时候,我按照的是一般APP的竖屏设置方法,结果发现没有源码的效果好,需要把码放到很小才能完成。后面在调试过程中发现扫码解析的区域和屏幕画出来的区域不一样,才知道这部分的修改出了问题。然后我搜索找到一篇前辈的文章,参考了一下发现没有改完全。附上文件链接:

http://blog.csdn.net/aaawqqq/article/details/24804939

其中第五点我没有修改,文章中的源码可能比较旧,并不适合替换。第5点不替换替换是没有问题的。

此外还有一点需要修改的是:

CameraManager.java:

//@ch change 240 to 120 for 320x240 machine
private static final int MIN_FRAME_WIDTH = 120; private static final int MIN_FRAME_HEIGHT = 120; //@ch change to vertical
private static final int MAX_FRAME_HEIGHT = (int) 1080; 
private static final int MAX_FRAME_WIDTH = (int) 1080; 
//此处为扫描框最大边界和最小边界的界定。但因为我最后做成了正方形,所以这里数值是一样的。如果为矩形,需要把两个值交换一下。 //设置最小值是为了保证解码的成功率,毕竟分辨率太小就没法识别了。最大值是为了保证解码速度。其实最大值应该通过插值来重新构图,不然框的大小不一致体验就不好了。不过这样的分辨率起码是2K屏以上了,所以最大值设定不会有什么影响

5、扫码速度优化(主要分三点,现只完成了一点)

1.二值化算法优化。ZXing一共提供了两种二值化算法,一种是HybridBinarizer,另一种是GlobalHistogramBinarizer,默认使用的是HybridBinarizer。我搜集资料的时候发现,HybridBinarizer算法用了更高级的算法,运算要求更高,而总体来讲GlobalHistogramBinarizer识别率要更高。在我的实际测试中(我用的Nexus5,Android 5.0.1),GlobalHistogramBinarizer效果确实是要好不少。

这里的修改很简单,换一个类就可以了:
DecodeHandler.java:

private void decode(byte[] data, int width, int height) {
    ...... 
    //@ch you can use HybridBinarizer or GlobalHistogramBinarizer 
    //but in most of situations HybridBinarizer is shit
    BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source)); 
    ...... 
}

2.对焦优化。ZXing中的对焦功能在AutoFocusManager.java中,功能非常简单,设置自动对焦并2秒对焦一次。但自动对焦可能会带来一个问题,如下图:

jingshen

(图片源自网络)

把二维码当作图中的那朵花,自动对焦则容易使摄像头对焦到背景(图中女性)中去。我在测试中使用三星S4的自动对焦经常对不了二维码。这里我们可以使用区域对焦的方法(对焦的区域即是扫描框的区域):

//定点对焦的代码 
private void pointFocus(int x, int y) {
    if (cameraParameters.getMaxNumMeteringAreas() > 0) { 
        List<Camera.Area> areas = new ArrayList<Camera.Area>(); 
        Rect area = new Rect(x - 100, y - 100, x + 100, y + 100); 
        areas.add(new Camera.Area(area, 600));
        cameraParameters.setMeteringAreas(areas); 
    } 
    mCamera.cancelAutoFocus(); 
    cameraParameters.setFocusMode(Parameters.FOCUS_MODE_AUTO); 
    mCamera.setParameters(cameraParameters); 
    mCamera.autoFocus(autoFocusCallBack); 
}

区域对焦需要API 14以上。关于对焦暂时没有更多的想法,但我觉得确实是有优化的余地的。

3.算法优化。算法主要分两部分,第一部分是二值化,第二部分是提取码值。第二部分又分为1.寻找定位符,2.寻找校正符,3.转换矩阵。在测试过程中,影响识别的最大问题就是找不到定位符,即二维码左上角、右上角、左下角的三个黑白相间的矩形点。比较大的原因可能是二值化部分的问题。这一部分暂时也还没有深入。

6、设备兼容(针对低分辨率设备)

CameraManager.java中有设置最小扫描框大小的参数MIN_FRAME_WIDTH和MIN_FRAME_HEIGHT。默认是320×240。这对于屏幕分辨率为320×240的设备,扫描框就会变成全屏的。这里我改成了120×120,实际在屏幕分辨率为320×240的设备上也可以扫到二维码。

 

7、自定义界面

界面写在ViewFinderView.java中。扫描框大小由CameraManager决定。修改的时候需要注意和CameraManager的配置关联起来,否则会出现扫描框和实际解码的区域不一致。(ZXing的android外围模块代码默认使用的是全屏,如果你想改为非全屏(比如加一个action bar),肯定会造成扫描框区域和实际解码的区域不一致的问题。这也是文首建议只使用core模块的原因)

 

8、优化调试方法

为了应对7可能带来的问题,我自己做了一个调试的方法,以保证扫描框内容和实际解码内容一致。首先在CaptureActivity.java初始化cameraManager的地方,把

cameraManager = new CameraManager(getApplication())

改为cameraManager = new CameraManager(this)

这样做的目的是:传入了activity给CameraManager,使得后面在CameraManager中的调试内容可以直接输出在activity上(dialog也一样,改为getContext()即可)。但注意这样会带来Activity生命周期问题和因为互相引用导致内存泄漏的问题。所以只能在调试阶段修改成上面的写法。

然后在CameraManager.buildLumianceSource中加入:

if (context instanceof Activity) {
    final Bitmap bitmap = Bitmap.createBitmap(BitmapUtil.createBitmapfromYUV420(data, width, height), rect.left, rect.top, rect.width(), rect.height()); 
    CaptureActivity activity = ((CaptureActivity) context); 
    final ViewfinderView view = activity.getViewfinderView(); 
    activity.runOnUiThread(new Runnable() { 
        @Override 
        public void run() { 
            view.drawResultBitmap(bitmap); 
        } 
    }); 
}

这样,屏幕上的扫描框中就会显示出实际解码的图像,你可以通过比对预览图像查看是否对齐。

记得调试完改回来getApplication()

 

版权所有,转载请注明出处:

http://sickworm.com/?p=41