使用Gradle生成包含所有依赖库(.jar或.aar)的aar包

Android Library项目中如果使用Android Gradle plugin打aar包,通过maven依赖的库,或者是local依赖的aar都不会包含在生成的aar包里,如果项目是发布一个SDK,为了方便开发者使用,我们倾向于生成一个包含所有依赖库以及.so等文件的aar包。

通过反复研究和测试,以下Gradle脚本能满足需求,如果需要对代码运行ProGuard混淆,则需要使用Gradle 2.1

方法是为项目增加一个sub project(如pkg_project)专门用于打包,该项目中build.gradle内容如下:

apply plugin: 'java'
version = 1.0

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:19.1.0'
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile project(':<your_library_project>') //此处填写需要打包的Android Library Project name
}

task sync_jars() << {
	 //把所有依赖的.jar库都拷贝到build/aar/libs下
    copy {
        into buildDir.getPath() +"/aar/libs"
        from configurations.compile.findAll {
            it.getName().endsWith(".jar")
        }
    }
}

task sync_aars(dependsOn:':<your_library_project>:assembleRelease') << {
	 //把所有依赖的.aar库里包含的classes.jar都拷贝到build/aar/libs下,并重命名以不被覆盖
    def jar_name
    def aar_path
    def dest_dir = buildDir.getPath()+"/aar"
    configurations.compile.findAll {
        it.getName().endsWith(".aar")
    }.collect {
        aar_path = it.getPath()
        jar_name = "libs/"+it.getName().replace(".aar",".jar")
        copy {
            from zipTree(aar_path)
            into dest_dir
            include "**/*"
            rename 'classes.jar', jar_name
        }
    }
}

task fataar(dependsOn:[sync_aars, sync_jars]) << {
    task (obfuse_classes_jar, type: proguard.gradle.ProGuardTask) {
    	//build/aar/libs/*.jar混淆后生成build/aar/classes.jar
        configuration "proguard.cfg"
        injars buildDir.getPath()+"/aar/libs"
        outjars buildDir.getPath()+"/aar/classes.jar"
        libraryjars "${System.getProperty('java.home')}/lib/rt.jar"
        libraryjars "${System.getProperty('java.home')}/Contents/Classes/classes.jar"
        libraryjars System.getenv("ANDROID_HOME")+"/platforms/android-19/android.jar"
    }.execute()

    task (gen_aar, type: Zip) {
    	//把生成最终的aar包,注意libs目录需要被排除
        def dest_dir = buildDir.getPath()+"/aar/"
        baseName = "mysdk-all"
        extension = "aar"
        destinationDir = file(buildDir.getPath())
        from dest_dir
        exclude "libs"
    }.execute()
}

最后就可以使用gradlew pkg_project:fataar来打包了

[Read More]

简单手机应用同步协议设计和实现

版本:Ver 0.3

在手机上我们需要持久化应用的一些数据(典型的如本地的设置信息),同时又希望能重装应用或换一台手机登录后能把这些数据再同步回来。业界有SyncMl标准,覆盖的功能很完善,正因为要保证兼容性,开源的实现都较重。如何借鉴这个标准自己来实现一个多端双向同步可扩展的功能呢?

App使用同步协议可以将原本必须在线操作的功能(如:删除一个联系人,修改一个联系人的备注信息)也可以在断网情况下完成。

我们假设一些前提:

  1. 同一时刻只有一端(iPhone,iPad或其他移动设备)能和服务器同步;
  2. 客户端和服务端的时间一致或误差较小;可在长连建立时通过协议记录时间差
  3. 客户端保存全量数据(对于客户端只保存部分数据的情况后面再做讨论);

应用场景

  1. 通讯录同步
  2. 最近联系人
  3. App客户端设置
  4. 最近会话列表
  5. 黑名单
  6. 群设置
  7. 群成员
  8. 用户的一些设置和开关

名词解释

  1. LCID - Local Unique Identifiers, 客户端生成的记录ID,客户端唯一;
  2. GUID - Global Unique Identifiers, 服务端生成的记录ID,全局唯一;
  3. Anchor - 同步锚点,可以使用递增的序列号或时间戳来表示,用来发现两端数据变化的部分;
  4. Session - 同步会话,由客户端发起,Session Id唯一。

客户端表设计

每条记录包含两个同步用的字段:

status - 用来标识记录的状态

Status含义
0本地新增
-1标记删除
1本地更新
9已同步

anchor - 用来记录服务端同步过来的时间戳。

服务端表设计

modified - 记录在服务端的修改时间

双向同步流程示例

1. Client 增加2条记录

idkeyvaluestatusmodifiedanchor
1FooBar010
2HelloWorld020

客户端新增记录时,需要将Status设为0

[Read More]

使用Gradle生成一个App的不同版本,且可以同时安装在一个手机上

背景

开发一个App一般会生成内测版和正式版,甚至还会有不同渠道的版本,不同版本的配置可能会不一样,比如内测版会需要记录完整的日志。

Android手机对于同样的Application Id的App只能安装一个版本,如果我们需要同时安装内测版和正式版,就必须修改其中一个版本的Application Id。

解决方案

Gradle支持buildTypes和productFlavors两种定制方法,这里只介绍通过buildType的解决方案。通过productFlavors则可有效解决渠道包,arm,x86等分平台以及付费版和广告版的打包问题。

修改debug版的包名

配置如下:

android {
    buildTypes {
        release {
            ...
        }

        debug {
            applicationIdSuffix '.debug'
            ...
        }
    }
}

修正资源文件里的包名

如果你的项目里使用了自定义的View,且有自定义的属性时,会需要修正一下xml命名空间里的包名。

android.applicationVariants.all { variant ->
    def buildType = variant.buildType
    def encoding = java.nio.charset.Charset.defaultCharset().toString()
    if (buildType.applicationIdSuffix) {
        def defaultPackageId = variant.packageName.replaceAll(buildType.applicationIdSuffix,'')
        variant.mergeResources.doLast {
            def dir = file("${buildDir}/intermediates/res/${variant.dirName}/layout")
            dir.listFiles().each { f->
                String content = f.getText(encoding)
                content = content.replaceAll("res/"+defaultPackageId, "res/"+variant.packageName)
                f.write(content, encoding)
            }
        }
    }
}

定制APK的应用名称

如果同时安装两个版本,那么最好能从应用名称上来区别一下,一般我们在AndroidManifest.xml中使用String resource来命名,如下:

    <application
    	...
        android:label="@string/app_name" >

build.gradle里增加下面的代码就可以为debug版一个特殊的命名了

android.applicationVariants.all { variant ->
    def buildType = variant.buildType
    def encoding = java.nio.charset.Charset.defaultCharset().toString()
    if (buildType.applicationIdSuffix) {
        def defaultPackageId = variant.packageName.replaceAll(buildType.applicationIdSuffix,'')
        variant.mergeResources.doLast {
            def f = file("${buildDir}/intermediates/res/${variant.dirName}/values/values.xml")
            String content = f.getText(encoding)
            content = content.replaceAll('来往','来往Beta')
            f.write(content,encoding)
        }
    }
}

修改ContentProvider Authority

如果你的应用里还提供ContentProvider的话,如下:

[Read More]

安装第三方App到电视盒子

安装第三方App方法1

  1. 准备一个U盘,注意用FAT格式(Windows能读写就OK)
  2. 将需要安装的App下载到U盘,文件后缀名必须是.apk
  3. 将U盘插入电视盒子,然后通过盒子自身带的文件管理App安装:如Magic Box:应用--> 本地播放,进入后选中U盘上的.apk文件即可安装

安装第三方App方法2

  1. 如果盒子不带USB盘,还可以用adb远程安装
  2. adb connect <your_magic_box_ip>
  3. adb install <you_app_to_install>.apk

直播和回放App

  1. 在电脑上用浏览器下载:http://app.shafa.com/shafa.apk 到U盘
  2. 按方法1安装好后,在“应用“中找到“沙发管家”
  3. 启动“沙发管家”,安装直播App:如“龙龙直播”,也有支持回放的App,如“电视猫视频”等。

Gradle的Properties

问题背景

团队一起在开发一个Android项目,工程师有的使用Eclipse,有个使用Intellij IDEA,有的使用Android Studio。每个人安装的Android SDK build-tools可能都不一样,有的是19.0.3,有的是19.1.0,不同版本的build-tools对Gradle Plugin也有相应的要求,如19.0.3对应的是com.android.tools.build:gradle:0.10.+,19.1.0对应的是com.android.tools.build:gradle:0.12.+,下面是一个典型的build.gradle配置文件。

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:0.10.+'
    }
}

apply plugin: 'android-library'

android {
    compileSdkVersion 19
    buildToolsVersion 19.0.3

    defaultConfig {
        minSdkVersion 8
        targetSdkVersion 19
    }
}

在合作开发中遇到的一个尴尬的问题是,IDEA最新版还不能很好的支持Gradle Plugin 0.12+,而Android Studio最新版则要求使用0.12+。大家又共用一个Git仓库。可能的解决方案是,从Git checkout出来的项目需要有一个基础的版本号,但是开发者可以在本地通过一处文件(不check in到git)来重载版本号。

解决方案

Gradle支持三种Properties, 这三种Properties的作用域和初始化阶段都不一样,下面分别列出了其部分特点。:

  1. System Properties:

    1. 可通过gradle.properties文件,环境变量或命令行-D参数设置 2. 可在setting.gradle或build.gradle中动态修改,在setting.gradle中的修改对buildscript block可见;
    2. 所有工程可见,不建议在build.gradle中修改
    3. 多子工程项目中,子工程的gradle.properties会被忽略掉,只有root工程的gradle.properties有效;
  2. Project Properties:

    1. 可通过gradle.properties文件,环境变量或命令行-P参数设置,优先级是:
    2. 可在build.gradle中动态修改,但引用不存在的project properties会立即抛错
    3. 动态修改过的project properties在buildscript block中不可见
  3. Project ext properties:

    1. 可在项目的build.gradle中声明和使用,本工程和子工程可见
    2. 不能在setting.gradle中访问

如果有多处设置,加载次序如下(注意:gradle 2.0是这样的, 1.10~1.12有bug), 后面的覆盖前面的设置

  1. from gradle.properties located in project build dir.
  2. from gradle.properties located in gradle user home.
  3. from system properties, e.g. when -Dsome.property is used in the command line.
  4. setting.gradle
  5. build.gradle

根据其特点,这里给出一个使用System Properties来解决问题的方案。

[Read More]

Android异步编程

Android的线程和内存模型

Android操作系统在boot后,会启动一个Zygote(受精卵)进程,Zygote进程负责创建大部分应用程序进程。Zygote进程启动加载核心程序库和数据结构到内存后会创建一个Dalvik虚拟机(DVM)进程--SystemServer,此进程会包含大部分的系统服务(包括管理Activity的服务ActivityManagerService),SystemServer初始化后,Zygote进程会侦听本地的socket端口, 等待进一步的指令。当新的app被启动时,Zygote会为这个app创建一个DVM—-直接fork出一个子进程,这种架构的好处是同时启动多个App时,多个App进程可以访问共享内存。

Android App的进程也是一个DVM,内部有许多线程在执行,比如,主UI线程(Main Thread),垃圾回收线程等。其中主UI线程负责执行我们写的应用代码。对于只做很少的I/O操作或耗时操作的App,单一线程开发模式问题不大,但是如果有大量IO或者CPU计算的任务,我们就必须在其他线程内完成了。

因为主UI线程需要根据硬件刷新率[^3]同步用户界面的重绘。手机应用体验流畅要求界面帧率[^3]达到每秒60,也就是说每16.67毫秒就需要重绘一帧,这意味着如果我们在主线程上执行的任务超过16毫秒,就会出现丢帧现象,也就是界面会开始变卡。。。

Android异步执行任务的方法有以下几种:

AsyncTask

AsyncTask是最常用的异步方法,功能结构设计的也很丰富,给使用者足够的控制,使用上主要是将异步执行的任务放在下面方法里。

protected Result doInBackground(Params... params)

然后调用.execute(params)方法即可。

AsyncTask的执行逻辑在API Level 3只能串行执行, 在API Level 4改成了最多128个线程的线程池执行,API Level 11则改成了缺省所有的AsyncTask是在一个线程中顺序执行的,这样可以保证执行和提交的次序一致,如果希望能并发的执行,可以用下面的方法在线程池内执行:

task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);

AsyncTask.THREAD_POOL_EXECUTORThreadPoolExecutor的一个实例,配置是最少5个线程,最多128个。如果需要自己来配置线程池大小,你可以传递自己配置的一个实例到上述方法。

使用AsyncTask需要注意的几个问题:

碎片化问题

因为不同版本的Android AsyncTask缺省执行逻辑并不一样,可能在不同机型上表现不一致。如果要自己控制AsyncTask的并发度,解决这个问题的建议是复制Android SDK的AsyncTask源码自己实现一个AsyncTask。

Activity生命周期问题

Activity可能早于AsyncTask执行完被销毁,如果AsyncTask还继续执行,有可能会浪费资源,并且如果AsyncTask里引用了Activity或部分的View Hierarchy,还会造成引用的对象不会被垃圾回收而引起内存泄漏。通常AsyncTask会定义为Activity的一个匿名inner class,这会建立一个隐式的引用到Activity。

解决的方法是:

  1. 在Activity里的onPause方法里及时取消不需要再执行的AsyncTask(这种方法在切换到横屏时会重启异步任务,有点浪费)
  2. 更好的是使用retained headless fragment来解决生命周期问题,具体演示代码见参考链接

Async比较合适的是较短的(1,2秒),CPU密集计算或读写文件等阻塞IO操作。耗时较长的网络调用用Async不是最合适的。

Handler & HandlerThread

Handler的异步编程是基于消息队列模型的。执行任务的线程称之为Looper线程,其他线程则将需要异步执行的任务发送给Looper线程–插入其消息队列,方法有:post(较方便使用,但每次需要创建新对象)或sendMessage(较高效,复用消息实例,适合执行大量类似的异步任务)

Looper

Looper和它的名字意思一样就是Looper线程会永远循环,当没有消息的时候,Looper线程(消费者)会使用(Object.wait)方法等待其他线程(生产者)插入新的任务消息,这时候其他线程(Object.notify)

Android的主线程其实就是一个Looper线程。

需要注意的是,使用Handler和AsyncTask一样,要注意匿名inner class对Activity的隐式引用而造成内存泄漏,所以使用的时候要记得清理;

解决方法是使用对使用的Activity中的View对象用Weak Reference,并处理当View对象为null的情况。

Handler适合更长一点的(>2秒)的异步任务处理。

Loader

Loader在Android编程框架中被广泛用于后台加载数据(从文件,数据库甚至网络)。

AsyncTaskLoader

具体的Loader实现。

CursorLoader

用户数据库数据后台加载。

Loader在使用上比较大的优势是和Activity的部分解耦,更见到的生命周期管理。

IntentService

IntentService是Service的一个实现类。其内部实现包含了一个HandlerThread,当任务提交给IntentService时,会被加到队列并顺序处理。

public class MyIntentService extends IntentService {
     public MyIntentService() {
       super("thread-name");
     }
     protected void onHandleIntent(Intent intent) {
       // executes on the background HandlerThread.
     } 
}

Intent intent = new Intent(context, MyIntentService.class);
intent.setData(uri); intent.putExtra("param", "some value"); 
startService(intent);

IntentService返回任务结果给Activity可以有下面几种方法(Service和Activity的通信方法):

[Read More]

提高Android开发效率的小贴士

查看日志 adb logcat

下面命令将只显示错误日志,和所有Tag=mytag的调试日志,-C 会用不同颜色区分不同级别的日志,但只有Android 4.3以后才支持。

adb logcat [-C] *:E <mytag>:D

远程调试 adb over TCP

首先在手机或Pad上执行以下命令(要求root)

su
setprop service.adb.tcp.port 5555
stop adbd
start adbd

再执行下面命令则可以看到手机的网络地址

netcfg | grep wlan

在电脑上则执行

adb connect <mobile_phone_ip> 5555
adb shell

安装运行

gradle installDebug && adb shell am start -n com.laiwang.protocol.android/.MainActivity

Over

拷贝Android应用的数据

有root权限

adb shell su -c cat /data/data/app.package.name/databases/application.sqlite | sed 's/\r$//' > application.sqlite

应用可调试的话

adb shell
run-as app.package.name \
cp /data/data/package.name/databases/application.sqlite /sdcard/
exit
adb pull /sdcard/application.sqlite ~/

使用备份方法

adb backup -f ~/data.ab -noapk app.package.name
dd if=data.ab bs=1 skip=24 | python -c "import zlib,sys;sys.stdout.write(zlib.decompress(sys.stdin.read()))" | tar -xvf -

参考链接

  1. http://blog.shvetsov.com/2013/02/access-android-app-data-without-root.html

在Android上使用tcpdump

tcpdump工具是分析网络协议和数据包的利器,也可以在Android上使用(需要root)。

首先在android上安装tcpdump

wget http://www.strazzere.com/android/tcpdump
adb push tcpdump /data/local/tmp/tcpdump
adb chmod 755 /data/local/tmp/tcpdump

然后使用root用户启动tcpdump,在android上进行相应的操作后,按ctrl+c中断

adb shell
shell@android:/ $ su
root@android:/ # /data/local/tmp/tcpdump -h                                    
tcpdump version 3.9.8
libpcap version 0.9.8
Usage: tcpdump [-aAdDeflLnNOpqRStuUvxX] [-c count] [ -C file_size ]
		[ -E algo:secret ] [ -F file ] [ -i interface ] [ -M secret ]
		[ -r file ] [ -s snaplen ] [ -T type ] [ -w file ]
		[ -W filecount ] [ -y datalinktype ] [ -Z user ]
		[ expression ]
root@android:/ # /data/local/tmp/tcpdump -p -vv -s 0 w /sdcard/capture.pcap

tcpdump会在/sdcard下生成文件,可以通过adb pull /sdcard/capture.pcap把文件传到PC上用wireshark看,也可以直接在android上通过SharkReader看。

[Read More]