2014年1月30日 星期四

初次使用 paper prototyping 做使用性測試

雖然時常看到有人推薦用紙筆畫 app 介面做使用性測試, 但是要自己動手畫重覆的東西, 實在是提不起勁。前陣子聽阿修介紹 Paper Prototype, 也有同事動手畫了幾頁, 實際跟著流程走一次, 感覺還不錯。剛好我有在 iPad 裡裝 POP, 當場用 POP 結合同事做的紙張雛型, 互動效果出乎意料的好。

最近想幫爸媽做一款「待做事項」的軟體, 順便練習從頭開發一款 app 要經過的流程。中午花了大概一小時畫草稿加上用 POP 制作雛型, 過程滿順暢的。下面是部份原稿和 POP Android 版的畫面。

和爸媽分別測試後有找到一些小問題, 不過確定大方向應該可行。後來找我哥測試, 原以為應該會更順暢, 但是因為我哥使用 iPhone, 我是針對 Android 畫的草圖, 結果不容易融入情境。再加上我哥不是我的目標對象, 主要操作流程和他的期望不同, 也影響測試的流暢度。

歸納一下目前對使用 POP 做 paper prototyping 的看法。

目的:

  • 做使用性測試。了解使用者是否了解如何操作。功能是否有滿足使用者的需求。

注意事項:

  • 需要篩選目標使用者。
  • 測試前要做情境描述, 使用者才能進入狀況 (看著草圖想像要做的事)。
  • 盡量觀察使用者的反應, 少開口提醒。

優點:

  • 制作速度快, 適合早期討論, 甚至能在開會中當場做。
  • 易於專注在操作流程, 不會討論配色、字型、畫面排版等細節。
  • 任何人都可以製作, 工程師和設計師以外的人也可藉此展示自己的想法。

限制:

  • 只適合能融入情境的使用者。也許可透過幾次體驗後克服。
  • 還是有可能插題討論圖示, 所以也不能畫得太糟。

總結來說, 現在我覺得用 POP 做 paper prototyping 滿快的, 但若是只用紙張互動, 一來我做得比較慢, 二來使用者也比較難聯想。我自己不適合用純紙張版本的 paper prototyping。

附帶一提, 做得過程中要自己用紙筆畫出概要, 才留意到一些 Android UI 的小細節。還有實際測試流程不像影片裡那樣順。看了再多資料, 還是得親身走一遍才行。

2014年1月27日 星期一

C++ 下 thread-safe 的 lazy initialization

《clang 避免 non-local static 物件初始化順序的方法》提到可用 static local variable, 然後用 method 傳回的方式避免產生 global static variable (藉此避免不同編譯單元的初始化問題)。以下是一個例子:

const struct Point* center()
{
  static Point* s_center = CreateCenterPoint();
  return s_center;
}

但是, 在 multi-thread 的環境下, s_center 可能被初始化兩次。假設 CreateCenterPoint() 會傳回不同的值, 或是外界可以改變取得的 Point, 有兩份 Point 會造成問題。

好消息是:

所以情況比想像中的安全。

不過, 其它 lazy initialization 的實作方式有可能出錯。更一般化的 lazy initialization 會用 Double-Checked Lock Pattern, 但是這個作法有不易察覺的漏洞

直覺的作法如下:

Singleton* Singleton::instance()
{
  if (pInstance == 0) { // 1st test
    Lock lock;
    if (pInstance == 0) { // 2nd test
      pInstance = new Singleton;
    }
  }
  return pInstance;
}

注意初始化 singleton 是三個步驟組成的:

  1. 配置一塊新記憶體
  2. 初始化新記憶體
  3. 將新記憶體的位置指向目標指標 (即 pInstance)

compiler 有可能更動三者的順序。最壞的情況下, thread A 執行了 1, 3, 還沒執行 2, 這時 thread B 發覺 pInstance 不是 0, 於是回傳尚未初始化的 pInstance。解法是要使用 memory barrier。或是在 C++11 後更可用跨平台的解法。詳情見 Double-Checked Locking Is Fixed In C++11

另外, Java 1.4 以前 Double-Checked Locking 也有問題。Java 1.5 後多了 volatile 表示「取出最新的值」, 才有辦法修正此問題。Effective Java 2/e Item 66 "Synchronize access to shared mutable data" 和 Item 71 "Use lazy initialization judiciously" 有詳細的討論。

2014年1月20日 星期一

尋找 memory error 的強力工具: Address Sanitizer (ASan)

官網有很詳細的介紹, ASan 可以偵測出以下的問題:

  • Use after free (dangling pointer dereference)
  • Heap buffer overflow
  • Stack buffer overflow
  • Global buffer overflow
  • Use after return

都是出錯時很難察覺, 之後會造成奇異的行為, 或是讓程式掛在莫明奇妙的地方。

clang 3.1 和 gcc 4.8 開始內建 ASan 的功能。號稱執行速度平均慢兩倍, 比起 Valgrind 的 20 倍, 實在相當誘人。這裡有說明 ASan 怎麼做的。這樣在 Linux 上也有不錯的 memory error detector 了。不知什麼時候能追上 OS X 的腳步

除了 Chromium 有用之外, Firefox 也有使用

clang 避免 non-local static 物件初始化順序的方法

C++ 沒有定義 non-local static 物件在不同編譯單元 (translation unit) 之間初始化的順序, 所以要極力避免 non-local static 物件相互間的存取。

今天試用 clang 編譯程式, 發覺它有個不錯的選項: -Wglobal-constructors。用這選項編譯, 遇到有 non-local static 物件會依賴其它函式 (包含 constructor) 設值時, 會輸出 warning。

這裡引用 Address Sanitizer 提供的例子:

$ cat a.cc
#include <stdio.h>
extern int extern_global;
static int __attribute__((noinline)) read_extern_global() {
  return extern_global;
}
int x = read_extern_global() + 1;
int main() {
  printf("%d\n", x);
  return 0;
}

$ cat b.cc
int foo();
int foo() { return 42; }
int extern_global = foo();
$ clang++ a.cc b.cc && ./a.out
1
$ clang++ b.cc a.cc && ./a.out
43
$ clang++ a.cc b.cc -Weverything
a.cc:6:5: warning: declaration requires a global constructor [-Wglobal-constructors]
int x = read_extern_global() + 1;
    ^   ~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
b.cc:3:5: warning: declaration requires a global constructor [-Wglobal-constructors]
int extern_global = foo();
    ^               ~~~~~
1 warning generated.

由上可知, 編譯 a.cc 和 b.cc 的順序不同, 輸出的結果不同。加上 -Weverything 後, -Wglobal-constructors 有在第一時間抓出有問題的部份。附帶一提, clang 的錯誤訊息不止有標示錯誤的位置, 而且還是彩色的!

解決這個 warning 的方法, 和 Effective C++ Item 4 的說法一樣, 就是改用函式傳回 local static method, 這樣就會依執行的順序在執行期間初始化。不過實際情況稍微複雜了一點, 後述。

clang 另有一個參數 -Wexit-time-destructors, 會找出在結束程式時執行 destructor 的物件。雖然 destructor 執行的順序有明確的定義 (和初始化的順序相反), 不過開發者八成沒有考慮週全, 很容易在一連串 destructor 執行中用到已執行完 destructor 的物件。這個問題和 non-local static 物件初始化一樣棘手。

clang 的解決方案一樣單純: 「本來無一物, 何處惹塵埃。」統統不準用, 就不會出亂子。

$ cat p.cc
struct Point
{
  Point() {}
  ~Point() {}
  int x, y;
};


const struct Point& center();
const struct Point& center()
{
  static Point s_center;
  return s_center;
}
$ clang++ p.cc -c -Weverything
p.cc:12:16: warning: declaration requires an exit-time destructor [-Wexit-time-destructors]
  static Point s_center;
               ^
1 warning generated.

那要怎麼解決這個 warning 呢? 就是 new 一個物件, 並且不要釋放它。若懶得修改已經存取它的程式, 可以這麼做:

const struct Point& center()
{
  static Point& s_center = *new Point();
  return s_center;
}

在這兩個 warning 的夾擊下會少掉很多難以察覺的錯誤, 不過寫程式時也會有一點點不便。比方說需要用到常數字串時, 不能直接寫

const std::string kMyString = "...";

得改用

const char* kMyString = "...";

對於用 std::string 做為函式參數或 STL container 的物件, 得付出一點生成 std::string 的成本。

2014年1月5日 星期日

使用 CSS position 組合 div 的佈局小技巧

這篇談的方法在舊的瀏覽器大概會出包, 沒有實際研究。跨瀏覽器的議題太複雜, 希望在我不得不面對這議題前, 跨瀏覽器的痛苦可以減少很多 (大概要等 Windows XP 絕跡吧)。

以下圖為例, 這篇討論兩個使用 CSS position 的小技巧。

使用 relative position 重疊版型

上圖的書本和內文, 實際上是兩個 div 組成的, 還原後如下圖:

組合的方式如下:

HTML

<div class="wrapper">
  <div class="content"></div>
  <div class="book"></div>
</div>
CSS

.wrapper {
  height: 750px;
  overflow: hidden;
}

.content {
  height: 500px;
  position: relaltive;
  z-index: 1;
}

.book {
  height: 700px;
  width: 500px;
  position: relative;
  top: -600px;
}
數據是我大概填的, 示意用。

幾個要點:

  • .book 用 position: relative 從原有的位置往上移到 .content 做為背景。
  • 為了讓 .content 在上方顯示, 設 z-index: 1。由於 z-index 只有在 non-static position 下才有效, 所以改一下 position。
  • .book 使用 position: relative 往上移後, 仍會在原位置留下空間, 就像 visibility: hidden 一樣。為了避免占用原位置的空間, 限製 .wrapper 的 height。
這樣做的好處是內文和背景分離, 兩者各自依需求配置 div 結構, 不會互相影響。不然要顧及內文的排版而調整背景書本的 div, 切版型會滿辛苦的。

缺點是必須限制高度, 不像一般網頁彈性地隨內文增加而自動增加高度。

使用 absolute position 疊出下方的書緣

下方展開後, 中間長這個樣子, 我隱藏了左側一個 div, 方便看拆開的 div。

HTML

<div class="page-container-bottom">
  <div class="page-bottom page-bottom-left">
    <div class="page-bottom-inner page-bottom-left-inner"></div>
  </div>
  <div class="page-bottom page-bottom-right">
    <div class="page-bottom-inner page-bottom-right-inner"></div>
  </div>
</div>
CSS

.page-bottom {
  height: 30px;
  position: absolute;
  bottom: 0;
  background-color: #8F8F8F;
}

.page-bottom-left {
  border-top-right-radius: 90px 30px;
  width: 50%;
}

.page-bottom-inner {
  height: 15px;
  width: 100%;
  bottom: 0px;
  position: absolute;
  background-color: #FFF;
}

.page-bottom-left-inner {
  border-top-right-radius: 90px 15px;
}
CSS 部份只列出左半邊, 右半邊作法一樣, 只是改用 border-top-left-radius。

幾個要點:

  • 左右各一個 div, 外層的 div ( .page-bottom ) 用 position: absolute + bottom: 0 做到貼齊下緣的效果。注意使用 position: absolute 的 tag 會跟著上層第一個使用 non-static position 的 tag, 在這個例子裡, 是前面提過的 div.book。
  • 內層的 div ( .page-bottom-inner ) 用一樣的方式貼齊底部, 但是 height 比較矮, 且圓角弧度比較小。兩者疊出書緣的效果。

用 position: absolute 貼齊底步, 做起來出呼意料地容易。

2014年1月4日 星期六

Ubuntu 安裝 package 出現 conflicts 時的解法

這篇只討論我自己常遇到的一個情境: 明明只是升級某個套件或加裝相關套件, aptitude 卻發瘋似的說有些相關套件的相依版本不合, 要移掉一大堆套件才能繼續。

解法是下指令時同時安裝目標套件和 aptitude 抱怨不合的相依套件。

實例如下:

$ sudo aptitude install git-svn
[100%] Reading package lists
...
The following NEW packages will be installed:
  git-svn libsvn-perl{a} libterm-readkey-perl{a}
The following packages will be upgraded:
  perl perl-base perl-modules
3 packages upgraded, 3 newly installed, 0 to remove and 793 not upgraded.
Need to get 10.3 MB of archives. After unpacking 4,449 kB will be used.
The following packages have unmet dependencies:
 libperl5.14 : Depends: perl-base (= 5.14.2-6ubuntu2.1) but 5.14.2-6ubuntu2.3 is to be installed.
Internal error: the solver Install(libc-bin:amd64 2.15-0ubuntu10.5 <libc6:i386 2.15-0ubuntu10.5 -> {libc-bin:amd64 2.15-0ubuntu10.5 libc-bin:i386 2.15-0ubuntu10.5}>) of a supposedly unresolved dependency is already installed in step 153
Internal error: the solver Install(libc-bin:amd64 2.15-0ubuntu10 <libc6:i386 2.15-0ubuntu10 -> {libc-bin:amd64 2.15-0ubuntu10 libc-bin:i386 2.15-0ubuntu10}>) of a supposedly unresolved dependency is already installed in step 335
Internal error: the solver Install(fontconfig-config:amd64 2.8.0-3ubuntu9 <libfontconfig1:i386 2.8.0-3ubuntu9 -> {fontconfig-config:amd64 2.8.0-3ubuntu9}>) of a supposedly unresolved dependency is already installed in step 337
The following actions will resolve these dependencies:

      Remove the following packages:
1)      gtk2-engines-murrine:i386
2)      hplip
3)      libatk1.0-0:i386
...
73)     printer-driver-hpijs
74)     telepathy-haze
75)     zlib1g:i386

      Leave the following dependencies unresolved:
76)     empathy recommends telepathy-haze
77)     foomatic-db-compressed-ppds recommends printer-driver-hpijs
78)     libsane-hpaio recommends hplip (= 3.12.2-1ubuntu3)
79)     ubuntu-desktop recommends hplip


Accept this solution? [Y/n/q/?] q
Abandoning all efforts to resolve these dependencies.
Abort.
$ sudo aptitude install git-svn libperl5.14
...
The following NEW packages will be installed:
  git-svn libsvn-perl{a} libterm-readkey-perl{a}
The following packages will be upgraded:
  libperl5.14 perl perl-base perl-modules
4 packages upgraded, 3 newly installed, 0 to remove and 792 not upgraded.
Need to get 10.3 MB of archives. After unpacking 4,449 kB will be used.
Do you want to continue? [Y/n/?] y
Selecting previously unselected package git-svn.
Unpacking git-svn (from .../git-svn_1%3a1.7.9.5-1_all.deb) ...
Processing triggers for man-db ...
Setting up libperl5.14 (5.14.2-6ubuntu2.3) ...
Setting up perl-modules (5.14.2-6ubuntu2.3) ...
Setting up perl (5.14.2-6ubuntu2.3) ...
Setting up libsvn-perl (1.6.17dfsg-3ubuntu3.3) ...
Setting up libterm-readkey-perl (2.30-4build3) ...
Setting up git-svn (1:1.7.9.5-1) ...
Processing triggers for libc-bin ...
ldconfig deferred processing now taking place
...
Current status: 792 updates [-4].
[fcamel@fc-vm ~/dev ]
$ git svn
fatal: Not a git repository (or any of the parent directories): .git
Already at toplevel, but .git not found
 at /usr/lib/git-core/git-svn line 308

2015/01/04 更新

可配合 apt-cache show PKG 了解套件的相依性, 實用例子見《Ubuntu 12.04 安裝 graphviz 遇到 confclit》

另外這個不知要記在那裡, 順便記在這裡: 安裝指定版本的方法: aptitude install PKG=VERSION。PKG=VERSION 的語法可用在 isntall, show, download, forbid-version 等指令。

C++ 能否用 memcpy 複製 class / struct 的資料?

答案是: POD (plain old data) type 可以。POD type 可和 C 互通, CPP Reference POD Type 的介紹: Specifies that the type is POD (Plain Old Data) type. Thi...