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重定向的场景包括: startActivitystartServicesendBroadcast 或 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实体的引用。

     

     

 

posted @ 2022-09-12 21:51  是个写代码的  阅读(138)  评论(0编辑  收藏  举报