Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

DOTween-Ease缓动函数

$
0
0

Ease.InQuad 不知道Quad代表什么意思
这里写图片描述
Ease.InQuart 有1/4的时间是没有缓动。
这里写图片描述
Ease.InQuint, 是1/5时间没有缓动.
这里写图片描述
Ease.InExpo 一直很平缓,在最后一点完成所有变化。
这里写图片描述

Ease.InSine 表示正弦加速动作
Ease.OutSine 表示正弦减速动作
Ease.InOutSine, 表示正弦加速减速动作
其它Enum 值也一样。
这里写图片描述
这里写图片描述

作者:yy763496668 发表于2017/10/12 15:01:24 原文链接
阅读:137 评论:0 查看评论

Android--adb命令详解

$
0
0

ADB,即 Android Debug Bridge,它是 Android 开发/测试人员不可替代的强大工具,也是 Android 设备玩家的好玩具。

基本用法

命令语法

adb 命令的基本语法如下:

adb [-d|-e|-s <serialNumber>] <command>

如果只有一个设备/模拟器连接时,可以省略掉 [-d|-e|-s <serialNumber>] 这一部分,直接使用 adb <command>

为命令指定目标设备

如果有多个设备/模拟器连接,则需要为命令指定目标设备。

参数 含义
-d 指定当前唯一通过 USB 连接的 Android 设备为命令目标
-e 指定当前唯一运行的模拟器为命令目标
-s <serialNumber> 指定相应 serialNumber 号的设备/模拟器为命令目标

在多个设备/模拟器连接的情况下较常用的是 -s <serialNumber> 参数,serialNumber 可以通过 adb devices 命令获取。如:

$ adb devices

List of devices attached
cf264b8f	device
emulator-5554	device
10.129.164.6:5555	device

输出里的 cf264b8femulator-5554 和 10.129.164.6:5555 即为 serialNumber。

比如这时想指定 cf264b8f 这个设备来运行 adb 命令获取屏幕分辨率:

adb -s cf264b8f shell wm size

又如想给 10.129.164.6:5555 这个设备安装应用(这种形式的 serialNumber 格式为 <IP>:<Port>,一般为无线连接的设备或 Genymotion 等第三方 Android 模拟器):

adb -s 10.129.164.6:5555 install test.apk

遇到多设备/模拟器的情况均使用这几个参数为命令指定目标设备,下文中为简化描述,不再重复。

启动/停止

启动 adb server 命令:

adb start-server

(一般无需手动执行此命令,在运行 adb 命令时若发现 adb server 没有启动会自动调起。)

停止 adb server 命令:

adb kill-server

查看 adb 版本

命令:

adb version

示例输出:

Android Debug Bridge version 1.0.36
Revision 8f855a3d9b35-android

以 root 权限运行 adbd

adb 的运行原理是 PC 端的 adb server 与手机端的守护进程 adbd 建立连接,然后 PC 端的 adb client 通过 adb server 转发命令,adbd 接收命令后解析运行。

所以如果 adbd 以普通权限执行,有些需要 root 权限才能执行的命令无法直接用 adb xxx 执行。这时可以 adb shell 然后su 后执行命令,也可以让 adbd 以 root 权限执行,这个就能随意执行高权限命令了。

命令:

adb root

正常输出:

restarting adbd as root

现在再运行 adb shell,看看命令行提示符是不是变成 # 了?

有些手机 root 后也无法通过 adb root 命令让 adbd 以 root 权限执行,比如三星的部分机型,会提示 adbd cannot run as root in production builds,此时可以先安装 adbd Insecure,然后 adb root 试试。

相应地,如果要恢复 adbd 为非 root 权限的话,可以使用 adb unroot 命令。

指定 adb server 的网络端口

命令:

adb -P <port> start-server

默认端口为 5037。

设备连接管理

查询已连接设备/模拟器

命令:

adb devices

输出示例:

List of devices attached
cf264b8f	device
emulator-5554	device
10.129.164.6:5555	device

输出格式为 [serialNumber] [state],serialNumber 即我们常说的 SN,state 有如下几种:

  • offline —— 表示设备未连接成功或无响应。

  • device —— 设备已连接。注意这个状态并不能标识 Android 系统已经完全启动和可操作,在设备启动过程中设备实例就可连接到 adb,但启动完毕后系统才处于可操作状态。

  • no device —— 没有设备/模拟器连接。

以上输出显示当前已经连接了三台设备/模拟器,cf264b8femulator-5554 和 10.129.164.6:5555 分别是它们的 SN。从emulator-5554 这个名字可以看出它是一个 Android 模拟器,而 10.129.164.6:5555 这种形为 <IP>:<Port> 的 serialNumber 一般是无线连接的设备或 Genymotion 等第三方 Android 模拟器。

常见异常输出:

  1. 没有设备/模拟器连接成功。

    List of devices attached
  2. 设备/模拟器未连接到 adb 或无响应。

    List of devices attached
    cf264b8f	offline

USB 连接

通过 USB 连接来正常使用 adb 需要保证几点:

  1. 硬件状态正常。

    包括 Android 设备处于正常开机状态,USB 连接线和各种接口完好。

  2. Android 设备的开发者选项和 USB 调试模式已开启。

    可以到「设置」-「开发者选项」-「Android 调试」查看。

    如果在设置里找不到开发者选项,那需要通过一个彩蛋来让它显示出来:在「设置」-「关于手机」连续点击「版本号」7 次。

  3. 设备驱动状态正常。

    这一点貌似在 Linux 和 Mac OS X 下不用操心,在 Windows 下有可能遇到需要安装驱动的情况,确认这一点可以右键「计算机」-「属性」,到「设备管理器」里查看相关设备上是否有黄色感叹号或问号,如果没有就说明驱动状态已经好了。否则可以下载一个手机助手类程序来安装驱动先。

  4. 通过 USB 线连接好电脑和设备后确认状态。

    adb devices

    如果能看到

    xxxxxx device

    说明连接成功。

无线连接(需要借助 USB 线)

除了可以通过 USB 连接设备与电脑来使用 adb,也可以通过无线连接——虽然连接过程中也有需要使用 USB 的步骤,但是连接成功之后你的设备就可以在一定范围内摆脱 USB 连接线的限制啦!

操作步骤:

  1. 将 Android 设备与要运行 adb 的电脑连接到同一个局域网,比如连到同一个 WiFi。

  2. 将设备与电脑通过 USB 线连接。

    应确保连接成功(可运行 adb devices 看是否能列出该设备)。

  3. 让设备在 5555 端口监听 TCP/IP 连接:

    adb tcpip 5555
  4. 断开 USB 连接。

  5. 找到设备的 IP 地址。

    一般能在「设置」-「关于手机」-「状态信息」-「IP地址」找到,也可以使用下文里 查看设备信息 - IP 地址 一节里的方法用 adb 命令来查看。

  6. 通过 IP 地址连接设备。

    adb connect <device-ip-address>

    这里的 <device-ip-address> 就是上一步中找到的设备 IP 地址。

  7. 确认连接状态。

    adb devices

    如果能看到

    <device-ip-address>:5555 device

    说明连接成功。

如果连接不了,请确认 Android 设备与电脑是连接到了同一个 WiFi,然后再次执行 adb connect <device-ip-address> 那一步;

如果还是不行的话,通过 adb kill-server 重新启动 adb 然后从头再来一次试试。

断开无线连接

命令:

adb disconnect <device-ip-address>

无线连接(无需借助 USB 线)

注:需要 root 权限。

上一节「无线连接(需要借助 USB 线)」是官方文档里介绍的方法,需要借助于 USB 数据线来实现无线连接。

既然我们想要实现无线连接,那能不能所有步骤下来都是无线的呢?答案是能的。

  1. 在 Android 设备上安装一个终端模拟器。

    已经安装过的设备可以跳过此步。我使用的终端模拟器下载地址是:Terminal Emulator for Android Downloads

  2. 将 Android 设备与要运行 adb 的电脑连接到同一个局域网,比如连到同一个 WiFi。

  3. 打开 Android 设备上的终端模拟器,在里面依次运行命令:

    su
    setprop service.adb.tcp.port 5555
  4. 找到 Android 设备的 IP 地址。

    一般能在「设置」-「关于手机」-「状态信息」-「IP地址」找到,也可以使用下文里 查看设备信息 - IP 地址 一节里的方法用 adb 命令来查看。

  5. 在电脑上通过 adb 和 IP 地址连接 Android 设备。

    adb connect <device-ip-address>

    这里的 <device-ip-address> 就是上一步中找到的设备 IP 地址。

    如果能看到 connected to <device-ip-address>:5555 这样的输出则表示连接成功。

节注一:

有的设备,比如小米 5S + MIUI 8.0 + Android 6.0.1 MXB48T,可能在第 5 步之前需要重启 adbd 服务,在设备的终端模拟器上运行:

restart adbd

如果 restart 无效,尝试以下命令:

stop adbd
start adbd

应用管理

查看应用列表

查看应用列表的基本命令格式是

adb shell pm list packages [-f] [-d] [-e] [-s] [-3] [-i] [-u] [--user USER_ID] [FILTER]

即在 adb shell pm list packages 的基础上可以加一些参数进行过滤查看不同的列表,支持的过滤参数如下:

参数 显示列表
所有应用
-f 显示应用关联的 apk 文件
-d 只显示 disabled 的应用
-e 只显示 enabled 的应用
-s 只显示系统应用
-3 只显示第三方应用
-i 显示应用的 installer
-u 包含已卸载应用
<FILTER> 包名包含 <FILTER> 字符串

所有应用

命令:

adb shell pm list packages

输出示例:

package:com.android.smoketest
package:com.example.android.livecubes
package:com.android.providers.telephony
package:com.google.android.googlequicksearchbox
package:com.android.providers.calendar
package:com.android.providers.media
package:com.android.protips
package:com.android.documentsui
package:com.android.gallery
package:com.android.externalstorage
...
// other packages here
...

系统应用

命令:

adb shell pm list packages -s

第三方应用

命令:

adb shell pm list packages -3

包名包含某字符串的应用

比如要查看包名包含字符串 mazhuang 的应用列表,命令:

adb shell pm list packages mazhuang

当然也可以使用 grep 来过滤:

adb shell pm list packages | grep mazhuang

安装 APK

命令格式:

adb install [-lrtsdg] <path_to_apk>

参数:

adb install 后面可以跟一些可选参数来控制安装 APK 的行为,可用参数及含义如下:

参数 含义
-l 将应用安装到保护目录 /mnt/asec
-r 允许覆盖安装
-t 允许安装 AndroidManifest.xml 里 application 指定 android:testOnly="true" 的应用
-s 将应用安装到 sdcard
-d 允许降级覆盖安装
-g 授予所有运行时权限

运行命令后如果见到类似如下输出(状态为 Success)代表安装成功:

[100%] /data/local/tmp/1.apk
	pkg: /data/local/tmp/1.apk
Success

上面是当前最新版 v1.0.36 的 adb 的输出,会显示 push apk 文件到手机的进度百分比。

使用旧版本 adb 的输出则是这样的:

12040 KB/s (22205609 bytes in 1.801s)
        pkg: /data/local/tmp/SogouInput_android_v8.3_sweb.apk
Success

而如果状态为 Failure 则表示安装失败,比如:

[100%] /data/local/tmp/map-20160831.apk
        pkg: /data/local/tmp/map-20160831.apk
Failure [INSTALL_FAILED_ALREADY_EXISTS]

常见安装失败输出代码、含义及可能的解决办法如下:

输出 含义 解决办法
INSTALL_FAILED_ALREADY_EXISTS 应用已经存在,或卸载了但没卸载干净 adb install 时使用 -r参数,或者先 adb uninstall <packagename> 再安装
INSTALL_FAILED_INVALID_APK 无效的 APK 文件  
INSTALL_FAILED_INVALID_URI 无效的 APK 文件名 确保 APK 文件名里无中文
INSTALL_FAILED_INSUFFICIENT_STORAGE 空间不足 清理空间
INSTALL_FAILED_DUPLICATE_PACKAGE 已经存在同名程序  
INSTALL_FAILED_NO_SHARED_USER 请求的共享用户不存在  
INSTALL_FAILED_UPDATE_INCOMPATIBLE 以前安装过同名应用,但卸载时数据没有移除;或者已安装该应用,但签名不一致 先 adb uninstall <packagename> 再安装
INSTALL_FAILED_SHARED_USER_INCOMPATIBLE 请求的共享用户存在但签名不一致  
INSTALL_FAILED_MISSING_SHARED_LIBRARY 安装包使用了设备上不可用的共享库  
INSTALL_FAILED_REPLACE_COULDNT_DELETE 替换时无法删除  
INSTALL_FAILED_DEXOPT dex 优化验证失败或空间不足  
INSTALL_FAILED_OLDER_SDK 设备系统版本低于应用要求  
INSTALL_FAILED_CONFLICTING_PROVIDER 设备里已经存在与应用里同名的 content provider  
INSTALL_FAILED_NEWER_SDK 设备系统版本高于应用要求  
INSTALL_FAILED_TEST_ONLY 应用是 test-only 的,但安装时没有指定 -t 参数  
INSTALL_FAILED_CPU_ABI_INCOMPATIBLE 包含不兼容设备 CPU 应用程序二进制接口的 native code  
INSTALL_FAILED_MISSING_FEATURE 应用使用了设备不可用的功能  
INSTALL_FAILED_CONTAINER_ERROR 1. sdcard 访问失败;
2. 应用签名与 ROM 签名一致,被当作内置应用。
1. 确认 sdcard 可用,或者安装到内置存储;
2. 打包时不与 ROM 使用相同签名。
INSTALL_FAILED_INVALID_INSTALL_LOCATION 1. 不能安装到指定位置;
2. 应用签名与 ROM 签名一致,被当作内置应用。
1. 切换安装位置,添加或删除 -s 参数;
2. 打包时不与 ROM 使用相同签名。
INSTALL_FAILED_MEDIA_UNAVAILABLE 安装位置不可用 一般为 sdcard,确认 sdcard 可用或安装到内置存储
INSTALL_FAILED_VERIFICATION_TIMEOUT 验证安装包超时  
INSTALL_FAILED_VERIFICATION_FAILURE 验证安装包失败  
INSTALL_FAILED_PACKAGE_CHANGED 应用与调用程序期望的不一致  
INSTALL_FAILED_UID_CHANGED 以前安装过该应用,与本次分配的 UID 不一致 清除以前安装过的残留文件
INSTALL_FAILED_VERSION_DOWNGRADE 已经安装了该应用更高版本 使用 -d 参数
INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE 已安装 target SDK 支持运行时权限的同名应用,要安装的版本不支持运行时权限  
INSTALL_PARSE_FAILED_NOT_APK 指定路径不是文件,或不是以.apk 结尾  
INSTALL_PARSE_FAILED_BAD_MANIFEST 无法解析的 AndroidManifest.xml 文件  
INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION 解析器遇到异常  
INSTALL_PARSE_FAILED_NO_CERTIFICATES 安装包没有签名  
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES 已安装该应用,且签名与 APK 文件不一致 先卸载设备上的该应用,再安装
INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING 解析 APK 文件时遇到CertificateEncodingException  
INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME manifest 文件里没有或者使用了无效的包名  
INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID manifest 文件里指定了无效的共享用户 ID  
INSTALL_PARSE_FAILED_MANIFEST_MALFORMED 解析 manifest 文件时遇到结构性错误  
INSTALL_PARSE_FAILED_MANIFEST_EMPTY 在 manifest 文件里找不到找可操作标签(instrumentation 或 application)  
INSTALL_FAILED_INTERNAL_ERROR 因系统问题安装失败  
INSTALL_FAILED_USER_RESTRICTED 用户被限制安装应用  
INSTALL_FAILED_DUPLICATE_PERMISSION 应用尝试定义一个已经存在的权限名称  
INSTALL_FAILED_NO_MATCHING_ABIS 应用包含设备的应用程序二进制接口不支持的 native code  
INSTALL_CANCELED_BY_USER 应用安装需要在设备上确认,但未操作设备或点了取消 在设备上同意安装
INSTALL_FAILED_ACWF_INCOMPATIBLE 应用程序与设备不兼容  
does not contain AndroidManifest.xml 无效的 APK 文件  
is not a valid zip file 无效的 APK 文件  
Offline 设备未连接成功 先将设备与 adb 连接成功
unauthorized 设备未授权允许调试  
error: device not found 没有连接成功的设备 先将设备与 adb 连接成功
protocol failure 设备已断开连接 先将设备与 adb 连接成功
Unknown option: -s Android 2.2 以下不支持安装到 sdcard 不使用 -s 参数
No space left on device 空间不足 清理空间
Permission denied ... sdcard ... sdcard 不可用  
signatures do not match the previously installed version; ignoring! 已安装该应用且签名不一致 先卸载设备上的该应用,再安装

参考:PackageManager.java

adb install 内部原理简介

adb install 实际是分三步完成:

  1. push apk 文件到 /data/local/tmp。

  2. 调用 pm install 安装。

  3. 删除 /data/local/tmp 下的对应 apk 文件。

所以,必要的时候也可以根据这个步骤,手动分步执行安装过程。

卸载应用

命令:

adb uninstall [-k] <packagename>

<packagename> 表示应用的包名,-k 参数可选,表示卸载应用但保留数据和缓存目录。

命令示例:

adb uninstall com.qihoo360.mobilesafe

表示卸载 360 手机卫士。

清除应用数据与缓存

命令:

adb shell pm clear <packagename>

<packagename> 表示应用名包,这条命令的效果相当于在设置里的应用信息界面点击了「清除缓存」和「清除数据」。

命令示例:

adb shell pm clear com.qihoo360.mobilesafe

表示清除 360 手机卫士的数据和缓存。

查看前台 Activity

命令:

adb shell dumpsys activity activities | grep mFocusedActivity

输出示例:

mFocusedActivity: ActivityRecord{8079d7e u0 com.cyanogenmod.trebuchet/com.android.launcher3.Launcher t42}

其中的 com.cyanogenmod.trebuchet/com.android.launcher3.Launcher 就是当前处于前台的 Activity。

查看正在运行的 Services

命令:

adb shell dumpsys activity services [<packagename>]

<packagename> 参数不是必须的,指定 <packagename> 表示查看与某个包名相关的 Services,不指定表示查看所有 Services。

<packagename> 不一定要给出完整的包名,比如运行 adb shell dumpsys activity services org.mazhuang,那么包名org.mazhuang.demo1org.mazhuang.demo2 和 org.mazhuang123 等相关的 Services 都会列出来。

与应用交互

主要是使用 am <command> 命令,常用的 <command> 如下:

command 用途
start [options] <INTENT> 启动 <INTENT> 指定的 Activity
startservice [options] <INTENT> 启动 <INTENT> 指定的 Service
broadcast [options] <INTENT> 发送 <INTENT> 指定的广播
force-stop <packagename> 停止 <packagename> 相关的进程

<INTENT> 参数很灵活,和写 Android 程序时代码里的 Intent 相对应。

用于决定 intent 对象的选项如下:

参数 含义
-a <ACTION> 指定 action,比如 android.intent.action.VIEW
-c <CATEGORY> 指定 category,比如 android.intent.category.APP_CONTACTS
-n <COMPONENT> 指定完整 component 名,用于明确指定启动哪个 Activity,如 com.example.app/.ExampleActivity

<INTENT> 里还能带数据,就像写代码时的 Bundle 一样:

参数 含义
--esn <EXTRA_KEY> null 值(只有 key 名)
`-e --es <EXTRA_KEY> <EXTRA_STRING_VALUE>`
--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE> boolean 值
--ei <EXTRA_KEY> <EXTRA_INT_VALUE> integer 值
--el <EXTRA_KEY> <EXTRA_LONG_VALUE> long 值
--ef <EXTRA_KEY> <EXTRA_FLOAT_VALUE> float 值
--eu <EXTRA_KEY> <EXTRA_URI_VALUE> URI
--ecn <EXTRA_KEY> <EXTRA_COMPONENT_NAME_VALUE> component name
--eia <EXTRA_KEY> <EXTRA_INT_VALUE>[,<EXTRA_INT_VALUE...] integer 数组
--ela <EXTRA_KEY> <EXTRA_LONG_VALUE>[,<EXTRA_LONG_VALUE...] long 数组

调起 Activity

命令格式:

adb shell am start [options] <INTENT>

例如:

adb shell am start -n com.tencent.mm/.ui.LauncherUI

表示调起微信主界面。

adb shell am start -n org.mazhuang.boottimemeasure/.MainActivity --es "toast" "hello, world"

表示调起 org.mazhuang.boottimemeasure/.MainActivity 并传给它 string 数据键值对 toast - hello, world

调起 Service

命令格式:

adb shell am startservice [options] <INTENT>

例如:

adb shell am startservice -n com.tencent.mm/.plugin.accountsync.model.AccountAuthenticatorService

表示调起微信的某 Service。

发送广播

命令格式:

adb shell am broadcast [options] <INTENT>

可以向所有组件广播,也可以只向指定组件广播。

例如,向所有组件广播 BOOT_COMPLETED

adb shell am broadcast -a android.intent.action.BOOT_COMPLETED

又例如,只向 org.mazhuang.boottimemeasure/.BootCompletedReceiver 广播 BOOT_COMPLETED

adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -n org.mazhuang.boottimemeasure/.BootCompletedReceiver

这类用法在测试的时候很实用,比如某个广播的场景很难制造,可以考虑通过这种方式来发送广播。

既能发送系统预定义的广播,也能发送自定义广播。如下是部分系统预定义广播及正常触发时机:

action 触发时机
android.net.conn.CONNECTIVITY_CHANGE 网络连接发生变化
android.intent.action.SCREEN_ON 屏幕点亮
android.intent.action.SCREEN_OFF 屏幕熄灭
android.intent.action.BATTERY_LOW 电量低,会弹出电量低提示框
android.intent.action.BATTERY_OKAY 电量恢复了
android.intent.action.BOOT_COMPLETED 设备启动完毕
android.intent.action.DEVICE_STORAGE_LOW 存储空间过低
android.intent.action.DEVICE_STORAGE_OK 存储空间恢复
android.intent.action.PACKAGE_ADDED 安装了新的应用
android.net.wifi.STATE_CHANGE WiFi 连接状态发生变化
android.net.wifi.WIFI_STATE_CHANGED WiFi 状态变为启用/关闭/正在启动/正在关闭/未知
android.intent.action.BATTERY_CHANGED 电池电量发生变化
android.intent.action.INPUT_METHOD_CHANGED 系统输入法发生变化
android.intent.action.ACTION_POWER_CONNECTED 外部电源连接
android.intent.action.ACTION_POWER_DISCONNECTED 外部电源断开连接
android.intent.action.DREAMING_STARTED 系统开始休眠
android.intent.action.DREAMING_STOPPED 系统停止休眠
android.intent.action.WALLPAPER_CHANGED 壁纸发生变化
android.intent.action.HEADSET_PLUG 插入耳机
android.intent.action.MEDIA_UNMOUNTED 卸载外部介质
android.intent.action.MEDIA_MOUNTED 挂载外部介质
android.os.action.POWER_SAVE_MODE_CHANGED 省电模式开启

(以上广播均可使用 adb 触发)

强制停止应用

命令:

adb shell am force-stop <packagename>

命令示例:

adb shell am force-stop com.qihoo360.mobilesafe

表示停止 360 安全卫士的一切进程与服务。

文件管理

复制设备里的文件到电脑

命令:

adb pull <设备里的文件路径> [电脑上的目录]

其中 电脑上的目录 参数可以省略,默认复制到当前目录。

例:

adb pull /sdcard/sr.mp4 ~/tmp/

*小技巧:*设备上的文件路径可能需要 root 权限才能访问,如果你的设备已经 root 过,可以先使用 adb shell 和 su 命令在 adb shell 里获取 root 权限后,先 cp /path/on/device /sdcard/filename 将文件复制到 sdcard,然后 adb pull /sdcard/filename /path/on/pc

复制电脑里的文件到设备

命令:

adb push <电脑上的文件路径> <设备里的目录>

例:

adb push ~/sr.mp4 /sdcard/

*小技巧:*设备上的文件路径普通权限可能无法直接写入,如果你的设备已经 root 过,可以先 adb push /path/on/pc /sdcard/filename,然后 adb shell 和 su 在 adb shell 里获取 root 权限后,cp /sdcard/filename /path/on/device

模拟按键/输入

在 adb shell 里有个很实用的命令叫 input,通过它可以做一些有趣的事情。

input 命令的完整 help 信息如下:

Usage: input [<source>] <command> [<arg>...]

The sources are:
      mouse
      keyboard
      joystick
      touchnavigation
      touchpad
      trackball
      stylus
      dpad
      gesture
      touchscreen
      gamepad

The commands and default sources are:
      text <string> (Default: touchscreen)
      keyevent [--longpress] <key code number or name> ... (Default: keyboard)
      tap <x> <y> (Default: touchscreen)
      swipe <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen)
      press (Default: trackball)
      roll <dx> <dy> (Default: trackball)

比如使用 adb shell input keyevent <keycode> 命令,不同的 keycode 能实现不同的功能,完整的 keycode 列表详见KeyEvent,摘引部分我觉得有意思的如下:

keycode 含义
3 HOME 键
4 返回键
5 打开拨号应用
6 挂断电话
24 增加音量
25 降低音量
26 电源键
27 拍照(需要在相机应用里)
64 打开浏览器
82 菜单键
85 播放/暂停
86 停止播放
87 播放下一首
88 播放上一首
122 移动光标到行首或列表顶部
123 移动光标到行末或列表底部
126 恢复播放
127 暂停播放
164 静音
176 打开系统设置
187 切换应用
207 打开联系人
208 打开日历
209 打开音乐
210 打开计算器
220 降低屏幕亮度
221 提高屏幕亮度
223 系统休眠
224 点亮屏幕
231 打开语音助手
276 如果没有 wakelock 则让系统休眠

下面是 input 命令的一些用法举例。

电源键

命令:

adb shell input keyevent 26

执行效果相当于按电源键。

菜单键

命令:

adb shell input keyevent 82

HOME 键

命令:

adb shell input keyevent 3

返回键

命令:

adb shell input keyevent 4

音量控制

增加音量:

adb shell input keyevent 24

降低音量:

adb shell input keyevent 25

静音:

adb shell input keyevent 164

媒体控制

播放/暂停:

adb shell input keyevent 85

停止播放:

adb shell input keyevent 86

播放下一首:

adb shell input keyevent 87

播放上一首:

adb shell input keyevent 88

恢复播放:

adb shell input keyevent 126

暂停播放:

adb shell input keyevent 127

点亮/熄灭屏幕

可以通过上文讲述过的模拟电源键来切换点亮和熄灭屏幕,但如果明确地想要点亮或者熄灭屏幕,那可以使用如下方法。

点亮屏幕:

adb shell input keyevent 224

熄灭屏幕:

adb shell input keyevent 223

滑动解锁

如果锁屏没有密码,是通过滑动手势解锁,那么可以通过 input swipe 来解锁。

命令(参数以机型 Nexus 5,向上滑动手势解锁举例):

adb shell input swipe 300 1000 300 500

参数 300 1000 300 500 分别表示起始点x坐标 起始点y坐标 结束点x坐标 结束点y坐标

输入文本

在焦点处于某文本框时,可以通过 input 命令来输入文本。

命令:

adb shell input text hello

现在 hello 出现在文本框了。

查看日志

Android 系统的日志分为两部分,底层的 Linux 内核日志输出到 /proc/kmsg,Android 的日志输出到 /dev/log。

Android 日志

命令格式:

[adb] logcat [<option>] ... [<filter-spec>] ...

常用用法列举如下:

按级别过滤日志

Android 的日志分为如下几个优先级(priority):

  • V —— Verbose(最低,输出得最多)
  • D —— Debug
  • I —— Info
  • W —— Warning
  • E —— Error
  • F —— Fatal
  • S —— Silent(最高,啥也不输出)

按某级别过滤日志则会将该级别及以上的日志输出。

比如,命令:

adb logcat *:W

会将 Warning、Error、Fatal 和 Silent 日志输出。

注: 在 macOS 下需要给 *:W 这样以 * 作为 tag 的参数加双引号,如 adb logcat "*:W",不然会报错 no matches found: *:W。)

按 tag 和级别过滤日志

<filter-spec> 可以由多个 <tag>[:priority] 组成。

比如,命令:

adb logcat ActivityManager:I MyApp:D *:S

表示输出 tag ActivityManager 的 Info 以上级别日志,输出 tag MyApp 的 Debug 以上级别日志,及其它 tag 的 Silent 级别日志(即屏蔽其它 tag 日志)。

日志格式

可以用 adb logcat -v <format> 选项指定日志输出格式。

日志支持按以下几种 <format>

  • brief

    默认格式。格式为:

    <priority>/<tag>(<pid>): <message>

    示例:

    D/HeadsetStateMachine( 1785): Disconnected process message: 10, size: 0
  • process

    格式为:

    <priority>(<pid>) <message>

    示例:

    D( 1785) Disconnected process message: 10, size: 0  (HeadsetStateMachine)
  • tag

    格式为:

    <priority>/<tag>: <message>

    示例:

    D/HeadsetStateMachine: Disconnected process message: 10, size: 0
  • raw

    格式为:

    <message>

    示例:

    Disconnected process message: 10, size: 0
  • time

    格式为:

    <datetime> <priority>/<tag>(<pid>): <message>

    示例:

    08-28 22:39:39.974 D/HeadsetStateMachine( 1785): Disconnected process message: 10, size: 0
  • threadtime

    格式为:

    <datetime> <pid> <tid> <priority> <tag>: <message>

    示例:

    08-28 22:39:39.974  1785  1832 D HeadsetStateMachine: Disconnected process message: 10, size: 0
  • long

    格式为:

    [ <datetime> <pid>:<tid> <priority>/<tag> ]
    <message>

    示例:

    [ 08-28 22:39:39.974  1785: 1832 D/HeadsetStateMachine ]
    Disconnected process message: 10, size: 0

指定格式可与上面的过滤同时使用。比如:

adb logcat -v long ActivityManager:I *:S

清空日志

adb logcat -c

内核日志

命令:

adb shell dmesg

输出示例:

<6>[14201.684016] PM: noirq resume of devices complete after 0.982 msecs
<6>[14201.685525] PM: early resume of devices complete after 0.838 msecs
<6>[14201.753642] PM: resume of devices complete after 68.106 msecs
<4>[14201.755954] Restarting tasks ... done.
<6>[14201.771229] PM: suspend exit 2016-08-28 13:31:32.679217193 UTC
<6>[14201.872373] PM: suspend entry 2016-08-28 13:31:32.780363596 UTC
<6>[14201.872498] PM: Syncing filesystems ... done.

中括号里的 [14201.684016] 代表内核开始启动后的时间,单位为秒。

通过内核日志我们可以做一些事情,比如衡量内核启动时间,在系统启动完毕后的内核日志里找到 Freeing init memory 那一行前面的时间就是。

查看设备信息

型号

命令:

adb shell getprop ro.product.model

输出示例:

Nexus 5

电池状况

命令:

adb shell dumpsys battery

输入示例:

Current Battery Service state:
  AC powered: false
  USB powered: true
  Wireless powered: false
  status: 2
  health: 2
  present: true
  level: 44
  scale: 100
  voltage: 3872
  temperature: 280
  technology: Li-poly

其中 scale 代表最大电量,level 代表当前电量。上面的输出表示还剩下 44% 的电量。

屏幕分辨率

命令:

adb shell wm size

输出示例:

Physical size: 1080x1920

该设备屏幕分辨率为 1080px * 1920px。

如果使用命令修改过,那输出可能是:

Physical size: 1080x1920
Override size: 480x1024

表明设备的屏幕分辨率原本是 1080px * 1920px,当前被修改为 480px * 1024px。

屏幕密度

命令:

adb shell wm density

输出示例:

Physical density: 420

该设备屏幕密度为 420dpi。

如果使用命令修改过,那输出可能是:

Physical density: 480
Override density: 160

表明设备的屏幕密度原来是 480dpi,当前被修改为 160dpi。

显示屏参数

命令:

adb shell dumpsys window displays

输出示例:

WINDOW MANAGER DISPLAY CONTENTS (dumpsys window displays)
  Display: mDisplayId=0
    init=1080x1920 420dpi cur=1080x1920 app=1080x1794 rng=1080x1017-1810x1731
    deferred=false layoutNeeded=false

其中 mDisplayId 为 显示屏编号,init 是初始分辨率和屏幕密度,app 的高度比 init 里的要小,表示屏幕底部有虚拟按键,高度为 1920 - 1794 = 126px 合 42dp。

android_id

命令:

adb shell settings get secure android_id

输出示例:

51b6be48bac8c569

IMEI

在 Android 4.4 及以下版本可通过如下命令获取 IMEI:

adb shell dumpsys iphonesubinfo

输出示例:

Phone Subscriber Info:
  Phone Type = GSM
  Device ID = 860955027785041

其中的 Device ID 就是 IMEI。

而在 Android 5.0 及以上版本里这个命令输出为空,得通过其它方式获取了(需要 root 权限):

adb shell
su
service call iphonesubinfo 1

输出示例:

Result: Parcel(
  0x00000000: 00000000 0000000f 00360038 00390030 '........8.6.0.9.'
  0x00000010: 00350035 00320030 00370037 00350038 '5.5.0.2.7.7.8.5.'
  0x00000020: 00340030 00000031                   '0.4.1...        ')

把里面的有效内容提取出来就是 IMEI 了,比如这里的是 860955027785041

参考:adb shell dumpsys iphonesubinfo not working since Android 5.0 Lollipop

Android 系统版本

命令:

adb shell getprop ro.build.version.release

输出示例:

5.0.2

IP 地址

每次想知道设备的 IP 地址的时候都得「设置」-「关于手机」-「状态信息」-「IP地址」很烦对不对?通过 adb 可以方便地查看。

命令:

adb shell ifconfig | grep Mask

输出示例:

inet addr:10.130.245.230  Mask:255.255.255.252
inet addr:127.0.0.1  Mask:255.0.0.0

那么 10.130.245.230 就是设备 IP 地址。

在有的设备上这个命令没有输出,如果设备连着 WiFi,可以使用如下命令来查看局域网 IP:

adb shell ifconfig wlan0

输出示例:

wlan0: ip 10.129.160.99 mask 255.255.240.0 flags [up broadcast running multicast]

wlan0     Link encap:UNSPEC
          inet addr:10.129.168.57  Bcast:10.129.175.255  Mask:255.255.240.0
          inet6 addr: fe80::66cc:2eff:fe68:b6b6/64 Scope: Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:496520 errors:0 dropped:0 overruns:0 frame:0
          TX packets:68215 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:3000
          RX bytes:116266821 TX bytes:8311736

如果以上命令仍然不能得到期望的信息,那可以试试以下命令(部分系统版本里可用):

adb shell netcfg

输出示例:

wlan0    UP                               10.129.160.99/20  0x00001043 f8:a9:d0:17:42:4d
lo       UP                                   127.0.0.1/8   0x00000049 00:00:00:00:00:00
p2p0     UP                                     0.0.0.0/0   0x00001003 fa:a9:d0:17:42:4d
sit0     DOWN                                   0.0.0.0/0   0x00000080 00:00:00:00:00:00
rmnet0   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet1   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet3   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet2   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet4   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet6   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet5   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet7   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rev_rmnet3 DOWN                                   0.0.0.0/0   0x00001002 4e:b7:e4:2e:17:58
rev_rmnet2 DOWN                                   0.0.0.0/0   0x00001002 4e:f0:c8:bf:7a:cf
rev_rmnet4 DOWN                                   0.0.0.0/0   0x00001002 a6:c0:3b:6b:c4:1f
rev_rmnet6 DOWN                                   0.0.0.0/0   0x00001002 66:bb:5d:64:2e:e9
rev_rmnet5 DOWN                                   0.0.0.0/0   0x00001002 0e:1b:eb:b9:23:a0
rev_rmnet7 DOWN                                   0.0.0.0/0   0x00001002 7a:d9:f6:81:40:5a
rev_rmnet8 DOWN                                   0.0.0.0/0   0x00001002 4e:e2:a9:bb:d0:1b
rev_rmnet0 DOWN                                   0.0.0.0/0   0x00001002 fe:65:d0:ca:82:a9
rev_rmnet1 DOWN                                   0.0.0.0/0   0x00001002 da:d8:e8:4f:2e:fe

可以看到网络连接名称、启用状态、IP 地址和 Mac 地址等信息。

Mac 地址

命令:

adb shell cat /sys/class/net/wlan0/address

输出示例:

f8:a9:d0:17:42:4d

这查看的是局域网 Mac 地址,移动网络或其它连接的信息可以通过前面的小节「IP 地址」里提到的 adb shell netcfg 命令来查看。

CPU 信息

命令:

adb shell cat /proc/cpuinfo

输出示例:

Processor       : ARMv7 Processor rev 0 (v7l)
processor       : 0
BogoMIPS        : 38.40

processor       : 1
BogoMIPS        : 38.40

processor       : 2
BogoMIPS        : 38.40

processor       : 3
BogoMIPS        : 38.40

Features        : swp half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt
CPU implementer : 0x51
CPU architecture: 7
CPU variant     : 0x2
CPU part        : 0x06f
CPU revision    : 0

Hardware        : Qualcomm MSM 8974 HAMMERHEAD (Flattened Device Tree)
Revision        : 000b
Serial          : 0000000000000000

这是 Nexus 5 的 CPU 信息,我们从输出里可以看到使用的硬件是 Qualcomm MSM 8974,processor 的编号是 0 到 3,所以它是四核的,采用的架构是 ARMv7 Processor rev 0 (v71)

内存信息

命令:

adb shell cat /proc/meminfo

输出示例:

MemTotal:        1027424 kB
MemFree:          486564 kB
Buffers:           15224 kB
Cached:            72464 kB
SwapCached:        24152 kB
Active:           110572 kB
Inactive:         259060 kB
Active(anon):      79176 kB
Inactive(anon):   207736 kB
Active(file):      31396 kB
Inactive(file):    51324 kB
Unevictable:        3948 kB
Mlocked:               0 kB
HighTotal:        409600 kB
HighFree:         132612 kB
LowTotal:         617824 kB
LowFree:          353952 kB
SwapTotal:        262140 kB
SwapFree:         207572 kB
Dirty:                 0 kB
Writeback:             0 kB
AnonPages:        265324 kB
Mapped:            47072 kB
Shmem:              1020 kB
Slab:              57372 kB
SReclaimable:       7692 kB
SUnreclaim:        49680 kB
KernelStack:        4512 kB
PageTables:         5912 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:      775852 kB
Committed_AS:   13520632 kB
VmallocTotal:     385024 kB
VmallocUsed:       61004 kB
VmallocChunk:     209668 kB

其中,MemTotal 就是设备的总内存,MemFree 是当前空闲内存。

更多硬件与系统属性

设备的更多硬件与系统属性可以通过如下命令查看:

adb shell cat /system/build.prop

这会输出很多信息,包括前面几个小节提到的「型号」和「Android 系统版本」等。

输出里还包括一些其它有用的信息,它们也可通过 adb shell getprop <属性名> 命令单独查看,列举一部分属性如下:

属性名 含义
ro.build.version.sdk SDK 版本
ro.build.version.release Android 系统版本
ro.build.version.security_patch Android 安全补丁程序级别
ro.product.model 型号
ro.product.brand 品牌
ro.product.name 设备名
ro.product.board 处理器型号
ro.product.cpu.abilist CPU 支持的 abi 列表[节注一]
persist.sys.isUsbOtgEnabled 是否支持 OTG
dalvik.vm.heapsize 每个应用程序的内存上限
ro.sf.lcd_density 屏幕密度

节注一:

一些小厂定制的 ROM 可能修改过 CPU 支持的 abi 列表的属性名,如果用 ro.product.cpu.abilist 属性名查找不到,可以这样试试:

adb shell cat /system/build.prop | grep ro.product.cpu.abi

示例输出:

ro.product.cpu.abi=armeabi-v7a
ro.product.cpu.abi2=armeabi

修改设置

注: 修改设置之后,运行恢复命令有可能显示仍然不太正常,可以运行 adb reboot 重启设备,或手动重启。

修改设置的原理主要是通过 settings 命令修改 /data/data/com.android.providers.settings/databases/settings.db 里存放的设置值。

分辨率

命令:

adb shell wm size 480x1024

表示将分辨率修改为 480px * 1024px。

恢复原分辨率命令:

adb shell wm size reset

屏幕密度

命令:

adb shell wm density 160

表示将屏幕密度修改为 160dpi。

恢复原屏幕密度命令:

adb shell wm density reset

显示区域

命令:

adb shell wm overscan 0,0,0,200

四个数字分别表示距离左、上、右、下边缘的留白像素,以上命令表示将屏幕底部 200px 留白。

恢复原显示区域命令:

adb shell wm overscan reset

关闭 USB 调试模式

命令:

adb shell settings put global adb_enabled 0

恢复:

用命令恢复不了了,毕竟关闭了 USB 调试 adb 就连接不上 Android 设备了。

去设备上手动恢复吧:「设置」-「开发者选项」-「Android 调试」。

状态栏和导航栏的显示隐藏

本节所说的相关设置对应 Cyanogenmod 里的「扩展桌面」。

命令:

adb shell settings put global policy_control <key-values>

<key-values> 可由如下几种键及其对应的值组成,格式为 <key1>=<value1>:<key2>=<value2>

key 含义
immersive.full 同时隐藏
immersive.status 隐藏状态栏
immersive.navigation 隐藏导航栏
immersive.preconfirms ?

这些键对应的值可则如下值用逗号组合:

value 含义
apps 所有应用
* 所有界面
packagename 指定应用
-packagename 排除指定应用

例如:

adb shell settings put global policy_control immersive.full=*

表示设置在所有界面下都同时隐藏状态栏和导航栏。

adb shell settings put global policy_control immersive.status=com.package1,com.package2:immersive.navigation=apps,-com.package3

表示设置在包名为 com.package1 和 com.package2 的应用里隐藏状态栏,在除了包名为 com.package3 的所有应用里隐藏导航栏。

实用功能

屏幕截图

截图保存到电脑:

adb exec-out screencap -p > sc.png

如果 adb 版本较老,无法使用 exec-out 命令,这时候建议更新 adb 版本。无法更新的话可以使用以下麻烦点的办法:

先截图保存到设备里:

adb shell screencap -p /sdcard/sc.png

然后将 png 文件导出到电脑:

adb pull /sdcard/sc.png

可以使用 adb shell screencap -h 查看 screencap 命令的帮助信息,下面是两个有意义的参数及含义:

参数 含义
-p 指定保存文件为 png 格式
-d display-id 指定截图的显示屏编号(有多显示屏的情况下)

实测如果指定文件名以 .png 结尾时可以省略 -p 参数;否则需要使用 -p 参数。如果不指定文件名,截图文件的内容将直接输出到 stdout。

另外一种一行命令截图并保存到电脑的方法:

Linux 和 Windows

adb shell screencap -p | sed "s/\r$//" > sc.png

Mac OS X

adb shell screencap -p | gsed "s/\r$//" > sc.png

这个方法需要用到 gnu sed 命令,在 Linux 下直接就有,在 Windows 下 Git 安装目录的 bin 文件夹下也有。如果确实找不到该命令,可以下载 sed for Windows 并将 sed.exe 所在文件夹添加到 PATH 环境变量里。

而在 Mac 下使用系统自带的 sed 命令会报错:

sed: RE error: illegal byte sequence

需要安装 gnu-sed,然后使用 gsed 命令:

brew install gnu-sed

录制屏幕

录制屏幕以 mp4 格式保存到 /sdcard:

adb shell screenrecord /sdcard/filename.mp4

需要停止时按 Ctrl-C,默认录制时间和最长录制时间都是 180 秒。

如果需要导出到电脑:

adb pull /sdcard/filename.mp4

可以使用 adb shell screenrecord --help 查看 screenrecord 命令的帮助信息,下面是常见参数及含义:

参数 含义
--size WIDTHxHEIGHT 视频的尺寸,比如 1280x720,默认是屏幕分辨率。
--bit-rate RATE 视频的比特率,默认是 4Mbps。
--time-limit TIME 录制时长,单位秒。
--verbose 输出更多信息。

重新挂载 system 分区为可写

注:需要 root 权限。

/system 分区默认挂载为只读,但有些操作比如给 Android 系统添加命令、删除自带应用等需要对 /system 进行写操作,所以需要重新挂载它为可读写。

步骤:

  1. 进入 shell 并切换到 root 用户权限。

    命令:

    adb shell
    su
  2. 查看当前分区挂载情况。

    命令:

    mount

    输出示例:

    rootfs / rootfs ro,relatime 0 0
    tmpfs /dev tmpfs rw,seclabel,nosuid,relatime,mode=755 0 0
    devpts /dev/pts devpts rw,seclabel,relatime,mode=600 0 0
    proc /proc proc rw,relatime 0 0
    sysfs /sys sysfs rw,seclabel,relatime 0 0
    selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0
    debugfs /sys/kernel/debug debugfs rw,relatime 0 0
    none /var tmpfs rw,seclabel,relatime,mode=770,gid=1000 0 0
    none /acct cgroup rw,relatime,cpuacct 0 0
    none /sys/fs/cgroup tmpfs rw,seclabel,relatime,mode=750,gid=1000 0 0
    none /sys/fs/cgroup/memory cgroup rw,relatime,memory 0 0
    tmpfs /mnt/asec tmpfs rw,seclabel,relatime,mode=755,gid=1000 0 0
    tmpfs /mnt/obb tmpfs rw,seclabel,relatime,mode=755,gid=1000 0 0
    none /dev/memcg cgroup rw,relatime,memory 0 0
    none /dev/cpuctl cgroup rw,relatime,cpu 0 0
    none /sys/fs/cgroup tmpfs rw,seclabel,relatime,mode=750,gid=1000 0 0
    none /sys/fs/cgroup/memory cgroup rw,relatime,memory 0 0
    none /sys/fs/cgroup/freezer cgroup rw,relatime,freezer 0 0
    /dev/block/platform/msm_sdcc.1/by-name/system /system ext4 ro,seclabel,relatime,data=ordered 0 0
    /dev/block/platform/msm_sdcc.1/by-name/userdata /data ext4 rw,seclabel,nosuid,nodev,relatime,noauto_da_alloc,data=ordered 0 0
    /dev/block/platform/msm_sdcc.1/by-name/cache /cache ext4 rw,seclabel,nosuid,nodev,relatime,data=ordered 0 0
    /dev/block/platform/msm_sdcc.1/by-name/persist /persist ext4 rw,seclabel,nosuid,nodev,relatime,data=ordered 0 0
    /dev/block/platform/msm_sdcc.1/by-name/modem /firmware vfat ro,context=u:object_r:firmware_file:s0,relatime,uid=1000,gid=1000,fmask=0337,dmask=0227,codepage=cp437,iocharset=iso8859-1,shortname=lower,errors=remount-ro 0 0
    /dev/fuse /mnt/shell/emulated fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0
    /dev/fuse /mnt/shell/emulated/0 fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0

    找到其中我们关注的带 /system 的那一行:

    /dev/block/platform/msm_sdcc.1/by-name/system /system ext4 ro,seclabel,relatime,data=ordered 0 0
  3. 重新挂载。

    命令:

    mount -o remount,rw -t yaffs2 /dev/block/platform/msm_sdcc.1/by-name/system /system

    这里的 /dev/block/platform/msm_sdcc.1/by-name/system 就是我们从上一步的输出里得到的文件路径。

如果输出没有提示错误的话,操作就成功了,可以对 /system 下的文件为所欲为了。

查看连接过的 WiFi 密码

注:需要 root 权限。

命令:

adb shell
su
cat /data/misc/wifi/*.conf

输出示例:

network={
	ssid="TP-LINK_9DFC"
	scan_ssid=1
	psk="123456789"
	key_mgmt=WPA-PSK
	group=CCMP TKIP
	auth_alg=OPEN
	sim_num=1
	priority=13893
}

network={
	ssid="TP-LINK_F11E"
	psk="987654321"
	key_mgmt=WPA-PSK
	sim_num=1
	priority=17293
}

ssid 即为我们在 WLAN 设置里看到的名称,psk 为密码,key_mgmt 为安全加密方式。

设置系统日期和时间

注:需要 root 权限。

命令:

adb shell
su
date -s 20160823.131500

表示将系统日期和时间更改为 2016 年 08 月 23 日 13 点 15 分 00 秒。

重启手机

命令:

adb reboot

检测设备是否已 root

命令:

adb shell
su

此时命令行提示符是 $ 则表示没有 root 权限,是 # 则表示已 root。

使用 Monkey 进行压力测试

Monkey 可以生成伪随机用户事件来模拟单击、触摸、手势等操作,可以对正在开发中的程序进行随机压力测试。

简单用法:

adb shell monkey -p <packagename> -v 500

表示向 <packagename> 指定的应用程序发送 500 个伪随机事件。

Monkey 的详细用法参考 官方文档

开启/关闭 WiFi

注:需要 root 权限。

有时需要控制设备的 WiFi 状态,可以用以下指令完成。

开启 WiFi:

adb root
adb shell svc wifi enable

关闭 WiFi:

adb root
adb shell svc wifi disable

若执行成功,输出为空;若未取得 root 权限执行此命令,将执行失败,输出 Killed

刷机相关命令

重启到 Recovery 模式

命令:

adb reboot recovery

从 Recovery 重启到 Android

命令:

adb reboot

重启到 Fastboot 模式

命令:

adb reboot bootloader

通过 sideload 更新系统

如果我们下载了 Android 设备对应的系统更新包到电脑上,那么也可以通过 adb 来完成更新。

以 Recovery 模式下更新为例:

  1. 重启到 Recovery 模式。

    命令:

    adb reboot recovery
  2. 在设备的 Recovery 界面上操作进入 Apply update-Apply from ADB

    注:不同的 Recovery 菜单可能与此有差异,有的是一级菜单就有 Apply update from ADB

  3. 通过 adb 上传和更新系统。

    命令:

    adb sideload <path-to-update.zip>

更多 adb shell 命令

Android 系统是基于 Linux 内核的,所以 Linux 里的很多命令在 Android 里也有相同或类似的实现,在 adb shell 里可以调用。本文档前面的部分内容已经用到了 adb shell 命令。

查看进程

命令:

adb shell ps

输出示例:

USER     PID   PPID  VSIZE  RSS     WCHAN    PC        NAME
root      1     0     8904   788   ffffffff 00000000 S /init
root      2     0     0      0     ffffffff 00000000 S kthreadd
...
u0_a71    7779  5926  1538748 48896 ffffffff 00000000 S com.sohu.inputmethod.sogou:classic
u0_a58    7963  5926  1561916 59568 ffffffff 00000000 S org.mazhuang.boottimemeasure
...
shell     8750  217   10640  740   00000000 b6f28340 R ps

各列含义:

列名 含义
USER 所属用户
PID 进程 ID
PPID 父进程 ID
NAME 进程名

查看实时资源占用情况

命令:

adb shell top

输出示例:

User 0%, System 6%, IOW 0%, IRQ 0%
User 3 + Nice 0 + Sys 21 + Idle 280 + IOW 0 + IRQ 0 + SIRQ 3 = 307

  PID PR CPU% S  #THR     VSS     RSS PCY UID      Name
 8763  0   3% R     1  10640K   1064K  fg shell    top
  131  0   3% S     1      0K      0K  fg root     dhd_dpc
 6144  0   0% S   115 1682004K 115916K  fg system   system_server
  132  0   0% S     1      0K      0K  fg root     dhd_rxf
 1731  0   0% S     6  20288K    788K  fg root     /system/bin/mpdecision
  217  0   0% S     6  18008K    356K  fg shell    /sbin/adbd
 ...
 7779  2   0% S    19 1538748K  48896K  bg u0_a71   com.sohu.inputmethod.sogou:classic
 7963  0   0% S    18 1561916K  59568K  fg u0_a58   org.mazhuang.boottimemeasure
 ...

各列含义:

列名 含义
PID 进程 ID
PR 优先级
CPU% 当前瞬间占用 CPU 百分比
S 进程状态(R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程)
#THR 线程数
VSS Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
RSS Resident Set Size 实际使用物理内存(包含共享库占用的内存)
PCY 调度策略优先级,SP_BACKGROUND/SPFOREGROUND
UID 进程所有者的用户 ID
NAME 进程名

top 命令还支持一些命令行参数,详细用法如下:

Usage: top [ -m max_procs ] [ -n iterations ] [ -d delay ] [ -s sort_column ] [ -t ] [ -h ]
    -m num  最多显示多少个进程
    -n num  刷新多少次后退出
    -d num  刷新时间间隔(单位秒,默认值 5)
    -s col  按某列排序(可用 col 值:cpu, vss, rss, thr)
    -t      显示线程信息
    -h      显示帮助文档

其它

如下是其它常用命令的简单描述,前文已经专门讲过的命令不再额外说明:

命令 功能
cat 显示文件内容
cd 切换目录
chmod 改变文件的存取模式/访问权限
df 查看磁盘空间使用情况
grep 过滤输出
kill 杀死指定 PID 的进程
ls 列举目录内容
mount 挂载目录的查看和管理
mv 移动或重命名文件
ps 查看正在运行的进程
rm 删除文件
top 查看进程的资源占用情况

常见问题

启动 adb server 失败

出错提示

error: protocol fault (couldn't read status): No error

可能原因

adb server 进程想使用的 5037 端口被占用。

解决方案

找到占用 5037 端口的进程,然后终止它。以 Windows 下为例:

netstat -ano | findstr LISTENING

...
TCP    0.0.0.0:5037           0.0.0.0:0              LISTENING       1548
...

这里 1548 即为进程 ID,用命令结束该进程:

taskkill /PID 1548

然后再启动 adb 就没问题了。

adb 的非官方实现

  • fb-adb - A better shell for Android devices (for Mac).

作者:chaoyu168 发表于2017/10/12 15:20:27 原文链接
阅读:269 评论:0 查看评论

微信企业号开发:企业支付openid的获取 appid and openid not match

$
0
0

openid是微信支付的一个必要参数,但官方的demo里针对的都是公众号,企业号的如何获取呢?

基本的原理都是根据code获取到对应微信用户的openid,分成三步

1调用的接口为https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE

官方文档参考

非企业用户直接返回openID,企业用户时不返回openid!!


2但企业用户会返回UserId,需要userid与openid互换


需要进一步调用接口https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid?access_token=ACCESS_TOKEN

官方文档参考

3在调用userid与openid互换互换接口时,有一个细节需要注意。

  传递的参数agentid

agentid需要分成两种类型,一种情况这个参数必须传递,一种一定不要传递,否则就会出现错误
在调用UnfiedOrder接口时,返回如下错误
UnfiedOrder response : 

<xml><return_code><![CDATA[FAIL]]></return_code>
<return_msg><![CDATA[appid and openid not match]]></return_msg>
</xml>

其实也就是说在调用https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid?access_token=ACCESS_TOKEN

这个接口时有没有参数agenti适用于不同的情况,会返回不同的openid


https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid?access_token=ACCESS_TOKEN






作者:xuexiaodong2009 发表于2017/10/12 17:02:57 原文链接
阅读:41 评论:0 查看评论

坑中速记整理! 使用 kotlin 写第一个 ReactNative Android 模块

$
0
0

预览
Kotlin 和 Swift, 两大新宠! 借 ReactNative 熟悉下 kotlin 的用法,不料掉坑里面了.昨晚花了大半夜,趁这会儿思路清晰,把涉及到的一些关键信息,迅速整理下.

最佳的使用 Kotlin 快速开始写Android模块的方式

  1. react-native init AwesomeProject 生成的 android 目录,是一个标准的 Android Studio 工程,详见: http://facebook.github.io/react-native/docs/getting-started.html
  2. 直接在 Android Studio 中打开 AwesomeProject/android 目录.
  3. 参考文章 http://facebook.github.io/react-native/docs/native-modules-android.html,先用 java 实现
  4. 顶部菜单 –> code –> Convert Java File to Kotlin File ,自动转换为 kotlin .
package com.awesomeproject.AnExampleReactPackage

import android.widget.Toast

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod

import java.util.HashMap

/**
 * Created by yanfeng on 2017/10/12.
 */

class ToastModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {

    override fun getName(): String {
        return "ToastExample"
    }

    override fun getConstants(): Map<String, Any>? {
        val constants = HashMap<String, Any>()
        constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT)
        constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG)
        return constants
    }

    @ReactMethod
    fun show(message: String, duration: Int) {
        Toast.makeText(reactApplicationContext, message, duration).show()
    }

    companion object {

        private val DURATION_SHORT_KEY = "SHORT"
        private val DURATION_LONG_KEY = "LONG"
    }
}

一些坑中的经验

  • 如果 ReactNative 初始化慢,可以改用淘宝源,后面的 –verbose 参数,主要用来辨别是否卡住.
npm install -g nrm
nrm use taobao
npm install -g react-native-cli --verbose
react-native init AwesomeProject --verboses
  • RN 与已有项目集成的原理是,把已有的 Android 项目复制到 android 文件夹,然后改下配置.

  • 如果没有已有的运行良好的项目,不要尝试用 Android Studio 直接新建项目,因为 Android Studio 的默认 SDK 版本(25.3.1) 和 ReactNative 的SDK版本(23.0.1) 不一致,所以在根据 RN 文档,改配置,会遇到各种问题.如果非要模拟,建议直接基于 AwesomeProject/android 这个项目改.

  • 改淘宝源,可以加快速度,但是每次安装还是需要 20~40 分钟(取决于网络环境等).如果本地再起一个 sinopia ,这样第二次初始化 RN 时,只需要 3 ~ 5 分钟.详见: https://github.com/rlidwka/sinopia

  • 如果遇到 All com.android.support libraries must use the exact same version specification 一类的错误,又必须解决的话,可以尝试查看依赖关系,看到底是哪里在冲突:

命令是:

./gradlew -q dependencies app:dependencies --configuration compile

可能的输出:

+--- com.android.support.constraint:constraint-layout:1.0.0-beta2
|    \--- com.android.support.constraint:constraint-layout-solver:1.0.0-beta2
\--- com.facebook.react:react-native:+ -> 0.20.1
     +--- com.google.code.findbugs:jsr305:3.0.0
     +--- com.facebook.stetho:stetho-okhttp:1.2.0
     |    +--- com.google.code.findbugs:jsr305:2.0.1 -> 3.0.0
     |    +--- com.facebook.stetho:stetho:1.2.0
     |    |    +--- com.google.code.findbugs:jsr305:2.0.1 -> 3.0.0
     |    |    \--- commons-cli:commons-cli:1.2
     |    \--- com.squareup.okhttp:okhttp:2.2.0 -> 2.5.0
     |         \--- com.squareup.okio:okio:1.6.0
     +--- com.squareup.okhttp:okhttp-ws:2.5.0
     |    \--- com.squareup.okhttp:okhttp:2.5.0 (*)
     +--- com.facebook.fresco:fresco:0.8.1
     |    +--- com.facebook.fresco:imagepipeline:0.8.1
     |    |    +--- com.nineoldandroids:library:2.4.0
     |    |    +--- com.facebook.fresco:fbcore:0.8.1
     |    |    +--- com.android.support:support-v4:21.0.3 -> 23.0.1
     |    |    |    \--- com.android.support:support-annotations:23.0.1
     |    |    \--- com.parse.bolts:bolts-android:1.1.4
     |    +--- com.facebook.fresco:fbcore:0.8.1
     |    \--- com.facebook.fresco:drawee:0.8.1
     |         +--- com.facebook.fresco:fbcore:0.8.1
     |         \--- com.android.support:support-v4:21.0.3 -> 23.0.1 (*)
     +--- org.webkit:android-jsc:r174650
     +--- com.fasterxml.jackson.core:jackson-core:2.2.3
     +--- com.squareup.okhttp:okhttp:2.5.0 (*)
     +--- com.facebook.fresco:imagepipeline-okhttp:0.8.1
     |    +--- com.squareup.okhttp:okhttp:2.3.0 -> 2.5.0 (*)
     |    +--- com.facebook.fresco:imagepipeline:0.8.1 (*)
     |    \--- com.facebook.fresco:fbcore:0.8.1
     +--- com.squareup.okio:okio:1.6.0
     +--- com.android.support:recyclerview-v7:23.0.1
     |    +--- com.android.support:support-v4:23.0.1 (*)
     |    \--- com.android.support:support-annotations:23.0.1
     +--- com.facebook.stetho:stetho:1.2.0 (*)
     \--- com.android.support:appcompat-v7:23.0.1
          \--- com.android.support:support-v4:23.0.1 (*)

(*) - dependencies omitted (listed previously)
  • kotlin,会自动引入库; java,点击提示不存在的类,然后使用 Alt + 回车 也可以快速引入.

  • RN 的文档可能是错的.如果提示方法名总是不对,可以尝试下手动输入,看下提示,可能真的变了.

  • 执行 react-native run-android 可能比在 Android Studio 中运行方便;但是第二次执行原生 Android 代码时, Android Studio Run Build 的速度非常快,是更好的选择.

  • 如果是真机,可能需要:

adb reverse tcp:8081 tcp:8081
  • 遇到诡异的问题时,可以尝试先: clean build

源码参考:

https://github.com/ios122/kotlin-module-sample-for-reactnative

参考文章

作者:sinat_30800357 发表于2017/10/12 23:31:52 原文链接
阅读:207 评论:0 查看评论

Android--adb权限拒绝访问(permission denied)解决

$
0
0

用户机已经root了,但是在执行删除系统文件出现permission denied,查找了下原因:

默认运行所有命令时都仍然是普通用户身份,除非显式切换到root用户。

命令: adb root

然后执行命令就OK,注意:真机必须root

作者:chaoyu168 发表于2017/10/13 9:32:17 原文链接
阅读:222 评论:0 查看评论

Android踩坑日记:监听软键盘多次调用和刷新系统相册和获取所有相片

$
0
0

EditText设置监听软键盘删除键(或enter)

  • 一般使用方法
edittext.setOnKeyListener(new View.OnKeyListener(){
    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
      if (keyCode == KeyEvent.KEYCODE_DEL ){
        //处理操作
       }
    }
});
  • 问题:
    当点击软键盘的删除键时,处理操作会只执行两次或多次

  • 爬坑姿势:

edittext.setOnKeyListener(new View.OnKeyListener(){
    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
      /*必须是这两个条件,否则会执行多次*/
      if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction()==KeyEvent.ACTION_DOWN){
        //处理操作
       }
    }
});

刷新系统相册

当我们在系统的相册中增加或者删除图片,需要再次获取系统相册的所有图片时,需要通知系统刷新一下系统相册


    /*通知系统刷新相册*/
    public static void noticeSystemRefreshAlbum(Context context,String path){

        Intent mediaScanIntent=new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        Uri uri=null;
        if (path!=null){
            uri= Uri.fromFile(new File(path));
        }

        mediaScanIntent.setData(uri);
        mediaScanIntent = PermissionUtil.getUriAuthority(mediaScanIntent);
        context.sendBroadcast(mediaScanIntent);
    }

    public static Intent getUriAuthority(Intent intent) {
        //对于android M及更高版本,intent需要提供URI相关权限以操作文件
        //文件uri需要通过FileProvider获取
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        }
        return intent;
    }

获取系统所有图片和按相册名分类

/*本地相片文件描述类*/
public class LocalPhotoBean implements Serializable{

    /** 系统数据库中的id */
    public long mediaId;
    /*文件名*/
    public String fileName;
    /*文件路径*/
    public String path;
    /*缩略图路径*/
    public String thumbNailPath;
    /*文件大小*/
    public long fileSize;
    /*所属文件夹名称*/
    public String albumName;
    /*文件夹识别id*/
    public String albumId;

    /*缓存用于显示的路径*/
    private String showPicPath;
}
/*相册文件夹描述类*/
public class PhotoAlbumBean  implements Serializable{
    /*文件夹识别Id*/
    public String albumId;
    /*文件夹名称*/
    public String albumName;
    /*文件夹封面路径*/
    public String albumCoverPath;
    /*文件夹封面缩略图路径*/
    public String albumCoverThumbNailPath;
    /*文件夹包含的图片*/
    public ArrayList<LocalPhotoBean> imageList;

    /*缓存的用于显示的路径*/
    private String showPicPath;
    }

    /*查询返回结果集*/
    public static class LocalPhotoDataList{
        //所有图片列表
        public ArrayList<LocalPhotoBean> photoList;
        //相册文件夹列表
        public ArrayList<PhotoAlbumBean> albumList;

        public LocalPhotoDataList(ArrayList<LocalPhotoBean> photoList, ArrayList<PhotoAlbumBean> albumList) {
            this.photoList = photoList;
            this.albumList = albumList;
        }
    }

    /**
     *  通过ContentResolver ,查询本地图片信息
     * @param resolver
     * @return
     */
    public static LocalPhotoDataList getLocalPhotos(ContentResolver resolver, int minImageSize){
        ArrayList<LocalPhotoBean> photoList=new ArrayList<>();
        HashMap<String ,PhotoAlbumBean> albumMap=new HashMap<>();
        ArrayList<PhotoAlbumBean> albumList=new ArrayList<>();

        Cursor cursor=null;
       try {
            //需要查询的数据'
        String[] queryColumns={
                MediaStore.Images.ImageColumns.DISPLAY_NAME,//图片名
                MediaStore.Images.ImageColumns.DATA,//图片路径
                MediaStore.Images.ImageColumns._ID,//图片数据库
                MediaStore.Images.ImageColumns.BUCKET_ID,//图片所在文件夹Id
                MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,//图片所在文件夹文件名
                MediaStore.Images.ImageColumns.SIZE,//图片大小
                MediaStore.Images.ImageColumns.MIME_TYPE,//图片格式
                MediaStore.Images.ImageColumns.MINI_THUMB_MAGIC,//缩略图Id
                MediaStore.Images.ImageColumns.DATE_MODIFIED//修改时间
        };
        //查询的条件
        String selection=MediaStore.Images.ImageColumns.SIZE +" >= ? AND "+MediaStore.Images.ImageColumns.MIME_TYPE + " != ? ";
        //排序规则 修改时间降序排列
        String orderBy=MediaStore.Images.ImageColumns.DATE_MODIFIED+" DESC";

        String[] args=new String[]{String.valueOf(minImageSize),"image/gif"};
         //查询资源数据
        cursor=resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,queryColumns,selection,args,orderBy);
        if (cursor!=null&&cursor.moveToFirst()){
            int IndexName=cursor.getColumnIndex(MediaStore.Images.ImageColumns.DISPLAY_NAME);
            int IndexPath=cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
            int IndexId=cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID);
            int IndexBucket=cursor.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_ID);
            int IndexBucketName=cursor.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME);
            int IndexSize=cursor.getColumnIndex(MediaStore.Images.ImageColumns.SIZE);
            int IndexThumbId=cursor.getColumnIndex(MediaStore.Images.ImageColumns.MINI_THUMB_MAGIC);

            do{

                LocalPhotoBean photoItem=new LocalPhotoBean()
                        .setMediaId(cursor.getLong(IndexId))
                        .setFileName(cursor.getString(IndexName))
                        .setPath(cursor.getString(IndexPath))
                        .setAlbumName(cursor.getString(IndexBucketName))
                        .setAlbumId(cursor.getString(IndexBucket))
                        .setFileSize(cursor.getLong(IndexSize))
                        /*暂时缩略图*/
                        ;
                photoList.add(photoItem);
                //刷新文件夹集合数据
                PhotoAlbumBean photoAlbum=albumMap.get(photoItem.albumId);
                if (photoAlbum==null){
                    photoAlbum=PhotoAlbumBean.getInstance(photoItem.albumName,photoItem.albumId);
                    albumMap.put(photoItem.albumId,photoAlbum);
                }
                photoAlbum.addItem(photoItem);

            }while (cursor.moveToNext());
        }
        /*相册文件夹列表*/
        albumList.addAll(albumMap.values());
        /*排序*/
        Collections.sort(albumList);

        return new LocalPhotoDataList(photoList, albumList);
       }catch (SecurityException se) {

       } catch (Exception e) {
           e.printStackTrace();
           LogUtils.d(ApiConstants.MEDIA_LOG_TAG, "Error getLocalPhotos");
       } finally {
           if (cursor != null) {
               cursor.close();
           }
       }
        return null;
    }
作者:tuke_tuke 发表于2017/10/13 10:43:35 原文链接
阅读:175 评论:0 查看评论

Android--开机自启动(activity或service)

$
0
0

Android手机在启动的过程中会触发一个Standard Broadcast Action,名字叫android.intent.action.BOOT_COMPLETED(记得只会触发一次呀),在这里我们可以通过构建一个广播接收者来接收这个这个action。必须要注意的一点是:这个广播必须的静态注册的,不能是动态注册的广播(这种接受开机广播的,一定要静态注册,这样应用还没运行起来时也照样能够接收到开机广播  ,动态广播就不行了)。

在装上demo让demo运行后,先关机,再启动。也就是说装上应用运行后,一定要重启机器。

如果失败:看看有没有装360之类的被限制,还有手机自带的管理自启动的软件,进去点击允许;

1、注册开机广播,在清单文件中注册:

<receiver
            android:name=".MyReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter android:priority="1000">
                <action android:name="android.intent.action.BOOT_COMPLETED"></action>
            </intent-filter>
		</receiver>
		
        <service android:name="com.example.openandroid.MyService">
            <intent-filter >
               <action android:name="com.example.openandroid.MyService" />  
               <category android:name="android.intent.category.default" />
            </intent-filter>
        </service>

2.在开机广播的onReceive方法里面做跳转:

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class MyReceiver extends BroadcastReceiver
{
    public MyReceiver()
    {
    }

    @Override
    public void onReceive(Context context, Intent intent)
    {
        if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED"))
        {
//            Intent i = new Intent(context, MainActivity.class);
//            i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//            context.startActivity(i);
        	Intent i = new Intent(context, MyService.class);
        	context.startService(i);
        }
    }

}

3.MainActivity

mport android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;


public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		Toast.makeText(this, "哈哈,我成功启动了!", Toast.LENGTH_LONG).show();
        Log.e("AutoRun","哈哈,我成功启动了!");

	}

	
}

4.MyService

import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.os.IBinder;

public class MyService extends Service {

	@Override
	public IBinder onBind(Intent intent) {
		// TODO Auto-generated method stub
		return null;
	}
	@Override
	public void onStart(Intent intent, int startId) {
		// TODO Auto-generated method stub
		super.onStart(intent, startId);
		
		Intent i = new Intent(Intent.ACTION_MAIN);    
		i.addCategory(Intent.CATEGORY_LAUNCHER);                
		ComponentName cn = new ComponentName(packageName, className);                
		i.setComponent(cn);    
		startActivity(i);    
	}

}


作者:chaoyu168 发表于2017/10/13 10:45:37 原文链接
阅读:198 评论:0 查看评论

Kotlin基本类型自动装箱的一点问题

$
0
0

问题

在Kotlin官方文档介绍基本类型时,给我们说明了在有些情况下会对基本类型自动进行装箱操作。 但是具体是如何进行装箱,以及何时进行装箱缺没有提供详细介绍。只是提供了一个例子,如下:

val a: Int = 10000
print(a === a) // Prints 'true'
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA === anotherBoxedA) // !!!Prints 'false'!!!

对于上述代码,废了好大力气 写了好多demo才搞清楚。 接下来先通过几个简单的栗子来理解一下Kotlin是如何进行装箱操作的

####**第一个栗子**
fun main(args: Array<String>) {
    test1()
}

fun test1() {
    val i: Int = 1000
    println(i)
}
给大家提供一点技巧,在看不懂Kotlin是如何编译运行的情况下,我们可以先将其反编译成Java字节码,对于Java我们就驾轻就熟啦。具体做法就是 1 显示Kotlin的字节码 ![这里写图片描述](http://img.blog.csdn.net/20171013135245711?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenhtMzE3MTIyNjY3/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 2 将Kotlin字节码反编译成Java字节码 ![这里写图片描述](http://img.blog.csdn.net/20171013135324119?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenhtMzE3MTIyNjY3/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 通过这种方法,将上面的test1()方法反编译之后得出如下字节码
   public static final void test1() {
      short i = 1000;
      System.out.println(i);
   }

可以看出Kotlin编译器将 i 单纯的看做是一个基本类型short,并将其打印

再举个栗子

fun main(args: Array<String>) {
    test2()
}

fun test2() {
    val i: Int? = 1000
    println(i)
}

看到test1和test2的区别了吗?? 在test2中多了一个 ?
val i: Int? = 1000
这个“`?“`代表的意思是这个i可以被赋值为null, 既然可以是null,那就不能是原始类型,只能是对象,因此Kotlin会自动的为其进行装箱操作。因此反编译test2之后,我们会得到如下字节码

   public static final void test2() {
      Integer i = Integer.valueOf(1000);
      System.out.println(i);
   }

分析

理解了上述两个小栗子之后,在回头看一下官方提供的demo,就可以理解了。我们不妨自己也写一个类似的代码

fun test3() {
    //Kotlin并不会自动装箱
    val i: Int = 1000

    println(i)

    //因为j和k都被当做对象操作,因此会将i进行装箱做操,然后复制给j、k
    val j: Int? = i
    val k: Int? = i

    println(j === k)
}

反编译成java字节码之后结果同我们猜想的一致:

public static final void test3() {
      short i = 1000;
      System.out.println(i);
      Integer j = Integer.valueOf(i);
      Integer k = Integer.valueOf(i);
      boolean var3 = j == k;
      System.out.println(var3);
}

总结

注:在Kotlin中,字符类型不是基本数值类型,是一个独立的数据类型。
上面的整形类型的表示方式并没有使用int、double等java中的关键字,而是使用了封装类来表示 这是因为在Kotlin中一切都是对象(没有如同java中的基本类型)。 当我们在代码中使用整形数字的时候,Kotlin会自动的将其进行装箱操作

作者:zxm317122667 发表于2017/10/13 10:48:32 原文链接
阅读:471 评论:1 查看评论

Unity3D-Android跳转到指定平台(华为商店为例)

$
0
0

我们可以利用前面说博客说的,直接在Unity这边写代码去实现一些简单的Android功能,那么跳转到各大平台的商店,也是类似的做法。

直接看代码:

//跳转到指定应用商店,这里以华为为例
	public void OnRateToHuawei(){
		RateToOther("com.google.android.apps.maps", "com.huawei.appmarket");
	}
	public static void RateToOther(string appPkg, string marketPkg){
		if (!Application.isEditor)
        {
			AndroidJavaClass intentClass = new AndroidJavaClass("android.content.Intent");
            AndroidJavaObject intentObject = new AndroidJavaObject("android.content.Intent");
            intentObject.Call<AndroidJavaObject>("setAction", intentClass.GetStatic<string>("ACTION_VIEW"));
            AndroidJavaClass uriClass = new AndroidJavaClass("android.net.Uri");
            AndroidJavaObject uriObject = uriClass.CallStatic<AndroidJavaObject>("parse", "market://details?id=" + appPkg);
            intentObject.Call<AndroidJavaObject>("setData", uriObject);
            intentObject.Call<AndroidJavaObject>("setPackage", marketPkg);
            AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            AndroidJavaObject currentActivity = unity.GetStatic<AndroidJavaObject>("currentActivity");
            currentActivity.Call("startActivity", intentObject);
		}
	}

我这里以华为为例,将下面的代码添加到任意脚本中,可以直接调用RateToOther,第一个参数为你需要跳转到的App应用,我上面是跳到商店中谷歌地图的下载页面;第二个参数为目标平台的BundleID,比如我填的 华为商店的id,这样就可以跳转到你需要个个平台商店中去了。如果平台APP未找到,则会去默认的商店。

下面列出一些常用的平台ID:

//天朝各应用商店的PackageName
	//包名	商店
	//com.android.vending	Google Play
	//com.tencent.android.qqdownloader	应用宝
	//com.qihoo.appstore	360手机助手
	//com.baidu.appsearch	百度手机助
	//com.xiaomi.market	小米应用商店
	//com.wandoujia.phoenix2	豌豆荚
	//com.huawei.appmarket	华为应用市场
	//com.taobao.appcenter	淘宝手机助手
	//com.hiapk.marketpho	安卓市场
	//cn.goapk.market	安智市场

当然也可以自行去搜索各种其他平台


作者:pz789as 发表于2017/10/13 11:02:58 原文链接
阅读:192 评论:0 查看评论

kotlin学习笔记——单元测试

$
0
0

Kotlin学习笔记系列:http://blog.csdn.net/column/details/16696.html


Unit Test

kotlin也可以进行unit testing,如果项目中之前没有,那么需要做一些准备工作。

首先引入依赖
testCompile 'junit:junit:4.12'
这里注意不能是androidTestCompile,否则会报错Unresolved reference: xxxx

然后创建目录
在src目录下(main的同级)创建test/java目录,创建完会发现java目录的颜色自动为绿色,表示ide知道我们要使用unit testing模式。
在java目录下创建package(与项目主包名一致)

创建测试代码
在package下创建测试类编写代码即可,例如:
import org.junit.Test
import kotlin.test.assertTrue
class SimpleTest {
 @Test fun unitTestingWorks() {
     assertTrue(true)
 }
}
运行即可



Instrumentation Test

与unit testing一样,首先引入依赖
defaultConfig {
    ...
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
    exclude group: 'com.android.support', module: 'support-annotations'
})
androidTestCompile ("com.android.support.test.espresso:espresso-contrib:2.2.1"){
    exclude group: 'com.android.support', module: 'appcompat'
    exclude group: 'com.android.support', module: 'support-v4'
    exclude group: 'com.android.support', module: 'support-annotations'
    exclude module: 'recyclerview-v7'
}
exclude去掉一些依赖,防止重复引入
(contrib这个增加了一些额外功能,比如测试recyclerview)

然后创建目录,与unit一样,只不过根目录不是test而是androidTest,其他一样。

创建测试代码
import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.action.ViewActions.click
import android.support.test.espresso.assertion.ViewAssertions.matches
import android.support.test.espresso.contrib.RecyclerViewActions
import android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom
import android.support.test.espresso.matcher.ViewMatchers.withId
import android.support.test.rule.ActivityTestRule
import android.support.v7.widget.RecyclerView
import android.widget.TextView
import org.junit.Rule
import org.junit.Test

class SimpleActivityTest {

    @get:Rule
    val activity = ActivityTestRule(MainActivity::class.java)

    @Test fun testItem(){
        onView(withId(R.id.recyclerview)).perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0click()))
        onView(withId(R.id.textView)).check(matches(isAssignableFrom(TextView::class.java)))
    }
}
testitem中第一行代码是模拟点击recyclerview的第一个item。第二行是判断id是textview的组件是否是TextView。

作者:chzphoenix 发表于2017/10/13 14:16:01 原文链接
阅读:136 评论:0 查看评论

MediaPlayer之音频播放

$
0
0

一 前言

       android提供了简单的播放音频和视频的类MediaPlayer,它可以播放raw、assets、sdk、网络上的音视频资源,在前面已经介绍过MediaPlayer(点击查看)。

二 音频播放

      播放音频步骤:
(1)创建MediaPlayer对象实例;可以通过new创建该实例,也可以通过静态方法create创建。
(2)装载音频资源,如果在(1)中你使用create创建的MediaPlayer,此时该步骤可省略,否则你将调用setDataSource添加音频资源。
(3)调用prepare()或prepareAsync()方法来做播放前的准备工作,如果(2)省略的话,该步骤也要省略(即如果你掉用setDataSource方法添加音频资源了,就要调用prepare()或prepareAsync()方法做播放前的准备工作,否则就不需要了)。
(4)调用MediaPlayer的start、stop、pause等方法控制播放过程。

1 播放raw中的音频资源文件

           首先在res目录创建目录raw ,在raw添加你要播放的音频资源文件,然后就可以按照上述步骤就行了,创建MediaPlayer对象实例代码如下:
    private MediaPlayer player;
@AfterViews
void initData(){
player = new MediaPlayer();
// loadRaw();
}
播放音频代码如下:
    void loadRaw(){
/*
* 判断音频是否在正在播放,如果是正在播放,
* 就要调用reset方法进行重置,此时player会处于空闲状态,
* 如果不进行重置的话,再调用setDataSource设置资源时,会抛出异常 IllegalStateException
*/
if (player.isPlaying()){
// player.release();
player.stop();
}
player.reset();
// player = MediaPlayer.create(this, R.raw.hongyanjiu);
AssetFileDescriptor afd = getResources().openRawResourceFd(R.raw.hongyanjiu);
try {
player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
afd.close();
player.prepare();
player.start();
//设置播放监听
setListener();
} catch (IOException e) {
e.printStackTrace();
}

}
        说明,需要注意的是在装载资源之前,先要判断是否有资源正在播放,如果有的话就要调用stop停止播放,然后调用reset方法使MediaPlayer处于空闲状态,否则的将会出现非法状态异常。接下来看看是如何设置监听的,这里直接继承了监听类,如下:
public class MainActivity extends AppCompatActivity
implements MediaPlayer.OnCompletionListener,MediaPlayer.OnErrorListener ,
MediaPlayer.OnPreparedListener,MediaPlayer.OnSeekCompleteListener {
......
}
然后
void setListener(){
player.setOnCompletionListener(this);
player.setOnErrorListener(this);
player.setOnPreparedListener(this);
player.setOnSeekCompleteListener(this);
}
需要实现的方法,如下:
/**
* 音频播放完成调用该方法
* @param mp
*/
@Override
public void onCompletion(MediaPlayer mp) {
Toast.makeText(this, "音频播放完毕", Toast.LENGTH_SHORT).show();
}

/**
* 音频播放错误调用该方法
* @param mp
* @param what 错误类型
* @param extra
* @return
*/
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
Toast.makeText(this, "音频播放发生错误", Toast.LENGTH_SHORT).show();
return false;
}

/**
* 当方法prepare被调用触发该方法
* @param mp
*/
@Override
public void onPrepared(MediaPlayer mp) {
Toast.makeText(this, "音频播放已就绪", Toast.LENGTH_SHORT).show();
}


/**
* 当调用seek方法时触发该方法
* @param mp
*/
@Override
public void onSeekComplete(MediaPlayer mp) {
Toast.makeText(this, "音频播放已seek", Toast.LENGTH_SHORT).show();
}

2 播放assets中的音频资源文件

           首先在src/main目录创建目录assets ,在assets添加你要播放的音频资源文件,然后就可以按照上述步骤就行了,实现fang发如下:
    void loadAsset(){
/*
* 判断音频是否在正在播放,如果是正在播放,
* 就要调用reset方法进行重置,此时player会处于空闲状态,
* 如果不进行重置的话,再调用setDataSource设置资源时,会抛出异常 IllegalStateException
*/
// player.isLooping()
if (player.isPlaying()){
// player.release();
player.stop();
}
player.reset();
//AssetManage对象
AssetManager am = getAssets();
//打开指定音频文件
try {
AssetFileDescriptor afd = am.openFd("hongyanjiu.mp3");
//给MediaPlayer添加资源文件
player.setDataSource(afd.getFileDescriptor(),afd.getStartOffset(),afd.getLength());
//做准备工作:装载音频 使用setDataSource()方法添加资源 必须再调用prepare()装载音频, 而后才能调用start方法开始播放
afd.close();
player.prepare();
player.start();
setListener();
} catch (IOException e) {
e.printStackTrace();
}
}

      说明获取assets资源文件需要用到AssetManage,而AssetManage可以通过Context.getAssets()获取,然后调用AssetManage的openFd方法获取音频资源。

3 播放手机内存中的音频资源文件

          在这里我的示例是先把assets中的音频资源copy到手机的sdk里,再来播放资源文件,copy实现代码如下:

    public void  copyFileFromAssetToSDK(){
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ){
path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + getPackageName();
}else {
path = Environment.getDataDirectory().getAbsolutePath() + File.separator + getPackageName();
}

File file = new File(path);
if (!file.exists()){
file.mkdirs();
}
//AssetManage对象
AssetManager am = getAssets();
try {
AssetFileDescriptor afd = am.openFd("hongyanjiu.mp3");
InputStream is = afd.createInputStream();
// InputStreamReader reader = new InputStreamReader(afd.createInputStream());
// OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(new File(path,"hongyanjiu.mp3")));
File needFile = new File(path,"hongyanjiu.mp3");
// if (!needFile.exists()){
// needFile.mkdirs();
// }
path = needFile.getAbsolutePath();
Log.e("音频文件路径====>", path);
OutputStream os = new FileOutputStream(needFile);
int hasRead = 0;
byte[] buffer = new byte [1024];
while ((hasRead = is.read(buffer)) != -1){
os.write(buffer,0,hasRead);
}
os.flush();
os.close();
is.close();
afd.close();
} catch (IOException e) {
e.printStackTrace();
}

}

然后播放该音频资源代码如下:

   void loadSDK(){
/*
* 判断音频是否在正在播放,如果是正在播放,
* 就要调用reset方法进行重置,此时player会处于空闲状态,
* 如果不进行重置的话,再调用setDataSource设置资源时,会抛出异常 IllegalStateException
*/
if (player.isPlaying()){
// player.release();
player.stop();
}
player.reset();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
//动态判断手机储存空间的权限
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == -1 ||
ActivityCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE ) == -1){
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE},0x123);
}else {
try {
copyFileFromAssetToSDK();
if (TextUtils.isEmpty(path)){
return;
}
player.setDataSource(path);
player.prepare();
player.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}else {
try {
copyFileFromAssetToSDK();
if (TextUtils.isEmpty(path)){
return;
}
player.setDataSource(path);
player.prepare();
player.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}

4 播放网络音频资源

void loadNet(){
/*
* 判断音频是否在正在播放,如果是正在播放,
* 就要调用reset方法进行重置,此时player会处于空闲状态,
* 如果不进行重置的话,再调用setDataSource设置资源时,会抛出异常 IllegalStateException
*/
if (player.isPlaying()){
player.stop();
}
player.reset();
Uri url = Uri.parse(urlPath);
try {
player.setDataSource(this,url);
player.prepare();
player.start();
} catch (IOException e) {
e.printStackTrace();
}

}

说明,urlPath为网络音频资源地址。最后不要忘记在onDestroy方法释放资源:

@Override
protected void onDestroy() {
super.onDestroy();
if (player.isPlaying()){
player.stop();
player.release();
}
}

                                          完整示例代码点击查看




作者:lu1024188315 发表于2017/10/13 16:37:20 原文链接
阅读:19 评论:0 查看评论

Android Bitmap加载内存占用彻底分析

$
0
0

背景

在某个版本应用上线后,偶然测得首页占用的内存非常的大而且一直不能回收掉,经过一轮的排查后最终确定是3张图片引起的!当时每张图片占用了将近20m内存。当时紧急处理好后还一直惦记着此事,后来对Android加载Bitmap的内存占用作了彻底的分析,跟踪了相关的源码,在这里总结一下。

图片加载测试

先抛开结论,现在先直观的看一下加载如下一张图片需要多少内存

这里写图片描述

其中图片的宽高都为300像素

计算内存的方法采用 android.graphics.Bitmap#getByteCount

public final int getByteCount() {
    // int result permits bitmaps up to 46,340 x 46,340
    return getRowBytes() * getHeight();
}

预期占用的内存大小为

图片宽*图片高*表示每个像素点的字节数,即 

这里写图片描述

加载SD卡的图片

加载SD中的图片结果为

这里写图片描述

assets的图片

加载asset目录中的图片结果为

这里写图片描述

加载Resources的图片

  • drawable目录

    这里写图片描述

  • drawable-mdpi目录

    这里写图片描述

  • drawable-hdpi目录

    这里写图片描述

  • drawable-xhdpi目录

    这里写图片描述

  • drawable-xhhdpi目录

    这里写图片描述

  • drawable-xhhhdpi目录

    这里写图片描述

内存占用分析

理论上,300 * 300像素的图片,默认以4byte表示1个像素的情况下,占用的内存为
300 * 300 * 4 = 360000 byte

但是,实际上,只有从SD卡、assets目录、drawable-xhdpi目录下加载图片才等于理论数值,其他数值都不等!

等等!,从图片的大小看,不等于理论值的图片好像被放大或者缩小了?我们可以验证一下,把图片在内存中的实际宽高打印出来

SD卡的

这里写图片描述

drawable-mdpi的

这里写图片描述

发现没有?在drawable-mdpi目录中的图片在加载内存中时的宽高都放大了两倍!!
其实,加载在SD卡和assets目录的图片时,图片的尺寸不会被改变,但是drawable-xxxdpi目录的照片的尺寸会被改变,这里篇幅所限,就不一一截图了,想验证的可以下载demo(文末给出链接)试验一下。至于尺寸改变的原因,下文会讨论,这里卖个关子。

查看源码

正所谓源码面前,了无秘密,欲知原理,还须从源码下手,首先查看BitmapFactory.java文件

BitmapFactory.decodeFile 
BitmapFactory.decodeResourceStream

这两个方法的重载函数最终都会调用到

private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
            Rect padding, Options opts);

这是一个本地方法,其相关实现在

frameworks/base/core/jni/android/graphics/BitmapFactory.cpp

打开文件,找到如下的方法,就是本地方法的实现

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
        jobject padding, jobject options) {

    jobject bitmap = NULL;
    SkAutoTUnref<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));

    if (stream.get()) {
        SkAutoTUnref<SkStreamRewindable> bufferedStream(
                SkFrontBufferedStream::Create(stream, BYTES_TO_BUFFER));
        SkASSERT(bufferedStream.get() != NULL);
        bitmap = doDecode(env, bufferedStream, padding, options);
    }
    return bitmap;
}

抓住我们要看的部分,这里还调用了doDecode方法,调到doDecode会发现,bitmap解码的逻辑基本框架都在里面了,分析清楚它的逻辑,我们就能找到答案,方法非常长,有200多行,我把枝干提取出来,并加上注释如下

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {

    int sampleSize = 1;

    SkImageDecoder::Mode decodeMode = SkImageDecoder::kDecodePixels_Mode;
    SkColorType prefColorType = kN32_SkColorType;

    bool doDither = true;
    bool isMutable = false;
    float scale = 1.0f;
    bool preferQualityOverSpeed = false;
    bool requireUnpremultiplied = false;

    jobject javaBitmap = NULL;

    if (options != NULL) {
         //options是BitmapFactory.Options的java对象,这里获取该对象的成员变量值并赋值给本地代码的变量,下面类似格式的方法调用作用相同
        sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
        if (optionsJustBounds(env, options)) {
            decodeMode = SkImageDecoder::kDecodeBounds_Mode;
        }

        // initialize these, in case we fail later on
        env->SetIntField(options, gOptions_widthFieldID, -1);
        env->SetIntField(options, gOptions_heightFieldID, -1);
        env->SetObjectField(options, gOptions_mimeFieldID, 0);

        jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
        prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig);
        isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
        doDither = env->GetBooleanField(options, gOptions_ditherFieldID);
        preferQualityOverSpeed = env->GetBooleanField(options,
                gOptions_preferQualityOverSpeedFieldID);
        requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
        javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);

          //java里,inScaled默认true,所以这里总是执行,除非手动设置为false
        if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
            const int density = env->GetIntField(options, gOptions_densityFieldID);
            const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
            const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
            //重点就是这里了,density、targetDensity、screenDensity的值决定了是否缩放、以及缩放的倍数
            if (density != 0 && targetDensity != 0 && density != screenDensity) {
                scale = (float) targetDensity / density;
            }
        }
    }

    const bool willScale = scale != 1.0f;

    ...省略若干行

     //真正的decode操作,decodingBitmap是解码的的结果,但如果要缩放,则返回缩放后的bitmap,看后面的代码
     SkBitmap decodingBitmap;
    if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
                != SkImageDecoder::kSuccess) {
        return nullObjectReturn("decoder->decode returned false");
    }

    int scaledWidth = decodingBitmap.width();
    int scaledHeight = decodingBitmap.height();

    if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
        scaledWidth = int(scaledWidth * scale + 0.5f);
        scaledHeight = int(scaledHeight * scale + 0.5f);
    }

    // update options (if any)
    if (options != NULL) {
        jstring mimeType = getMimeTypeString(env, decoder->getFormat());
        if (env->ExceptionCheck()) {
            return nullObjectReturn("OOM in getMimeTypeString()");
        }
        env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
        env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
        env->SetObjectField(options, gOptions_mimeFieldID, mimeType);
    }

    // if we're in justBounds mode, return now (skip the java bitmap)
    if (decodeMode == SkImageDecoder::kDecodeBounds_Mode) {
        return NULL;
    }

    ...省略若干行

     //scale != 1.0f就缩放bitmap,缩放的步骤概扩起来就是申请缩放后的内存,然后把所有的bitmap信息记录复制到outputBitmap变量上;否则直接复制decodingBitmap的内容
    if (willScale) {
        // This is weird so let me explain: we could use the scale parameter
        // directly, but for historical reasons this is how the corresponding
        // Dalvik code has always behaved. We simply recreate the behavior here.
        // The result is slightly different from simply using scale because of
        // the 0.5f rounding bias applied when computing the target image size
        const float sx = scaledWidth / float(decodingBitmap.width());
        const float sy = scaledHeight / float(decodingBitmap.height());

        // TODO: avoid copying when scaled size equals decodingBitmap size
        SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType());
        // FIXME: If the alphaType is kUnpremul and the image has alpha, the
        // colors may not be correct, since Skia does not yet support drawing
        // to/from unpremultiplied bitmaps.
        outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
                colorType, decodingBitmap.alphaType()));
        if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
            return nullObjectReturn("allocation failed for scaled bitmap");
        }

        // If outputBitmap's pixels are newly allocated by Java, there is no need
        // to erase to 0, since the pixels were initialized to 0.
        if (outputAllocator != &javaAllocator) {
            outputBitmap->eraseColor(0);
        }

        SkPaint paint;
        paint.setFilterLevel(SkPaint::kLow_FilterLevel);

        SkCanvas canvas(*outputBitmap);
        canvas.scale(sx, sy);
        canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
    } else {
        outputBitmap->swap(decodingBitmap);
    }

    ...省略若干行

     //后面的部分就是返回bitmap对象给java代码了

    if (javaBitmap != NULL) {
        bool isPremultiplied = !requireUnpremultiplied;
        GraphicsJNI::reinitBitmap(env, javaBitmap, outputBitmap, isPremultiplied);
        outputBitmap->notifyPixelsChanged();
        // If a java bitmap was passed in for reuse, pass it back
        return javaBitmap;
    }

    int bitmapCreateFlags = 0x0;
    if (isMutable) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Mutable;
    if (!requireUnpremultiplied) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Premultiplied;

    // now create the java bitmap
    return GraphicsJNI::createBitmap(env, outputBitmap, javaAllocator.getStorageObj(),
            bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}

上面的解析能勾画出大概的逻辑了,其中秘密就在这一小段

//java里,inScaled默认true,所以这里总是执行,除非手动设置为false
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
    const int density = env->GetIntField(options, gOptions_densityFieldID);
    const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
    const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
    //重点就是这里了,density、targetDensity、screenDensity的值决定了是否缩放、以及缩放的倍数
    if (density != 0 && targetDensity != 0 && density != screenDensity) {
        scale = (float) targetDensity / density;
    }
}

可以看到,BitmapFactory.Options对象的inScaled、inDensity、inTargetDensity、screenDensity四个值共同决定了bitmap是否被缩放以及缩放的倍数。

下面回到java部分的代码继续分析

为什么在drawable文件夹的图片会被缩放而SD卡、assets的图片不会

现在要解决这个问题就是要看BitmapFactory.Options对象的inScaled、inDensity、inTargetDensity、screenDensity四个值是怎样被赋值了

之前提到过,inScaled默认值为true

public Options() {
    inDither = false;
    inScaled = true;
    inPremultiplied = true;
}

decodeFile方法在调用本地方法前调用会decodeStream和decodeStreamInternal

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
    // we don't throw in this case, thus allowing the caller to only check
    // the cache, and not force the image to be decoded.
    if (is == null) {
        return null;
    }

    Bitmap bm = null;

    Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
    try {
        if (is instanceof AssetManager.AssetInputStream) {
            final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
            bm = nativeDecodeAsset(asset, outPadding, opts);
        } else {
            bm = decodeStreamInternal(is, outPadding, opts);
        }

        if (bm == null && opts != null && opts.inBitmap != null) {
            throw new IllegalArgumentException("Problem decoding into existing bitmap");
        }

        setDensityFromOptions(bm, opts);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
    }

    return bm;
}

private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
    // ASSERT(is != null);
    byte [] tempStorage = null;
    if (opts != null) tempStorage = opts.inTempStorage;
    if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
    return nativeDecodeStream(is, tempStorage, outPadding, opts);
}

可以看到,如果opts直到调用本地方法之前也没有并没有改变,故加载SD卡的图片和assets的图片并不会被缩放(加载assets的图片对应的本地方法为nativeDecodeAsset,最后都会调用doDecode)

decodeResource方法的调用栈为 decodeResource->decodeResourceStream->decodeStream,后面就跟之前的一样了,其中decodeResourceStream方法如下

/**
 * Decode a new Bitmap from an InputStream. This InputStream was obtained from
 * resources, which we pass to be able to scale the bitmap accordingly.
 */
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
        InputStream is, Rect pad, Options opts) {

    if (opts == null) {
        opts = new Options();
    }

    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }

    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }

    return decodeStream(is, pad, opts);
}

方法的注释已经提示此方法会缩放bitmap了,哈哈
在这里,opts对象的内容被改变了inDensity和inTargetDensity被赋值了,具体来说inDensity被赋值成资源对应的屏幕dpi值,而inTargetDensity则被赋值为当前设备的屏幕的dpi。

我们知道,android系统去获取资源的时候,会根据屏幕的密度去选取最适合的资源,也就是对应屏幕密度的资源,所以才有了drawable-mdpi、drawable-hdpi、drawable-xhdpi等目录,放在对应目录的资源,加载的时候都会记录其对应的密度等信息,存放在TypedValue的对象里,在decodeResource方法里有如下代码

final TypedValue value = new TypedValue();
is = res.openRawResource(id, value);
bm = decodeResourceStream(res, value, is, null, opts);

DisplayMetrics类记录了不同屏幕密度的dpi值,如下

public static final int DENSITY_LOW = 120;

public static final int DENSITY_MEDIUM = 160;

public static final int DENSITY_HIGH = 240;

public static final int DENSITY_XHIGH = 320;

public static final int DENSITY_XXHIGH = 480;

public static final int DENSITY_XXXHIGH = 640

public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;

分别是drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi、drawable-xxxhdpi目录的dpi值,在这些目录的图片,加载的时候就会被附上对应的值。因为默认的值是DENSITY_MEDIUM,所以drawable目录和drawable-mdpi的图片缩放的大小是一样的

小结

图片被缩放的原因在于资源目录对应着dpi,当加载资源的dpi和屏幕实际的dpi不一样时,进行缩放以使资源显示效果得到优化

图片资源放置选择

前文所述,当我们的图片资源只有一张的时候,该放到哪个目录?放到assets目录似乎是最安全的,不会因图片被放大造成OOM,也不会因图片缩小失真。但是assets目录的资源用起来不方便啊!我认为,在现在屏幕密度基本为720p以上的时代,如果UI设计师只提供了一张图片,就放到xhdpi或者xxhdpi目录吧,不然放在drawable目录会被放大几倍的

总结

本文先通过一个简单的测试引出图片加载时不同地址的图片内存占用不同的问题,继而通过分析源码得出内存占用不同的原因。实际上,利用这个原理也可以手动控制bitmap的大小呢,聪明的读者应该会有所启发了吧!

本文demo下载地址:https://github.com/Axlchen/BitmapLoadingDemo

作者:axlchen 发表于2017/10/13 22:12:55 原文链接
阅读:235 评论:1 查看评论

深度理解并设置 placeholder 属性,定制自己的 textField

$
0
0

textField 是 IOS 开发中比较常用的控件,绝大多数时候,系统所提供的简易 textField 功能是不够的。面对 UI 给出的各种属性和特点,需要 DIY 具有较强扩展功能的 textField,此时,就需要了解 textField 的一些深层属性。placeholder 是 textField 中比较重要和常用的属性,在剖析 placeholder 之前,先来讲讲如何给 textField 设置左右侧的图片。

textField 中有两个属性,leftView 与 rightView,用来设置 textField 的左右侧图片。这里直接封装成函数,以供参考:

/**
 * 设置 textfield 左侧图片
 */
- (void)setLeftViewWithTextField:(UITextField *)textField imageName:(NSString *)imageName {

    UIImageView *leftView = [[UIImageView alloc] init];
    leftView.image = [UIImage imageNamed:imageName];
    leftView.frame = CGRectMake(0,0,16,16);
    leftView.contentMode = UIViewContentModeCenter;
    textField.leftView = leftView;
    textField.leftViewMode = UITextFieldViewModeAlways;
}
/**
 * 设置 textfield 右侧图片
 */
- (void)setRightViewWithTextField:(UITextField *)textField imageName:(NSString *)imageName {

    UIImageView *rightView = [[UIImageView alloc] init];
    rightView.image = [UIImage imageNamed:imageName];
    rightView.frame = CGRectMake(0,0,16,16);
    rightView.contentMode = UIViewContentModeCenter;
    textField.rightView = rightView;
    textField.rightViewMode = UITextFieldViewModeAlways;
}

光设置图片还不够美观,还需要控制 textField 左右侧图片的位置缩进。此处同样给出代码示例(控制 textField 左侧图片缩进12,右侧图片向右缩进12):

/**
 * 控制文本框左侧图片的位置,缩进12
 */
- (CGRect)leftViewRectForBounds:(CGRect)bounds {
    CGRect rect = [super leftViewRectForBounds:bounds];
    return CGRectOffset(rect, 12, 0);
}
/**
 * 控制文本框右侧图片的位置,缩进12
 */
-(CGRect)rightViewRectForBounds:(CGRect)bounds {
    CGRect rect = [super rightViewRectForBounds:bounds];
    return CGRectOffset(rect, -12, 0);
}

由此,便完成了对 textField 左右侧图片得的设置。

下面讲解 placeholder 的相关设置,这是非常常用的设置,针对 placeholder 一些简单设置,这里不作详解,主要讲讲 placeholder 的缩进相关知识。

要调整 textField 中的 placeholder 的位置,可考虑先调整 placeholderLabel 的位置,再调整 placeholder 在 placeholderLabel 中绘制的位置以及范围。具体实现如下:

/**
 * 返回placeholderLabel的bounds,改变返回值,调整placeholderLabel的位置
 */
- (CGRect)placeholderRectForBounds:(CGRect)bounds {
    return CGRectMake(37, 0, self.bounds.size.width - 37, self.bounds.size.height);
}
/**
 * 调整placeholder在placeholderLabel中绘制的位置以及范围
 */
-(void)drawPlaceholderInRect:(CGRect)rect {
    [super drawPlaceholderInRect:CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)];
}

上述方法也间接设置了输入前 placeholder 的位置,如果要控制输入后 placeholder 的位置,可如下方式设置缩进:

/**
 * 控制输入后文本的位置,缩进80
 */
- (CGRect)editingRectForBounds:(CGRect)bounds {
    return CGRectInset(bounds, 80, 0);
}

关于 CGRectInset 与 CGRectOffset 的相关知识可自行查阅资料,也可参考博主之前的博客:对比 CGRectInset 与 CGRectOffset

最后,放上自定义的 textField 效果图(作图为输入前,右图为输入后,可以发现两者缩进不同,颜色也不同):
这里写图片描述

本博客相应的 demo 已上传至 GitHub :https://github.com/herojack/CustomTextfield

作者:huangfei711 发表于2017/10/14 21:00:12 原文链接
阅读:215 评论:0 查看评论

手动绕过百度加固Debug.isDebuggerConnected反调试的方法

$
0
0

本文博客地址:http://blog.csdn.net/qq1084283172/article/details/78237571


1.调用Debug.isDebuggerConnected函数这种反调试的Android加固比较少,主要是因为Debug.isDebuggerConnected这种方法的反调试作用不大,过掉也比较容易。百度的Android应用加固使用了调用Debug.isDebuggerConnected函数检测程序被调试的反调试方法。



2.绕过基于Debug.isDebuggerConnected函数检测进行反调试的方法整理:


【1】.对基于Debug.isDebuggerConnected函数检测进行反调试的百度加固的Android应用使用Apktool工具进行解包处理,在解包后的所有smali文件中全局搜索关键字符串“isDebuggerConnected”,查找到Debug.isDebuggerConnected函数检测反调试的smali汇编代码的位置,修改smali代码检测位置处的 判断条件 绕过Debug.isDebuggerConnected函数检测的反调试,使用Apktool工具对解包修改后的百度加固的Android应用的smali文件进行重新打包和签名处理,推荐使用AndroidKiller工具进行这所有的操作。



【2】.在 Dalvik模式下 进行百度加固的Andorid应用的动态so库文件调试时,使用IDA脚本IDC文件 Hook VMDebug.isDebuggerConnected函数的Native层实现函数dvmDbgIsDebuggerConnected,修改dvmDbgIsDebuggerConnected函数的返回值(基于VMDebug.isDebuggerConnected函数的Nativev层的函数dvmDbgIsDebuggerConnected)并且dvmDbgIsDebuggerConnected函数在libdvm.so库文件中还是导出函数,具体的原理参考《在百度加固中正确使用ida的姿势》。



Hook dvmDbgIsDebuggerConnected函数的IDA脚本(至于脚本中,第一次r0寄存器的值为1的时候为什么不改成0 ,需要参考一下Android源码的实现才能理解)。

from idaapi import *  
from idc import *

debug_addr = LocByName("_Z25dvmDbgIsDebuggerConnectedv")
end = FindFuncEnd(debug_addr) - 0x02
count = 0;

class DumpHook(DBG_Hooks):
    def dbg_bpt(self,tid,ea):
        global count
        r0 = GetRegValue('r0')
        if r0 == 1:
            count = count + 1
            if count == 2:
                SetRegValue(0,"r0")
        ResumeProcess()
        return 0

AddBpt(end)
debug = DumpHook()
debug.hook()

print "hook" 

dalvik虚拟机模式 下,VMDebug.isDebuggerConnected函数最终调用的是Native函数 dvmDbgIsDebuggerConnected



函数dvmDbgIsDebuggerConnected libdvm.so库文件 中的导出函数



art虚拟机模式 下,VMDebug.isDebuggerConnected函数最终调用的是Native函数 art::Dbg::IsDebuggerActive



函数art::Dbg::IsDebuggerActive libart.so库文件 中的导出函数



【3】.解包百度加固的Android应用,把百度加固的 libbaiduprotect.so 库文件单独拿出来,自己编写一个dalvik虚拟机模式下的 loader程序 调用百度加固的 libbaiduprotect.so 库文件中的JNI_Onload函数bypass掉壳代码和反调试,具体的方法可以参考看雪论坛的文章《百度加固逆向分析》。

#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <jni.h>

int main()
{
    JavaVM* vm;
    JNIEnv* env;
    jint res;
     
    JavaVMInitArgs vm_args;
    JavaVMOption options[1];
    options[0].optionString = "-Djava.class.path=.";
    vm_args.version=0x00010002;
    vm_args.options=options;
    vm_args.nOptions =1;
    vm_args.ignoreUnrecognized=JNI_TRUE;
     
    printf("[+] dlopen libdvm.so\n");
	// RTLD_LAZY RTLD_NOW
    void *handle = dlopen("/system/lib/libdvm.so", RTLD_LAZY);
    if(!handle) {
		
		printf("[-] dlopen libdvm.so failed!!\n");
		return 0;
    }
	
    // 先创建一个java虚拟机。因为JNI_ONload函数参数第一个参数为JavaVM。
    typedef int (*JNI_CreateJavaVM_Type)(JavaVM**, JNIEnv**, void*);
    JNI_CreateJavaVM_Type JNI_CreateJavaVM_Func = (JNI_CreateJavaVM_Type)dlsym(handle, "JNI_CreateJavaVM");
    if(!JNI_CreateJavaVM_Func) {
		
		printf("[-] dlsym failed\n");
		return 0;
    }
	
	// 创建java虚拟机
    res = JNI_CreateJavaVM_Func(&vm, &env, &vm_args)
    void* si = dlopen("/data/local/tmp/libbaiduprotect.so", RTLD_LAZY);
    if(si == NULL) {
		
		printf("[-] dlopen err!\n");
		return 0;
    }
	
    typedef jint (*FUN)(JavaVM* vm, void* res);
    FUN func_onload = (FUN)dlsym(si, "JNI_OnLoad");
	// 将断点下在了这里可以正好获取到JNI_Onload的函数地址。
    if(func_onload==NULL)
        return 0;
	
	// 调用JNI_Onload函数
    func_onload(vm,NULL);
    return 0;
}

关于Android系统上dalvik虚拟机模式下java虚拟机的创建方法,可以参考书籍《Android框架揭秘》102页~110页中,关于dalvik虚拟机模式下java虚拟机创建的代码分析。




Dalvik虚拟机模式下,java虚拟机的创建可以参考Andorid 4.4.4 r1的源码文件 /dalvik/vm/Jni.cpp  中的代码。

http://androidxref.com/4.4.4_r1/xref/dalvik/vm/Jni.cpp#3424



Art虚拟机模式下,java虚拟机的创建可以参考Android 4.4.4 r1的源码文件 /art/runtime/jni_internal.cc 中的代码。

http://androidxref.com/4.4.4_r1/xref/art/runtime/jni_internal.cc#2888



【4】.自己编写个简单的Android程序,自定义加载百度加固的动态库文件libbaiduprotect.so,然后在这个Android应用的基础上进行百度加固动态库文件libbaiduprotect.so的动态调试。



3.这里再介绍一种 手动绕过百度加固Debug.isDebuggerConnected反调试的方法,比较实用也比较简单不需要太多的操作。在介绍这种手动过掉Debug.isDebuggerConnected函数反调试的方法之前,先了解一下Debug.isDebuggerConnected函数的执行流程,以Android 4.4.4 r1的源码为分析基础。


【1】.Debug.isDebuggerConnected函数是在Android 4.4.4 r1源码的文件 /frameworks/base/core/java/android/os/Debug.java 中实现的,该函数最终调用的是VMDebug.isDebuggerConnected函数。

http://androidxref.com/4.4.4_r1/xref/frameworks/base/core/java/android/os/Debug.java#isDebuggerConnected



【2】.VMDebug.isDebuggerConnected函数是在Native层实现的,在Android 4.4.4 r1源码的文件 /libcore/dalvik/src/main/java/dalvik/system/VMDebug.java 中,到这里Debug.isDebuggerConnected函数的java层实现已经基本完成了,接下来是Debug.isDebuggerConnected函数在Native层的实现,由于Android系统可以运行在Dalvik虚拟机模式下或者Art虚拟机模式下,因此在Dalvik虚拟机模式下和Art虚拟机模式下,Debug.isDebuggerConnected函数底层的具体实现会有有所不同,需要分开来分析和学习。

http://androidxref.com/4.4.4_r1/xref/libcore/dalvik/src/main/java/dalvik/system/VMDebug.java#122



【3】.在dalvik虚拟机模式下,VMDebug.isDebuggerConnected函数是在Android 4.4.4 r1源码的文件 /dalvik/vm/native/dalvik_system_VMDebug.cpp 中实现的,具体就是对应Native层的函数 Dalvik_dalvik_system_VMDebug_isDebuggerConnected

http://androidxref.com/4.4.4_r1/xref/dalvik/vm/native/dalvik_system_VMDebug.cpp#Dalvik_dalvik_system_VMDebug_isDebuggerConnected


Dalvik_dalvik_system_VMDebug_isDebuggerConnected函数的实现,Dalvik_dalvik_system_VMDebug_isDebuggerConnected最终调用的是libdvm.so库文件的导出函数dvmDbgIsDebuggerConnected


【4】.dvmDbgIsDebuggerConnected函数是在Android 4.4.4 r1源码的文件 /dalvik/vm/Debugger.cpp 中实现的,最终Debug.isDebuggerConnected函数的返回值是由全局对象gDvm的成员变量gDvm.debuggerActive决定的。

http://androidxref.com/4.4.4_r1/xref/dalvik/vm/Debugger.cpp#443



【5】.在art虚拟机模式下,VMDebug.isDebuggerConnected函数是在Android 4.4.4 r1源码的文件 /art/runtime/native/dalvik_system_VMDebug.cc 中实现的,具体就是实现函数VMDebug_isDebuggerConnected



【6】.VMDebug_isDebuggerConnected函数最终是调用的 Dbg::IsDebuggerActive 函数。

http://androidxref.com/4.4.4_r1/xref/art/runtime/native/dalvik_system_VMDebug.cc#117



【7】.Dbg::IsDebuggerActive 函数是在Android 4.4.4 r1源码文件 /art/runtime/debugger.cc 中实现的,具体就是获取全局变量gDebuggerActive的状态值。

http://androidxref.com/4.4.4_r1/xref/art/runtime/debugger.cc#578



4.Debug.isDebuggerConnected函数的底层实现已经分析差不多啦,下面就说下 Dalvik虚拟机模式下 手动Debug.isDebuggerConnected反调试的方法。关于Android应用so库文件的动态调试就不详细介绍了,网上的教程很多自己去看。


【A】.由于Dalvik虚拟机模式下,函数dvmDbgIsDebuggerConnectedlibdvm.so库文件 中的导出函数,因此在进行Dalvik虚拟机模式下的so库文件的

动态调试时,想要 过掉Debug.isDebuggerConnected函数的反调试 需要在 libdvm.so库文件dvmDbgIsDebuggerConnected函数开头和结尾的位置下断点进行拦截,然后修改dvmDbgIsDebuggerConnected函数的返回值为0即可。

【B】.由于Art虚拟机模式下,函数art::Dbg::IsDebuggerActivelibart.so库文件 中的导出函数,因此在进行Art虚拟机模式下的so库文件的

动态调试时,想要 过掉Debug.isDebuggerConnected函数的反调试 需要在libart.so库文件art::Dbg::IsDebuggerActive函数开头和结尾的位置下断点进行拦截,然后修改art::Dbg::IsDebuggerActive函数的返回值为0即可。


下面就以dalvik虚拟机模式下的百度加固的 libbaiduprotect.so 库文件动态调试为例,进行手动绕过百度加固Debug.isDebuggerConnected反调试的方法步骤说明。


【1】.百度加固的Android应用以调试模式启动等待调试以后,IDA Pro附加调试该百度加固的Android应用成功以后如下图设置IDA Pro的调试运行选项,并在libdvm.so库文件dvmDbgIsDebuggerConnected函数开头和结尾的位置下断点进行拦截,然后 F9 运行当前被附加的Android应用程序几次,不过F9运行当前被附加的程序几次以后,该应用程序会断在dvmDbgIsDebuggerConnected函数的开头或者结尾的位置即当前函数断点被触发啦,没事,不用理会,继续做下面的操作即可。



【2】.另开启一个命令行终端Terminate,使用 jdb调试器 连接到被调试附加百度加固的Android应用, jdb调试器连接成功以后,dvmDbgIsDebuggerConnected函数开头的断点会被触发,断在dvmDbgIsDebuggerConnected函数开头的位置,再 F9运行1次 断在dvmDbgIsDebuggerConnected函数结尾的位置,此时dvmDbgIsDebuggerConnected函数的返回值R0的值为1,将其修改为0 即可手动绕过百度加固Debug.isDebuggerConnected的反调试。



作者:QQ1084283172 发表于2017/10/14 22:29:55 原文链接
阅读:31 评论:0 查看评论

Android单元测试(一):JUnit框架的使用

$
0
0

1.前言

网上有许多关于单元测试的好处,这里我就不去说了。我写单元测试的理由很简单粗暴,就是图一个方便。试想一下这个场景:我们在写一个新功能,每写一部分,我们就安装到手机上查看一下,这个过程中你要点击到对应的页面,做对应的操作,最后才能反馈给你结果。如果达到了预期效果,那么恭喜你。可是一旦这次失败了,是不是又要重复这一过程?是不是感到很麻烦?很费时间?如果你想早点写完下班,那么你就需要掌握单元测试。因为它能大大的缩短你自我验证的时间。

2.准备工作

我们新建一个项目,模板代码会默认在build文件中添加JUnit的依赖,而单元测试代码是放在src/test/java下面的,如下图:

这里写图片描述

用鼠标右键点击测试方法,选择菜单中的“Run”选项就可以执行对应的单元测试。我执行了图中的测试代码,可以看到执行方法只用了6毫秒,整个过程不到10秒。

3.JUnit介绍

JUnit是Java最基础的测试框架,主要的作用就是断言。

使用时在app的build文件中添加依赖。注意:用于测试环境框架一律是testCompile开头。

dependencies {
    testCompile 'junit:junit:4.12'
}

Assert类中主要方法如下:

方法名 方法描述
assertEquals 断言传入的预期值与实际值是相等的
assertNotEquals 断言传入的预期值与实际值是不相等的
assertArrayEquals 断言传入的预期数组与实际数组是相等的
assertNull 断言传入的对象是为空
assertNotNull 断言传入的对象是不为空
assertTrue 断言条件为真
assertFalse 断言条件为假
assertSame 断言两个对象引用同一个对象,相当于“==”
assertNotSame 断言两个对象引用不同的对象,相当于“!=”
assertThat 断言实际值是否满足指定的条件

注意:上面的每一个方法,都有对应的重载方法,可以在前面加一个String类型的参数,表示如果断言失败时的提示。


JUnit 中的常用注解:

注解名 含义
@Test 表示此方法为测试方法
@Before 在每个测试方法前执行,可做初始化操作
@After 在每个测试方法后执行,可做释放资源操作
@Ignore 忽略的测试方法
@BeforeClass 在类中所有方法前运行。此注解修饰的方法必须是static void
@AfterClass 在类中最后运行。此注解修饰的方法必须是static void
@RunWith 指定该测试类使用某个运行器
@Parameters 指定测试类的测试数据集合
@FixMethodOrder 指定测试类中方法的执行顺序

执行顺序:@BeforeClass –> @Before –> @Test –> @After –> @AfterClass

4.JUnit用法

我们测试下面这个简单的时间转换工具类,来说明一下具体的用法。

public class DateUtil {

    /**
     * 英文全称  如:2017-11-01 22:11:00
     */
    public static String FORMAT_YMDHMS = "yyyy-MM-dd HH:mm:ss";

    /**
     * 掉此方法输入所要转换的时间输入例如("2017-11-01 22:11:00")返回时间戳
     *
     * @param time
     * @return 时间戳
     */
    public static long dateToStamp(String time) throws ParseException{
        SimpleDateFormat sdr = new SimpleDateFormat(FORMAT_YMDHMS, Locale.CHINA);
        Date date = sdr.parse(time);
        return date.getTime();
    }

    /**
     * 将时间戳转换为时间
     */
    public static String stampToDate(long lt){
        String res;
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(FORMAT_YMDHMS, Locale.CHINA);
        Date date = new Date(lt);
        res = simpleDateFormat.format(date);
        return res;
    }

}

1.基础用法

1.首先测试stampToDate方法,在有注解@Before的方法中,我初始化了一个Date对象,给了一个”2017-10-15 16:00:02”时间对应的时间戳。测试时我认为返回的结果等于“预期时间”这个字符串。测试方法执行后如下图:

这里写图片描述

可以看到预期值与实际结果不符,测试失败!想要测试成功要么预期值为”2017-10-15 16:00:02”要么使用assertNotEquals方法断言。

2.接下来测试dateToStamp方法。

这里写图片描述

很简单,我认为返回结果不等于4,结果测试通过。

3.我们注意到在dateToStamp方法中,有抛出一个解析异常(ParseException)。也就是当参数没有按照规定格式去传,就会导致这个异常。

这里写图片描述

那我们怎么验证一个方法是否抛出了异常呢?可以给@Test注解设置expected参数来实现,如下:

这里写图片描述

抛出了对应的异常则测试成功,反之则测试失败。

2.参数化测试

这时,你是不是觉得还是很麻烦,因为每次测试一个方法都要去设置对应的值,就不能连续用不不同的值去测试一个方法,省的我们不断地修改。这时就用到了@RunWith@Parameters

首先在测试类上添加注解@RunWith(Parameterized.class),在创建一个由 @Parameters 注解的public static方法,让返回一个对应的测试数据集合。最后创建构造方法,方法的参数顺序和类型与测试数据集合一一对应。

这里写图片描述

上图就是一个简单的例子,可以看到连续执行了三次测试,其中第二次测试没有抛出异常,测试失败!

3.assertThat用法

上面我们所用到的一些基本的断言,如果我们没有设置失败时的输出信息,那么在断言失败时只会抛出AssertionError,无法知道到底是哪一部分出错。而assertThat就帮我们解决了这一点。它的可读性更好。

assertThat(T actual, Matcher<? super T> matcher);

assertThat(String reason, T actual, Matcher<? super T> matcher); 

其中reason为断言失败时的输出信息,actual为断言的值,matcher为断言的匹配器。

常用的匹配器整理:

匹配器 说明 例子
is 断言参数等于后面给出的匹配表达式 assertThat(5, is (5));
not 断言参数不等于后面给出的匹配表达式 assertThat(5, not(6));
equalTo 断言参数相等 assertThat(30, equalTo(30));
equalToIgnoringCase 断言字符串相等忽略大小写 assertThat(“Ab”, equalToIgnoringCase(“ab”));
containsString 断言字符串包含某字符串 assertThat(“abc”, containsString(“bc”));
startsWith 断言字符串以某字符串开始 assertThat(“abc”, startsWith(“a”));
endsWith 断言字符串以某字符串结束 assertThat(“abc”, endsWith(“c”));
nullValue 断言参数的值为null assertThat(null, nullValue());
notNullValue 断言参数的值不为null assertThat(“abc”, notNullValue());
greaterThan 断言参数大于 assertThat(4, greaterThan(3));
lessThan 断言参数小于 assertThat(4, lessThan(6));
greaterThanOrEqualTo 断言参数大于等于 assertThat(4, greaterThanOrEqualTo(3));
lessThanOrEqualTo 断言参数小于等于 assertThat(4, lessThanOrEqualTo(6));
closeTo 断言浮点型数在某一范围内 assertThat(4.0, closeTo(2.6, 4.3));
allOf 断言符合所有条件,相当于&& assertThat(4,allOf(greaterThan(3), lessThan(6)));
anyOf 断言符合某一条件,相当于或 assertThat(4,anyOf(greaterThan(9), lessThan(6)));
hasKey 断言Map集合含有此键 assertThat(map, hasKey(“key”));
hasValue 断言Map集合含有此值 assertThat(map, hasValue(value));
hasItem 断言迭代对象含有此元素 assertThat(list, hasItem(element));

下图为使用assertThat测试失败时所显示的具体错误信息。可以看到错误信息很详细!

这里写图片描述

当然了匹配器也是可以自定义的。这里我自定义一个字符串是否是手机号码的匹配器来演示一下。

只需要继承BaseMatcher抽象类,实现matchesdescribeTo方法。代码如下:

public class IsMobilePhoneMatcher extends BaseMatcher<String> {

    /**
     * 进行断言判定,返回true则断言成功,否则断言失败
     */

    @Override
    public boolean matches(Object item) {
        if (item == null) {
            return false;
        }

        Pattern pattern = Pattern.compile("(1|861)(3|5|7|8)\\d{9}$*");
        Matcher matcher = pattern.matcher((String) item);

        return matcher.find();
    }

    /**
     * 给期待断言成功的对象增加描述
     */
    @Override
    public void describeTo(Description description) {
        description.appendText("预计此字符串是手机号码!");
    }

    /**
     * 给断言失败的对象增加描述
     */
    @Override
    public void describeMismatch(Object item, Description description) {
        description.appendText(item.toString() + "不是手机号码!");
    }
}

执行单元测试如下:

正确的手机号码测试成功:

这里写图片描述

错误号码测试失败:

这里写图片描述

PS:计划开始写有关Android单元测试的内容,因为涉及的测试框架比较多,所以由简至难开始,最终达到日常开发实用的阶段。(没想到这篇前后就用了一整天。。。)我也尽量快速的更新这一系列。希望大家多多点赞支持给予我动力!

作者:qq_17766199 发表于2017/10/15 19:47:57 原文链接
阅读:276 评论:0 查看评论

微信企业号开发:企业支付成功后关闭交易页面问题

$
0
0

官方的demo有不少小问题导致支付成功后,依然留在支付页面,而且很奇怪,getBrandWCPayRequest方法的回调根本就不进去。


一直以为是自己的问题,后来在发现是demo的问题。

找到了文档解决方法

文章列出来了一下几点

1 除了上面说到的引用<script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>以外
登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”   “res.wx.qq.com”
(这一项我没有测试是不是必须)

2(必须)JsApiPayPage.aspx页面JS错误写法
if (typeof WeixinJSBridge == "undefined") 改成
if (typeof('WeixinJSBridge') == "undefined")

3(必须)还是JsApiPayPage.aspx页面Button的OnClientClick事件
OnClientClick="callpay()"改成
OnClientClick="javascript:callpay();return false;"


但我发现第一点不需要,也就是不需要添加js文件jweixin-1.0.0.js

另外在支付成功后添加一下代码就可以关闭交易页面

    WeixinJSBridge.invoke('closeWindow', {}, function (res) {
                            });


 function (res)
                    {                   
                        if (res.err_msg =="get_brand_wcpay_request:ok")
                        {
                          //  alert("支付成功err_code=" + res.err_code + ",err_desc=" + res.err_desc + ",err_msg=" + res.err_msg);
                            WeixinJSBridge.invoke('closeWindow', {}, function (res) {
                            });
                        }
                        else if (res.err_msg == "get_brand_wcpay_request:cancel") {
                          //  alert("用户取消err_code=" + res.err_code + ",err_desc=" + res.err_desc + ",err_msg=" + res.err_msg);
                        }
                        else {
                            alert("	支付失败err_code=" + res.err_code + ",err_desc=" + res.err_desc + ",err_msg=" + res.err_msg);
                        }                     
                     }




作者:xuexiaodong2009 发表于2017/10/16 10:02:57 原文链接
阅读:10 评论:0 查看评论

Android - Activity 启动过程

$
0
0

Android - Activity 启动过程

概述

Activity 是四大组件之一,在应用启动的时候,一般情况下首先启动的就是 Activity。 下面就来讨论下 Activity 是如何启动的?

本篇文章需要 Binder 进程间通讯的知识,不了解的请先看下 Binder 进程间通讯

启动流程

Activity 的整体启动流程如图所示:

这里写图片描述

下面是图中步骤的详细分析。

1. 发送 START_ACTIVITY_TRANSACTION 命令

Intent intent = new Intent(this, TestActivity.class);
startActivity(intent);

这段代码大家已经很熟悉,通过追踪代码可以发现最后调用了 startActivityForResult()。

  • startActivityForResult(intent, -1)

默认 requestCode = -1,也可通过调用 startActivityForResult() 传入 requestCode。 然后通过 MainThread 获取到 ApplicationThread 传入下面方法。

  • execStartActivity()

通过 ActivityManagerNative.getDefault() 获取到 ActivityManagerService 的代理为进程通讯作准备。

  • ActivityManagerProxy.startActivity()

调用代理对象的 startActivity() 方法,发送 START_ACTIVITY_TRANSACTION 命令。

2. 发送创建进程的请求

在 system_server 进程中的服务端 ActivityManagerService 收到 START_ACTIVITY_TRANSACTION 命令后进行处理,调用 startActivity() 方法。

  • ActivityManagerService.startActivity() -> startActivityAsUser(intent, requestCode, userId)

通过 UserHandle.getCallingUserId() 获取到 userId 并调用 startActivityAsUser() 方法。

  • ActivityStackSupervisor.startActivityMayWait() -> resolveActivity()

通过 intent 创建新的 intent 对象,即使之前 intent 被修改也不受影响。 然后调用 resolveActivity()。

然后通过层层调用获取到 ApplicationPackageManager 对象。

  • PackageManagerService.resolveIntent() -> queryIntentActivities()

获取 intent 所指向的 Activity 信息,并保存到 Intent 对象。

  • PackageManagerService.chooseBestActivity()

当存在多个满足条件的 Activity 则会弹框让用户来选择。

  • ActivityStackSupervisor.startActivityLocked()

获取到调用者的进程信息。 通过 Intent.FLAG_ACTIVITY_FORWARD_RESULT 判断是否需要进行 startActivityForResult 处理。 检查调用者是否有权限来调用指定的 Activity。 创建 ActivityRecord 对象,并检查是否运行 App 切换。

  • ActivityStackSupervisor.startActivityUncheckedLocked() -> startActivityLocked()

进行对 launchMode 的处理,创建 Task 等操作。 启动 Activity 所在进程,已存在则直接 onResume(),不存在则创建 Activity 并处理是否触发 onNewIntent()。

launchMode 可参考 Activity 启动模式

  • ActivityStack.resumeTopActivityInnerLocked()

找到 resume 状态的 Activity,执行 startPausingLocked() 暂停该 Activity,同时暂停所有处于后台栈的 Activity,找不到 resume 状态的 Activity 则回桌面。

如果需要启动的 Activity 进程已存在,直接设置 Activity 状态为 resumed。 调用下面方法。

  • ActivityStackSupervisor.startSpecificActivityLocked()

进程存在调用 realStartActivityLocked() 启动 Activity,进程不存在则调用下面方法。

3. fork 新进程

  • ActivityManagerService.startProcessLocked()

进程不存在请求 Zygote 创建新进程。 创建成功后切换到新进程。

切换至 App 进程

进入 app 进程后将 ActivityThread 类加载到新进程,并调用 ActivityThread.main() 方法

  • ActivityThread.main()

创建主线程的 Looper 对象,创建 ActivityThread 对象,ActivityThread.attach() 建立 Binder 通道,开启 Looper.loop() 消息循环。

  • ActivityThread.attach()

开启虚拟机各项功能,创建 ActivityManagerProxy 对象,调用基于 IActivityManager 接口的 Binder 通道 ActivityManagerProxy.attachApplication()。

  • ActivityManagerProxy.attachApplication()

发送 ATTACH_APPLICATION_TRANSACTION 命令

4. 发送 ATTACH_APPLICATION_TRANSACTION 命令

在 system_server 进程中的服务端 ActivityManagerService 收到 ATTACH_APPLICATION_TRANSACTION 命令后进行处理,调用 attachApplication()。

  • ActivityMangerService.attachApplication() -> attachApplicationLocked()

首先会获取到进程信息 ProcessRecord。 绑定死亡通知,移除进程启动超时消息。 获取到应用 ApplicationInfo 并绑定应用 IApplicationThread.bindApplication(appInfo)。

然后检查 App 所需组件。

Activity: 检查最顶层可见的 Activity 是否等待在该进程中运行,调用 ActivityStackSupervisor.attachApplicationLocked()。

Service:寻找所有需要在该进程中运行的服务,调用 ActiveServices.attachApplicationLocked()。

Broadcast:检查是否在这个进程中有下一个广播接收者,调用 sendPendingBroadcastsLocked()。

此处讨论 Activity 的启动过程,只讨论 ActivityStackSupervisor.attachApplicationLocked() 方法。

5. 调用 realStartActivityLocked()

  • ActivityStackSupervisor.attachApplicationLocked() -> realStartActivityLocked()

将该进程设置为前台进程 PROCESS_STATE_TOP,调用 ApplicationThreadProxy.scheduleLaunchActivity()。

  • ApplicationThreadProxy.scheduleLaunchActivity()

发送 SCHEDULE_LAUNCH_ACTIVITY_TRANSACTION 命令

6. 发送 SCHEDULE_LAUNCH_ACTIVITY_TRANSACTION 命令

发送送完 SCHEDULE_LAUNCH_ACTIVITY_TRANSACTION 命令,还会发送 BIND_APPLICATION_TRANSACTION 命令来创建 Application。

  • ApplicationThreadProxy.bindApplication()

发送 BIND_APPLICATION_TRANSACTION 命令

BIND_APPLICATION_TRANSACTION 命令处理

在 app 进程中,收到 BIND_APPLICATION_TRANSACTION 命令后调用 ActivityThread.bindApplication()。

  • ActivityThread.bindApplication()

缓存 Service,初始化 AppBindData,发送消息 H.BIND_APPLICATION。

  • ActivityThread.handleBindApplication()

设置进程名,获取 LoadedApk 对象,创建 ContextImpl 上下文,LoadedApk.makeApplication() 创建 Application 对象,调用 Application.onCreate() 回调方法。

SCHEDULE_LAUNCH_ACTIVITY_TRANSACTION 命令处理

app 进程中,收到 SCHEDULE_LAUNCH_ACTIVITY_TRANSACTION 命令后调用 ApplicationThread.scheduleLaunchActivity()。

  • ApplicationThread.scheduleLaunchActivity()

发送消息 H.LAUNCH_ACTIVITY。

7. 发送消息 H.LAUNCH_ACTIVITY

  • ActivityThread.handleLaunchActivity()

最终回调目标 Activity 的 onConfigurationChanged(),初始化 WindowManagerService。

  • ActivityThread.performLaunchActivity()

检查 Application 是否创建,最终回调目标 Activity 的 onCreate()。

8. 调用 handleResumeActivity()

  • ActivityThread.handleResumeActivity()

最终回调目标 Activity 的 onStart(),onResume()。

Activity 启动过程中涉及到的类

frameworks/base/services/core/java/com/android/server/am/
  - ActivityManagerService.java
  - ActivityStackSupervisor.java
  - ActivityStack.java
  - ActivityRecord.java
  - ProcessRecord.java

frameworks/base/core/java/android/app/
  - IActivityManager.java
  - ActivityManagerNative.java (内含 AMP)
  - ActivityManager.java

  - IApplicationThread.java
  - ApplicationThreadNative.java (内含 ATP)
  - ActivityThread.java (内含 ApplicationThread)

  - ContextImpl.java

总结

Activity 的启动流程已经分析完了,相对于 Binder 机制来说是不是很简单。 这里只是从大体逻辑分析了一遍 Activity 的启动过程,简单了解下 Activity 启动时都经历了什么,想要深入的了解实现细节还是需要看一下 Android 的源码。

参考资料

更多文章

https://github.com/jeanboydev/Android-ReadTheFuckingSourceCode

作者:freekiteyu 发表于2017/10/16 10:04:47 原文链接
阅读:42 评论:0 查看评论

Android RxJava操作符详解系列: 创建操作符

$
0
0

前言

  • Rxjava,由于其基于事件流的链式调用、逻辑简洁 & 使用简单的特点,深受各大 Android开发者的欢迎。

Github截图

如果还不了解 RxJava,请看文章:Android:这是一篇 清晰 & 易懂的Rxjava 入门教程


  • RxJava如此受欢迎的原因,在于其提供了丰富 & 功能强大的操作符,几乎能完成所有的功能需求
  • 今天,我将为大家详细介绍RxJava操作符中最常用的创建操作符,并附带 Retrofit 结合 RxJava的实例Demo教学,希望你们会喜欢。

  1. 本系列文章主要基于 Rxjava 2.0
  2. 接下来的时间,我将持续推出 AndroidRxjava 2.0 的一系列文章,包括原理、操作符、应用场景、背压等等 ,有兴趣可以继续关注Carson_Ho的安卓开发笔记!!

  3. 示意图

目录

示意图


1. 作用

创建 被观察者( Observable) 对象 & 发送事件。


2. 类型

  • 创建操作符包括如下:

示意图

  • 下面,我将对每个操作符进行详细介绍

3. 应用场景 & 对应操作符 介绍

注:在使用RxJava 2操作符前,记得在项目的Gradle中添加依赖:

dependencies {
      compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
      compile 'io.reactivex.rxjava2:rxjava:2.0.7'
      // 注:RxJava2 与 RxJava1 不能共存,即依赖不能同时存在
}

3.1 基本创建

  • 需求场景
    完整的创建被观察者对象

  • 对应操作符类型

create()

  • 作用
    完整创建1个被观察者对象(Observable

    RxJava 中创建被观察者对象最基本的操作符

  • 具体使用

/ **
   * 1. 通过creat()创建被观察者 Observable 对象
   */ 
        Observable<Integer> observable = Observable.create(new ObservableOnSubscribe<Integer>() {
          // 传入参数: OnSubscribe 对象
          // 当 Observable 被订阅时,OnSubscribe 的 call() 方法会自动被调用,即事件序列就会依照设定依次被触发
          // 即观察者会依次调用对应事件的复写方法从而响应事件
          // 从而实现由被观察者向观察者的事件传递 & 被观察者调用了观察者的回调方法 ,即观察者模式
/ **
   * 2. 在复写的subscribe()里定义需要发送的事件
   */ 
            @Override
            public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
                // 通过 ObservableEmitter类对象 产生 & 发送事件
                // ObservableEmitter类介绍
                    // a. 定义:事件发射器
                    // b. 作用:定义需要发送的事件 & 向观察者发送事件
                   // 注:建议发送事件前检查观察者的isUnsubscribed状态,以便在没有观察者时,让Observable停止发射数据
                    if (!observer.isUnsubscribed()) {
                           emitter.onNext(1);
                           emitter.onNext(2);
                           emitter.onNext(3);
                }
                emitter.onComplete();
            }
        });

// 至此,一个完整的被观察者对象(Observable)就创建完毕了。

在具体使用时,一般采用 链式调用 来创建

        // 1. 通过creat()创建被观察者对象
        Observable.create(new ObservableOnSubscribe<Integer>() {

            // 2. 在复写的subscribe()里定义需要发送的事件
            @Override
            public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {

                    emitter.onNext(1);
                    emitter.onNext(2);
                    emitter.onNext(3);

                emitter.onComplete();
            }  // 至此,一个被观察者对象(Observable)就创建完毕
        }).subscribe(new Observer<Integer>() {
            // 以下步骤仅为展示一个完整demo,可以忽略
            // 3. 通过通过订阅(subscribe)连接观察者和被观察者
            // 4. 创建观察者 & 定义响应事件的行为
            @Override
            public void onSubscribe(Disposable d) {
                Log.d(TAG, "开始采用subscribe连接");
            }
            // 默认最先调用复写的 onSubscribe()

            @Override
            public void onNext(Integer value) {
                Log.d(TAG, "接收到了事件"+ value  );
            }

            @Override
            public void onError(Throwable e) {
                Log.d(TAG, "对Error事件作出响应");
            }

            @Override
            public void onComplete() {
                Log.d(TAG, "对Complete事件作出响应");
            }

        });
    }
  • 测试结果

示意图

3.2 快速创建 & 发送事件

  • 需求场景
    快速的创建被观察者对象

  • 对应操作符类型

just()

  • 作用

    1. 快速创建1个被观察者对象(Observable
    2. 发送事件的特点:直接发送 传入的事件

    注:最多只能发送10个参数

  • 应用场景
    快速创建 被观察者对象(Observable) & 发送10个以下事件

  • 具体使用

        // 1. 创建时传入整型1、2、3、4
        // 在创建后就会发送这些对象,相当于执行了onNext(1)、onNext(2)、onNext(3)、onNext(4)
        Observable.just(1, 2, 3,4)   
            // 至此,一个Observable对象创建完毕,以下步骤仅为展示一个完整demo,可以忽略
            // 2. 通过通过订阅(subscribe)连接观察者和被观察者
            // 3. 创建观察者 & 定义响应事件的行为
         .subscribe(new Observer<Integer>() {

            @Override
            public void onSubscribe(Disposable d) {
                Log.d(TAG, "开始采用subscribe连接");
            }
            // 默认最先调用复写的 onSubscribe()

            @Override
            public void onNext(Integer value) {
                Log.d(TAG, "接收到了事件"+ value  );
            }

            @Override
            public void onError(Throwable e) {
                Log.d(TAG, "对Error事件作出响应");
            }

            @Override
            public void onComplete() {
                Log.d(TAG, "对Complete事件作出响应");
            }

        });
    }
  • 测试结果

示意图

fromArray()

  • 作用

    1. 快速创建1个被观察者对象(Observable
    2. 发送事件的特点:直接发送 传入的数组数据

    会将数组中的数据转换为Observable对象

  • 应用场景

    1. 快速创建 被观察者对象(Observable) & 发送10个以上事件(数组形式)
    2. 数组元素遍历
  • 具体使用

      // 1. 设置需要传入的数组
     Integer[] items = { 0, 1, 2, 3, 4 };

        // 2. 创建被观察者对象(Observable)时传入数组
        // 在创建后就会将该数组转换成Observable & 发送该对象中的所有数据
        Observable.fromArray(items) 
        .subscribe(new Observer<Integer>() {
            @Override
            public void onSubscribe(Disposable d) {
                Log.d(TAG, "开始采用subscribe连接");
            }

            @Override
            public void onNext(Integer value) {
                Log.d(TAG, "接收到了事件"+ value  );
            }

            @Override
            public void onError(Throwable e) {
                Log.d(TAG, "对Error事件作出响应");
            }

            @Override
            public void onComplete() {
                Log.d(TAG, "对Complete事件作出响应");
            }

        });
    }

// 注:
// 可发送10个以上参数
// 若直接传递一个list集合进去,否则会直接把list当做一个数据元素发送

/*
  * 数组遍历
  **/
        // 1. 设置需要传入的数组
        Integer[] items = { 0, 1, 2, 3, 4 };

        // 2. 创建被观察者对象(Observable)时传入数组
        // 在创建后就会将该数组转换成Observable & 发送该对象中的所有数据
        Observable.fromArray(items)
                .subscribe(new Observer<Integer>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.d(TAG, "数组遍历");
                    }

                    @Override
                    public void onNext(Integer value) {
                        Log.d(TAG, "数组中的元素 = "+ value  );
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "对Error事件作出响应");
                    }

                    @Override
                    public void onComplete() {
                        Log.d(TAG, "遍历结束");
                    }

                });
  • 测试结果

发送事件

数组遍历

fromIterable()

  • 作用

    1. 快速创建1个被观察者对象(Observable
    2. 发送事件的特点:直接发送 传入的集合List数据

    会将数组中的数据转换为Observable对象

  • 应用场景

    1. 快速创建 被观察者对象(Observable) & 发送10个以上事件(集合形式)
    2. 集合元素遍历
  • 具体使用

/*
 * 快速发送集合
 **/
// 1. 设置一个集合
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);

// 2. 通过fromIterable()将集合中的对象 / 数据发送出去
        Observable.fromIterable(list)
                .subscribe(new Observer<Integer>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.d(TAG, "开始采用subscribe连接");
                    }

                    @Override
                    public void onNext(Integer value) {
                        Log.d(TAG, "接收到了事件"+ value  );
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "对Error事件作出响应");
                    }

                    @Override
                    public void onComplete() {
                        Log.d(TAG, "对Complete事件作出响应");
                    }
                });


/*
 * 集合遍历
 **/
        // 1. 设置一个集合
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);

        // 2. 通过fromIterable()将集合中的对象 / 数据发送出去
        Observable.fromIterable(list)
                .subscribe(new Observer<Integer>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.d(TAG, "集合遍历");
                    }

                    @Override
                    public void onNext(Integer value) {
                        Log.d(TAG, "集合中的数据元素 = "+ value  );
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "对Error事件作出响应");
                    }

                    @Override
                    public void onComplete() {
                        Log.d(TAG, "遍历结束");
                    }
                });
  • 测试结果

发送集合

集合遍历

额外

// 下列方法一般用于测试使用

<-- empty()  -->
// 该方法创建的被观察者对象发送事件的特点:仅发送Complete事件,直接通知完成
Observable observable1=Observable.empty(); 
// 即观察者接收后会直接调用onCompleted()

<-- error()  -->
// 该方法创建的被观察者对象发送事件的特点:仅发送Error事件,直接通知异常
// 可自定义异常
Observable observable2=Observable.error(new RuntimeException())
// 即观察者接收后会直接调用onError()

<-- never()  -->
// 该方法创建的被观察者对象发送事件的特点:不发送任何事件
Observable observable3=Observable.never();
// 即观察者接收后什么都不调用

3.3 延迟创建

  • 需求场景
    1. 定时操作:在经过了x秒后,需要自动执行y操作
    2. 周期性操作:每隔x秒后,需要自动执行y操作

defer()

  • 作用
    直到有观察者(Observer )订阅时,才动态创建被观察者对象(Observable) & 发送事件

    1. 通过 Observable工厂方法创建被观察者对象(Observable
    2. 每次订阅后,都会得到一个刚创建的最新的Observable对象,这可以确保Observable对象里的数据是最新的
  • 应用场景
    动态创建被观察者对象(Observable) & 获取最新的Observable对象数据

  • 具体使用

       <-- 1.1次对i赋值 ->>
        Integer i = 10;

        // 2. 通过defer 定义被观察者对象
        // 注:此时被观察者对象还没创建
        Observable<Integer> observable = Observable.defer(new Callable<ObservableSource<? extends Integer>>() {
            @Override
            public ObservableSource<? extends Integer> call() throws Exception {
                return Observable.just(i);
            }
        });

        <-- 2.2次对i赋值 ->>
        i = 15;

        <-- 3. 观察者开始订阅 ->>
        // 注:此时,才会调用defer()创建被观察者对象(Observable)
        observable.subscribe(new Observer<Integer>() {

            @Override
            public void onSubscribe(Disposable d) {
                Log.d(TAG, "开始采用subscribe连接");
            }

            @Override
            public void onNext(Integer value) {
                Log.d(TAG, "接收到的整数是"+ value  );
            }

            @Override
            public void onError(Throwable e) {
                Log.d(TAG, "对Error事件作出响应");
            }

            @Override
            public void onComplete() {
                Log.d(TAG, "对Complete事件作出响应");
            }
        });
  • 测试结果

因为是在订阅时才创建,所以i值会取第2次的赋值
示意图

timer()

  • 作用

    1. 快速创建1个被观察者对象(Observable
    2. 发送事件的特点:延迟指定时间后,发送1个数值0(Long类型)

    本质 = 延迟指定时间后,调用一次 onNext(0)

  • 应用场景
    延迟指定事件,发送一个0,一般用于检测

  • 具体使用

        // 该例子 = 延迟2s后,发送一个long类型数值
        Observable.timer(2, TimeUnit.SECONDS) 
                  .subscribe(new Observer<Long>() {
            @Override
            public void onSubscribe(Disposable d) {
                Log.d(TAG, "开始采用subscribe连接");
            }

            @Override
            public void onNext(Long value) {
                Log.d(TAG, "接收到了事件"+ value  );
            }

            @Override
            public void onError(Throwable e) {
                Log.d(TAG, "对Error事件作出响应");
            }

            @Override
            public void onComplete() {
                Log.d(TAG, "对Complete事件作出响应");
            }

        });

// 注:timer操作符默认运行在一个新线程上
// 也可自定义线程调度器(第3个参数):timer(long,TimeUnit,Scheduler) 
  • 测试结果

示意图

interval()

  • 作用

    1. 快速创建1个被观察者对象(Observable
    2. 发送事件的特点:每隔指定时间 就发送 事件

    发送的事件序列 = 从0开始、无限递增1的的整数序列

  • 具体使用

       // 参数说明:
        // 参数1 = 第1次延迟时间;
        // 参数2 = 间隔时间数字;
        // 参数3 = 时间单位;
        Observable.interval(3,1,TimeUnit.SECONDS)
                // 该例子发送的事件序列特点:延迟3s后发送事件,每隔1秒产生1个数字(从0开始递增1,无限个)
                .subscribe(new Observer<Long>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.d(TAG, "开始采用subscribe连接");
                    }
                    // 默认最先调用复写的 onSubscribe()

                    @Override
                    public void onNext(Long value) {
                        Log.d(TAG, "接收到了事件"+ value  );
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "对Error事件作出响应");
                    }

                    @Override
                    public void onComplete() {
                        Log.d(TAG, "对Complete事件作出响应");
                    }

                });

// 注:interval默认在computation调度器上执行
// 也可自定义指定线程调度器(第3个参数):interval(long,TimeUnit,Scheduler)
  • 测试结果

示意图

intervalRange()

  • 作用

    1. 快速创建1个被观察者对象(Observable
    2. 发送事件的特点:每隔指定时间 就发送 事件,可指定发送的数据的数量

    a. 发送的事件序列 = 从0开始、无限递增1的的整数序列
    b. 作用类似于interval(),但可指定发送的数据的数量

  • 具体使用

// 参数说明:
        // 参数1 = 事件序列起始点;
        // 参数2 = 事件数量;
        // 参数3 = 第1次事件延迟发送时间;
        // 参数4 = 间隔时间数字;
        // 参数5 = 时间单位
        Observable.intervalRange(3,10,2, 1, TimeUnit.SECONDS)
                // 该例子发送的事件序列特点:
                // 1. 从3开始,一共发送10个事件;
                // 2. 第1次延迟2s发送,之后每隔2秒产生1个数字(从0开始递增1,无限个)
                .subscribe(new Observer<Long>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.d(TAG, "开始采用subscribe连接");
                    }
                    // 默认最先调用复写的 onSubscribe()

                    @Override
                    public void onNext(Long value) {
                        Log.d(TAG, "接收到了事件"+ value  );
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "对Error事件作出响应");
                    }

                    @Override
                    public void onComplete() {
                        Log.d(TAG, "对Complete事件作出响应");
                    }

                });
  • 测试结果

示意图

range()

  • 作用

    1. 快速创建1个被观察者对象(Observable
    2. 发送事件的特点:连续发送 1个事件序列,可指定范围

    a. 发送的事件序列 = 从0开始、无限递增1的的整数序列
    b. 作用类似于intervalRange(),但区别在于:无延迟发送事件

  • 具体使用


// 参数说明:
        // 参数1 = 事件序列起始点;
        // 参数2 = 事件数量;
        // 注:若设置为负数,则会抛出异常
        Observable.range(3,10)
                // 该例子发送的事件序列特点:从3开始发送,每次发送事件递增1,一共发送10个事件
                .subscribe(new Observer<Integer>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.d(TAG, "开始采用subscribe连接");
                    }
                    // 默认最先调用复写的 onSubscribe()

                    @Override
                    public void onNext(Integer value) {
                        Log.d(TAG, "接收到了事件"+ value  );
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "对Error事件作出响应");
                    }

                    @Override
                    public void onComplete() {
                        Log.d(TAG, "对Complete事件作出响应");
                    }

                });
  • 测试结果

示意图

rangeLong()

  • 作用:类似于range(),区别在于该方法支持数据类型 = Long
  • 具体使用
    range()类似,此处不作过多描述

至此,关于 RxJava2中的创建操作符讲解完毕。


4. 实际开发需求案例


5. Demo地址

上述所有的Demo源代码都存放在:Carson_Ho的Github地址:RxJava2_创建操作符


6. 总结

  • 下面,我将用1张图总结 RxJava2 中常用的创建操作符

示意图

  • 接下来的时间,我将持续推出 AndroidRxjava 2.0 的一系列文章,包括原理、操作符、应用场景、背压等等 ,有兴趣可以继续关注Carson_Ho的安卓开发笔记!!

示意图


请 帮顶 / 评论点赞!因为你的鼓励是我写作的最大动力!

作者:carson_ho 发表于2017/10/16 10:01:59 原文链接
阅读:547 评论:2 查看评论

kotlin学习笔记——枚举、封闭类

$
0
0

Kotlin学习笔记系列:http://blog.csdn.net/column/details/16696.html


1、枚举

kotlin中提供类枚举的实现,与java相同。枚举可以带参数,如
enum Icon(val res: Int){
     MENU(R.drawable.menu),
     BACK(R.drawable.back)
}

val backIcon = Icon.BACK.res
枚举可以通过字符串匹配名字来获取,也可以得到所以枚举的array,如
val back: Icon = Icon.valueOf("BACK")
val icons: Array<Icon> = Icon.values()
枚举也提供一些函数来获取名字和声明的位置
val name: String = Icon.BACK.name()
val position: Int = Icon.BACK.ordinal()
枚举根据顺序实现了Comparable接口,所以可以很方便的进行排序

2、封闭类sealed

用sealed修饰的类叫封闭类,它类似枚举enum,如:
sealed class Icon{
     class Menu(val id: Int) : Icon()
     class Back(val url: String) : Icon()
     object None : Icon()
}
可以看到它有固定个数的子类,而且子类与父类一起被定义出来。与enum不同的是,枚举的实例是唯一的,而封闭类可以有多个实例,它可以有不同的状态。
封闭类可以与when配合使用,如:
val result: String = when(icon){
     is Menu -> "xxx" + icon.id
     is Back -> icon.url
     is None -> ""
}


3、异常Excaption

kotlin中的异常与java中相似,但是kotlin中的异常都是未经检查,表示不会强迫在任何地方使用try/catch。
(kotlin中有throw关键字,但是没有throws关键字)
throw与try/catch的使用与java中一样,但是在kotlin中它们都是表达式都可以返回值。具体见kotlin学习笔记——过程控制和Range表达式


总结:

到本文章为止,kotlin学习笔记系列就结束了,这个系列主要是整理了学习kotlin过程中的一些知识点。但是kotlin学习笔记系列的专栏还会继续,会不定时的分享一些在使用kotlin开发过程中遇到的问题和新的知识,谢谢!


作者:chzphoenix 发表于2017/10/16 16:08:38 原文链接
阅读:81 评论:0 查看评论

410c安卓分区

$
0
0

首先是一个存放bootloader的分区。
Android分区还有这些:

/boot

zImage格式内核和ramdisk格式根文件系统、设备树文件

/system

安卓ROM

/userdata

用户数据,包含:联系人、短信、设置、安装的apk程序

/recovery

另一个启动分区,利用这个可以实现otg增量升级

/persist

系统的一个ext4分区,包含DRM相关文件、传感器注册表、wifi、蓝牙、mac地址

/cache

缓存分区

作者:qq_33160790 发表于2017/10/16 17:29:23 原文链接
阅读:65 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>