Android开发实用技巧Android/Java学习日记Android开发

带你开发一款给Apk中自动注入代码工具icodetools(完善

2016-12-07  本文已影响619人  JiangWei_App

一、前言
在前面已经介绍完了 自动给apk中注入日志代码工具icodetools原理了,在那里我们曾经说过其实离真正的可使用价值有点距离,本篇就对这个工具进行一些优化,让其真正意义上开始能工作量产。当时在前面一篇文章中说到遗留的三个主要问题:
第一个问题:****对每个类中都添加一个静态打印方法堆栈信息的方法,这样会导致有些应用的dex过大,方法数超了问题
第二个问题:****在从输入一个apk到给每个类中的每个方法添加日志代码然后在签名输出最终的apk,这个过程其实很多步,但是我们之前都是手动的去进行操作,非常麻烦,所以这里还得解决一键化问题
第三个问题:****在实际演练中会发现一些大型的app调用的方法特别多,导致咋们打印的日志信息过多霸屏,很难定位到我们真正想要的那个方法,而且打印日志调用次数过多会导致应用出现无响应状态。所以这里得做一个开关和过滤规则
只有解决了这三个问题咋们才算是真正意义上的自动化工具,可以经历各种app的考验。

二、方法数超了问题修复
我们可以使用在前一篇讲解原理的文章中的步骤来进行,用一个比较庞大的企业应用做案例,结果在使用dx将添加日志代码之后的jar文件转化成dex文件的时候出现报错了:


看到这个我们就猜到了,我们添加日志代码之后的jar中方法数超过了,原因其实很简单,因为我们之前的添加日志操作是在每个类中添加一个静态打印日志的方法,那么如果对于一个dex文件中有很多类,那么就添加了很多相同的打印方法,方法数超了是有可能的。那么如何解决这个问题呢?其实方案有两个:
第一个方案:因为方法数超了,每个类都被添加了一个打印堆栈日志的方法,咋们可以不用这个方法,把这个方法的代码直接拷贝到原来类中的每个方法前。但是这样会带来一个问题,如果一个类中的方法很多,那么就会增加非常多的同样代码。最后使用dx转化的时候会发现也是报错的。所以该方案不可行。
第二个方案:因为方法数超了,所以那个打印堆栈的方法肯定不能要了,但是又不能把代码都塞到每个方法的前面,那么正常的编码习惯是可以把这个打印方法抽取出来放到一个工具类中。从这里可以看到这个方案是靠谱的。一个应用中只有这么一个工具类,而且这个工具类包含了打印堆栈信息的方法,那么总体来看方法数是没多大变化的,只是多了一个工具类。

有了方案,咋们就得实现了,但是这个实现还是有点曲折的,因为从方案2来看,我们需要给dex中添加一个工具类了,但是我们在前一篇文章中了解到,可以通过ClassVisitor类操作dex中的每个类信息,通过MethodVisitor类操作dex中每个类的每个方法,但是没有途径可以添加一个类的。所以咋们得另想办法了。
我们现在能够往dex中塞入一个类有两个方案:
第一个方案:非常清楚dex文件格式之后,可以去手动的添加类信息到dex中。但是这个方案我只是敢想一想,实践的话我就算了,很简单因为我怕麻烦!
第二个方案:可以利用jar工具,在我们把利用dex2jar把dex添加代码变成jar文件之后,我们可以把jar文件解压,然后再把我们需要插入的类放到这个解压目录下,最后再用jar命令生成jar文件。最终在使用dx命令生成dex文件。这个方案有点复杂,但是靠谱好操作呀。现在看着有点复杂,但是下面会详细介绍一个一键化工具,到时候都不用你来操作,何谈复杂了。但是可惜的是,这个方案有一个缺点,就是解压过程中,Windows平台是不区分文件名的大小写的,但是如果原来jar中的包名中有两个类名是大小写的,那么解压到本地的时候会出错的。比如一个包里面有A.class和a.class文件,解压到本地前者会被后者覆盖,而且这个方案有点繁琐了。
第三个方案:可以直接把编译之后的classes文件塞到jar文件中。有了这个方案,实现比较简单了。

注意:
1、这里我们自己定义的类文件一定要注意,首先这个类的包名一定要具有自己的唯一性,千万不可与原来jar中的类重名了,要想做到完全唯一是不可能了。但是我们可以弄一个奇葩的包名和类名就可以了。这里我用的名称是:
cn.wjdiankong.jw.utils.JWUtils这个名称了。现阶段应该不会有重复。


代码比较简单,咋们直接来看看即可:



这里直接借助ZipEntry类进行添加一个文件到jar文件中即可,但是在添加的时候一定要注意ZipEntry的名称必须是类的全路径名称,我们是从工具命令外部传入获取到的类名:


下面咋们我们在每个类的每个方法之前调用这么一行代码即可:JWUtils.printStackTrace("jw");,而这段代码对应的asm代码为:


上面操作完成之后,就可以运行一下程序了,前提是你得先准备一个需要插入的类JWUtils


首先默认情况下我们必须得准备一个cn.wjdiankong.jw.utils.JWUtils类,而且类中有一个打印堆栈的方法:
public static void printStackTrace(String tag){....},然后编译获取到JWUtils.class文件放到指定工具根目录下即可。当成功的把JWUtils.class文件塞入到jar文件中之后就可以直接使用dx命令进行转化成dex了,这里有可能会遇到这个错误:

这个错误原因是因为我用JDK1.8编译了JWUtils.Java文件,而dx工具不兼容这个JDK版本,所以可以使用1.8以下的版本编译JWUtils.java文件即可。
成功之后,在把dex文件放到apk中,在重新签名之后的apk,可以使用jadx打开查看:

看到了,在apk中我们已经成功的把我们的JWUtils类插入进去了,然后我们在随便打开一个其他类看看:

在类的每个方法之前也成功调用了JWUtils.printStatckTrace方法进行打印日志信息了。
那么到这里其实我们就解决了第一个问题了,而且从这里可以看到以后如果我们想自己在打印什么消息可以自己实现JWUtils类,然后实现printStatckTrace方法即可,但是需要注意的是类名和包名以及方法的签名都必须一致,不然会报错的。
******三、一键化功能完善问题**
解决了第一个问题之后,咋们就可以来解决第二个问题了,第二个问题其实是为了工具更好的能够被使用。因为我们在前面操作可以看到从输入一个源apk到最终输出一个添加日志代码的apk有很多步骤,但是这些操作非常繁琐。所以这里我们就需要把每一步做到代码化,让使用该工具的人无感知。所以一键化主要从下面几步进行完善:
第一、解压apk文件获取其所有的dex文件

这个代码简单不解释了,但是这里需要注意的是,不要解压apk中所有的文件,那样没必要也很浪费时间,这里只需要解压dex文件和签名目录,因为签名目录在下一步需要用到:

第二、删除原始签名文件
这一步是为了后面签名工作准备,如果这里不删除签名文件的话,后面再进行二次签名会发现有的apk有冲突


看到了吧,这里如果不删除原始签名,重签名之后的apk会有两个签名文件,本来Android中是允许有多个签名文件的,但是这些签名文件信息必须保持一致也就是需要用同样的私钥进行签名,但是现在明显不是,所以在安装apk会报错的。从这里就可以看到咋们为了后面重签名方便,就在这里把原始签名文件删除即可。

这里为了方便就直接借助aapt命令进行删除apk中的签名文件了,命令很简单:
**aapt remove src.apk META-INF/XXX.RSA META-INF/XXX.SF MANIFEST.MF **
因为aapt不支持直接删除目录操作,所以这里需要借助第一步中的解压META-INF目录,得到应用的所有签名文件,然后在这里组装签名文件名即可。

第三、添加日志到dex文件中


这个依然用我们之前介绍的改造之后的dex2jar的接口,把dex转化成jar文件,并且在每个类每个方法中添加日志代码,这里需要注意的是有的apk中可能有多个dex文件,所以需要处理第一步中获取到解压之后的所有dex文件。

第四、编译JWUtils类文件


这里我们为了更好的后续拓展,所以只需要外部工具提供JWUtils.java文件,内部我们自动使用javac将其编译成JWUtils.class文件了,
注意:
1、这里使用javac命令编译的时候,指定了类文件格式是UTF-8的,所以如果想自己定义JWUtils类的话,需要注意这个文件的格式为UTF-8,不然在编译的时候可能会报错。
2、因为JWUtils.java中用到了Android的系统api,所以编译的时候需要携带系统的android.jar文件一起进行编译工作。
第五、添加JWUtils类文件到源jar文件中

这一步主要是我们把上面编译好的JWUtils.class文件直接塞入到jar文件中即可。

第六、将jar文件转化成dex文件


这里需要注意因为有的应用可能包含多个dex文件,所以咋们在给dex添加JWUtils类的时候,只需要在主dex中添加即可,不可重复添加。然后就借助dx.jar这个工具进行转化了。这个jar包在AndroidSDK目录下有。构造好dx命令之后直接调用main方法即可。

第七、添加dex文件到源apk中


这里需要把我们添加代码之后的dex文件插入到源apk中,依然借助aapt命令即可,因为aapt命令不支持文件覆盖功能,所以咋们得先删除原始的dex文件,然后在添加新的dex文件。

第八、重新签名apk


这一步就简单了,直接使用jarsigner工具命令进行apk签名即可。签名之后的文件为signed.apk。这里的签名文件信息写死了,不支持外部传入的功能。

到这里咋们就把所有的步骤用代码进行整合了,然后咋们需要把这个工具导出一个可执行的jar文件,但是我们还得想想为了让这个工具更好的拓展,我们可以让外部传入一些参数做成动态化功能,这里接受了外部传入的5个参数信息:


这里接受的5个参数如下:
1》工具运行的当前目录
2》需要处理的源apk文件路径
3》aapt命令的路径
4》打印信息的tag,默认是:jw
5》打印信息的类名,默认是:cn.wjdiankong.jw.utils.JWUtils
6》打印信息的方法名:默认是:printStackTrace
然后咋们还需要判断aapt命令路径是否有效,以及是否配置了JAVA_HOME环境变量,因为后面几步都会依赖aapt工具和java的一些工具。所以这两个内容是必要的。
有了这些接受参数,我们在外部就好扩展了,比如我们可以自己实现打印消息的类,类名和包名随意定义,也可以随意定义打印日志的方法名,但是方法的签名不可变也就是必须是这种格式:public static void XXX(String tag)。其实这个签名也是可以修改的,但是我觉得没那个必要了。有这些拓展应该足够用了。后续看用户反馈可以在进行详细修改。有了这个可执行jar包,咋们就可以简单的写一个批处理icodetools.bat:
cd %~dp0set aapt_path=D:\Android_tools\AndroidSdk\build-tools\23.0.1\aapt.exejava -jar libs\icode_tools.jar %~dp0 src.apk %aapt_path% jw cn.wjdiankong.jw.utils.JWUtils printStackTraceadb install -r signed.apkpause..
首先进入当前目录,然后设置aapt_path环境变量,接着就要调用咋们导出来的icode_tools.jar包了,看到这里输入了上面对应的6个参数信息。这里可以修改的,比如原始apk路径,aapt路径,打印消息的tag等。最后为了方便直接在一键化安装apk。当然这一步可能会失败,不过失败了可以自己想办法安装即可,签名之后的apk是signed.apk,未签名的apk是unsigned.apk,可以自己签名的。
然后就是需要额外的jar包,因为在编译类文件的时候需要引用到系统api,所以这里要用到android.jar文件,放在当前目录的libs下面,同时icode_tools.jar也是在这个目录下,最后就是还需要一个打印消息的工具类JWUtils.java了,所以最终咋们工具的目录是这样的结构:

这里的签名文件信息在工具中写死了,所以这里不支持修改,如果想自己重签名可以使用工具运行完之后在当前目录下有一个unsigned.apk文件进行操作即可。
有了这个工具,咋们肯定想迫不及待的尝试一下,现在我们只需要双击icodetools_1.0.bat批处理即可坐等结果了:

看到了,所有步骤一气呵成,多么智能一键化,再也不用那么费劲了。运行完命令之后的目录有了签名和未签名的apk文件如下:

后续还可以自己操作这两个文件。想怎么玩就怎么玩。

四、案例实践
工具现在已经有了,但是我们上面都是用了简单的案例apk进行操作的,这个明显不符合现实,我们为了检验此工具的牛逼性,必须用一些大型app来做实验。因为真的勇士敢于面对淋漓的鲜血!下面就用一个现在很火的直播软件来进行操作,有人说为何不用微信,别着急微信后面再用。


看到了,这里因为应用包含了多个dex,而且每个dex文件较大,所以在处理的过程中会比较耗时的,需要慢慢的等待
等操作结束之后,咋们直接运行应用,输出log信息:

日志刷刷的,太辣眼睛了完全把握晃晕了,所以这个就是我们这次需要解决的第三个问题了。如何让这些日志信息不霸屏,在指定地方打印我们想要的结果。

五、添加日志打印过滤规则
从上面可以看到我们完成了所有的一键化操作,但是可惜的是被那些日志霸屏了,完全懵逼的状态。所以这里我还得解决一个问题,就是给这个日志加一些过滤规则,能够很好的控制日志,让他受我控制。这个问题其实和上面的工具没多大关系了,因为在前面我们知道,那个打印方法已经被弄出来放到了JWUtils这个类中,而这个类是工具需要编译然后插入到dex中的,所以咋们就可以直接修改JWUtils中的打印日志的方法即可。


下面就是我写的JWUtils类,内部已经有了一些打印信息的过滤规则,主要包括:控制日志的总开关,需要打印日志的方法名,返回类型,参数类型,类名等规则。这个规则是一个字符串内容:
-s 1 -m JW -r int -p [int,java.lang.String] -c JWUtils
-s:表示日志总开关
-m:表示需要打印日志的方法名称
-r:表示方法返回的类型
-p:表示方法的参数类型,多个类型直接用分号隔开
-c:表示需要添加日志的类名
然后我把这行字符串内容保存到/data/local/tmp/log.txt文件中,为什么要保存到这里呢?有的同学想保存到SD卡中,但是假如有的应用没有声明SD读写权限,那怎么办?我们最终的JWUtils类是被插入到应用中的。所以就想到了系统有一个不需要权限有没有沙盒权限限制的目录:/data/local/tmp/

下面简要的说一下这个过滤规则吧:
首先我们可以在一个方法中获取当前方法的堆栈信息,所以就可以获取到当前方法名和类名了:
StackTraceElement[] stackElements = new Throwable().getStackTrace();
因为我们想要获取到被插入打印代码的方法信息,所以这里只要获取数组的第二个元素即可,第一个元素其实是JWUtils.printStackTrace方法信息了。有了这个元素之后直接调用它的两个方法就可以获取到当前方法名称和类名称了,这个也就可以做到方法名和类型的过滤规则了。
而对于上面只能简单的获取到指定的方法名和类型,却获取不到对应方法的签名信息,比如参数类型,返回值类型等。所以这里得费点事,就是需要通过上面获取到的类名然后用Class.forName方法获取到对应类的Class对象,然后在获取该类所有的方法信息:


然后用一个全局数组进行保存,在结合上面的方法名去遍历这个数组,就可以获取到指定方法名对应的方法信息了:

这样就可以做到方法的返回值类型,参数类型的过滤规则了,但是有同学会发现这里有一个问题,假如一个类中有重构方法,也就是方法名相同的但是方法签名不一样,这里因为只是通过检测方法名来获取到method的信息的。所以对于同一个类中重构方法是没有过滤效果的。
最后就是一个日志总开关了,所以最终的过滤规则如下:

有了这个规则之后,咋们再次操作一下,把这个JWUtils类放到icode_tools目录中,然后再次跑一下icode_tools批处理,安装即可,这时候我们先设置一个过滤规则,直接使用命令就进行操作了:

我开始的时候把日志关闭,然后在打开,在关闭看一下效果,我们使用echo命令写入一条规则关闭日志:

看到了吧,这里我们通过总开关可以控制日志输出了,下面再来一条实际的过滤规则,就是通过包名,方法名,返回类型等规则操作一下:-s 1 -m a -c com.meelive.ingkee.v1.core.a.a

这里我们通过方法名和类型进行过滤限制,打印之后的结果都是指定类名的方法日志了。
上面操作的是某直播软件,为了有说服力,我得用微信在尝试一下哈:

看到了,此刻我们加上规则之后,打开微信顿时觉得爽多了,日志不多,慢慢操作查看具体方法,从微信的日志看,这个版本已经开始使用热修复框架Tinker了。后面得赶快出一篇分析Tinker框架的文章了。
最后在来看一下QQ的日志:

注意:此过滤规则可以自己定义的,因为所有的打印消息逻辑都在JWUtils类中,上面说到这个类是开放出来的,也就是可以自己随便定义这个类的信息!

六、问题总结
好了,到这里我们已经解决了在前一篇文章中遇到的三个问题,也是填完了工具的坑,下面来总结一下这三个问题:
第一个问题:方法数超了问题,因为我们给每个类都添加了一个打印堆栈信息的方法,所以如果一个dex中包含很多类的话,那么就会额外增加很多方法,在使用dx工具进行转化的时候出现了方法数超的问题。
解决方案:可以把打印堆栈信息的方法抽取到一个工具类中,然后把这个类插入到dex中,这里采用的方案是通过dex2jar工具转化dex成jar文件,然后在编译需要插入的类文件,把编译之后的类文件直接塞入到jar文件中,最后在原先的dex基础上每个类中的每个方法只需要调用JWUtils.printStackTrace方法即可。

第二个问题:一键化完善工作,这个是因为我们在前面文章中操作的时候发现从抽离apk中的dex文件到最终重签名apk文件有很多步骤,但是都是人工操作的,非常繁琐,所以可以把这些步骤进行整合一步到位。
解决方案:先从apk中解压出dex文件和签名文件==》利用aapt命令删除apk中的签名文件==》添加代码到dex中==》编译工具类得到class文件,塞入到jar文件中==》使用dx命令转化成dex文件==》使用aapt命令覆盖apk中旧的dex文件==》使用jarsigner对apk重新签名。
然后把工程导出一个可执行的jar包,这里为了后续扩展,就提供了执行的入口参数:

1》工具运行的当前目录
2》需要处理的源apk文件路径
3》aapt命令的路径
4》打印信息的tag,默认是:jw
5》打印信息的类名,默认是:cn.wjdiankong.jw.utils.JWUtils
6》打印信息的方法名:默认是:printStackTrace
然后我们还需要提供一个批处理icodetools.bat文件,主要执行的命令是:
java -jar libs\icode_tools.jar %~dp0 src.apk %aapt_path% jw cn.wjdiankong.jw.utils.JWUtils printStackTrace

第三个问题:优化打印日志信息规则,这个是因为当我们使用一键化工具生成apk安装运行之后发现打印日志太多导致霸屏,而且应用本身还会出现了ANR问题。所以得想个办法控制打印日志输出。
解决方案:加一些日志输出过滤规则,首先咋们得有一个日志总开关,然后是可以指定需要打印方法所属的类名,方法名,以及方法的返回类型,参数类型等过滤规则。
需要注意的是,这个过滤规则都是工具使用者可以自己实现的,就在JWUtils类中,代码可以自行修改,如果不想修改,默认的我已经加了这些规则:

-s 1 -m JW -r int -p [int,java.lang.String] -c JWUtils
-s:表示日志总开关
-m:表示需要打印日志的方法名称
-r:表示方法返回的类型
-p:表示方法的参数类型,多个类型直接用分号隔开
-c:表示需要添加日志的类名
我们可以通过echo命令给/data/local/tmp/log.txt文件输入规则来控制日志输出。

七、增长经验值

本文中我们可以学习到一些知识点和经验值:
1、了解到一种往dex文件中插入一个类文件的方法
先把dex转成jar文件,然后解压jar文件,复制对应的类文件到解压目录下,然后在使用jar命令进行class文件重新打包成jar文件即可。最后在使用dx命令转成dex文件。
2、了解到如何在一个方法中得到该方法的签名信息。
通过堆栈信息获取到该方法的名称和所在的类名,然后在使用Class.forName方法通过类名得到类对象,然后在使用反射获取到该类对应的所有方法签名信息,然后通过之前的方法名进行检索获取到对应方法的签名信息。
3、javac,jarsiginer,aapt命令的常见用法。

八、工具使用说明
1、当前目录的需要操作的apk文件名称默认是src.apk文件,如果想修改apk名称,可以手动的修改icodetools.bat中的apk文件名
2、在icodetools.bat中可以指定当前日志的tag,默认值是jw
3、当前目录下还有一个JWUtils.java这个java文件,这个类中有一些打印方法,可以根据自己的需求定义一些方法,但是定义的方法必须有要求: 1》必须是static类型 2》方法只允许有一个参数是String类型的,而这个参数就是打印的日志tag 3》方法名称可以随意指定,但是必须在icodetools.bat中保持一致 所以最终的方法模板为: public static XXX YYY(String tag) 这个类的名称可以变动,但必须和icodetools.bat中保持一致
4、当前目录下的libs目录中是工具依赖的jar包,不可以随便修改
5、当前目录下的JWUtils.java文件名和包名都不可变动
6、cyy_game.keystore签名文件名不可进行修改
7、如果想自己再次签名,可以使用unsigned.apk文件操作,signed.apk是使用了cyy_game.keystore文件签名
8、在icodetools.bat中需要手动设置aapt命令的路径
9、工具运行前必须配置JAVA_HOME环境变量
10、现阶段只支持JDK1.7以及以下版本编译器,不支持1.8以及以上的

注意:工具目录下有两个脚本,一个是icodetools_1.0.bat,一个是icodetools_2.0.bat,这两个工具主要是因为为了兼容更多的apk,默认最好先采用icodetools_1.0.bat工具进行尝试,失败了可以在使用icodetools_2.0.bat工具,如果都失败了那就要反馈问题给我了!

九、工具使用常见问题
第一个问题:Uncaught translation error: com.android.dx.cf.code.SimException


这样的错误是因为工具版本不兼容问题,可以尝试使用另外一个版本操作。如果icodetools_1.0.bat操作有问题具使用icodetools_2.0.bat工具操作,如果icodetools_2.0.bat工具操作有问题就是用icodetools_1.0.bat工具进行操作。如果两个工具都有问题那就是真的有问题了,记得给我反馈!

第二个问题:拷贝文件失败:java.io.FileNotFoundException: JWUtils.class


这个错误主要是因为工具内部会采用javac编译JWUtils类文件,这里应该是编译失败了,大部分原因是因为JWUtils这个类文件的编码格式和语法格式导致的,所以解决版本,可以自己使用javac命令进行编译看看具体是哪里编译出了问题。

第三个问题:bad class file magic (cafebabe) or version (0034.0000)


这个问题是因为javac这个命令是1.7以上版本的,也就是JDK版本。但是此工具现在只支持JDK1.8版本以下的。所以这里需要设置JDK版本。

第四个问题:成功注入代码安装之后发现无日志信息
这个问题可能有两个原因:第一个是需要检查日志的tag是否正确,主要通过查看icodetools.bat文件中的执行命令,第二个原因是因为默认情况下开始日志开关是关闭的,所以我们还得手动打开,首先得去/data/local/tmp目录下,然后使用echo "-s 1" >log.txt,打开日志即可。
当然还有其他问题,所以我希望大家在使用的过程中遇到问题以及一些优化建议都可以提给我,我会尽快修复!

工具下载地址:https://github.com/fourbrother/icodetools

声明:有人认为有了这个工具迫不及待的手痒想立马下载尝试,但是我想说这还没有结束,因为后面一篇文章才是重点,任何一个工具都需要发挥其作用才是个好工具,所以下一篇文章就会带大家用这个工具来破解一些app!
后期优化:现阶段此工具支持Windows系统,后续会增加Mac和Linux系统,现阶段只支持apk根目录下的dex文件,不支持其他目录下的dex文件处理,所以对于有些apk此工具处理过程中会出现错误!

十、总结
解决了这三个问题之后,咋们的工具才算是比较完整的能够用于生产的工具了,但是因为是本人业余时间编写的,所以我相信这个工具肯定还有一些漏洞,以及需要优化改善的地方,所以我先将此工具的第一版本给出,非常欢迎大家一起使用,如果在使用的过程中发现有一些问题,一定要记得给我留言,我会立即修复和改善,我相信一个好的工具是靠大家一起贡献的。问题反馈可以在我微信私聊我或者是在微信公众号留言都是可以的,我会第一时间回复,先拜谢各位使用者了。写了这篇文章之后并没有结束,因为后面还有一篇文章会详细介绍这个工具的实际使用,如何用它来解决我们的实际问题,比如寻找hook点。文章算是写完了,精力却全没了,感觉自己身体被掏空了一样,感谢大家多多点赞,要是有打赏就在好不过了!

更多内容:点击这里

关注微信公众号,最新技术干货实时推送


扫一扫加小编微信添加时注明:“编码美丽”否则不予通过!

上一篇下一篇

猜你喜欢

热点阅读