Easoll


  • 首页

  • 标签

  • 分类

  • 归档

Android代码清理插件-Delogger

发表于 2018-11-23

今天想和大家分享一个在编译时清理Log语句的插件。

问题描述

在平时的开发中我们经常使用安卓的Log方法来打印一些调试信息。由于有很多日志只是在开发时期有需要,所以我们一般通常都会自定义一个Logger,这个Logger只在我们的Debug包中输入日志信息,而在Release包中则不输出,大致的逻辑如下所示:

1
2
3
4
5
6
7
8
9
Logger.i(TAG, "name is:" + name + "and age is: " + age)
class Logger{
pulic static void i(String tag, String msg){
if(!isDebug){
Log.i(tag, msg);
}
}
}

这种模式可以确保在release保重是不会打印调试信息的,但是却仍然存在性能问题,因为在调用Logge的i方法之前要先计算出实参的值,也就是说会执行:

1
"name is:" + name + "and age is: " + age

如果以上Logger的使用发生在onDraw等调用非常频繁的方法中的话,则对性能的影响更为严重。

解决方案

其实有过c/c++开发的同学就会想到,用宏可以完美解决这个问题,但奈何java不支持宏定义。那我们有没有其他的方案来实现呢?笔者一开始想用javassist来实现,可是查了一圈资料之后发现javassist并没有符合我们需求的api。最终确定的方案是,在gradle插件运行java compile任务之前对java源码进行扫描,并删除以”Log.”和”Logger.”开头的行。大致逻辑如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void doClean(File source, String[] prefixList){
if(source.isFile()){
def outputFile = new File(source.parentFile, "tmp_${source.name}\$")
for (singleLine in source.readLines()) {
def trimmed = singleLine.trim()
if (!startWith(trimmed, prefixList)) {
outputFile.append(singleLine)
}
outputFile.append("\n") //keep the line number correct
}
outputFile.renameTo(source)
}else{
def children = source.listFiles()
for(file in children){
doClean(file, prefixList)
}
}
}

处理逻辑已经实现了,那么我们在什么时候执行删除操作呢?在安卓的编译过程中有一个compile${variantName}JavaWithJavac,我们可以在该方法之前执行:

1
2
3
4
5
//hook javac compile
Task compileJavaTask = project.tasks.getByName("compile${variantName}JavaWithJavac")
compileJavaTask.doFirst {
doClean()
}

通过以上的代码,我们则可以将源代码中的Logger语句移除了。但是,我们可以直接操作将源代码中的Logger语句移除吗?不行的,我们应该先将源代码复制到一个临时目录中,然后将这些临时目录中的源码中的Logger移除,最后将compile${variantName}JavaWithJavac 任务的输入目录改为这个临时目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//hook javac compile
Task compileJavaTask = project.tasks.getByName("compile${variantName}JavaWithJavac")
compileJavaTask.doFirst {
if(compileJavaTask instanceof AndroidJavaCompile){
def androidJavaCompile = (AndroidJavaCompile) compileJavaTask
def sourceField = SourceTask.class.getDeclaredField("source")
sourceField.setAccessible(true)
def source = (List<DefaultConfigurableFileTree>)sourceField.get(androidJavaCompile)
def iterator = source.iterator()
while(iterator.hasNext()){
def fileTree = iterator.next()
if(fileTree instanceof DefaultConfigurableFileTree) {
if (fileTree.dir.absolutePath.endsWith("src/main/java")) { //replace source java dir
def cleanDir = new File(cleanDirPath)
FileUtils.copyDirectory(fileTree.dir, cleanDir)
doClean(cleanDir, prefixList)
fileTree.from(cleanDir)
}
}
}
}
}

至此,我们便能真正实现我们最初的目的,移除java文件中的Logger了,大家可以尝试移除kotlin文件中的Logger。

总结

文中的源码可以参考Delogger。笔者希望本文中使用的预处理思路可以起到抛砖引玉的作用,大家可以头脑风暴一下这种预处理方式的其他使用场景。

记一次安卓Handler.removeMessages引发的问题

发表于 2018-06-23
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static final int MESSAGE_WHAT_SAY_HI = 0;
private static final int MESSAGE_WHAT_SAY_HELLO = 1;
private static class MyHandler extends Handler{
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MESSAGE_WHAT_SAY_HI:
Log.i(TAG, "hi");
break;
case MESSAGE_WHAT_SAY_HELLO:
Log.i(TAG, "hello");
break;
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Handler handler = new MyHandler();
long delayTime = 5000;
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "en....");
}
}, delayTime);
handler.sendEmptyMessageDelayed(MESSAGE_WHAT_SAY_HI, delayTime);
handler.sendEmptyMessageDelayed(MESSAGE_WHAT_SAY_HELLO, delayTime);
handler.removeMessages(MESSAGE_WHAT_SAY_HI);
}
}

以上代码输入为:

1
hello

而不是:

1
2
en....
hello

安卓7.0动态链接库加载流程

发表于 2018-06-23

Runtime.c

1
Runtime_nativeLoad()

OpenjdkJvm.cc

1
JVM_NativeLoad()

java_vm_ext.cc

1
LoadNativeLibrary()

native_loader.cpp

1
OpenNativeLibrary()

dlfcn.cpp

1
2
dlopen()
dlopen_ext()

linker.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
do_dlopen()
find_library()
find_libraries()
find_library_internal
load_library(android_namespace_t* ns,
LoadTask* task,
ZipArchiveCache* zip_archive_cache,
LoadTaskList* load_tasks,
int rtld_flags)
load_library(android_namespace_t* ns,
LoadTask* task,
LoadTaskList* load_tasks,
int rtld_flags,
const std::string& realpath)
LoadTask->read()

linker_phdr.cpp

1
ElfReader->Read()

参考:http://wps2015.org/drops/drops/Android%20Linker%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.html

提升进程优先级的一种新姿势

发表于 2018-05-19

在安卓中调用Service的startForeground方法可以将service所在进程的优先级提高,减小进程被回收的概率。
调用startForeground方法的时候系统会在通知栏显示一个通知,这对于传统的多媒体应用来说是没有问题的。
但是对于那些只想提升优先级而不想让用户感知的应用来说强行显示个通知栏看起来太怪异了。

查看startForeground的源码之后,发现有很重要的两步,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ActiveServices.java
private void setServiceForegroundInnerLocked(ServiceRecord r, int id,
Notification notification, int flags) {
if (id != 0) {
......
r.postNotification(); //step1: 在通知栏显示通知
if (r.app != null) {
updateServiceForegroundLocked(r.app, true); //step2: 更新进程优先级
}
getServiceMapLocked(r.userId).ensureNotStartingBackgroundLocked(r);
mAm.notifyPackageUse(r.serviceInfo.packageName,
PackageManager.NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE);
} else {
......
}
}

那么我们有没有办法能让updateServiceForegroundLocked执行成功,而让postNotification()执行失败呢?
进一步查看postNotification()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
ServiceRecord.java
public void postNotification() {
if (foregroundId != 0 && foregroundNoti != null) {
// Do asynchronous communication with notification manager to
// avoid deadlocks.
final String localPackageName = packageName;
final int localForegroundId = foregroundId;
final Notification _foregroundNoti = foregroundNoti;
ams.mHandler.post(new Runnable() {
public void run() {
NotificationManagerInternal nm = LocalServices.getService(
NotificationManagerInternal.class);
if (nm == null) {
return;
}
Notification localForegroundNoti = _foregroundNoti;
try {
......
//step1: 向NotificationManagerServervice发送通知
nm.enqueueNotification(localPackageName, localPackageName,
appUid, appPid, null, localForegroundId, localForegroundNoti,
userId);
} catch (RuntimeException e) {
Slog.w(TAG, "Error showing notification for service", e);
// If it gave us a garbage notification, it doesn't
// get to be foreground.
ams.setServiceForeground(name, ServiceRecord.this,
0, null, 0);
//step2:如果出现异常,则将应用进程crash掉
ams.crashApplication(appUid, appPid, localPackageName, -1,
"Bad notification for startForeground: " + e);
}
}
});
}
}

由上可知,只要enqueueNotification执行出现异常,通知栏则不会显示通知了,但是此时却会导致应用进程crash调用。
那么如何使得crashApplication这个方法失效呢,我们进一步查看crashApplication方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ActivityManagerService.java
public void crashApplication(int uid, int initialPid, String packageName, int userId,
String message) {
if (checkCallingPermission(android.Manifest.permission.FORCE_STOP_PACKAGES)
!= PackageManager.PERMISSION_GRANTED) {
String msg = "Permission Denial: crashApplication() from pid="
+ Binder.getCallingPid()
+ ", uid=" + Binder.getCallingUid()
+ " requires " + android.Manifest.permission.FORCE_STOP_PACKAGES;
Slog.w(TAG, msg);
throw new SecurityException(msg);
}
synchronized(this) {
//最终会通过binder,调用应用进程中的scheduleCrash方法
mAppErrors.scheduleAppCrashLocked(uid, initialPid, packageName, userId, message);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ActivityThread.java
private class ApplicationThread extends IApplicationThread.Stub {
public void scheduleCrash(String msg) {
//通过handler发送一个类型为H.SCHEDULE_CRASH的消息
sendMessage(H.SCHEDULE_CRASH, msg);
}
}
private class H extends Handler {
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
......
case SCHEDULE_CRASH:
//当收到类型为SCHEDULE_CRASH的消息的时候则抛出一个异常,导致进程crash
throw new RemoteServiceException((String)msg.obj);
......
}
}

我们现在已经知道了ams是如何让我们的进程crash的了,基本就是ams跟我们应用进程说,你准备准备该去死了,然后应用进程就去死了。
但是做为一个有个性的进程,能不能在ams让他去死的时候假装没听见呢?显然是可以的,基本的流程就是:

  1. 先拿到ActivityThread的实例
  2. 拿到ActivityThread$H 的实例mH
  3. 向mH设置一个Handler.Callback
  4. 在Handler.Callback 中检测到 SCHEDULE_CRASH消息时则消费该消息

具体实现代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private static void hookH(){
if(mHasHookH){
return;
}
mHasHookH = true;
try {
try {
Class hClass = Class.forName("android.app.ActivityThread$H");
Field scheduleCrashField = hClass.getDeclaredField("SCHEDULE_CRASH");
mScheduleCrashMsgWhat = (int)scheduleCrashField.get(null);
Log.i(TAG, "get mScheduleCrashMsgWhat success");
}catch (Exception e){
Log.i(TAG, "get mScheduleCrashMsgWhat failed");
e.printStackTrace();
}
Handler.Callback callback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
Log.i(TAG, msg.toString());
if(msg.what == mScheduleCrashMsgWhat){
return true;
}
return false;
}
};
Class activityThreadClass = Class.forName("android.app.ActivityThread");
Field mH = activityThreadClass.getDeclaredField("mH");
mH.setAccessible(true);
Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
Object activityThreadInstance = currentActivityThread.invoke(null);
Handler hInstance = (Handler) mH.get(activityThreadInstance);
Class handlerClass = Handler.class;
Field mCallbackField = handlerClass.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(hInstance, callback);
}catch (Exception e){
e.printStackTrace();
}
}

这里写了一个工具类方便大家使用:RaisePriorityHack

Media.insert插入的图片文件大小比原图小

发表于 2017-11-04 | 分类于 android
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final String insertImage(ContentResolver cr, Bitmap source,
String title, String description) {
ContentValues values = new ContentValues();
values.put(Images.Media.TITLE, title);
values.put(Images.Media.DESCRIPTION, description);
values.put(Images.Media.MIME_TYPE, "image/jpeg");
url = cr.insert(EXTERNAL_CONTENT_URI, values);
OutputStream imageOut = cr.openOutputStream(url);
source.compress(Bitmap.CompressFormat.JPEG, 50, imageOut);
}

okhttp中interceptors和networkInterceptors的区别

发表于 2017-11-04

在学习okhttp时发现在OkHttpClient内部有两个用来存储interceptor的链表,分别是interceptors和networkInterceptors,那么这两个列表中存储的interceptor有什么区别呢?

1
2
3
4
public class OkHttpClien{
final List<Interceptor> interceptors;
final List<Interceptor> networkInterceptors;
}

要想弄清楚这个问题,我们首先要知道Interceptor是什么并且okhttp的网络请求处理流程是怎样的。

Interceptor简介

Interceptor是一个接口,类图如下所示
标准模板

java 文档中对其的介绍是:

Observes, modifies, and potentially short-circuits requests going out and the corresponding responses coming back in. Typically interceptors add, remove, or transform headers on the request or response.

就是说Interceptor观察,修改并且可能短路发送的请求和相应的服务端返回的内容。一些特殊的Interceptor会添加,删除或者转换网络请求和响应的头部。

okhttp网络请求处理流程

okhttp网络请求的核心代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
final class RealCall implements Call {
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
}
public final class RealInterceptorChain implements Interceptor.Chain {
private final List<Interceptor> interceptors;
private final int index;
public Response proceed(Request request, StreamAllocation
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
return response;
}
}

我们看到,最后调用了RealInterceptorChain的procced方法来处理我们的请求。RealInterceptorChain类内部维护了一个index,用来记录传进来的request应该由哪个Interceptor来处理。在Interceptor的intercept方法中只要通过调用chain.process就可将请求传递给下一个Interceptor来处理。这种模式是一个典型的责任链模式。

总结

由于interceptors和networkInterces在责任链中的位置不同,他们能做的事情也就不同了。比如,如果我们要对服务端返回的数据做自定义缓存就应该在interceptors中处理,而如果我们想控制网络请求的创建连接等就应该在networkInterceptor中处理了

安卓踩坑记录

发表于 2017-11-04 | 分类于 android

windows下运行adb device无法查看到自己的设备

  1. 手机未开启开发者模式, 如何开启可参考:https://developer.android.com/studio/debug/dev-options.html

  2. 电脑未安装google USB驱动, 具体如何安装可参考:https://developer.android.com/studio/run/win-usb.html?hl=zh-cn

常见开发错误

  1. 写布局文件时错把View写成view(注意首字母大写),导致layoutinflater找不到相应的类
  2. 新建activity,service后忘记在manifest文件中添加相应的声明,导致组件无法启动.
  3. 开发fragment时,使用了系统的fragment和support包中的fragment manage
  4. 使用LinearLayout时忘记设置orientation属性
  5. webview中的图片有时能加载有时不能加载,可能是因为你对WebSettings设置了setBlockNetworkImage

安卓源码在线查看

发表于 2017-11-04 | 分类于 android

关于如何下载安卓源码网上有一大堆的教程,但是鉴于国内的情况,想要正常下好一个版本的安卓源码可以说是长路漫漫了。这里推荐一个安卓源码在线查看网站,该网站可查看所有版本安卓的源码,还可查找引用。实在是安卓开发人员必备
http://androidxref.com/

gradle常用命令

发表于 2017-10-25 | 分类于 gradle

查看包含的项目

gradle projects

查看项目包含哪些task

gralde 项目路径:tasks

查看项目依赖

gradle 项目路径:dependencies

查看安卓依赖

gradle 项目路径:androidDepenedncies

查看详细输出

使用 –debug 选项,该选项可以输出详细信息,比如下载外部依赖失败时可以加上该参数来查看具体是哪个依赖无法下载

以debug模式执行grale插件

./gradlew :app:assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true

adb常用命令

发表于 2017-10-25

查看cpu信息

adb shell cat /proc/cpuinfo

查看当前Activity的信息,及所属应用包名

adb shell dumpsys activity top | head -n 10

提取当前Activity所属应用的安装包

adb shell pm list packages -f 包名 //可以获取apk的路径
adb pull apk路径

查看应用的uid

adb shell dumpsys package 包名 | grep uid

查看应用包含的so文件

截屏

方法1:
adb shell
screencap -p /data/local/tmp/文件名
adb pull /data/local/tmp/文件名 .
方法2:
adb exec-out screencap -p > 文件名

查看安装了多少非系统应用

adb shell pm list packages -3 | wc -l

查看支持的abi

adb shell cat /system/build.prop | grep product

设置启动时等待调试的应用

adb shell am set-debug-app -w –persistent 包名
adb shell am clear-debug-app 包名

清除应用数据

adb shell pm clear 包名

查看崩溃日志

adb shell dumpsys dropbox –print

查看设备支持的编解码格式

adb pull /system/etc/media_codecs.xml

12

20 日志
5 分类
19 标签
© 2018
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.2