android日记(十一)
上一篇:android日记(十)
1.Math.abs()一定返回正数吗?
- int型范围 -2^31 ~ 2^31 - 1 ,也就是 -2147483648 ~ 2147483647。
- 通常来说一个负int整数,经过Math.abs()后,会得到相应的正整数。
- 但是对于-2147483648就比较特殊,因为在int范围内,不存在2147483648的正数。当最小负数加绝对值后,已经超过了最大的正数。
- 实际上,Math.abs(-2147483648) = -2147483648;
2.Java8的Optional用法
- Optional不能避免空指针问题,但是可以对可能存在的Null值问题做到一种提示
- 使用Optional可以让判空变得优雅,使用if判空
if (user != null) { Address address = user.getAddress(); if (address != null) { Country country = address.getCountry(); if (country != null) { String isocode = country.getIsocode(); if (isocode != null) { isocode = isocode.toUpperCase(); } } } }
使用Optional简化
String nullName = null; String name = Optional.ofNullable(nullName).orElse("default_name");
3.java反编译
- 某个.java想要反编译,命令行目录切到这个文件
- 执行javac xx.java,将java文件编译成class文件
- 执行javap -c xx.class,对class文件进行反编译
4.使用logcat抓取本地和筛选日志
- 使用logcat命令,导出手机里的本地日志,输出日志文件“log.txt”
adb logcat -s >log.txt
- 使用-t筛选,导出手指定时间以后的日志
adb logcat -t '8-23 09:00:00.000' > log.txt
- 使用grep进行关键字筛选,导出筛选后的日志
adb logcat -t '8-23 12:00:00.000' | grep System.err > log.txt
- 对已存在的日志文件,使用cat工具抓取前10行
sed -n '1,10p' log.txt
- 对已存在的日志文件,截取指定时间断的日志,并输出为新日志文件newlog.txt
cat log.txt | sed -n '/08-23 09:09:09.133/,/08-23 09:29:09.104/p' >newlog.txt
- 直接通过logcat导出手机里指定时间段的日志
adb logcat | grep -E '08-23 19:50|08-23 19:52' > log.txt
5.关于Intent Redirection的安全风险问题
- 问题背景:google play上架核审rejected:Intent Redirection(com.sina.weibo.sdk.share.ShareTransActivity.onNewIntent)
- 根据google help center中的说法,对于外部组件(android:exported=true),intent重定向存在数据安全和漏洞隐患。
数据泄露:通过setResult(data),恶意窃取组件中的数据 执行漏洞:通过startActivity(intent),打开其他组件
- 可能导致intent重定向的场景包括: startActivity、startService、sendBroadcast 或 setResult。
- 修复方案一:将外部组件调整为专用组件,即android:exported=false。
- 修复方案二:在intent重定向前,先进行包名校验,只有为受信的包名时,才允许发起intent重定向操作。
// 检查源 Activity 是否来自可信软件包 if (getCallingActivity().getPackageName().equals(“known”)) { Intent intent = getIntent(); // 提取嵌套的 Intent Intent forward = (Intent) intent.getParcelableExtra(“key”); // 重定向嵌套的 Intent startActivity(forward); }
6.通过合并manifest操作重写library中manifest属性
- app总是免不了集成第三方页面,有时候集成的library中的manifest属性不符合需要,那有什么办法重写libarary呢?
- 以前文中的Intent Redirection(com.sina.weibo.sdk.share.ShareTransActivity.onNewIntent)漏洞为例,由于weiboSdk中ShareTransActivity组件,在manifest中声明为了外部组件:android:exported=true
<activity android:name="com.sina.weibo.sdk.share.ShareTransActivity" android:configChanges="orientation|screenSize|keyboardHidden" android:exported="true" android:launchMode="singleTask" android:theme="@android:style/Theme.Translucent.NoTitleBar" tools:replace="configChanges" > <intent-filter> <action android:name="com.sina.weibo.sdk.action.ACTION_SDK_REQ_ACTIVITY" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
- 一种修复方案是将manifest中android:exported=true调整为android:exported=false,这时候就可以通过manifest合并的方式进行调整。
- 首先外部manifest(高层)中再次声明该组件,在打包时,会将高层的manifest和低层的manifest进行合并,并且android提供了一系列不同功能的合并操作符。
- 默认的操作符号为tools:node='merge',默认合并行为如下:
- tools:remove操作符,用于在合并时移除低层的某个不需要的属性,tools:remove='android:exported',高层mainfest中重声明ShareTransActivity组件,
<activity android:name="com.sina.weibo.sdk.share.ShareTransActivity" android:configChanges="orientation|screenSize|keyboardHidden" android:launchMode="singleTask" android:theme="@android:style/Theme.Translucent.NoTitleBar" tools:remove="android:exported" //合并时,移除android:expotrted属性 tools:replace="configChanges"> <intent-filter> <action android:name="com.sina.weibo.sdk.action.ACTION_SDK_REQ_ACTIVITY" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
- tools:replace操作符,用于在合并时修改低层的某个属性值为高层的值,tools:replace='android:exported'
<activity android:name="com.sina.weibo.sdk.share.ShareTransActivity" android:exported="false" tools:replace="android:exported" //合并时,修改android:expotrted属性
/>
7.查看主线程卡顿日志
- android使用消息机制更新UI,主线程Looper的loop()方法会不断从MessageQueue取出message并执行
- 如果主线程发生了卡顿,说明出现了两种可能的情况,1)loop()执行某个message时间过长;2)有大量无关的message被提交到MessgaeQueue
- 可以通过Printer输出loop()方法的执行日志,分析导致卡顿的原因
- 看下Looper的源码,提供了一个供外部自定义设置Printer的入口
private Printer mLogging; public void setMessageLogging(@Nullable Printer printer) { mLogging = printer; }
并且,loop()执行过程会调用printer输出msg开始执行与执行结束的日志
public static void loop() { final Looper me = myLooper(); final MessageQueue queue = me.mQueue; for (; ; ) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } // 开始执行msg日志 final Printer logging = me.mLogging; if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } ... try { msg.target.dispatchMessage(msg); if (observer != null) { observer.messageDispatched(token, msg); } dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; } catch (Exception exception) { if (observer != null) { observer.dispatchingThrewException(token, msg, exception); } throw exception; } finally { ThreadLocalWorkSource.restore(origWorkSource); if (traceTag != 0) { Trace.traceEnd(traceTag); } } // msg执行完成的日志 if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } ... } }
- 在MainApplication中向mainLooper的注册Printer接口,并实现println()方法,打印出日志即可
@Override public void onCreate() { super.onCreate(); Looper.getMainLooper().setMessageLogging(new Printer() { @Override public void println(String x) { Log.d(TAG, x); } }); }
8.利用Looper检测主线程卡顿
- 在外部(通常是MainApplication)设置好主线程的Looper里的Printer监听,loop()方法中各msg执行开始与结束都会进行回调
- 开始执行与执行结束回调log有明显的格式,开始执行 >>>>> Dispatching to,结束执行 >>>>> Finished to
//开始执行 >>>>> Dispatching to
if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); }
//结束执行 <<<<< Finished to if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); }
- println()的自定义实现里,可以统计一个msg从开始到结束进行的时长,当超过阈值时,认为卡顿发生
Looper.getMainLooper().setMessageLogging(new Printer() { private static final String START = ">>>>> Dispatching"; private static final String END = "<<<<< Finished"; @Override public void println(String x) { if (x.startsWith(START)) { //从这里开启一个定时任务来打印方法的堆栈信息 } if (x.startsWith(END)) { //从这里取消定时任务 } } });
9.关于AIDL(Android Interface Define Language)
- 用于定义用于两个进程的通信接口,AS下新建AIDL文件,会自动在所选择目录的根层级创建aidl文件夹,并将新建的AIDL文件放置其中。
-
AIDL接口编写规则基本与java interface一样,只是多了一些输入输出修饰in/out。支持传输的数据类型包括:java基本数据类型、String、List、Map,也支持实现了Parcelabe的自定义对象,不过需要用parcelable进行声明和import完成路径导包(即便和aidl在同一个目录中)。
- 接下来,AS执行Build->make project,会自动生成AIDL的代理,可以在build/generated/aidl_source_output_dir/目录下查看生成的文件。
解读一下:生成了一个与AIDL接口同名的接口, 并继承自android.os.IInterface,其内部定义了一个叫Stub的静态内部类,并给定了AIDL接口中的方法。其核心在与Stub内部类,这个是直接提供给程序员做ipc使用的
-
Stub类集成自Binder,且实现了外部接口类,在其内部又定义了一个代理类Proxy,同样实现了外部接口类。
-
这时候看看程序员如何使用Stub类,在客户端进程中开启一个远程的Service(在另一个进程中),然后客户端要访问远程Service。
/** * desc: 通过AIDL自动实现IPC client * date: 2022/9/1 */ public class AIDLTestActivity extends AppCompatActivity { private String TAG = "AIDLDemo"; private IBookInterface binder; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { //客户端通过获取到的Stub对象,创建远程的代理对象Stub.Proxy。 binder = IBookInterface.Stub.asInterface(service); } @Override public void onServiceDisconnected(ComponentName name) { binder = null; } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_aidltest); findViewById(R.id.addBook).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Book book = new Book(); book.setName("Android学习日记"); book.setPrice(100); try { binder.addBook(book); } catch (RemoteException e) { e.printStackTrace(); } } }); findViewById(R.id.getBook).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { List<Book> list = binder.getBookList(); for (Book book : list) { Log.d(TAG, book.getName() + "这版本书值" + book.getPrice() + "块钱"); } } catch (RemoteException e) { e.printStackTrace(); } } }); binService(); } private void binService() { Intent intent = new Intent(this, BookService.class); intent.setAction("com.example.aidl"); bindService(intent, connection, Context.BIND_AUTO_CREATE); }
服务端代码:
<service android:name="com.example.aidl.BookService" android:enabled="true" android:exported="true" android:process=":aidl"> <intent-filter> <action android:name="com.example.aidl" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </service>
/** * 跨进程服务 */ public class BookService extends Service { private List<Book> list = new ArrayList<>(); @Override public void onCreate() { super.onCreate(); Book book = new Book(); book.setName("磊锅子"); book.setPrice(666); list.add(book); } private IBinder binder = new IBookInterface.Stub() { @Override public List<Book> getBookList() throws RemoteException { return list; } @Override public void addBook(Book book) throws RemoteException { list.add(book); } }; @Nullable @Override public IBinder onBind(Intent intent) { return binder;//服务端service onBind()方法返回的binder对象是就是Stub对象 } }
-
上面的示例中,两个地方与Stub进行了关联:1)服务端service onBind()方法返回的binder对象是就是Stub对象;2)客户端通过ServiceConnection回调获取到的Stub对象后,执行IBookInterface.Stub.asInterface(service),创建远程代理对象Stub.Proxy。
- 接下来梳理跨进程调用传递流程,客户端进程的调用,实际调用了Binder驱动,Binder驱动通过回调服务端的onTransact(int code, Parce data, Parcel reply, int flags),讲调用转到服务端,其中,code是调用方法的引用号。服务端在onTransact()方法中,根据传入的方法引用,调用对应的服务端中具体实现的方法。
10.关于Binder机制
- 上面AIDL的IPC过程,其底层实际是一个Binder机制
- Android基于Linux的,Linux已经提供了IPC机制包括:scoket、管道、共享内存、消息队列等,其原理如下图所示,各进程独立沙箱,而内核空间与各个沙箱之间可以访问。从而可以借助内核空间,让各沙箱进程互相访问。那为什么Android还要再设计Binder呢?
- Binder的安全性:传统IPC机制没有安全保护,无法获得对方进程的用户ID/进程ID(UID/PID),从而无法鉴别对方身份,直接用在android中,各个app的数据就会被随意访问出现安全问题。即便可以在数据包中填入UID,但是数据包可能被篡改造成不可靠。相比之下,Android内核会为每个安装好的App分配一个UID,内核在进行跨进程访问时,会通过来源进程的UID进行鉴别身份。
-
Binder的高性能:虽然共享内存不用数据拷贝,但是控制复杂、很难使用。相比Socket/管道/消息队列需要2次拷贝,Binder只用1次数据拷贝。
Socket/管道/消息队列:在内核空间开劈一个数据缓存区,数据发送进程将数据拷贝到内核空间数据缓存区,内核空间数据缓存区再将数据拷贝到数据接收进程。
Binder机制:在内核空间开辟一个内核缓存区和一个数据接收缓存区,两者之间形成映射关系,并且数据接收缓存区与数据接收进程也存在映射关系,数据从发送进程拷贝到内核缓存区 后,一路通过内存映射,直接传给了接收进程,只进行了一次数据拷贝。
- Linux内存映射(mmap):用户进程访问磁盘数据,如果通过read/write操作,数据会先拷贝到内核空间,再从内核空间拷贝到用户空间。使用mmap()可以得到用户进程的一个逻辑地址ptr,这样以后,进程无需再调用read/write读写磁盘,而是直接通过ptr操作文件,建立用户空间与磁盘的映射,文件直接从磁盘拷贝到了用户空间,只需要进行一次拷贝。
- Binder中的内存映射:将用户空间的一部分内存区域映射到内核空间,映射关系建立后,用户空间的改动,直接反应到内核空间上,反之,内核空间的改动也直接反应的用户空间。从而,数据只需要从发送进程拷贝到内核进程中,再借助内存映射,直接传给了接收进程。
- Linux动态内核可加载模块:Linux提供了动态内核可加载模块机制(Loadable Kenerl Module),允许外部向内核中动态加载可执行模块,运行时被内联到内核作为内核的一部分运行。
- Binder驱动:Binder由Android上层设计,本不存在Linux内核中,不过通过Linux的动态可加载模块机制,可以将Binder加载到Linux内核中。
- Binder通信模型:Client\Server\ServiceManager运行在用户空间,Binder驱动运行在内核空间,Binder驱动和ServiceManager由系统提供,Client和Service由开发人员自己实现。
其中,ServiceManager的作用是,将字符形式的Binder名字,转化成Client中对Binder的引用,使得Client中能够通过Binder名字获得Binder实体的引用。这非常类似于互联网中的服务器(Service)、客户端(Client)、DNS域名解析(ServiceManager)、路由器(Binder Driver)。
- ServiceManager与0号引用:Service向ServiceManager中注册了Binder以后,Client就可以通过Binder名字,获取Binder对象的引用。ServiceManager在一个进程当中,Service又在另一个进程当中,Service向ServiceManager中注册Binder也一定需要进程通信,也就是说实现进程通信又需要用到进程通信,那ServiceManager是如何解决的呢?ServiceManager作为Service端,其他进程都是它的Client端,它给它自己创建了一个Binder实体,并且这个Binder实体比较特殊,没有名字,不需要注册,引用固定是0。其他Client端进程,正是通过这个0号引用向ServiceManager中注册了自己的Binde实体。从而这个0号引用就相当于是DNS服务器的地址,其他进程访问带着Binder名字,查询0号引用,获得Binder实体的引用。