adb shell
, which would be more comfortable to use if it has the line editing functionality similar to bash
. After some research, I found that actually there are a number of line editing libraries available, so it should be a nice and easy addition. However, after some integration work, I was surprised to find that all these libraries (e.g. jline3, linenoise (which was also once imported into AOSP)) only worked on older versions of Android, that is, until Android 8.0.So I created a small sample app with linenoise
and debugged it to pinpoint the issue. Strangely, I found that it was the following line that failed:
1 | /* put terminal in raw mode after flushing */ |
Which returned -1
with errno
set to EACCES
(13
). But why would this terminal configuration fail anyway?
So I took a look at man tcsetattr
:
tcsetattr() sets the parameters associated with the terminal (unless support is required from the underlying hardware that is not available) from the termios structure referred to by termios_p. optional_actions specifies when the changes take effect:
TCSANOW
the change occurs immediately.
TCSADRAIN
the change occurs after all output written to fd has been transmitted. This function should be used when changing parameters that affect output.
TCSAFLUSH
the change occurs after all output written to the object referred by fd has been transmitted, and all input that has been received but not read will be discarded before the change is made.
The manual page didn’t contain any information for specific error codes, so it wasn’t very helpful for understanding why we’ve got EACCES
. Maybe we’ve lost some kind of access to the TTY device since Android 8.0+? However, the default Android shell mksh
(used by adb shell
) still has working line editing, and how that was done became another mystery to me.
So I began searching for tcsetattr Android
, and found that the Termux people also noticed the same issue years ago — the EACCES
was actually a SELinux denial. And as a workaround, they were redefining TCSAFLUSH
to be TCSANOW
, but that isn’t functionally equivalent and may be subject to subtle breakages. So I decided to find out what was actually going wrong in SELinux.
My next step was to figure out why line editing is still working for mksh
— is there any similar patch in AOSP, and how did AOSP do it? After some code search, I found out that mksh
was actually using TCSADRAIN
instead of TCSAFLUSH
since a long time ago, so it seems both TCSANOW
and TCSADRAIN
are working, and it’s just TCSAFLUSH
became broken on Android 8.0+. However, TCSAFLUSH
still sounds more robust and is still being used in a number of places in Android, notably toybox
‘s stty
implementation (which broke jline3
). So I think it’s still worth fixing TCSAFLUSH
.
To debug the SELinux denial, I looked at the SELinux logcat message on an Android 11 emulator:
1 | type=1400 audit(0.0:763): avc: denied { ioctl } for path="/dev/pts/0" dev="devpts" ino=3 ioctlcmd=0x5404 scontext=u:r:shell:s0 tcontext=u:object_r:devpts:s0 tclass=chr_file permissive=0 |
It is clear that tcsetattr
is actually a wrapper over an ioctl
operation on the TTY device, and Android do have SELinux rules over ioctl
. So I looked for devpts
and ioctl
inside system/sepolicy, and found the macro unpriv_tty_ioctls
in ioctl_macros
:
1 | # commonly used TTY ioctls |
Which is used in domain.te
:
1 | # Restrict PTYs to only allowed ioctls. |
So I took a look at man tty_ioctl
:
TCSETS
const struct termios *argp
Equivalent to tcsetattr(fd, TCSANOW, argp).
Set the current serial port settings.
TCSETSW
const struct termios *argp
Equivalent to tcsetattr(fd, TCSADRAIN, argp).
Allow the output buffer to drain, and set the current serial port settings.
TCSETSF
const struct termios *argp
Equivalent to tcsetattr(fd, TCSAFLUSH, argp).
Allow the output buffer to drain, discard pending input, and set the current serial port settings.
Notably, the macro unpriv_tty_ioctls
included TCSETS
and TCSETSW
, but not TCSETSF
. So it seems it’s indeed the missing TCSETSF
that caused this SELinux denial.
Now that we have the root cause, can we fix it by simply adding TCSETSF
to the macro unpriv_tty_ioctls
, or was there any other concerns about it so that it was deliberately left out?
So I looked into git history of the file. Actually, the variant TCSETSW
was the initial one added into unpriv_tty_ioctls
in aosp/306278, and the base TCSETS
was added later by aosp/310920. Looking at the initial change aosp/306278, the primary reason to restrict ioctl for TTY devices was to mitigate the security exploit of TIOCSTI
, so it indeed seems that adding TCSETSF
as another variant of TCSETS
and TCSETSW
would be fine, and should fix all the code relying on it.
Then I started testing my fix locally. As I’m working on Android already, I naturally starting tested on an eng build, however soon I was surprised to find that my sample app runs perfectly even without my fix. Was it somehow patched recently? However since I’ve been looking at the latest version of the source code anyway, was my entire theory going in the wrong direction? I became deeply confused, until I suddenly realized the special thing about eng build — adb shell
has root access.
So I immediately checked the SELinux context of the shell with ps -AZ | grep sh
, and the output was u:r:su:s0
, so actually this wouldn’t be the usual SELinux context on consumer devices because the shell process can’t be in the su
domain. The su
domain has a lot of privileges, and no wonder my sample app was working fine inside it. I became relieved that I’m not dealing with something crazy, and began testing on a user build instead. This time the sample app failed without the fix, and worked when my fix was applied, so this was finally confirmed to be the correct fix. Yay!
The remaining work was uploading the patch as aosp/1491378, getting it persubmit verified and code reviewed, and finally submitting the change. This concluded my small weekend journey into SELinux, and the TCSAFLUSH
issue will become fixed with the Android S release. However since there’s no way to backport the fix, we still have to stick with TCSADRAIN
for Android 8.0–11 compatibility for some years.
Menu
key with a PrtSc
. In my day to day work, I almost always accidentally hit that key while using my lovely Ctrl
and Alt
keys, upon which my laptop happily plays a shatter sound, flashes my screen white for a split second and spawns a screenshot file under my Pictures/
(thank you, GNOME). Whereas when I wanted to use my Menu
key, it’s nowhere to be found.However, there’s still an Insert
key lying quietly in the top-right corner, which I never used (except for checking if some app even supports it). So why not make my old Insert
PrtSc
and my old PrtSc
the new Menu
?
Moreover, there are also 4 special keys (Fn
+ F9
–F12
) that could have been my media keys, but are by default strange things like Settings
and Search
. Why not map them to media keys as well?
xmodmap
was the go-to tool for remapping keys, and it works on the X11 server level so the only thing you need to care about is the X11 key symbols (no key codes, scan codes and other nightmares). However, naturally this tool won’t work under Wayland (which supports fractional scaling etc), and it cannot get automatically loaded by my GNOME 3 (even with autostart). Some say it’s been deprecated, and people should use xkb
instead, so no luck here.
The other way is to modify the xkb
key symbol database. Although it doesn’t provide any means of overriding in /etc/
, you can directly edit the files under /usr/share/X11/xkb/symbols/
. The interesting files are pc
for the standard keys, and inet
for the special keys, and here’s the patch I’ve been using for two years:
1 | diff --git a/pc b/pc.zhnew |
It works perfectly, without any overhead. The only problem with it, is that because these things live inside /usr/
, which is managed by pacman
, it is reverted to packaged version every time xkeyboard-config
is updated, and it actually does get updated sometimes. In that case I’ll find me suddenly making screenshots again, and need to patch those files and reboot for things to work.
Recently when I was doing some brief research about new ThinkPads, I came across the ArchWiki for ThinkPad T480, which mentioned using something called hwdb to add support for its two special buttons. It looked promising, and I finally took some hours today to figure it out for my own remapping.
The hwdb in udev works on a much lower level: it maps the scan codes from your keyboard to standard key codes, and /etc/udev/hwdb.d/
provides a means of customization, which allows overriding the way scan codes are mapped. Some more detail can be found out on Arch Wiki.
And here is the final hwdb file I came up with:
1 | # /etc/udev/hwdb.d/90-zh-thinkpad.hwdb |
The hwdb rules we need to write consists of two parts: matching and mapping. The matching expression is a shell glob that matches the device, where as the mapping maps the scan code (in hex) to key code macro names in kernel’s include/uapi/linux/input-event-codes.h
. man hwdb
provided some simple example, but actually comments in the built-in hwdb file provides much more details about this file.
In order to find out the scan codes for my keys, I tried both methods in Arch Wiki. The traditional showkey --scancodes
didn’t work well for me, requiring switching to a tty, and was printing multiple bytes of hex for a single keystroke of mine. In contrast, evtest
was just the right tool. Just execute sudo evtest
in a terminal and select something like AT Translated Set 2 keyboard
for the builtin keyboard (mine is 3
), and you can test your keystrokes to find out its scan code in output like (MSC_SCAN), value <SCAN CODE HERE>
.
Another caveat is that, for the special keys on ThinkPad keyboard, unlike regular keys they are not listed under the AT Translated Set 2 keyboard
, but actually under another input device named ThinkPad Extra Buttons
. It took me some time to realize this, and I also tried showkey
which disappointed me again.
After we’ve got the scan codes from evtest
and key codes from input-event-codes.h
, it’s time to write the rule. We need the matching part for the two devices, whose format specification is available in the comment inside systemd’s bulitin hwdb file. The exact info for your current machine can be obtained from cat /sys/class/dmi/id/modalias
, and combining with other existing ThinkPad rules in /usr/lib/udev/hwdb.d/60-keyboard.hwdb
I derived mine successfully.
The actual rules read by udev upon boot is a compiled binary file called hwdb.bin
, so one will need to compile the configuration files into binary with sudo systemd-hwdb update
. To make the changes take effect immediately, run sudo udevadm trigger
, and finally, try out the new key mapping!
Most of my key mapping worked, except for my new Menu
key. I double checked the scan code and the key code name – they both seemed correct. In input-event-codes.h
, I also found KEY_OPITON
and KEY_CONTEXT_MENU
, but neither of them worked as Menu
key as well.
So I tried xev
. Interestingly, it printed XF86MenuKB
as the key symbol on the X11 level, instead of Menu
. This must be something with the X11 key symbol database. I did some grep, played around for around half an hour, and finally found out the answer when I expanded my search into /usr/share/X11/xkb/keycodes/evdev
:
1 | alias <MENU> = <COMP>; |
Combined with the output from xmodmap -pke | grep Menu
:
1 | keycode 135 = Menu NoSymbol Menu |
I surprisingly found out that my key code KEY_MENU
was mapped to key symbol XF86MenuKB
. And to map to key symbol Menu
, I actually need to map my scan code to key code KEY_COMP
.
So one last change, save, and sudo systemd-hwdb update && sudo udevadm trigger
. Hooray! All my keys are working flawlessly now, and I don’t need to worry about package updates anymore (just forget about xkeyboard-config
). Although the obscure and scattered documentation put me through these tedious trial-and-error attempts, it still kinda excited me when I finally got my keys remapped correctly, after all these years.
Lexicographical sort | Natural sort |
---|---|
1.txt | 1.txt |
10.txt | 2.txt |
100.txt | 3.txt |
101.txt | 4.txt |
102.txt | 5.txt |
103.txt | 10.txt |
104.txt | 11.txt |
105.txt | 12.txt |
11.txt | 13.txt |
12.txt | 14.txt |
13.txt | 15.txt |
14.txt | 100.txt |
15.txt | 101.txt |
2.txt | 102.txt |
3.txt | 103.txt |
4.txt | 104.txt |
5.txt | 105.txt |
Lexicographical sort can be easily implemented using String::compareToIgnoreCase
, but it is not very acceptable for end users. However for natural sort, things is actually a little more complicated.
There is a small Wikipedia article on Natural Sort Order, which mentioned the following definition for natural sort order:
Natural sort order is an ordering of strings in alphabetical order, except that multi-digit numbers are ordered as a single character.
However, there’s still some undefined behavior about this sort order when case-insensitivity is involved alongside treating numbers as a whole. In an article discussing natural sort order in C, the author mentioned the following case:
Name |
---|
Test1 |
tesT1 |
test1 |
tEst2 |
test2 |
test3 |
These string is listed in a reasonable way, however if you simply define the natural sorting to be sorting chunks of characters and numbers individually, the Test
string have to be equal to test
so that the ordering won’t be deterministic.
And there’s another case involving leading zeros:
Name |
---|
test1 |
test01 |
test001 |
test01a |
Intuitively, things that are shorter should come before those longer ones, so equal numbers with less leading zeros should come before those with more. Furthermore, if there’s more text following the number, it should come after those without, thus the aforementioned leading zero comparison has a lower priority.
I didn’t find any standard for these corner cases, so I’m merely deciding on the most reasonable approach.
There’s an article on CodingHorror and a StackOverflow question that referenced some existing implementations.
And here are some additional implementations that I found inside some battle-tested applications:
AlphanumComparator
.ls -v
.sort -V
.But there’s some common issues in existing implementations:
char
s (think of surrogates), so those code point related methods should be used instead of simple increment to advance the index of current “character”.c >= '0' && c <= '9'
, one should use Character.isDigit()
and Character.digit()
for getting numbers out of a character.So I wrote my own implementation with those issues in mind, and it basically iterates over the two string and handles consecutive digits together. Most of the code is self-explanatory and the comments can also describe the algorithm:
1 | public class NaturalOrderComparator implements Comparator<String> { |
The code is also available as a GitHub gist and is licensed under the Apache 2.0 license.
In fact, to account for Unicode’s compatibility decomposition, canonical composition and case folding, an ICU Normalizer
should be used with mode NFKC_Casefold
. However, since the ICU package is not available on Android until Android Oreo, and the JDK implementation didn’t account for such level of correctness, I’m currently leaving it out and you can trivially add it by applying the normalization before comparison.
And this is what it used to be like:
My degree Celsius symbol is missing!
The first thing I thought was, is this specific to OpenWeather? And I was surprised to find out that when I googled “degree Celsius symbol”, the °C
as U+00B0 ° degree sign
plus U+0043 C latin capital letter c
is present, while ℃ as U+2103 ℃ degree celsius
is simply empty. However a <span>
with font-family: serif
on Wikipedia showed the symbol correctly, so this must be a font/fontconfig issue.
And I less
ed my own fontconfig configuration with /etc/fonts/local.conf
:
1 |
|
So the different order of font families in sans-serif
and serif
might be the issue. I googled a while for finding out the font selected by fontconfig
for a glyph, and found an article suggesting the use of FC_DEBUG
and pango-view
:
1 | FC_DEBUG=4 pango-view -q --font='<YOUR_FONT_FAMILY>' -t '<YOUR_CHARACTER>' 2>&1 | grep -o 'family:"[^"]\+' | cut -c 10- | tail -n 1 |
The latter part basically filters out the last printed family name from the tedious debug output. And when I run this for my degree Celsius symbol (℃
), I found:
What’s wrong with WenQuanYi Micro Hei
? I’ve been using it for years, and now it’s giving me nothing for degree Celsius symbol? And why did fontconfig ever choose it seemed to have no such glyph?
Confused by this I opened my wqy-microhei.ttc
with FontForge, and was again surprised to find out that my WenQuanYi Micro Hei
is providing an empty glyph (instead of nothing) for certain codepoints!
This is crazy, and I’m left wondering how my degree Celsius symbol ever worked in the past few years.
There must be some configuration that allow me to blacklist certain glyphs in a font. And after another round of digging around, I found some interesting results.
The first one is a bug on fontconfig Bugzilla which requested such support back in 2006 and was marked fixed in 2011. Feeling excited, I scrolled down and found a single line of comment from the dev:
This is fixed in master with target=scan charset editing.
After all the hard work, why didn’t he or she celebrate the moment of closing this bug with a more detailed explanation, or at least a helpful pointer? Even after reading though man fonts-conf
, I found nothing describing how I can edit the charset of a font family. After all, the <charset>
element says it must contain one or more integer, however the whole charset normally contains a huge amount of hexadecimal integers.
So I searched harder and found another answer on StackOverflow. It suggested an undocumented way of configuration that’s mentioned somewhere in a RedHat bug thread, which was its only appearance on the whole Internet:
1 | <match target="scan"> |
This is just what i wanted! So I happily applied this to my mischievous WenQuanYi Micro Hei
, changing the compare
to "eq"
and setting the range to include 0x2103
(My precious degree Celsius symbol). Finally, sudo fc-cache -f && fc-cache -f
! [sigh of relief]
Nope.
This could have worked, in some time of the history, but is not working for now, on my latest Arch Linux installation.
No.
But wait. I did saw something mentioning, we have a <charset>
element and it must contain some integers? So I started to coin my own solution (You should also do the same for WenQuanYi Micro Hei Mono
):
1 | <match target="scan"> |
sudo fc-cache -f && fc-cache -f
, pango-view
again to confirm, and Alt+F2 r
to restart my GNOME shell. It worked! After all these hours, my degree Celsius symbol is back and alive again!
The above solution is only a workaround, and to actually fix this, The Wenquanyi Micro Hei
font itself should be patched. In fact this was what I first tried when I surprisingly found out those empty glyphs.
However, when I went to wenq.org, there latest update was posted in late 2011 (We are in 2018 now), and their forum is giving me a blank page with certain resources returning 404. Their SourceForge issue tracker is also unattended for years with that notorious hangul advance bug still open. Sadly, the whole project seems dead. So I decided to work around instead of fixing this.
And one sentence I encountered during my search kept lingering in my mind:
When you gaze long into fontconfig, fontconfig also gazes into you.
Update: With another link on their official website, I was able to find their real bug tracker and this bug, and a similar bug. The dev said it’s fixed, but maybe some recent fontconfig
or freetype
changes brought it back. Maybe some day I should fork it and try to actually fix these bugs.
And according to the dev’s comment, those glyphs existed in Droid Sans Fallback
, so a proper fix to my configuration should also include adding Droid Sans Fallback
to the sans-serif
and monospace
families, right below the WenQuanYi Micro Hei
entry.
JavaScript 是一门灵活的动态语言,在实现应用功能时十分易用。最近有写作图形界面程序的需要,又比较喜爱 GTK+ 3 的设计,因此想到可以尝试使用 JavaScript 来完成任务。
首选的运行时当然是 node,因为它有着良好的 ES2015 特性支持,并且可以方便地使用 npm 管理的模块。在经过一番搜索之后,我找到了 WebReflection/node-gtk 这个项目,却发现它无法使用 node-gyp 在我的 Arch Linux 上进行编译。另一个 creationix/node-gir 提供了对 GObject Introspection 的绑定,但是在它的 README 中写明了有 bug 和内存管理等问题,看上去也难以令人满意。
因此,我选择了使用 GJS 作为运行时。虽然没有 node 的 ES2015 支持和使用不同模块的便利,但对于使用 GObject Introspection 的绑定而言足够可靠。
GNOME 官方提供了 一个简单的 GJS 程序示例,可以按照它的架构进行编程。
Gtk.Widget
的所有属性均可以通过向构造器传入的 Object
进行设置,而可用的属性可以通过运行 gtk3-widget-factory
,然后启用 GtkInspector 来查看和实验。
可以一直使用 Gtk.Grid
进行布局,它的功能类似于 Android 中的 LinearLayout
。可以设置它的 border_width
、row_spacing
和 column_spacing
来控制留白,以及设置 expand
、hexpand
和 vexpand
控制大小。通过调用(继承自 Gtk.Container
并经过覆盖)add()
方法即可按顺序添加子控件,不需要使用 attach()
那样复杂的功能。
例如给 Gtk.Button
添加 suggested-action
CSS 类使其变蓝:
1 | button.get_style_context().add_class('suggested-action'); |
Gtk.Entry
相当于 Android 中的 EditText
。可以通过设置 width_char
来修改 Gtk.Entry
的最小大小。
Gtk.Widiget
的 sensitive
属性相当于 Android 中的 android:enabled
。
Gjs 使得我们可以使用灵活的 JavaScript 编写美观的原生程序,总体来说体验是很好的。
我把自己编写的一个小程序放在了 GitHub Gist,可以作为参考。
最后,在编写这篇文章时,我找到了另一个 结合了 Gjs、NPM 和 Babel 的示例项目,读者也可以参考它进行编程。
]]>临近毕设,学院里提供了毕业设计报告的 LaTeX 模板,于是决定离开简单的 Markdown ,开始使用 LaTeX 进行写作。以下就是我使用过程中的一些记录。
我对学院提供的旧 LaTeX 模板进行了较多的修改,使其符合了学院方面的最新格式要求,并且修复了设置字号时行间距不正确等等错误,可以直接使用或者用于参考学习:
浙江大学计算机科学与技术、软件工程专业本科毕业设计开题报告 LaTeX 模板
其中的 install-fonts.sh
可以用于从 Windows 分区获取需要的字体并进行安装。
在 Arch Linux 上安装 LaTeX(TeXLive)较为简单,可以参考 Arch Wiki 安装对应的软件包。
也可以直接执行以下命令进行安装,这样基本不会在使用时遇到无法找到常用宏包的情况。
1 | sudo pacman -S texlive-most texlive-langchinese |
xeCJK 是提供 LaTeX 中文支持的宏包,并且依赖于 XeLaTeX,因此,我们需要使用 xelatex
命令进行构建。
LaTeX 在构建交叉索引时需要多次运行,才能最终解析所有的引用,并且期间需要 BibTeX 对参考文献数据库进行处理。因此,一般的手动构建命令是:
1 | xelatex main |
不同于部分网页上给出的示例,这些命令都可以接受不带后缀名的参数,并且有时写出后缀名可能会阻碍多文件等情况下的正确构建。
对于子目录和多文件使用引用的情况,在主文件中应该使用 \include
而非 \input
,否则会需要其他处理。
为了简化构建步骤,实际上应当使用 latexmk
,它会根据需要自动调用各种命令。因此之前的手动构建步骤可以被以下命令替代:
1 | latexmk -xelatex main |
LaTeX 有很多历史问题带来的不同,也有多重相似但不同的工具选择,因此网上提供的解决方案不一定可以工作,需要自己试验。
可以通过 ShareLaTeX 提供的文档 获得对 LaTeX 的初步了解。但是,这份文档中提到的引用等高级特性的使用可能与我们的方式不同。
推荐使用 TeX - LaTeX Stack Exchange 作为解决问题时的主要信息来源。
可以通过 texdef
命令查看 LaTeX 中命令的定义。例如:
1 | texdef -t xelatex -s -c csbachelor \thebibliography |
\\
:换行,不新建段落。可选参数可以控制空白长度。\par
:创建段落。\parskip
和 \parindent
将生效。部分命令需要对段落才能生效,因此可能需要在最后一段文字最后加上 \par
。\
:一个空格。~
:一个 NBSP,不会被分行打断,常用于类似 图~1
这样的场景。\hspace{1em}
:一个 1 em 长度的空格。\renewcommand
:修改命令定义。\setlength
:修改长度变量。\setcounter
:修改计数器。LaTeX 十分强大,并且作为纯文本格式依然比 Word 文档等更加清晰、明确、可靠。但是,整个 LaTeX 环境也正因为它的高自由度和复杂,在许多方面缺乏一致性,这给使用者带来了不少困难。
总体来说,在经过大量的学习和尝试之后,我依然认为 LaTeX 是一个优秀的排版工具。它可以让使用者在一次配置之后获得强大的功能,并且在写作过程中始终保持工作在纯文本层面上。
关于各种命令和功能的常见用法,读者可以继续参考我在文章开头提到过的 报告模板,我也会在写作毕业设计论文时继续更新。
]]>因为计算机图形学课程作业的需要,我在使用过 OpenGL 的基础上学习了使用 WebGL 进行二维图形渲染。
WebGL 拥有与 OpenGL ES 相似的 API(前者基于后者),但与 OpenGL 相比,两者缺少基础图形的渲染管线,而是需要手写 shader 进行渲染。
请阅读 WebGL Fundamentals 以获得对于 WebGL 的大体了解。
在 WebGL 中需要两种 shader 进行渲染。第一种是 vertex shader,负责对顶点进行变换,例如将像素坐标映射到 clip space。而第二种是 fragment shader,负责对像素点进行着色。
Shader 可以有两种参数。第一种是 attribute,在每次调用中不同,例如当前顶点的位置;第二种是 uniform,在渲染过程中共享,例如渲染的变换矩阵。
以下是两个实用的简单 2D shader:
Vertex shader:
1 | attribute vec2 a_position; |
Fragment shader:
1 | precision mediump float; |
以下是我的一些工具函数,参考了 WebGL Boilerplate 和一部分 WebGL - Less Code, More Fun:
1 | function createAndCompileShader(gl, id) { |
以下是我自己写作的一些工具函数:
1 | /** |
以下代码参考了 WebGL 2D Translation 和 WebGL Orthographic 3D。需要注意的是,原教程的代码中有部分矩阵错误地以行优先而非列有限的方式进行表示或运算。
1 | function makeIdentityMatrix() { |
1 | var _gl = null; |
以下代码参考了 WebGL Resizing the Canvas 和 WebGL Anti-Patterns。
1 | function draw(gl) { |
作为一个复杂度适中的示例,我的作业代码可以在 这里 看到。
]]>Reveal.js 是一个美观实用 HTML 演示文稿框架,只需要你决定内容,就可以方便地产出外观优雅的演示文稿。你可以在线查看 Reveal.js 的 Demo。
为了分享已经制作好的演示文稿,可以使用 GitHub Pages 进行部署,同时也能够不用安装依赖地开启演讲者视图。以下是我建立 slides.zhanghai.me 的过程。
为了建立一个 GitHub Pages 站点,我们需要准备以下一些文件:
404.html
:GitHub Pages 将使用此页面作为默认 404 页面的替代,一般可以换成一个符合自己站点风格的页面。CNAME
:对于绑定自定义域名的 GitHub Pages,可以使用这个文件指定自己的自定义域名。.nojekyll
:我们的站点不需要 Jekyll 的特性干预,因此将它关闭来避免可能的问题。然后在 GitHub 上建立仓库,执行以下的命令:
1 | git init |
当然,使用自定义域名的话还要配置一下 DNS 记录。
为了部署 Reveal.js 的演示文稿,可以利用 Git 的 Submodule 来完成对 Reveal.js 项目本身的引用。如此以来,我们的站点项目的 Git 仓库就不用跟踪 Reveal.js 中各个文件的状态,又可以记住所引用的 Reveal.js 仓库当时的版本(Commit SHA1)。
比较幸运的是,GitHub Pages 的构建过程是可以支持 Submodule 的。需要注意的是,Submodule 的仓库地址必须采用 https 而非 ssh 协议(否则会收到构建失败的邮件通知)。以下是将 Reveal.js 作为 Submodule 引入的命令:
1 | git submodule add https://github.com/hakimel/reveal.js.git |
之后将你的 index.html
中对于 Reveal.js 文件的应用都加上 reveal.js/
的目录前缀即可。
其他详情可以参考我的站点仓库 zhanghai/slides.zhanghai.me。
Reveal.js 是一个易用而优雅的 HTML 演示文稿框架,借助于它,我已经多年没有用过 PowerPoint 了。至于利用 GitHub Pages 将它放置在网页端,则可以在免运维的情况下可靠地分享自己的演示文稿,同时也可以不用安装大量 npm
依赖来开启演讲者视图,算是一个简单又有用的小技巧。
因此,将我的配置过程和结果记录在此,希望能对他人有所帮助。
]]>Travis CI 是 GitHub 上开源项目采用持续集成的常见选择。为了给 豆芽 提供持续集成版本用于公开测试,我配置了 Travis CI,并自己编写了脚本将构建结果发布到另一个空项目的 Release 中,并将其间的过程和遇到的问题记录于此。
Travis CI 构建 Android 项目的时间较长,因此调试配置时十分耗时。希望我的经验能对他人有所帮助。
Travis CI 分为免费版(travis-ci.org)和付费版(travis-ci.com),两者之间没有相互的链接或说明,第一次配置时容易混淆。开源项目选择免费版即可。配置过程可以参考官方的 Getting Started 和 Android 项目配置。
关于 Android 项目有一些较为微妙的配置问题,我自己调试并查阅了一些 Issue 方才解决。
- tools
)。-platform-tools
)。- tools
放置在 - platform-tools
之前。因此我的 Android 部分最终配置如下:
1 | android: |
详细配置可以参考我的 .travis.yml。
Gradle 是一个为缓存优化过的工具,因此官方也提供了相应的 开启缓存的方法。
1 | before_cache: |
为了给 CI 版本的 APK 签名,需要提供相应的 keystore 和密码。Travis CI 提供了 在设置中定义环境变量 的方式来传递低敏感级的信息。
基于以上文档和一些搜索结果,并参考过 Shadowsocks-Android 的配置方式,我将我的签名配置编写为了从 signing.properties
、环境变量、终端输入三个层级进行获取的方式。
以下是我的 signing.gradle
:
1 | android { |
在 Android Studio 中进行 Gradle 同步时,System.console()
返回 null
,因而密码均为 null
,不会中断同步过程,也不影响调试版本的构建。
我创建了 signing.properties
和 signing.properties.travis
两个文件,在前者中填写 keystore 的路径并加入 .gitignore
,而将后者在 .travis.yml
的 before_script
中复制为 signing.properties
。
而在 Travis CI 的设置中,添加 STORE_PASSWORD
和 KEY_PASSWORD
两个环境变量即可。建议在环境变量值的两端加上单引号以避免特殊字符被 shell 错误处理。
直接在 .travis.yml
中调用 git describe
或 git log
等命令是无法成功的,因为 Travis CI 采用了 git clone --depth=50
来进行 clone。相应地,需要先执行 git fetch --unshallow
来完成 clone。
我采用了 语义化版本(Semver)来命名版本。因此,我的版本名称采用了如下方式获取:
1 | version="$(git describe --long --tags | sed's/^v//;s/-\([0-9]\+\)-g\([0-9a-f]\+\)/+\1.\2/')" |
例如,在名为 1.0.0-alpha
的 tag 后第 227 次短哈希值为 886f8ce
的 commit,对应的版本名即为 1.0.0-alpha.1+227.886f8ce
。
然后使用 sed -i 's/versionName .*/versionName "'"${version}"'"/' app/build.gradle
即可更新 build.gradle
中的 versionName
字段。
GitHub 提供了在 Release 中上传二进制构建输出的功能。然而,如果直接在项目仓库中为每次 commit 添加 Release(和相应的 tag)则未免过于杂乱,因此我选择了新建 一个只有 README 的仓库,并将所有持续集成版本的 Release 创建在此仓库中。
为了实现此功能,我选择了通过 curl 调用 GitHub API 的方式来完成。经过查阅文档和大量的调试,我的脚本最终是如下编写的:
1 |
|
而设置 GITHUB_ACCESS_TOKEN
环境变量后的用法则如:
1 | ./upload-to-releases.sh 'zhanghai/DouyaCiBuilds' "${version}" "${description}" "douya-ci-${version}.apk" |
如果无法确定错误原因,就多加些 echo
或者 cat
吧。
如果希望在 Lint 失败时查看输出,可以在 after_failure
中加入 cat app/build/outputs/lint-results-*.html
。
我所采用的配置都可以在 豆芽 中找到。
为 Android 项目使用 Travis CI 的过程还算简单,但是也有一些微妙的问题需要解决,这令我花费了不少时间。而将每次的构建输出上传至另一个仓库的 Release 则是我考虑了一段时间后得出的方案,之前没见到过这种方式,用 curl 调用 GitHub API 也是第一次,同时再次感受到了 bash 的得心应手,总体上是一次十分有趣的体验。
因此,将我的配置过程和结果记录在此,希望对其他开发者有所帮助。
]]>这是我在大学修习逻辑与计算机设计基础、计算机组成和计算机体系结构三门硬件课的过程中积累的 Verilog 笔记。
Verilog 是一门主要用于逻辑电路设计的硬件描述语言。语言标准主要有 Verilog-1995 与 Verilog-2001 两个版本,建议在创建工程时选择 Verilog-2001 标准以支持更多实用的语法。
虽然 Verilog 的语法与 C 相似,但是二者是面向各自的目标硬件设计的。Verilog 是一门面向逻辑电路的具体实现而设计的语言(正如 C 在某种程度上是面向汇编等底层实现的),因此写作 Verilog 时不可以将 C 等编程语言的思维方式代入,而是要始终清晰地思考正在编写的代码将能够综合成怎样的逻辑电路实现——如果可以,那么大多能够写出在字面和实现上都优雅的代码;如果不行,那么综合时大概也会报错或消耗大量资源,此时则应该考虑调整思路。
Verilog 的标准文档 IEEE 1364-2001 在网络上可以找到下载,但相比之下,这份标准还是稍显冗长或不够友好。而在标准文档之外,Xilinx ISE 的 XST 综合器也提供了实现文档,并且其中包含了许多 Verilog 语法的实用描述,因此可以作为一本更加友好的手册进行查阅。
XST User Guide for Virtex-6, Spartan-6, and 7 Series Devices
以及这里还有一份简单的非官方 Verilog-2001 手册:
Verilog HDL Quick Reference Guide
在本文草稿的同时,我的某位同学也完成了一份 Verilog 指南。指南十分详尽而实用,因此希望读者先行阅读。
在 Verilog 中,你可以使用 {COUNT{singal}}
将指定的信号重复数次后连接。
例如,想要写一个 32 位 0/1 相见的字面量,不需要写出 32'b01010101010101010101010101010101
,而是写 {16{2'b01}}
即可简洁而直观地完成。
另一个例子是 16 位到 32 位的符号扩展,也可以用 {16{signal[15]}, signal[15:0]}
来完成。
parameter
, localparam
, `define
Verilog 提供了三种定义“常量”的方式。
parameter
是可以由模块外部改变的参数。除了在模块内部定义的语法,我更加推荐采用 Verilog-2001 中在模块接口声明中定义的语法:
1 | module module_name |
localparam
是模块内部定义的参数,语法如 localparam localparam_name = localparam_value
。
`define
是 Verilog 的宏定义,与 C 宏的文本替换相似。宏除了用于定义常量,还可以用来简化代码的编写,例如:
1 |
|
可以使用 wire_name[index_wire_name]
的方式来实现一位宽度的多路选择器。相应地,Verilog-2001 也提供了 wire_name[WIDTH * index_wire_name +: WIDTH]
的方式来实现多位宽度的多路选择器。详情可以参考这个 StackOverflow 问题:
What is this operator called as “+:” in verilog - Electrical Engineering Stack Exchange。
然而这个语法的综合有一些怪异的地方,有时会导致综合成移位器而非多路选择器,消耗较多的资源,使用时需要留意一下综合报告。
在 Verilog 中,无法直接对 wire_name + 1'b1
这样的表达式选择某些位,但可以其实通过加上花括号的方式进行选择,例如 {wire_name + 1'b1}[3:0]
。这个形式可以用于显式地截短运算结果并且不触发警告。
然而这个 trick 对于变基部分选择无效。以及似乎在某些旧型号的硬件上 XST 无法识别这个语法。
可以采用 generate
、task
或 function
简化代码逻辑的编写,详细的使用方法在 Verilog General Guide 中已经说明,故不再赘述。
信号宽度警告是一种十分有用的警告,因为许多低级错误都是由于错误地(或忘记)指定信号宽度导致的。请务必仔细检查综合报告。
由于裸写的非零十进制整数字面量默认为 32 位,容易增加多余的警告,因此建议在书写字面量时总是使用 2'b
、10'd
、16'h
这样的前缀形式。
至于对信号进行递增并且在溢出后自动归零,可以采用 reg_name = reg_name + 1'b1
的形式,综合器不会将此判断为溢出而发出警告。
在 Verilog 中组合逻辑可以通过在 always @*
块中使用 if
、switch
和阻塞赋值 =
实现,也可以直接使用 assign
和 ?:
这个条件运算符实现。
我个人的建议是避免使用 always
块实现组合逻辑,因为在用 if
和 switch
实现逻辑时,很容易忘记书写某些情况下的默认值,导致综合结果为 FF/Latch 的时序电路而非组合电路,并且这件事情很难在综合报告中发现。
我目前采取的实践是仅将 always
块用于实现时序电路,而使用条件运算符实现组合逻辑。
顺带提及,always @*
用于实现组合电路,应该只使用阻塞赋值 =
;always @(posedge clock)
用于实现时序电路,应该只使用非阻塞赋值 <=
;两者混用依然可以成功综合,但是意义不明,十分容易出错。
对于长时间调试难以找出的错误,可以尝试使用 Lint 工具对代码进行静态检查,它比 XST 综合器的检查更加严格,也常常能指出代码中埋藏的许多低级错误(我曾经用它在十分钟内找出了一个调试了一整天未能找出的 Bug)。
Verilator 是一个免费的 Verilog 模拟器,其中也包含了 Lint 功能。安装 verilator
后,执行 verilator --lint-only top.v
即可开始检查。
综合过程中输出的 Simulation mismatch 警告不能无视,否则可能会导致不稳定的综合结果和奇怪的错误。
除了某些语法检查不够严格,Verilog 其实是一门设计得较为直觉的语言,语法结构大多可以映射到电路实现,有着 C 一般的简洁和直观。如果遵循一定的编码规范和最佳实践,使用硬件而非软件的思维进行编码,还是可以较为简单地达成目标的。
]]>这篇文章是我在操作系统原理课程的作业。作业选题本身很宽,大意是只要描述操作系统的发展即可。因为早就对 Plan 9 这个旨在取代 Unix 的操作系统有兴趣,又可以借此花时间仔细理解和实际使用一次这个系统,于是毫不犹豫地选择了这个题目。
以下就是我自己对 Plan 9 这个操作系统在设计上的理解和概括。
Plan 9 是一款分布式操作系统,由贝尔实验室计算机科学研究中心在 19 世纪 80 年代中期开始开发,旨在成为 Unix 的后继者。Plan 9 的团队正是曾开发 Unix 和 C 语言的团队,开发过程中参与者包括 Ken Thompson、Dennis Ritchie、Bjarne Stroustrup 等人。
本文将主要介绍 Plan 9 的部分设计理念。
Plan 9 继承了 Unix 中“ 任何事物都是文件”的哲学,并且对其进一步扩展,将所有计算资源都视为文件进行管理,使用读取和写入操作作为与资源交互的统一方式。
例如,在 Unix 中对许多具体硬件控制需要使用 ioctl
系统调用进行,以及 X Window 等系统中对于资源也使用函数调用控制而非用文件代表,但在 Plan 9 中,无论是 CPU、外围设备、网络,还是图形界面本身,设计者始终采用文件来代表资源并与之交互。
在这个意义下,其实文件的意义并非传统的磁盘存储的单元,而是一个指代一般意义上计算资源的名字。
Plan 9 与 Unix 同样采用的是层次性的文件系统。由于文件实际上就是资源,所以文件的路径实际上也与 URI 的意义类似,即计算资源可以通过特定的路径访问。
在文件的组织方式上,Plan 9 使用了与 Unix 类似约定的命名方式,例如也可以使用 ls /proc
查看所有进程等。
Plan 9 的文件系统并非传统意义上局限于磁盘的文件系统。在 Plan 9 中,由于资源即是文件,资源本身来自本地设备还是需要通过网络访问其实只是实现细节,所以两者在文件系统中具有统一的表示。Plan 9 采用一种名为 9P 的协议对资源的访问过程进行封装。经过这种抽象,系统中对远端和本机资源进行访问的接口达成了一致,由此通过分布式的文件系统从系统设计上实现了分布式计算的基础,例如即使是 CPU 资源也可以通过文件方便地共享。
文件系统只是资源的组织形式,而允许不同程序对资源采取不同的组织形式可以带来很大的可扩展性,因此 Plan 9 对每个进程使用使用了不同的文件系统,称为命名空间,并由此实现了资源的进程隔离。
此外,父进程还可以以 9P 协议向子进程的命名空间提供虚拟文件(以文件作为服务),这样实现了一种低成本而原生的跨进程资源访问,也使得提供给子进程资源可以被自由地替换。由此,实现 VPN 连接只需要将 /net
目录代理,实现窗口系统只需要将 bitblt
文件代理,等等。
因为要替换子进程文件系统中的目录,但目录中的内容又并非全部都需要重新提供,合并目录就成为了一个良好的解决方案。
Plan 9 中的合并目录是一种类似 Unix 中挂载和搜索路径相结合的做法。通过将目录甲合并至目录乙,访问目录乙时将会先在目录甲中进行查找,未找到时再在目录乙本身中查询。将这个设计与网络结合,则可以产生将远端的 /bin
目录合并至本地即可直接使用远端程序这样的使用方法。
Plan 9 更加为人知晓的一个组件则是由 Ken Thompson 提出的 UTF-8 编码。通过这种编码方式,Unicode 自动兼容于原先的 ASCII 字符,不会出现需要编码转换的问题,因此时至今日已经被广泛采用。而 UTF-8 本身则早已是 Plan 9 的原生编码。
虽然 Plan 9 有着相对于 Unix 更加普适和可扩展的设计,但在实际使用中它并没有赢得太多的用户。
Plan 9 在 1992 年向大学公开,并在 1995 年由 AT&T 商业发布。然而 1996 年,AT&T 已将重心转移至 Inferno,一个基于 Plan 9 思想设计并且旨在与 Java 竞争的系统。到了 2000 年,掌管贝尔实验室的朗讯决定停止 Plan 9 的商业支持,并在几年之后将 Plan 9 的代码以自由软件许可的形式向大众公开。
Plan 9 对 Unix 中的许多理念做了进一步的提升和扩展,对资源管理和分布式计算进行了优秀而统一的实现,在操作系统架构的历史上具有不可磨灭的意义;来自其中的 UTF-8、rc shell 等组件,也被其他操作系统所吸纳接受;并且,Plan 9 的后继者 Inferno 和 Plan B 还在继续发展。
但是,仅有优雅的设计,却缺乏相应的生态,这导致了 Plan 9 在实际应用中的结果不成功,令人惋惜。这一切正如 Eric Raymond 所说,足以让目标高远的软件工程师们时时警醒:
Plan 9 会失败,纯粹只是因为它的改进程度没有大到能取代 Unix。与 Plan 9 相比,Unix 虽然破破烂烂带有明显的缺陷,但还是能够把工作良好地完成,而这就足以保住它的地位了。
这件事情教给了那些怀有雄心壮志的系统架构师一个道理:一个更优解决方案所面临的最危险的敌人,其实是那些能把事情刚好完成的程序。
本文写作过程中参考了 Plan 9 from Bell Labs - Wikipedia, the free encyclopedia 和 贝尔实验室九号计划 - 维基百科,自由的百科全书 等页面的内容,在此表示感谢。
]]>For content-oriented Android application, how and where to store the content to display is a issue every developer will be concerned with. The Android framework provided a comprehensive solution with ContentProvider
(and a lot more) which suits quite well with a SQLite database; but in a lot of other scenarios, the app only need to have some information cached, while the majority of content is directly retrieved from network, thus eliminating the need for a database (and a complicated content scheme).
But when we look into the core of this issue, we will soon find out that it is in fact a problem of whether to have a central storage, and how to notify different components about a change.
Let’s look at the naive solution first, retrieving content from network and only keeping them in memory. In this case, no central storage is needed, and every component fetches its content independently, simple enough. But this approach immediately becomes flawed when it comes to content updating such as user modification. For instance, we have a list activity and a detail activity, and the user can often modified something in the detail activity, say, they clicked ‘like’, and the like button shall look activated not only in the detail activity, but also in the list activity now in background.
Content is fetched independently, so every component have no knowledge of whether someone else is holding the same content as itself, and that content will become stale when shown to user if the content this component newly retrieved is an updated version. Ideally speaking, any piece content is a single resource that should have only one state at point of time, and keeping it represented by one entity in memory is the best way to achieve this — yes, a central storage (whether in memory or also backed by disk), and the DRY (don’t repeat yourself) principle, a different story of the solution framework provided.
Anyway, we always need a notifying mechanism, instead of having some holder of a unique entity shared among clients, because clients may need to transform the content into something else instead of using the unique one directly, or they need notify others when content is changed.
The idea behind ContentProvider
is that, ContentProvider
is the only central storage and all its content is the only real entity that will always be up-to-date, while the content its clients hold is only a copy that will easily get out-of-date. So when a change happened, the central storage is refreshed, and ContentObserver
s are notified of the change and then they will query the central ContentProvider
for an updated copy of content.
In this case, only the ContentProvider
(or a delegated SyncAdapter
) can fetch data from network; the clients can only request a fetch and wait for the change callback just as any other client. The central ContentProvider
acts as a middle man between clients and network, to ensure the uniqueness of any content entity.
But a principally-correct and comprehensive solution may not be the best solution for a specific type of scenario. We will face at least the following difficulties:
Identifier: In order to ensure only one central entity per resource, We need URIs to uniquely identify resources just as its name suggests, so we must defining a content URI scheme. But things go complex quickly considering the complex logical hierarchy an interlinked content system will need.
Action: We’ll lose the flexibility on handling changes for special cases. Think of infinite loading, liking and post deleting, we’ll need a generic mechanism to notify all the observers on an URI if some items are newly loaded, modified or removed, while framework’s ContentObserver
only offers an onChanged()
method. However if we handle this only for specific cases, it can be much more easier to implement.
Releasing memory: Because most of the content is dynamic, there is little point in data persistence on disk. So we’ll store retrieved items only in memory, and then, we need to release them once nobody needs them. This can be tricky when, for instance, we have an observer on a collection, and another observer on a detail of this collection is removed, now whether the detailed content should be released depends on whether the collection observer actually observes on this detail, which cannot be inferred by the central content manager from URI scheme.
JSON interoperability: The framework ContentProvider
mechanism uses Cursor
, where only basic types and blob are valid column type, making it uninteroperable with the widely used JSON approach. We’ll have to roll our own.
Implementing and using such a framework is a complex and heavy task, and complexity is error-prone, while it brings little advantage over the decentralized solution we are going to talk about.
So we want to stay on the track of not having any central storage, and allow duplicated (so possibly inconsistent) entities of content in memory.
This way, we are to do a sync among components once we any content is newly fetched from network in this case. The solution is much more specific to each scenario in this case: we can utilize some already present event bus system to get notified of updates, listen to specific event code of content change, and then respond accordingly.
On the first sight, this solution may seem not generic enough, lacking in the beauty of unity. However, considering the overall cost of using a centralized storage with URI scheme, I believe this decentralized-and-syncing mechanism is the way to go for applications in this scenario.
Different mechanisms apply to different scenarios. Android’s centralized ContentProvider
mechanism fits for content that should be persisted and synced, while the decentralized mechanism works well with application with highly-dynamic content.
从我入门 Android 开发至今已经两年有余,过程中有了一些经验和积累,所以在此将我对于入门者的建议记录下来。
学习 Android 开发,首先要有扎实的 Java 基础。如果不理解一门语言的设计思想,编写代码时将常常感觉思维与语法发生冲突,同时也无法针对这门语言扬长避短,最终代码将会难以实现和维护。
对于尚无 Java 基础的同学,我推荐 《Thinking in Java》 这本书(中文译本为《Java 编程思想》),因为它对 Java 语法和设计的讲解较为精确详细,同时对设计模式和最佳实践也有合适的涉猎。这本书除了最后一章关于图形界面的部分可以跳过,之前所有的章节,都应当仔细地阅读和理解一遍。我个人在阅读时使用了两周大约 40 小时的时间。
此外,初学者请格外重视代码风格,始终遵循 Java 的编码规范,因为这可以使代码风格统一、结构有序、逻辑清晰和易于维护。IntelliJ Idea(Android Studio)的默认代码风格正与之符合。
在拥有了 Java 语法基础之后,就可以开始学习 Android 的框架了。
Android API 和最佳实践的更新较为迅速,所以几乎没有可靠的 Android 开发书籍,不推荐购买书籍入门。而 Android 官方网站的文档 是保持与框架同步的,同时也有很多关于最佳实践的介绍。
所以我的建议是,从官方文档的 Training 开始,读完最基础的部分后,自己想好要做的一个小型项目,再按照需求查找 Guide 和 Reference,结合 StackOverflow 等资源,在实践中完成第一个自己的 Android 应用,掌握一些 Android 开发的基础知识。这个过程我使用了另外两周的时间。
此外作为补充,Code Path 的教程 也可以参考,虽然并非必须;大多数最佳实践和陷阱都需要开发者自己通过搜索多方资料和阅读源代码积累。
关于开发时使用的环境,Eclipse 早已经是日薄西山,而构建于 IntelliJ Idea 之上的 Android Studio 才是 Google 和社区支持的重心,也是在官方文档中建议使用的开发环境。
同时,Android Studio 的 Refactor,Generate,Lint Check 等功能也十分强大,能够显著地提高开发体验和效率。此外 Emacs 键绑定在调节个别设置之后就完全可用。
Android Studio 的连击 Shift 全局搜索功能在想要打开特定文件时也十分好用,默认在工程范围内搜索,再次连击可以将框架代码也纳入搜索范围。
在工具的选择上还是应该以功能性和主流选择为依据,因此我建议即使之前熟悉 Eclipse 的同学,也开始使用 Android Studio 进行 Android 开发。
官方文档中有关于 Android Studio 和 Android SDK 配置的详细说明。Android Studio 的更新动态可以在 Anroid Tools Project Site 上查看,也提供了 RSS 订阅。
在开发的过程中如果遇到问题,首先可以尝试 Google 搜索。搜索结果中,又以 Android 官方的教程结果为好,StackOverflow 辅之,某些编写精良的国外博客可以参考。
此外,如果是框架或者支持库的 Bug,Google Code Issues 和 StackOverflow 上也可能有人贴出 Workaround;如果暂时没有,自己在 Google Code 上提交 issue 也有一定可能收到关注(和 Chris Banes 搭话玩)。
对于 CSDN、博客园等国内博客,则应该尽量避免,即使读时,应抱有将信将疑的态度,因为国内博客的总体质量偏低,常常代码风格混乱、不遵循最佳实践、只以达成效果为目的,甚至可能有明显的错误。当然少数排版良好、编写细致的博客除外。
至于 Android 开发 QQ 群、微信群等,由于质量和氛围,在总体上个人极其不建议新手加入。
如果问题没有直接的解答,则可以尝试查看 Android 的 JavaDoc 和源代码,来理解框架的具体行为。
在这个过程中,Android Studio 是很好的工具。通过按住 Ctrl + 单击,可以跳转到源代码所在的位置,或者资源文件的定义;点击类或方法左侧行号旁边的标记,可以访问继承或重载当前片段的代码,查看框架的细节行为,或是内部的使用范例;这些功能使得查看框架代码十分方便快捷。
此外,GrepCode、GitHub 和 Google Source 可以在线查看框架代码的变更,以及未进入 SDK 的代码,使用时 Google“类名 网站名”即可。三家各有千秋。GrepCode 类名或方法名旁的向下箭头,类似 Android Studio,可以点击查看使用情况或者继承、覆盖者等,便于快速定位问题、理解架构;GitHub 和 Google Source 则有完整的 git 记录,可以查看 diff 和 blame(GoogleSource 是真正的版本库所在地,GitHub 会自动拉取 GoogleSource 上的更新);个人感觉 GitHub 的界面和功能相对更加友好一些。
顺带一提,涉及到 IPC 的 IBinder
之类代码,具体实现可以在 com.android.server
这个包下找到。
虽然 Android 的 Training 和 Guide 中大部分是以原生框架 API 来教学的,但实际上有一些功能是在后续版本的 Android 中才引入的,直接使用这些功能会导致在旧的 Android 上找不到相应方法而发生崩溃。
在 Android 开发的早期,问题不是如此明显,开发者经常自己条件执行与版本相关的代码。久而久之,也有人将部分方面的兼容性代码封装成库提供给开发者使用,直到后来 Android 官方终于发布了 Support Library 来全面地完成这项工作。
我曾经对支持库有所保留,有时还是偏向于使用原生实现而非支持库实现。但后来我慢慢感受到,Android 官方的支持重点还是使用支持库的方式,并且这样也能够不断带来更好的兼容性。因此,我还是建议新手在使用新 API 时,选择支持库而非原生实现。
目前支持库主要向下支持至 V7,而官方建议开发者向下支持至 V14(Android 4.0) 就可以了,这样覆盖了 95% 左右的用户。关于 Android 版本的统计数据可以在 这里 查看。
Android 中界面、网络、数据存储等许多高级功能的实现可能较为复杂,这时开源库就变得十分方便。现在大多数 Android 的开源库都托管在 GitHub 上,并支持在 gradle 中一行引入。但是,GitHub 并非专注于 Android,搜索功能也还有待提升,因此以下介绍其他一些常用的技巧。
建议:
了解:
在选择开源库时,建议考察它的 Star/Fork 数、文档、代码风格、作者回复 Issue 的活跃程度,谨慎作出决定。一旦使用,建议 Watch 此项目,关注更新。
建议使用的部分开源第三方库:
findViewById()
。另外提一下其他几个项目,且并非针对作者个人。在我个人看来,虽然很多很全,但 android-common 和 android-demo 两个项目的代码质量并不算很高,也无法确定是否始终遵循着 Android 最新的变更和最佳实践。因此,我个人极其反对直接复制粘贴其中的代码(或者其他任何代码),也不推荐直接参考其中的代码而不查阅其他来源;相应地,应该始终使用 Google 搜索解决方案,这样才能保持最新。
作为一名合格的 Android 开发者,也应该拥有 Android 相关的设计素养,而不是设计师给了一个不标准(或者像 iOS)的设计,却不知道提出意见或修正。
Android 现有的设计规范是 Material Design,已经初步成熟,但有时还是会有小的补充,因此建议始终阅读这个文档。在入门 Android 开发后,建议抽出时间,将这份文档通读一遍,以了解符合 Android 平台的设计。另外,我见过的几个中文版设计文档翻译质量一般,排版较差,还有一些时效性问题,因此不是十分建议阅读。
Material Design 的实现主要依靠支持库中的 AppCompat 和 Design 库,目前文档不算十分完善,可以参考 BlogSpot 上的相关介绍 和 Chris Banes 的 CheeseSquare,辅助以 StackOverflow、Google Code Issues 和部分国外博客。
为了精确地实现界面布局,可以使用开发者选项中的“显示视图边界”来确定各个视图的大小和位置,此外 KeylinePushing 是一款十分好用的工具应用,可以在屏幕上显示关键线和网格,帮助检视视图布局。
关于设计资源。Material Icons 是 Google 的开源 Material Design 图标库,内容十分丰富,也符合设计规范。Android Asset Studio 曾经十分流行,但由于 Material Design 的引入开始有些过时,不过启动器图标和设备预览图的生成器依然可以使用。
Android 的更新发展较为快速,为了跟进 Android 最新的动态,可以了解、关注或订阅以下的人或内容。其中,Google Plus 可以作为一个干净的技术社交平台(屏蔽几个热门推荐就好),有很多活跃的 Android 开发者和社群,十分建议加入。
建议:
推荐:
学习和开发是一个循序渐进的过程,通过一个实际项目,不断接触 Android 的方方面面,例如网络、数据库、视图、资源系统、组件、支持库等等,最终才可以对 Android 开发拥有深入的了解。祝各位学习顺利。
]]>这是我在作为学生组织的面试官时,对熟练掌握 C/C++ 的面试者提出几个的问题,以及对于这几个问题我自己的答案。
语法规则和特定技巧很大程度上只是记忆的问题,而在几个事实上知晓与否并不能断定面试者的高下。C 语言是一门十分接近底层实现的语言,许多的设计决定与实现直接相关,如果想要合适地运用,也要求程序员对于 C 的实现有透彻的理解。因此,这几个问题主要考察的是面试者在 C 原理与设计层面上的理解。
为什么以下的语句不会导致非法内存访问?
1 | struct { |
C++ 中的类实现了继承。请简单阐述如何在 C 中实现 struct
的继承。
在 C 中 void *
类型可以自动转换成其他指针类型,但在 C++ 中需要显式转换。请谈一谈 C 和 C++ 为什么采取了这两种设计。
(请简述 C 语言中变量自动初始化的规则。)
在 C 语言中,全局变量和静态局部变量可以自动初始化为全零,但局部变量不会进行自动初始化。请谈一谈 C 为什么采取了这样的设计。
C 语言中类型只存在于编译前,struct
只是对偏移量计算等操作的简化而已,因此,计算 &(*s).f
,不过是计算s + sizeof(s.i)
而已,并不会像其他语言那样导致空指针异常。
因为 C 中的 struct
不过是对偏移量计算的简化,可以通过在子类的头部直接放置一个父类成员,之后进行类型转换,就能实现数据成员的继承,示例如下:
1 | typedef struct { |
为了实现函数的继承和多态,则需要函数指针,this
指针参数,构造器等实现,此处不再赘述。
在 C 中,void *
指向的是未知类型的内存区域,无论按照什么类型理解,都是合法的;并且 malloc 这样的常用调用返回类型只能是void *
,允许自动转换可以大大减少麻烦。
在 C++ 中,类型可能是类,而为了实现面向对象编程的特性,在类中保存了一些元信息,因此将一块未知的内存区域作为一个类来理解,这个操作不一定合法;为了保持类和非类指针在语法上的统一,选择了禁止 void *
的自动转换。
由于很多全局变量的初始值都需要是零,而在可执行文件中存储这么多零并没有意义,因此 C 语言规定全局变量初值为零,由此可以在生成的可执行文件中只记录全局变量所需空间的大小,而省略为零的值。至于静态局部变量,它的生命周期要求与全局变量相同,只是在编译时赋予了不同作用域的限制而已。为了实现这个特性,在大多数操作系统中,全局变量和静态局部变量被存放在一个全局的数据存储位置(BSS 段),在程序开始执行前由系统进行分配和清零。
局部变量与函数调用相关,在系统调用栈上动态分配,自动初始化将会在运行时不断带来额外负担,因此 C 语言将初始化交给程序员处理,不进行自动初始化。
理解了 C 的实现与设计之后,使用 C 就得心应手了--你了解语言这样设计的原因和目的,也知道自己的一行代码会被翻译成怎样的指令,由此,豁然开朗。
当然,C 有适合的任务,也有不适合的任务,任何语言都是如此,所以复杂性上的欠缺并不影响我对 C 语言的评价。
大道至简,C 的许多设计与 Unix 哲学若合一契。也是因此我对 C/C++ 这种说法不太喜欢。
]]>将项目发布至 Maven Central,可以使开发者在使用时十分方便地进行整合和依赖管理。然而发布过程并不简单,因此将我发布的过程和遇到的问题记录于此。
作为一个 Android 开发者,一般会使用 Gradle 进行构建与发布。广为使用的脚本是 Chris Banes 所写的 gradle-mvn-push
,项目中的 README.md
有详细的步骤说明。
外部文章gradle-mvn-push 的 README
我不喜欢将密码明文写在配置文件中,而是习惯从终端读入密码,因此我对 gradle-mvn-push.gradle
进行了一些修改:
1 | def getReleaseRepositoryUrl() { |
此外,这个脚本在构建 JavaDoc
时会使用系统的语言设置,导致生成的文档可能是中文版本。通过将 LANG="en_US.UTF-8"
附加在 gradle
命令之前可以将语言设置为英文。
关于我自己采用的配置,可以参考我 在 Github 上的项目。
为了在 Maven Central 上发布项目,一般会选择在 Sonatype OSSRH (OSS Repository Hosting) 上注册账户进行操作。最顶层的帮助是以下这篇文章,从其中的链接可以进入其他子页面查看详细步骤。
外部文章OSSRH Guide
同时,这篇中文文章也较为详尽,对我的发布过程有很大帮助。
外部文章使用 Gradle 发布 AAR 到 Maven 仓库
以下是我的步骤:
Group Id 请填写顶级域名的反转,因为每个用户只需要创建一次工单,之后会将整个 Group Id 下的权限开放给用户。我填写了项目的 Group Id,之后被处理者修正。
SCM URL 可以直接填写 GitHub 仓库的地址。我开始时一直尝试使用 SSH 克隆地址,但总是不能通过验证。
发布项目至暂存区。
使用 gradle-mvn-push
,执行 LANG="en_US.UTF-8" gradle clean build uploadArchives
。这需要正确的 PGP 签名设置,将在这篇文章的稍后进行说明。
公开发布项目。
前往 Sonatype OSS,登录,点击左侧边栏中的 Staging Repositories,找到并选择自己的项目,点击 Close,刷新并等待操作完成,之后点击 Release,即完成公开发布。
回复工单
在处理者对工单的回复中,会要求你在公开发布第一个项目后回复此工单,照做即可。
GnuPG 2.1 的使用是我在这次发布过程中遇到的最为棘手的问题,因为网络上大部分的教程还是基于之前的版本,导致许多已经更改的操作方式不再可用;并且 Gradle 也尚未支持这个版本,尽管 2.1 已在去年 11 月发布。因此在这里简单记录一些我遇到的问题和解决方案。
无法选定密钥长度。
在 GnuPG 2.1 中,需要使用 --full-gen-key
以获取之前在 --gen-key
中可用的选项。
无法使用命令行输入密码。
GnuPG 默认在有图形界面时使用会 XGrabKeyboard
的窗口进行密码输入,并且无法从剪贴板粘贴文本,给我使用随机密码带来了很多不便,因此我希望从命令行输入密码。
在 GnuPG 2.1 中,一切相关的操作被交由 gpg-agent
完成,同时,--pinentry-mode loopback
被提示为不支持,即使在 gpg-agent.conf
中添加 allow-loopback-pinentry
也没有作用。
最终我的解决方案是在执行 gpg
前 DISPLAY=""
使其使用一个基于 curses
的命令行界面,然后对终端进行粘贴文本。
secring.gpg
中找不到私钥。
GnuPG 2.1 已经不再使用 secring.gpg
,因此新创建的私钥不会向其中写入,而是会由 gpg-agent
写入至 private-keys-v1.d
目录。为了兼容 Gradle 签名插件,我将私钥导出,模拟了一个 secring.gpg
。
1 | DISPLAY="" gpg --output secring.gpg --export-secret-keys <YOUR_KEY_ID> |
需要将公钥发布至公钥服务器才能通过审核。
在对项目进行 Close 时,会对签名进行验证,因此需要将你的公钥上传以使其可以被公开获取。我选择了发布至 MIT 公钥服务器。
1 | gpg --keyserver pgp.mit.edu --send-key <YOUR_KEY_ID> |
可以使用 Java 的 keytool
工具进行生成,但 Android Studio 提供了自带推荐参数并且简单易用的界面。
在 Android Studio 中,点击“Build > Generate Signed APK…”,如果已有密钥存储则点击“Choose existing…”并选择,之后点击“Create new…”填写信息并创建密钥即可。
这种方法并不会覆盖密钥存储中已有的密钥。但还是建议在操作之前,进行备份。
发布项目到 Maven Central 的过程确实十分复杂,同时 GnuPG 进行了不兼容升级并且缺乏文档,所以令我花费了不少时间。
相比之下,通过 BinTray 发布则更为简单和自动化一些,然而它要求你将私钥上传至它的服务器,这令我不太舒服,因此并没有采用。
虽然麻烦,但是对于开发者来说,发布之后的使用还是十分舒服的,因此这些努力也算没有白费。记录在此,希望能够在其他开发者发布自己的项目时有所帮助。
]]>在我的计算机组成课程上,教授要求编写一个 MIPS 汇编器并提供图形界面。由于我的工作环境并非 Windows,同时希望在各个平台的界面中使用原生控件,并且我偏好使用静态类型语言,所以决定使用 SWT 进行图形界面的编写。
鉴于 Maven 上的版本老旧,推荐直接前往 SWT 官方网站 下载稳定发行版本。我使用的开发环境是 IntelliJ Idea,因此在 Project Structure 中的 Libraries 和 Artifacts 内添加相应的设置即可。
我是一名 Android 开发者,因此我在学习 SWT 时也会与 Android 框架进行对比。二者多有类似,也有不同。
在 SWT 中,Display
与 Android 中 Context
和 Application
的设计有所相近,具有系统资源管理和应用层面操作的功能。而 Display
的一个实用的扩展是 Display.getCurrent()
和 Display.getDefault()
,即可以通过静态方法获取当前线程或默认的 Display
。
而与 Android 中的 Activity
相近的则是 Shell
。Shell
代表一个平台无关的抽象窗口,由此可以进行窗口相关的操作,并建立视图结构。
与 Andorid 不同,SWT 中没有 XML 布局系统,所有视图的创建需要由应用程序的 Java 代码完成(但有自动将 XML 转换为 Java 代码的工具,如 SWTXML 等)。Control
与 Android 中的 View
类似,是所有控件的基类,但不同的是,所有的 Control
在构造时都需要一个 Composite
作为父对象,既是 Android 中的 Context
,也是视图结构上的父容器。而之后如果需要更改控件的父容器,可以调用Control.setParent()
,但是否成功则需要由平台支持决定。
一个窗口最初的 Composite
实例即是 Shell
。
SWT 的主循环是由应用程序自己书写的。常见的范式如下:
1 | onCreateDisplay(); |
在创建 Shell
实例之后,一般会添加界面元素和布局信息。示例代码如下:
1 | private void onCreateShell() { |
SWT 中的布局方式分为两步:使用 Composite.setLayout()
设置父元素布局子元素时使用的布局方式,使用 Control.setLayoutData()
提供子元素的布局参数。这与 Android 中的布局方式有些许不同,即 SWT 中的 Layout
是父容器的参数而非父容器本身的类型。
SWT 中常用的 Layout
包括 FillLayout
、RowLayout
、GridLayout
、FormLayout
和 SashForm
等。对于前三种基础布局,可以参考 Eclipse 的这篇文章 进行学习。
与 Android 框架不同,SWT 只是一个图形界面库,因此不提供资源系统。
在写作一般规模的 SWT 应用时,可以使用 Java 自带的 ResourceBundle
进行资源管理与国际化。
常见的范式如下:
1 | ResourceBundle resourceBundle = ResourceBundle.getBundle("res/string/mipside", new Utf8Control()); |
其中 Utf8Control
是为了使得 ResourceBundle 支持直接读取 UTF-8 编码的资源文件的类,可以从之后我的项目中获得我编写的版本;ResourceBundle 默认只支持 ASCII 编码。
为了保存用户的设置,可以使用 Java 自带的 Preferences
进行键值对的存储。使用时可以通过 Preferences.userNodeForPackage()
获取一个节点的 Preferences
实例,而之后则与使用 Android 中的 SharedPreferences
类似。
在 Linux 上,一个 Shell 脚本执行 java -jar
已经足够。
在 Windows 上,经过一些比较,我决定使用 launch4j,至今体验良好。
在 Mac OS X 上,JVM 需要特定参数才能正常启动应用,以及其他打包应用程序的细节,可以参考 这篇 Eclipse 的文档。
在使用 SWT 完成我的项目后,总体来说,感觉这是一个可用的图形库。
从接口方面看,SWT 遵循着一致的命名规范,但 Builder 模式的缺失使得构造控件的过程较为冗长,此外将样式、键码等大量常量放置在 SWT
类中也显得较为奇怪。
从功能方面看,SWT 跨平台调用原生控件的能力确实令人惊喜。众多部件在 Windows、Linux GTK+ 3 和 GTK+ 2、Mac OS X Cocoa 上不需要经过调试即表现良好,并且通过打包与原生应用几乎没有什么差异。美中不足的只是一些小的设计细节,例如标签中的文本不能使用光标选择复制、带有链接的多行标签需要使用另一个类却不能指定多行文本居中对齐。但总体来说,这些瑕疵要么可以绕过,要么相比于 SWT 本身带来的便捷来说,无伤大雅。
菜单、字体、图像、对话框、StyledText
,还有关于 SWT 的很多方面这篇文章没有覆盖到。但网络上相关的资源是十分丰富的,例如 Google、Eclipse 的 Javadoc 和 Java2s 上的代码示例都是有用的信息来源。
如果有兴趣,我的项目中也有一些按照我自己对 SWT 的理解写出的代码,可以 移步至 GitHub 浏览。
]]>Android 中ListView
的拖放操作和动画实现已经被 这个 DevByte 和 相关的样例 说明,并且也有 ListViewAnimations 这样强大的开源库进行了集成。但是,一番 Google 后,我发现基于 LinearLayout
的相关实现却不多。
然而,有时我们可能需要使用 LinearLayout
替代 ListView
来实现列表,例如不需要 ListView
的视图回收机制(比如使用 Fragment
作为列表项),或者我们需要把这个视图放在 ScrollView
中。
在使用 LinearLayout
实现拖放和动画时,实现代码相比于之前提到的 ListView
实现也需要一些变动。因为我在网络上没有找到相应的资料,所以写下这篇文章来记录这个过程。
前置阅读Drag and Drop | Android Developers
为 LinearLayout
设定 View.OnDragListener
很简单,其机制在官方教程中有详细说明,在此不再赘述。
但是,官方教程中给出的样例在释放被拖动条目后只会显示一条 Toast
,而一般的需求则是拖放排序。所以在参考了网上的一些文章后,我给出了下面这个简单的实现。与官方样例相比,添加的主要是在ViewGroup
中交换子视图的实现,以及将被拖动的视图作为 LocalState
传递。
1 | public static void setupDragSort(View view) { |
这个实现已经可以完成拖动排序,然而界面效果却不理想:被拖动的条目没有消失,列表在拖动过程中也没有作出相应的改变。下面我们将使用 Android 的属性动画实现这种界面效果。
言归正传。为了实现拖放过程中的动画,我们的目标是使用 LinearLayout
的列表视图能够对用户的拖动实时作出相应,也就是每次当用户的拖动越过某个临界线的时候,就将列表展现为被拖动条目在这里放下时的预览。因此,需要完成的工作就是将被拖动视图的 Visibility
设置为View.INVISIBLE
,此时被拖动视图参与布局计算,但不进行绘制(已经被用户拖起),再不断改变列表中各个条目的位置。
1 | public static void setupDragSort(View view) { |
一个很自然的想法是,在用户拖动条目经过某个其他条目超过一半高度时,就将这个条目在父视图中的位置与被拖动条目互换(而不是等到用户拖动完成时)。这样就基本实现了布局系统中的改变。然而,由于在用户快速拖动时,Android 可能来不及向每个经过的视图发送消息,这种方式可能导致列表顺序的改变的问题(我在自己测试时就遇到了)。
所以在实现视图交换时,我们需要使用递归的方式进行,直到两个视图达到相邻。实现代码如下。
1 | public static void setupDragSort(View view) { |
接下来是交换过程中动画的实现。在实现过程中,我参考了 justasm 的 DragLinearLayout 中的代码,在此表示感谢。
前置阅读Property Animation | Android Developers
在实现动画时,我们主要利用的是 Android 的属性动画机制,涉及到的是 View
的Y
这个属性。
在谈及实际实现之前,值得在此提及的是 View
的Left
和 Top
与X
和 Y
的关系。Left
和 Top
是在视图树布局过程中按照视图层级和布局参数等得出的,表示特定视图在屏幕上被布局系统分配的位置;而 X
和Y
则是用于在实际绘制视图时定位的依据。
这种实现的好处是,通过将实际绘制时与布局时的视图位置独立起来,可以实现动画过程中视图的位移、旋转等视觉变换,而不必受到布局系统中视图定位的拘束。顺带一提,X
和 Y
其实是由 Left
和Top
分别加上 TRANSLATION_X
和TRANSLATION_Y
得到的,这是因为实际上视图还是要依赖于布局才能定位。
言归正传。为了让视图位置的变化更加平滑,需要让视图的绘制位置从上一个位置渐变到下一个位置。我们在需要改变视图位置时可以通过 View.getY()
得到视图当前的绘制位置,但视图的下一个位置则需要经过下一次布局计算后才能获得。因此,我们使用一个常见的技巧,也就是利用ViewTreeObserver.OnPreDrawListener
,在绘制之前获取已经计算完成的布局位置,在这时开始进行视图动画。
1 | private static void swapViews(ViewGroup viewGroup, final View view, int index, |
如此,我们就基本完成了拖放操作和动画的实现。效果如下:
既然写了这么多,最后再顺带给出一个删除条目及相应动画的实现。其中的 view
参数是在 viewGroup
外的一个拖放目标,用于删除。
1 | public static void setupDragDelete(View view, final ViewGroup viewGroup) { |
需要注意的是,如果 LinearLayout
的高度设置为 wrap_content
,则为了避免动画被视图边界剪裁,以及在ScrollView
中高度正确变化,需要手动对 LinearLayout
的高度进行动画;这同时涉及到需要覆盖 ScrollView
中measureChild()
方法来计算我们所请求的高度。我在下面的完整实现中完成了这个部分。
]]>完整实现在 GitHub 上浏览
Use Google style guide for shell.
Use functions; use a main
function and call it by main "$@"
at the end of the script.
Use 1
or 0
literal for boolean; test by [[${bool_val} -eq 1 ]]
.
Use local
for function local variable; seprate declaration and assignment.
See this manual.
Always quote "$@"
to preserve correct word splitting.
Use read -p <prompt> -s <variable>; echo
to read password.
Include this function and call it before you’re going to use sudo
.
1 | prepare_sudo() { |
Use Here document for usage(), etc.
You can utilize the following technique to write a here document as root
1 | sudo tee "${profile_path}" >/dev/null <<EOF |
``
name=$(basename “$0”)
1 |
|
error() {
echo “${FUNCNAME[1]}: $@” >&2
}
1 |
|
1 | for i in $(seq 0 "${timeout}"); do |
Use inotifywait
.
1 | msg_blue()·{ |
这是我在大一时 C 语言课程考试前总结的坑们,每次相关考试前都会复习一次。如果你发现可以补充的地方,欢迎评论。
一言以概任何看上去有简单答案的问题都藏有坑。
看见定义变量,总是注意变量是否被初始化过。
注意 if
中的等号是 =
还是 ==
。
if
语句没有花括号时只包含之后一行,如:
1 | if (condition) |
if else
语句在没有花括号时,else
总是匹配最近的if
(Dangling Else),如:
1 | if (a) |
实际上是:
1 | if (a) { |
switch
语句在缺少 break
时将 Fall through。
求值逻辑表达式时 真
为 1
, 假
为 0
。 判断逻辑表达式时, 非 0
为 真
,0
为 假
。
true
、false
、TRUE
、FALSE
是合法的变量名,因为在 C 中它们不是关键字。
*p++
等价于 *(p++)
,因后自增++
优先级高于解引用*
。
注意整型除法结果被截断, int n = 2; 1 / n == 0
。
注意本地变量需要初始化。
注意变量作用域屏蔽,如:
1 | int b; |
注意函数为传值,指针也是值传递,例如:
1 | void swap(int *a, int *b) { |
并不影响外部。
注意 int
溢出,若易发生溢出(如阶乘操作)应使用 long
或double
代替。
char a = 255
,打印后值为 -1
,因为 char
为有符号类型并采用补码表示,其范围为 -128 ~ 127
。
负数的位移操作中符号位也参与位移;左操作数为负数的右移 >>
结果由实现定义,Turbo C 为补 1。
a/b
与 a%b
结果在 a
与b
中有一个为负数时由实现定义,但保证 a / b * b + a % b
结果为a
。
过滤回车结束的输入字符串:
1 | i = 0; |
C 语言二维数组存储采用 Row-major 方式,即在内存中存储为 row1, row2, row3…,初始化时必须给出列数以确定行中元素个数,静态变量中未初始化元素自动为 0,使用时可以将列数溢出到下一行,即偏移值计算为 row_count * row + column。
注意分号:
1 | for (...); |
do_something
只执行一次。
1 | if (0); |
do_something
一定会被执行。
sizeof()
表达式的值在编译时确定。编译器不计算其中表达式的值,仅将其替换为对应类型。sizeof
应用于数组时结果为 数组元素个数 * 元素大小,应用于指针时为指针变量长度(在 32 位机器上地址长度为 32,故值为 8;64 位机器上为 16)。
无论 x
是数组还是指针,在定义上编译器认为 x[3]
与*(x+3)
是等价的。但根据 x 的类型是数组还是指针, 编译器将为 x+3
或sizeof(x)
生成不同的代码。
类似 char []
类型的数组名被视为指向 char
的指针,char[][]
也被视为指向 char[]
的指针。
注意 &&
或||
具有 短路求值
特性,不会执行无必要求值的表达式。
注意含中文字符的文件应保存为 GB*
编码,例如GB18030
。
strcpy(dst, src)
中目标在前,源在后。
注意 int a, b, c
,c = 2a + b
非法(需要*
)。
注意 x<=y<=z
意为(x<=y)<=z
,意义非预期但合法。
scanf
遇到空白字符截止,为输入一行可使用 scanf("[^\n]",str)
或gets()
。
注意:scanf("%s", s)
输入 "How are you?"
遇到空格截断,只得到How
。
strcpy(char *dst, char *src) {while(*dst++ = *src++); };
中 ++
优先级高于*
[]
、()
优先级高于 *
,故int *array[1]
等价于 (int *)array[1]
,为 int 指针的数组;int (*p)[1]
为指向 int
数组的指针。
解读方法:以 int **a[1][2]
为例。从 a 出发,优先向右解读。(来源)
a
is … int
a
is array
of 1 … int
a
is array
of 1 array
of 2 … int
a
is array
of 1 array
of 2 pointer
s to … int
a
is array
of 1 array
of 2 pointer
s to poiner
to int
优先级:后置自增 / 自减 、函数调用、数组元素、结构成员 > 前置自增 / 自减、正负、类型装换、 解引用 *、取地址、数据类型大小、内存操作、(逻辑、位)非 > 结构成员解引用 > 乘除、取余 > 加减 > * 位移 > 比较 > 等价 > (逻辑、位)与、或、异或 > 三元条件、(复合)赋值。
字符串字面值与字符常量中转义序列 \ddd
可以为 1、2、3 位 8 进制数(07),上限为 377;F)。\xhh
可以为 1、2 位十六进制数(0
转义序列列表:
转移序列 | 含义 |
---|---|
\a | BEL |
\b | BS |
\f | FF |
\n | LF |
\r | CR |
\t | HT |
\v | VT |
\\ | \ |
\' | ' |
\" | " |
\0 | NULL |
\ddd | 八进制 |
\xhh | 十六进制 |
0~255 的数字也可以作为有效的字符取值。
int i = -1; printf("%d", (unsigned int)i);
打印出 -1
,因为%d
为有符号整型。
使用草稿纸记录变量取值以计算程序输出,或对某些类型可理解程序意图猜测程序输出,或两者结合。
10^-6
表示为1e-6
。
通过 memset(array,0,sizeof(array))
可以实现数组重初始化。(需要string.h
)
宏 #
代表将此后的文本变为字符串;##
代表连接文本;含有 #
与##
的宏不进行参数展开,可利用包装宏展开。(参考)
宏函数定义:
1 |
|
调用与函数调用一致:
1 | func_name(var); |
scanf
中 f
为float
,lf
为 double
;printf
中f
为 double
;lf
未定义。
,
逗号表达式对左侧表达式求值并丢弃返回值,之后对右侧表达式求值并返回其返回值。
#define SWAP(a,b) (a)^=(b)^=(a)^=(b)
可实现无临时变量的交换。
字符 | ASCII |
---|---|
0 | 48 |
A | 65 |
a | 97 |
变量命名法则:[A-Za-z_][A-Za-z0-9_]*
,且不含关键字。
auto
,register
,volatile
均为 C 关键字。
007
为八进制数字,注意范围为 0-7,0x0F
为十六进制数字。
strcat
实现:
1 | void strcat(char *str1, char *str2) { |
]]>相关阅读《C 陷阱与缺陷》笔记 | 孙耀珠的博客