使用JitPack发布你的Github开源库

最近从手头的一个项目中独立出一个Android的开源组件(EmotionLayoutDetector)发布到了Github上,想用Gradle来管理依赖。

一般来说,想在Android Studio使用Gradle依赖,有几种方法:

  • jCenter
  • Maven Central
  • 使用其他自定义的仓库

使用jCenter和MavenCentral的一般流程是,注册、登陆、配置参数、Build、Push、等待验证,最后才能通过,整个网站有一种上世纪的感觉,发布过程繁杂冗长,让我觉得十分不舒服。

在使用别的开源组件的过程中发现,越来越多的Github项目开始使用JitPack替代jCenter进行发布

JitPack实际上是一个自定义的Maven仓库,不过它的流程极度简化,只需要输入Github项目地址就可发布项目,大大方便了像我这种懒得配置环境的人。

发布

在网站首页输入你的Github项目地址,点击「Look up」就可发布

image1

使用

在配置中加入自定义的仓库

allprojects {
    repositories {
        jcenter()
        maven { url "https://jitpack.io" }
    }
}

加入依赖

dependencies {
    compile 'com.github.username:Project-name:v1.0'
}

这样就行了,是不是很简单?

Android: Activity在Restore时的数据缓存问题

红米Note上频繁NullPointException

继解决了Fragment中使用getActivity()返回null的问题后,在测试中又发现,在红米Note上离开程序后从后台返回时经常Crash,错误仍然是NullPointException。。。

项目需求是要求先登录,在LoginActivity登录完毕获取到Token后,我直接用Intent传给了主界面MainActivity再进行后续操作。但是当程序后台被杀掉后再恢复时,虽然会重新执行MainActivity的onCreate(),但是Intent内的数据却不会再有,我取出Token后没有做判断就使用,于是就抛出了NullPointException。

红米系列内存比较小,App切换到后台以后极易被干掉,所以这个问题在红米Note上比较容易重现。

保存数据

错误的原因在于没有缓存Intent传入的数据,那我们就缓存一下好了。

最简单的办法莫过于写入SharedPreference了,存入取出都很方便:

SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
// 存入数据
sp.edit().putString("token", token).apply();
// 取出数据
String token = sp.getString("token", null);

但是sp只适合存放少量的数据,若需要缓存的数据稍复杂一点用sp就会很麻烦,另一种办法是用数据库缓存,但数据库缓存代码量比较大,改动也不是很方便。

其实,Android已经考虑到了这种问题,提供了一种系统级的界面缓存数据的方法,即InstanceState机制。

利用Android的InstanceState机制

我们可以看到,在Activity的onCreate()方法是带参数的:

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
}

同时,Activity还带有两个方法:onSaveInstanceState()onRestoreInstanceState():

Activity的 onSaveInstanceState() 和 onRestoreInstanceState()并不是生命周期方法。

它们不同于 onCreate()、onPause()等生命周期方法,它们并不一定会被触发。当应用遇到意外情况(如:内存不足、用户直接按Home键)由系统销毁一个Activity时,onSaveInstanceState()会被调用。

但是当用户主动去销毁一个Activity时,例如在应用中按返回键,onSaveInstanceState()就不会被调用。因为在这种情况下,用户的行为决定了不需要保存Activity的状态。

通常onSaveInstanceState()只适合用于保存一些临时性的状态,而onPause()适合用于数据的持久化保存。

因此,在InstanceState内保存数据的方法类似SP,非常简单:

@Override
public void onSaveInstanceState(Bundle savedInstanceState){
    super.onSaveInstanceState(savedInstanceState);
    savedInstanceState.putString("token", token);
}

@Override
public void onRestoreInstanceState(Bundle savedInstanceState){
    super.onRestoreInstanceState(savedInstanceState);
    token = savedInstanceState.getString("token");
}

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	// 你也可以在onCreate()里恢复数据
	// 但这样做需要判断恢复的数据出来是否为null
	token = savedInstanceState.getString("token");
}

这样,在后台内存不足程序被回收时,会自动缓存数据,下次进入时自动恢复就不会抛出异常了。

参考

  1. Android开发之InstanceState详解
Android: Fragment中getActivity()返回null的问题

getActivity()返回null

在一个项目中使用了ViewPager+Fragment的组合,但是在实际使用中频繁的Crash。排查后发现,我在Fragment内有一些AsyncTask联网操作,在网络链接失败的时候会弹出Toast消息提示。而生成Toast时传入的Context参数是getActivity(),该函数返回null,于是就抛出了NullPointException:

Toast.makeText(getActivity(), "Some Message", Toast.LENGTH_LONG).show();

查阅一下Fragment的生命周期:

image1

在Fragment的生命周期中,onAttach()onDetach()之间getActivity()函数才会返回正确的对象,否则的话返回null。

因此,如果我正在做某些操作联网,在等待过程中点击Back键返回,使得这个Fragment被销毁了,这时Fragment就会和Activity解除附着(onDetach),当再试图弹出Toast的时候,getActivity()返回null,于是就Crash了。

保存Context引用

明白了问题出在哪就好解决了,常用的方法是在Fragment附着在Activity上时用一个变量保存引用,即:

@Override
public void onAttach(Activity activity){
	this.mContext = activity;
}

使用全局Application来得到Context

在每个Fragment内都用一个变量保存Context的引用确实可以满足需求,但是代码冗余了不少,更一劳永逸的办法就是使用全局Application来得到Context。

Android程序中Application、Service和Activity都实现了Context,但只有Application才能保证在程序运行期间一直存在并且具有唯一性,因此在程序中可以使用Application来获得Context而不用担心空指针。

首先在manifest文件中注册Application

<application
	android:name=".MyApplication"
	android:icon="@drawable/ic_launcher"
	android:label="@string/app_name" >

然后创建MyApplication.java,我们在这里使用单例模式来对外保持Application的引用

public class MyApplication extends Application {
	private static MyApplication instance;

	@Override
	public void onCreate() {
		super.onCreate();
		instance = this;
	}

	public static MyApplication getInstance(){
		// 这里不用判断instance是否为空
		return instance;
	}
}

这样在程序的任何地方都可以使用Application来得到Context了。

Context context = MyApplication.getInstance();
Toast.makeText(context, "Your Toast Message", Toast.SHORT_TOAST).show();

参考

  1. Android Fragment 生命周期
  2. Android应用程序的Activity启动过程简要介绍
Mac OSX:Powerline风格的zsh配置

image1

上图就是效果图啦,是不是很炫,配置教程如下:

需要的工具

  1. iTerm,一个替代OSX自带终端的软件,基于iTerm才能实现上面的效果;
  2. oh-my-zsh,zsh是OSX上最强大的shell,没有之一,但是配置过程较为复杂,这个脚本能够帮你一键配置。
  3. powerline主题,基于oh-my-zsh的主题,也就是上面的效果了。

iTerm

目前最新版本是iTerm2,下载地址http://iterm2.com/,这个没什么好说的,解压以后扔到Application里,然后你就可以把系统自带的终端从Dock栏移除了~

oh-my-zsh

手动安装前需要先安装git,这里就不说了,Google一下即可。

然后安装oh-my-zsh,执行这个自动安装脚本:

https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh

或者使用手动安装:

git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh
cp ~/.oh-my-zsh/tmplates/zshrc.zsh-template ~/.zshrc

都不是很复杂,安装完成后会打开终端(iTerm)窗口,不过这个时候终端貌似还不是很好看,因为它使用的是自带的主题,下一步我们就要换上高大上的Powerline主题了

Powerline 主题

同样的有两种安装方法,自动安装脚本:

git clone git://github.com/jeremyFreeAgent/oh-my-zsh-powerline-theme ~/.ohmyzsh-powerline
cd ~/.ohmyzsh-powerline
./install_in_omz.sh

自动安装脚本的原理是git clone下来这个项目,然后在oh-my-zsh的theme文件夹内创建了一个符号链接,实际上我们可以直接将主题文件放进theme包里,不用留着~/.ohmyzsh-powerline这个文件夹,如果你希望这么做,则可以手动下载zip包,将powerline.zsh-theme放入~/.oh-my-zsh/themes/内,也可以执行下面的脚本。

git clone git://github.com/jeremyFreeAgent/oh-my-zsh-powerline-theme ~/.ohmyzsh-powerline
cd ~/.ohmyzsh-powerline
cp powerline.zsh-theme ~/.oh-my-zsh/themes/powerline.zsh-theme
rm -rf ~/.ohmyzsh-powerline

然后打开~/.zshrc,将ZSH_THEME=”robbyrussell”改为ZSH_THEME=”powerline”就更换了主题,重启iTerm,就能看到效果了

配置Powerline

现在的Powerline主题已经有一点雏形了,还有一点点问题。

  1. 为了显示Powerline风格,它使用了一些特殊符号来显示箭头,系统自带的字体并不支持,所以需要手动下载别人打包好的字体,下载地址是http://github.com/powerline/fonts,执行里面的install.sh,然后在iTerm的设置里选中你喜欢的字体即可。

  2. 默认的颜色看起来有点奇怪,我们可以调整一下iTerm对ANSI颜色的实现颜色,我在这里修改了一下背景颜色和蓝色,更改了一下字号等等。

image1

最后就大功告成了!

Git:如何还原一个已经同步到远程仓库的Commit?

本文译自git howto: revert a commit already pushed to a remote repository - Christoph Rüegg

如果你刚刚将你本地的分支推送到了远程仓库中,但是却突然发现到其中的一个Commit有很严重的错误,就必须在别人pull这些Commit前还原它,否则被坑了的同事会画小圈圈诅咒你的:)

首先,有两个备选方案可以让你的历史记录完好无损:

备选方案一:在一个新的Commit中改正错误

最简单的方法就是将错误的文件修复好后作为一个新的Commit提交,并同步到远程仓库中。这是一种很直观、也很安全的修复方式,在99%的情况下你都应该使用这种方法,除非这个错的Commit中包含敏感信息。

备选方案二:完全恢复(Revert)这个Commit

有时候你会希望撤销一个Commit的全部更改,幸运的是你可以告诉Git去恢复这个Commit而不用手动地去做这件事。恢复的时候甚至可以不必是最后一个Commit。恢复一个Commit意味着你会撤销这个错误的Commit中的所有更改。就像刚刚的例子中,这个错误的Commit的记录仍然存在,只不过它不再影响当前工作的分支和任何后续的Commit了。

$ git revert dd61ab32

关于历史重写

一般来说我们会避免重写提交的Commit历史,一个很重要的原因就是它会使其他Clone过或Fork过你代码的人和你岔出不同的分支,导致他们不再能pull你重写了的历史记录和后续Commit。如果他们在本地做了一些更改想同步到远程仓库上,需要一些高级的Git知识来理解怎样让Git正常进行工作。

然而有时候你确实就像重写历史记录,无论是因为泄露了敏感信息还是想清除一些本不应该在代码中的非常庞大的文件,亦或者你仅仅就是想要一个干净的历史记录(当然我确实这么做过)。

我平常迁移Subversion或Mercurial项目到Git中时也做过大量的重写历史记录的工作,无论是为了统一LF行结束符,修改提交者的名字、邮箱地址,还是为了从版本库中彻底删除那些大文件。最近我也因为一个大的版本库中一个很早的Commit引起了越来越来多错误而重写了版本库历史。

是的,如果可能的话你应该重写那些已经扩散到了其他fork代码中的历史记录,即使你这样做了也不会世界末日。比如,你仍然可以用Cherry-pick在历史记录中来移动Commit来处理那些在旧历史记录中的Pull请求。

请记住,在开源项目中重写历史记录前先联系版本库的管理者。有的管理者一般情况下不允许任何重写历史,并且会拉黑那些这么做的人,而另外一些管理者倾向于他们自己来做重写。

情形1:删除最后一次Commit

删除最后一次Commit是最简单的一种情况。例如,我们现在有一个拥有master分支的远程仓库mathnet,它现在处于最新的Commitdd61ab32下。我们希望移除这个最新的Commit,或者用Git语言说,我们想force远程仓库mathnetmaster分支到dd61ab32的父Commit上。

$ git push mathnet +dd61ab32^:master

在Git命令中,x^代表x的父节点,+代表一个强制的非快速推送(forced non-fastforward push)。如果你在本地已经处于master分支下了,你也可以通过两个简单的步骤来实现同样的效果:先回退到父Commit,然后强制推送给远程仓库。

$ git reset HEAD^ --hard
$ git push mathnet -f

情形2:删除倒数第二个Commit

现在我们假设需要删除的Commit没有在历史记录的顶部,而是一个稍旧一点的Commit,比如倒数第二个。我们希望删除它同时保留其后的所有Commit,换句话说我们希望重写历史记录然后将重写强制应用到mathnetmaster分支上。重写历史的最简单的方法是对错误的Commit做一个互相的变基:

$ git rebase -i dd61ab32^

这个命令会打开一个vi编辑窗口,显示了从我们想删除的Commit以来的所有Commit。

pick dd61ab32
pick dsadhj278

简单地删除我们想删除的Commit信息,在这里我们删除第一行即可(在vi中删除当前行的命令为dd)。保存退出编辑器(在vi中输入:wq然后按回车键),然后解决一下有可能的冲突,然后强制推送到远程仓库就大功告成了:

$ git push mathnet -f

情形3:修复一个Commit中的拼写错误

这种情形下的操作与情形2十分相似,但不是删除Commit信息,而是将pickedit换一下位置然后保存退出即可。然后变基时,操作将会停在那个Commit上,这时你可以对这个索引做任何你想做的操作。完成后Commit改动然后继续变基(如果你想的话Git会告诉你如何保持Commit的附加信息),然后继续按情形2进行推送。用同样的方法可以将一个Commit分成很多小的Commit,或者将很多Commit合并到一起。