Android开发环境
22 Oct 2016Android Studio
从这里下载Android Studio,下载最大最新的那个,含sdk和各种tools,省得后续下载的痛苦。如果找不到完整包,只安装了studio,可以在上面网站的sdk-tools下安装android-sdk_rxxx-windows.zip,这个包下面会包含SDK Manager.exe,然后用这个exe去安装其余的sdk组件(不要用android studio的向导去安装sdk组件,没有这个单独的SDK Manager好用),无论用哪种方式安装了sdk,记得设置环境变量ANDROID_HOME到sdk的目录。因为后续很多步骤是依赖这个变量来找sdk。
使用SDK Manager安装sdk时,如果不用安卓虚拟机,可以不安装每个sdk下的各种system image,又大又不好用。
国情设置
在启动Android Studio之前,针对我们国情,建议增加c:\users\xxx\.gradle\init.gradle
文件,内容如下:
allprojects {
repositories {
maven{ url 'http://maven.aliyun.com/nexus/content/groups/public'}
}
}
这个为gradle设置一个镜像,可以防止gradle从中央库下载文件(非常慢)。
运行studio
第一次运行,会有的一个向导,可以取消掉,没有关系。但是接下来,一定不要open之前的项目,因为项目需要的gradle版本不一致,一样可能会卡死,所以保险的方式是先创建一个HelloWorld,成功之后把其下gradle\wrapper\gradle-wrapper.proerties文件的最后一行拷贝出来,以后所有的项目都用这一行,类似下面这个:
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
也就是说,所有的项目都要修改这个proerties文件,确保用同一个gradle版本,以免去下载,理论上这样可能有些问题(万一gradle不兼容),所以另一个解决下载gradle卡死的方法是先让他卡,然后杀掉Android Studio,再去 这里 下载对应的版本,然后放到
C:\Users\用户名\.gradle\wrapper\dists\gradle-xxx-all\sdfdfa4w5fzrksdfaweeut
后面这串乱码是Android Studio在打开工程时自动建出来的,所以要让他先卡一下,把这个目录建出来(有点奇葩的设计),然后就可以放进去了。
gradle
gradle缺省设置会导致内存消耗过大,因为它驻留后台,有必要调整一下,修改用户目录下的.gradle/gradle.properties文件,加上
org.gradle.jvmargs=-Xmx256m -XX:MaxPermSize=64m
模拟器
在Android Studio内可以管理模拟器,一定记得使能Intel Haxm,可以大大加速模拟器的速度,使能Intel Haxm会自动从intel下载一个驱动并自动安装,但是要在下次重启计算机之后,Haxm才会起到加速效果,所以如果安装了Haxm,还是很慢的话,可能需要重启下。
真机调试
真机调试需要在手机上开USB调试选项,然后用USB接到开发机上,安装驱动,就可以调试了,不过不是每个手机都容易安装驱动,有些手机非常不容易安装驱动,如果实在搞不定,参考这个帖子 一步一步安装应该可以成功。
有的时候,换个手机还是不能识别,可以从设备管理器里手动安装驱动,选择上面安装好的驱动程序,就可以了,参考下图:
几个调试时遇到的问题:
- 运行的时候如果出This version of android studio is incompatible with the gradle version used.Try disabling the instant run,可以在 Settings/Preferences > Build, Execution, Deployment option > Instant Run 下面禁用Instance run所有选项即可。
- Android studio启动调试时出现unable to open debugger port, 重启Android studio也不管用,最后的解决方法是
adb kill-server
再adb start-server
- 有些手机每次调试安装应用的时候会弹出一个安装确认(比如华为的emui),这个可以在开发者选项中关闭。
无线连接adb
很多时候,usb连adb不是很方便,可以改用wifi,先在usb连接的情况下执行adb tcpip,这个命令把手机切换到无线adb模式,然后adb connect 192.168.xx.xx
就可以了,只要不重启手机,每次都可以用这个adb connect连到手机。偶尔有时在wifi下会连接不成功,但使用手机做热点就没有问题。
如果手机已经root,上面使用usb的步骤可以忽略,通过app切换到wifi模式。
adb有很多有用的命令
adb bugreport
使用这个命令可以直接获取错误日志,相对手机上的操作要方便一些。
logcat
logcat用来看android日志,不过这个命令设计的不那么友好,常见的使用场景都要加很多参数,不琢磨一会,用起来比较麻烦,下面是两个例子:
导出日志到文件(-v time是为了加时间,-d是为了导出后退出,否则他一直监听日志就退不出了):
adb logcat -v time -d > logcat.txt
要想过滤某个进程的日志,logcat并不支持,logcat只支持按tag(就是log命令的第一个参数)和level过滤,显然按tag过滤很多时候不合适,多个进程可能有同名的tag,所以只能再加grep来按pid过滤,用 adb shell ps,找到进程pid,再用下面的命令来过滤:
adb logcat -v time | grep pid
这样的话,windows下只能用findstr了,另一个常见的问题是,logcat的内容太多了,有时候某些进程不停的写log,导致log很快满了而冲掉有用的log,但是android没有提供一个方法可以禁止某个进程记录log(除了杀死进程),有的时候(定位一些偶发问题)很不方便。
还有一种场景,应用crash了,因此只想看crash相关的日志(crash日志是一个单独的buffer),可以用
adb logcat --buffer=crash
log的级别有多个,有些手机(比如魅族)缺省会不记录debug级别的日志,可以在设置》开发者选项里调整。总之logcat的设计有点反人性,并且logcat并不persist,所以手机重启之后就没有了。
如果设备不再手边,或没有条件使用USB线,可以在设备上安装一个catlog工具,可以直接显示或保存日志,挺好用,但是这个工具需要root权限。
theme
签名冲突
apk的debug版本和release版本签名是不一致的,导致无法替换安装,会报错
signatures do not match the previously installed version; ignoring!
大部分时候下载之前的版本就可以了,可是有的时候,会发现根本这就找不到之前的版本,甚至通过adb shell pm list packages也找不到,那么可以强制“卸载”一次,再安装就可以了,这样:
adb unintall package-id
app文件浏览
app可以使用内部存储或外部存储,内部存储仅对app开放,可以使用adb来方便浏览和检查,这样做:
adb shell
run-as your.package.id
ls -all
开发与亮屏
开发调试时,通常需要不停查看屏幕,默认的安卓设置会导致一会就熄屏了,虽然可以修改设置来禁止熄屏,但是改来改去还是蛮麻烦的,简单的方法当然是安装一个软件,这里软件很多,我用的是Stay Alive,还不错。
多语言
如果开发多语言APP,想测试英文版本,需要修改测试手机的locale,可以安装小软件来做,比如MoreLocale 2,切换的时候,需要ROOT权限,或者也可以通过adb赋给权限,这样做:
adb shell pm grant jp.co.c_lis.ccl.morelocale android.permission.CHANGE_CONFIGURATION
修改后,app都不需要重启,权限即刻生效。
应用程序想要支持多语,自然是要在应用里做,比如Vue可以用vue-i18n,但是如果想要应用程序的名称、权限根据语言来定,就需要在cordova上安装这个插件,很好用。
google play
发布版本如果要上google play,过程如下:
- 注册并登录play console
- 添加app,在左侧标记的内容区依次填写(有些内容有先后,有些没有),基本信息区可以使用多语言(填写多份)
- 上传apk,填写分级信息,设置价格
- 如果应用有特殊的权限要求,必须填写隐私声明网址,这个比较麻烦
- 如果所有信息充分,play console会提示可以发布了,进入版本管理的地方,查看后就可以提交发布了。
提醒几点注意:
- 不要使用google 签名
- 如果你想app被全世界用户使用,最好缺省语言选英文
- 价格可以从收费转为免费,但不能反向操作
apk优化
- AppCompact是把高版本的功能,比如Material Theme,包成一个库,可以兼容低版本安卓,如果不需要(比如minSdk已经到21),可以在build.gradle中移除它,可以减小大约1M的体积。
Intent
Intent是安卓的一个核心概念,intent用于组件之间的调用和通讯,通常用于startActivity。intent有两种:
Implicit intent
用于发起一个调用,但是不知道哪个应用会处理,也可能有多个应用可以处理,如果有多个,会让用户选择要给,典型的例子是分享,intent是这样的:
Intent i=new Intent();
i.setAction(Intent.ACTION_SEND);
如果想知道系统中有没有可以处理这个intent的app,可以这样判断
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
}
Explicit intent
这种intent指定了component,可以直接调用指定的app里的activity或service,比如:
Intent I = new Intent(getApplicationContext(),NextActivity.class);
I.putExtra(“value1” , “This value for Next Activity”);
activity的filter属性
主Activity通常有这个属性:
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
MAIN的意思表示是一个入口,通常和LAUNCHER一起出现,让桌面程序或第三方启动类程序可以发现这个activity。但是MAIN也可以和其他类别category组合比如和CATEGORY_CAR_DOCK组合则表示在dock时会执行的activity。也可以通过对data uri的过滤让activity只处理部分intent,比如这样注册:
<activity android:name=".YourBrowserActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" android:host="www.example.com" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
mipmap和drawable
res下这两个目录都可以放图标。
actionBar
早期安卓有一个ActionBar组件,后来安卓5.0增加了Toolbar,更灵活一些。
service开发
service用于完成后台操作,它可以通过start或者bind启动,一个简单的service如下:
public class MyService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return Service.START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
//TODO for communication return IBinder implementation
return null;
}
}
start一个service时,可以通过extra带参数,像这样:
Intent i= new Intent(context, MyService.class);
i.putExtra("KEY1", "Value to be used by the service");
context.startService(i);
这样会触发service里的onStartCommand方法,并且如果service还没有创建的话,service会被先创建起来,并调用onCreate方法,如果service已经创建了,就直接调用onStartCommand,这些调用都是通过UI thread进行,不会并发。
onStartCommand的返回值表示service是否需要restart,取值可以为:
- Service.START_STICKY 会restart,并且restart时传的Intent为null
- Service.START_REDELIVER_INTENT 会restart,并且restart时传上次的Intent
- Service.START_NOT_STICKY 不会restart
restart特性在不同的厂商间差别很大,很多厂商禁止了restart,比如小米、华为等,也就是说只要用户在多任务页面杀掉app,service也会被杀(即使标记了START_STICKY),解决方法似乎是加入白名单,参考这里
执行stopService来停止service, 无论startService调用多少次,stopService一次就可以,或者也可以在service里,调用stopSelf停止自己。
Activity和service有几种通讯手段
- 通过startService带参数
- 通过bind
- 通过broadcasts和receivers,这中方法可以和非本进程的service通讯
AccessibilityService
AccessibilityService比较特殊,体现在:
- AccessibilityService需要用户在系统设置里授权,授权后会由系统来启动,不过,手动startService再授权也可以,但如果没有授权,AccessibilityService的回调和操作不能生效。
- 如果是通过系统设置里授权启动,实际上只启动了Service,应用的Activity不会启动,也就是在多任务列表中看不到应用,但清理内存时还是会被清理掉。
- 清理内存或App强杀之后,无障碍授权也丢失,需要重新授权。
JobScheduler
在API21及以上可以用jobscheduler,包括一次性或周期性两种job,可惜在API24以上,周期性job的周期最少为15分钟,所以有些场景不能用,只能用一次性job+重新调度规避。不管是哪种job都会被doze优化,doze后不再调度,即使加省电优化白名单也不行。
一次性job,如果加省电白名单,可以一解锁就立刻触发,如果不加省电白名单,锁屏时间一长,解锁后就需要过段时间才能重新触发,这个时间很诡异,有长有短。但切换一下网络就很容易再次触发。一次性job有两个重要参数:
- latency是等待时间,可以用这个加上重新调度来模拟周期job
- deadline是指即使条件不满足时,隔多久也会强制触发,deadline甚至可以小于latency,或者0表示不会强制触发。但如果此时处于锁屏,仍然不行。
总之,JobScheduler不能在锁屏下触发,并且如果不加省电白名单,解屏后有一段诡异的不触发时间。 Job除了时间限制外,可以加一些限制条件,比如:
- Network type
- Charging
- Idle 这个不是dumpsys的那个idle,这个idle是指屏幕关闭后的一段时间,荣耀8x下实测下来很难触发
如果有多个条件,是与关系。
设备状态
可以用下面的命令查询当前设备状态:
-
adb shell dumpsys deviceidle grep mState
也可以用adb shell dumpsys deviceidle step,手工向后步进状态,直到IDLE和IDLE_MAINTENANCE。
设备id
- Android ID,不需要权限,但刷机和设备重置后会变,安卓8.0之前到8.0之后也会变,因为8.0之后是加上app签名一起算的。
- IMEI,Android6之后需要READ_PHONE_STATE 权限,10.0之后有权限也读不了了。
- Build.SERIAL,直接访问在8.0之后总是返回“unknown”,需要READ_PHONE_STATE权限,然后调用Build.getSerial(),但10.0之后也不行了。
- wifi的mac地址,目前10.0还可用, 需要INTENET权限,但部分机型每次重启都会变(三星S10)
public static String getWifiMac() { try { Enumeration<NetworkInterface> enumeration = NetworkInterface.getNetworkInterfaces(); if (enumeration == null) { return ""; } while (enumeration.hasMoreElements()) { NetworkInterface netInterface = enumeration.nextElement(); if (netInterface.getName().equals("wlan0")) { return formatMac(netInterface.getHardwareAddress()); } } } catch (Exception e) { Log.e("tag", e.getMessage(), e); } return ""; }
多用户
安卓的多用户和Linux原生多用户完全不是一个概念,从adb shell ps可以看出
root 20448 2 0 0 0 0 S [cabc_pwm_task]
radio 20455 605 5420336 27228 0 0 S com.huawei.android.AutoRegSms
u0_a29 20477 605 6127268 101764 0 0 S com.huawei.intelligent
radio 20503 605 5437960 28268 0 0 S com.huawei.android.UEInfoCheck
root 20525 2 0 0 0 0 I [kworker/u17:0]
u0_a146 20531 605 5510632 42292 0 0 S com.huawei.pengine
u0_a29 20555 605 6305892 77468 0 0 S com.huawei.intelligent:intelligentService
root 20560 1 33264 720 0 0 S vendor.huawei.hardware.hwfs@1.0-service
u128_a18 20591 605 5990428 71692 0 0 S com.google.android.gms
安卓支持多”用户”,比如可以做分身,但即使没有多用户,从ps上看,每个app都对应一个唯一的Linux uid,比如上面的u0_a29,u0_a146,其中u0就是安卓的用户0,如果开了分身,上面就多出一个u128,但也依然对应到多个Linux user。
监测home键
这种方法的优点是可以支持虚拟键和系统手势,但如果使用全面屏手势,上划触发的home会被监测为recentapps
mReceiver = new InnerReceiver();
registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
class InnerReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {
String reason = intent.getStringExtra("reason");
if (reason != null) {
Log.d(Const.TAG, "action:" + action + ",reason:" + reason);
if (reason.equals("homekey")) {
//
} else if (reason.equals("recentapps")) {
//
}
}
}
}
}
camera
android的camera2 api比较camera功能更多一些,但是很难用,推荐使用Fotoapparat代替,Fotoapparat也推荐了一个FaceDetector搭配做人脸识别。
红外
控制小米电视电源:
irManager.transmit(38028, new int[]{946, 604, 552, 552, 604, 1446, 604, 1446, 604, 552, 604, 1446, 604, 578, 604, 1420, 604, 578, 604, 1446, 604, 1446, 604, 10572, 999, 578, 578, 578, 604, 1446, 604, 1446, 604, 552, 604, 1446, 604, 578, 578, 1420, 604, 578, 604, 1446, 604, 1446, 578, 10572, 999, 552, 604, 578, 604, 1446, 604, 1446, 604, 552, 604, 1446, 604, 552, 604, 1446, 604, 578, 604, 1446, 604, 1446, 578});
- 38028是大部分家电红外遥控的频率
- 后面的数据是编码后的载波和空波的间隔时间,单位是微秒。注意老版本安卓4.4以下,这个数据不是时间,而是波长的倍数,后来改为时间了
- 编码协议很多种,各有不同。需要通过红外码库找到对应遥控器的编码。