代码拉取完成,页面将自动刷新
[{"title":"Android每日一问笔记-Parcelable 为什么效率高于 Serializable?","date":"2019-08-27T12:40:25.000Z","path":"post/14203/","text":"基于https://www.wanandroid.com每日一问的笔记,做一些整理,方便自己进行查看和记忆。 原文链接:https://www.wanandroid.com/wenda/show/9002 为什么Parcelable的效率比Serializable高? 可以从设计目的和实现原理两个方面分析 设计目的 Serializable是Java API,是一个通用的序列化机制,通过将文件保存到本地文件、网络流等实现便数据的传递,这种数据传递不仅可以在单个程序中进行,也可以在两个不同的程序中进行;Parcelable是Android SDK API,为了在同个程序的不同组件之间和不同程序(AIDL)之间高效的传输数据,是通过IBinder通信的消息的载体。从设计目的上可以看出Parcelable就是为了Android高效传输数据而生的。 实现原理 Serializable是通过I/O读写存储在磁盘上的,使用反射机制,序列化过程较慢,且在序列化过程中创建许多临时对象,容易触发GC。Parcelable是直接在内存中读写的,自已实现封送和解封(marshalled &unmarshalled)操作,将一个完整的对象分解成Intent所支持的数据类型,不需要使用反射,所以Parcelable具有效率高,内存开销小的优点。 Parcelable为了效率损失了什么 Serializable是通用的序列化机制的,将数据存储在磁盘,可以做到有限持久化保存,文件的生命周期不受程序影响,Parcelable的序列化操作完全由底层实现,不同版本的Android是实现方式可能不相同,所以不能进行持久化存储。 一个对象可以序列化的关键 序列化是将一个对象从存储态转化成传输态的过程,把对象转化成字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。 在序列化时,对象的各属性都必须是可序列化的,声明为static和transient类型的成员数据不能被序列化。 并非所有的对象都可以序列化,,至于为什么不可以,有很多原因了,比如: 安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现。","tags":[{"name":"每日一问笔记","slug":"每日一问笔记","permalink":"http://sorgs.cn/tags/每日一问笔记/"},{"name":"面试","slug":"面试","permalink":"http://sorgs.cn/tags/面试/"},{"name":"笔记","slug":"笔记","permalink":"http://sorgs.cn/tags/笔记/"}]},{"title":"Android为什么卡?关于Android编译","date":"2019-08-22T09:42:11.000Z","path":"post/34316/","text":"9102年了,还不知道Android为什么卡?笔记,原文地址:https://mp.weixin.qq.com/s/wyN6I4KTNWI-VE3KbjtgUQ 基础概念编译&解释 某些编程语言(如Java)的源代码通过编译-解释的流程可被计算机读懂 如 123public static void main(String[] args){ print('Hello World')} 写完这段代码并执行,电脑或手机就会打印出Hello World。 那么问题来了,英文是人类世界的语言,计算机(CPU)是怎么理解英文的呢? 众所周知,0和1是计算机世界的语言,可以说计算机只认识0和1。那么我们只需要把上面那段英文代码只通过0和1表达给计算机,就可以让计算机读懂并执行。 image 结合上图,Java源代码通过编译变成字节码,然后字节码按照模版中的规则解释为机器码。 机器码&字节码机器码 机器码就是能被CPU直接解读并执行的语言。 但是如果使用上图中生成的机器码跑在另外一台计算机中,很可能就会运行失败。 这是因为不同的计算机,能够解读的机器码可能不同。通俗而言就是能在A电脑上运行的机器码,放到B电脑上就可能就不好使了。 举个🌰,中国人A认识中文,英语;俄国人B认识俄语,英语。这时他两同时做一张中文试卷,B大概连写名字的地方都找不到。 所以这时候我们需要字节码。 字节码 中国人A看不懂俄文试卷,俄国人B看不懂中文试卷,但是大家都看得懂英文试卷。 字节码就是个中间码,Java能编译为字节码,同一份字节码能按照指定模版的规则解释为指定的机器码。 字节码的好处: 实现了跨平台,一份源代码只需要编译成一份字节码,然后根据不同的模版将字节码解释成当前计算机认识的机器码,这就是Java所说的“编译一次,到处运行”。 同一份源码被编译成的字节码大小远远小于机器码。 image 编译语言&解释语言编译语言 我们熟知的C/C++语言,是编译语言,即程序员编译之后可以一步到位(编译成机器码),可以被CPU直接解读并执行。 image 可能有人会问,既然上文中说过字节码有种种好处,为什么不使用字节码呢?\\ 这是因为每种编程语言设计的初衷不同,有些是为了跨平台而设计的,如Java,但有些是针对某个指定机器或某批指定型号的机器设计的。 举个🌰,苹果公司开发的OC语言和Swift语言,就是针对自家产品设计的,我才不管你其他人的产品呢。所以OC或Swift语言设计初衷之一就是快,可直接编译为机器码使iPhone或iPad解读并执行。这也是为什么苹果手机的应用比安卓手机应用大的主要原因。 编译-解释语言 拿开发Android的语言Java为例,Java是编译-解释语言,即程序员编译之后不可以直接编译为机器码,而是会编译成字节码(在Java程序中为.class文件,在Android程序中为.dex文件)。然后我们需要将字节码再解释成机器码,使之能被CPU解读。 这第二次解释,即从字节码解释成机器码的过程,是程序安装或运行后,在Java虚拟机中实现的。 造成Android卡顿的三大因素虚拟机——解释过程慢 通过上文描述,我们可以知道,iOS之所以不卡是因为他一步到位,省略了中间解释的步骤,直接跟硬件层进行通信。而Android由于没有一步到位,每次执行都需要实时解释成机器码,所以性能较iOS明显低下。 Andorid 1.0 Dalvik(DVM)+解释器 DVM是Google开发的Android平台虚拟机,可读取.dex的字节码。上文中所说的从字节码解释成机器码的过程在Java虚拟机中,在Android平台中虚拟机指的就是这个DVM。 在Android1.0时期,程序一边运行,DVM中的解释器(翻译机)一边解释字节码。可想而知,这样效率绝对低下。一个字,卡。 Android 2.2 DVM+JIT 其实解决DVM的问题思路很清楚,我们在程序某个功能运行前就解释就可以了。 在Android2.2时期,聪明的谷歌引入了JIT(Just In Time)机制,直译就是即时编译。 举个🌰,我经常去一家餐馆吃饭,老板已经知道我想吃什么菜了,在我到之前就把菜准备好了,这样我就省去了等菜的时间。 JIT就相当于这个聪明的老板,它会在手机打开APP时,将用户经常使用的功能记下来。当用户打开APP的时候立马将这些内容编译出来,这样当用户打开这些内容时,JIT已经将’菜’准备好了。这样就提高了整体效率。 虽然JIT挺聪明的,且总体思路清晰理想丰满,但现实是仍然卡的要死。 存在的问题: 打开APP的时候会变慢 每次打开APP都要重复劳动,不能一劳永逸。 如果我突然点了一盘之前从来没点过的菜,那我只好等菜了,所以如果用户打开了JIT没有准备好的’菜’,就只能等DVM中的解释器去边执行边解释了。 Android 5.0 ART+AOT 聪明的谷歌又想到个方法,既然我们能在打开APP的时候将字节码编译成机器码,那么我们何不在APP安装的时候就把字节码编译成机器码呢?这样每次打开APP也不用重复劳动了,一劳永逸。 这确实是个思路,于是谷歌推出了ART来替代DVM,ART全称Android Runtime,它在DVM的基础上做了一些优化,它在应用被安装的时候就将应用编译成机器码,这个过程称为AOT(Ahead-Of-Time),即预编译。 但是问题又来了,打开APP是不卡了,但是安装APP慢的要死,可能有人会说,一个APP又不是会频繁安装,可以牺牲下这点时间。但是不好意思,安卓手机每次OTA启动(即系统版本更新或刷机后)都会重新安装所有APP! Android 7.0 混合编译 谷歌最终祭出了终极大招,DVM+JIT不好,ART+AOT又不好。把他们都混合起来! 于是谷歌在Android7.0的时候,发布了混合编译。即安装时先不编译成机器码,在手机不被使用的时候,AOT偷偷的把能编译成机器码的那部分代码编译了(至于什么是能编译的部分,下文字节码的编译模板详述)。其实就是把之前APP安装时候干的活偷偷的在手机空的时候干了。 如果来不及编译的话,再把JIT和解释器这对难兄难弟叫起来,让他们去编译或实时解释。 Android 8.0 改进解释器 在Android8.0时期,谷歌又盯上了解释器,其实纵观上面的问题,根源就是这个解释器解释的太慢了!那我们何不让这个解释器解释的快一点呢?于是谷歌改进了解释器,解释模式执行效率大大提升。 Android 9.0 改进编译模板 简单而言就是,在Android9.0上提供了预先放置热点代码的方式,应用在安装的时候就能知道常用代码会被提前编译。 JNI——Java和C互相调用慢 JNI又称为 Java Native Interface,翻译过来就是Java原生接口,就是用来跟C/C++代码交互的。 如果不做Android开发的可能不知道,Android项目里的代码除了Java,很有可能还有部分C语言的代码。 这个时候有个严重的问题,首先上图 (图片参考方舟编译器原理PPT): image 在开发阶段Java源代码在开发阶段打包成.dex文件,C语言直接就是.so库,因为C语言本身就是编译语言。 在用户手机中,APK中的.dex文件(字节码)会被解释为.oat文件(机器码)运行在ART虚拟机中,.so库则为计算机可以直接运行的二进制代码(机器码),两份机器码要互相调用肯定是有开销的。 下面就来阐述下为什么两份机器码会不同。 这边需要深入理解字节码->机器码的编译过程,在图上虽然都被编译成了机器码,都能被硬件直接调用,但是两份机器码的性能,效率,实现方式相差甚多,这主要是由以下两个点造成的: 编程语言不同导致编译出的字节码不同导致编译出的机器码不同。 举个🌰,针对同样是静态语言的C和Java,对int a + b 的运算.C语言可以直接加载内存,在寄存器中计算,这是由于C语言是静态语言,a和b是确定的int对象。 在Java中虽然定义对象我们也要明确的指出对象的类型,例如int a = 0,但是Java拥有动态性,Java拥有反射,代理,谁也不敢保证a在被调用时还是int类型,所以Java的编译需要考虑上下文关系,即具体情况具体编译。 所以连字节码已经不同了,编译出的机器码肯定不同。 运行环境不同导致编译出的机器码不同 图中明显看到由Java编译而来的机器码包裹在ART中,ART全称Android RunTime,即安卓运行环境,跟虚拟机差不多是一个意思。而C语言所在的运行环境不在ART中。 RunTime提供了基本的输入输出或是内存管理等支持,如果要在两个不同的RunTime中互相调用,则必然有额外开销。 举个🌰,由于Java有GC(垃圾回收机制),在Java中的一个对象地址不是固定的,有可能被GC挪动了。即在ART环境中跑的机器码中的对象的地址不固定。可是C语言哪管那么多幺蛾子,C就直接问Java要一个对象的地址,但万一这个对象地址被挪动了,那就完蛋了。解决方案有两个:(此处参考知乎@张铎在华为公布的方舟编译器到底对安卓软件生态会有多大影响?中的回答)https://www.zhihu.com/question/319688949 把这个对象在C里再拷一份。很明显这造成了很大的开销。 告诉ART,我要用这个对象了,GC这个对象的地址你不能动!你先一边呆着去。这样相对而言开销倒是小了,但如果这个地址如果一直不能被回收的话,可能造成OOM。 字节码的编译模板——未针对具体APP进行优化 我们举个🌰来理解编译模版,“Hello world”可以被翻译为“你好,世界”,同样也可以被翻译为“世界,你好”,这个差别就是编译模版不同导致的, 统一的编译模版(vm模版) 字节码可以通过不同的编译模版被编译为机器码,而编译模版的不同将直接导致编译完后的机器码性能大相径庭。 image 在安卓中,ART有一套规定的,统一的编译模版,暂且称为VM模版,这套模版虽算不上差劲,但也算不上优秀。 因为它是谷歌爸爸搞出来的,肯定算不上差劲,但由于没有针对每一个APP进行特定的优化,所以也算不上优秀。 vm模版存在的问题 问题就存在于没有针对每一个APP进行优化。 在上文谷歌对于Android2.2的虚拟机优化中已经讲到过,那时候谷歌使用JIT将用户常用的功能记下来(热点代码),当用户打开APP的时候立马将这些内容编译出来,即优先编译热点代码。 但是到了Android7.0的混合编译时代,由于AOT的存在,这个功能被弱化了,这时JIT记录下的热点代码并非是持久化的。AOT的编译优先级遵循于vm模版,AOT根据模板的内容将一些字节码优先编译为机器码。 那么这个时候就产生了一个问题。先举个🌰,一家中餐馆的招牌菜是番茄炒蛋,那么番茄炒蛋的备菜肯定很足,但是顾客A特立独行,他偏偏不要吃番茄炒蛋,他每次都点一个冷门的牛排套餐,那这时候只能让顾客等着老板将牛排套餐做完。 如果一个APP的热点代码(如首页),刚好游离于VM模板之外,那么AOT就其实形同虚设了。(比如vm模版优先编译名称不大于15个字符的类和方法,但是首页的类名刚好高于15个字符。此处仅为举例并没有实际论证过) 下面用首页和设置页来举例:由于遵循vm模版,AOT因为某个原因没有优先编译首页部分代码,而转而去编译了不太重要的设置页代码: image 上图的流程说明了在特殊情况下,AOT编译实则不起作用,完全是靠解释器和JIT在进行实时编译,整个编译方案退步到了Android2.2时期。 聪明的ART 虽然这个问题存在,但并不是特别严重。因为ART并没有我说的那么笨。在之后应用使用过程中,ART会记录并学习用户的使用习惯(保存热点代码),然后更新针对当前APP的定制化vm模版,不断的补充热点代码,补充定制化模版。 这是不是听起来很熟悉?在手机发布大会上的宣传语“基于用户操作习惯进行学习,APP打开速度不断提高”的部分原理就是这个。 最终大招,一劳永逸 其实要一劳永逸的解决这个问题思路也不难:我们只需要在吃饭前跟老板提前预定想吃啥就行,让老板先准备起来,这样等我们到了就不用等餐了。 在最新的Android9.0版本中,谷歌推出了这个类似提前预定的功能:编译系统支持在具有蓝图编译规则的原生 Android 模块上使用 Clang 的配置文件引导优化 (PGO)。说人话:谷歌允许你在开发阶段添加一个配置文件,这个配置文件内可指定“热点代码”,当应用安装完后,ART在后台悄悄编译APP时,会优先编译配置文件中指定的“热点代码”。 虽然谷歌支持,但是这块技术对于APP开发人员而言国内资料过于缺乏,普及面不广。笔者先贴上官方链接,以及这篇博客,其中介绍的还是挺详细的。(隔壁Xcode针对PGO都有UI界面了) 华为方舟解决思路 针对虚拟机问题,方舟说:我不要你这个烂虚拟机了,我们裸奔 针对JNI调用问题,方舟说:我们让Java在编译阶段跟C一样直接编译成机器码,干掉虚拟机,跟.so库直接调用,毫无JNI开销问题 针对编译模版问题,方舟说:我们支持针对不同APP进行不同的编译优化 总结一下:方舟支持在打包编译阶段针对不同APP进行不同的编译优化,然后直接打包成机器码.apk(很可能已经不叫apk了),然后直接运行。","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"},{"name":"编译器","slug":"编译器","permalink":"http://sorgs.cn/tags/编译器/"}]},{"title":"Android每日一问笔记-Handler中的IdleHandler","date":"2019-08-19T09:10:44.000Z","path":"post/14041/","text":"基于https://www.wanandroid.com每日一问的笔记,做一些整理,方便自己进行查看和记忆。 原文链接:https://www.wanandroid.com/wenda/show/8723 IdleHandler1234567891011121314/** * Callback interface for discovering when a thread is going to block * waiting for more messages. */public static interface IdleHandler { /** * Called when the message queue has run out of messages and will now * wait for more. Return true to keep your idle handler active, false * to have it removed. This may be called if there are still messages * pending in the queue, but they are all scheduled to be dispatched * after the current time. */ boolean queueIdle();} 注释中很明确地指出当消息队列空闲时会执行IdleHandler的queueIdle()方法,该方法返回一个boolean值,如果为false则执行完毕之后移除这条消息,如果为true则保留,等到下次空闲时会再次执行,下面看下MessageQueue的next()方法可以发现确实是这样 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647Message next() { ...... for (;;) { ...... synchronized (this) { // 此处为正常消息队列的处理 ...... if (mQuitting) { dispose(); return null; } if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { pendingIdleHandlerCount = mIdleHandlers.size(); } if (pendingIdleHandlerCount <= 0) { // No idle handlers to run. Loop and wait some more. mBlocked = true; continue; } if (mPendingIdleHandlers == null) { mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; } mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); } for (int i = 0; i < pendingIdleHandlerCount; i++) { final IdleHandler idler = mPendingIdleHandlers[i]; mPendingIdleHandlers[i] = null; // release the reference to the handler boolean keep = false; try { keep = idler.queueIdle(); } catch (Throwable t) { Log.wtf(TAG, \"IdleHandler threw exception\", t); } if (!keep) { synchronized (this) { mIdleHandlers.remove(idler); } } } pendingIdleHandlerCount = 0; nextPollTimeoutMillis = 0; } } 处理完IdleHandler后会将nextPollTimeoutMillis设置为0,也就是不阻塞消息队列,当然要注意这里执行的代码同样不能太耗时,因为它是同步执行的,如果太耗时肯定会影响后面的message执行。 mPendingIdleHandlers它里面放的IdleHandler实例都是临时的,也就是每次使用完(调用了queueIdle方法)之后,都会置空(mPendingIdleHandlers[i] = null) 在什么时候用到呢? 就在MessageQueue的next方法里面。 大概流程是这样的: 如果本次循环拿到的Message为空,或者!这个Message是一个延时的消息而且还没到指定的触发时间,那么,就认定当前的队列为空闲状态; 接着就会遍历mPendingIdleHandlers数组(这个数组里面的元素每次都会到mIdleHandlers中去拿)来调用每一个IdleHandler实例的queueIdle方法; 如果这个方法返回false的话,那么这个实例就会从mIdleHandlers中移除,也就是当下次队列空闲的时候,不会继续回调它的queueIdle方法了。 它有什么能力? 能力大概就是上面讲的那样,那么能力决定用处,用处从本质上讲就是趁着消息队列空闲的时候干点事情,当然具体的用处还是要看具体的处理。 要使用IdleHandler只需要调用MessageQueue#addIdleHandler(IdleHandler handler)方法即可1234567Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { //do something return false; }}); 合适场景 消息队列相关 主线程能干的事情 返回true和false带来的不同结果","tags":[{"name":"每日一问笔记","slug":"每日一问笔记","permalink":"http://sorgs.cn/tags/每日一问笔记/"},{"name":"面试","slug":"面试","permalink":"http://sorgs.cn/tags/面试/"},{"name":"笔记","slug":"笔记","permalink":"http://sorgs.cn/tags/笔记/"}]},{"title":"Android每日一问笔记-对于SharedPreferences的优缺点?","date":"2019-08-16T03:19:19.000Z","path":"post/42206/","text":"基于https://www.wanandroid.com每日一问的笔记,做一些整理,方便自己进行查看和记忆。 原文链接:https://www.wanandroid.com/wenda/show/8656 SharedPreferences,它是一个轻量级的存储类,特别适合用于保存软件配置参数 优点: 轻量级,以键值对的方式进行存储,使用方便,易于理解 采用的是xml文件形式存储在本地,程序卸载后会也会一并被清除,不会残留信息 缺点: 由于是对文件IO读取,因此在IO上的瓶颈是个大问题,因为在每次进行get和commit时都要将数据从内存写入到文件中,或从文件中读取 多线程场景下效率较低,在get操作时,会锁定SharedPreferences对象,互斥其他操作,而当put,commit时,则会锁定Editor对象,使用写入锁进行互斥,在这种情况下,效率会降低 不支持跨进程通讯4.由于每次都会把整个文件加载到内存中,因此,如果SharedPreferences文件过大,或者在其中的键值对是大对象的json数据则会占用大量内存,读取较慢是一方面,同时也会引发程序频繁GC,导致的界面卡顿。 建议 建议不要存储较大数据或者较多数据到SharedPreferences中 频繁修改的数据修改后统一提交,而不是修改过后马上提交 在跨进程通讯中不去使用SharedPreferences 键值对不宜过多 关于SharedPreference.Editor的apply()和commit()方法异同 在androidstudio上coding经常会提示一些警告,通过它我们能了解到一些自己不了解的好的编程习惯和少用的方法,本次发现就是一个例子,用习惯了SharedPreference.Editor的commit()方法,但是在studio提示使用apply()方法替换,看到apply()方法有点不知所措,因为根本不了解这个方法的作用随即翻阅android的api和google了一下apply()和commit()两者的区别。首先,两者都能实现shared存储的功能,但是两者还是有着一些不同 简书 apply方法是将share的修改提交到内存而后异步写入磁盘,但是commit是直接写入磁盘,这就造成两者性能上的差异,犹如apply不直接写入磁盘而share本身是单例创建,apply方法会覆写之前内存中的值,异步写入磁盘的值只是最后的值,而commit每次都要写入磁盘,而磁盘的写入相对来说是很低效的,所以apply方法在频繁调用时要比commit效率高很多。 apply虽然高效但是commit也有着自己的优势那就是它可以返回每次操作的成功与否的返回值,根据它我们就可以在操作失败时做一些补救操作。综上,studio提示我们使用apply是在效率上的优化考虑,但是如果你很重视share是否成功操作,并希望在失败时做相应的提示或者补救commit还是更好的选择。 stack overflow apply() was added in 2.3, it commits without returning a boolean indicating success or failure. commit() returns true if the save works, false otherwise. apply() was added as the Android dev team noticed that almost no one took notice of the return value, so apply is faster as it is asynchronous.","tags":[{"name":"每日一问笔记","slug":"每日一问笔记","permalink":"http://sorgs.cn/tags/每日一问笔记/"},{"name":"面试","slug":"面试","permalink":"http://sorgs.cn/tags/面试/"},{"name":"笔记","slug":"笔记","permalink":"http://sorgs.cn/tags/笔记/"}]},{"title":"Android Handler运行机制以及问题点解答","date":"2019-08-14T15:09:18.000Z","path":"post/62013/","text":"Handler机制笔记,原文地址:https://mp.weixin.qq.com/s/7PAMm_FPrA0P3jf0tn3yy Handler 如何运行Handler角色分配 Handler中存在四种角色 Handler Handler用来向Looper发送消息,在Looper处理到对应的消息时,Handler再对消息进行具体的处理。上层关键API为handleMessage(),由子类自行实现处理逻辑。 Looper Looper运行在目标线程里,不断从消息队列MessageQueue读取消息,分配给Handler处理。Looper起到连接的作用,将来自不同渠道的消息,聚集在目标线程里处理。也因此Looper需要确保线程唯一。 MessageQueue 存储消息对象Message,当Looper向MessageQueue获取消息,或Handler向其插入数据时,决定消息如何提取、如何存储。不仅如此,MessageQueue还维护与Native端的连接,也是解决Looper.loop() 阻塞问题的 Java 端的控制器。 Message Message包含具体的消息数据,在成员变量target中保存了用来发送此消息的Handler引用。因此在消息获得这行时机时,能知道具体由哪一个Handler处理。此外静态成员变量sPool,则维护了消息缓存池以复用。 运行过程 首先,需要构建消息对象。获取消息对象从Handler.obtainMessage()系列方法可以获取Message,这一系列的函数提供了相应对应于Message对象关键成员变量对应的函数参数,而无论使用哪一个方法获取,最终通过Message.obtain()获取具体的Message对象。 12345678910111213141516171819202122232425// 缓存池 private static Message sPool; // 缓存池当前容量 private static int sPoolSize = 0; // 下一节点 Message next; public static Message obtain() { // 确保同步 synchronized (sPoolSync) { if (sPool != null) { // 缓存池不为空 Message m = sPool; // 缓存池指向下一个Message节点 sPool = m.next; // 从缓存池拿到的Message对象与缓存断开连接 m.next = null; m.flags = 0; // clear in-use flag // 缓存池大小减一 sPoolSize--; return m; } } // 缓存池没有可用对象,返回新的Message() return new Message(); } Message成员变量中存在类型为Message的next,可以看出Message为链表结构,而上面代码从缓存池里获取消息对象的过程可以用下图描述: image 创建出消息之后,通过Handler将消息发送到消息队列,发送方法有很多,不一一陈列。发送有两种: 将Message对象发送到Looper。利用sendMessage() 发送Runnable,通过getPostMessage()将Runnable包装在Message里,表现为成员变量callback1234567private static Message getPostMessage(Runnable r) { // 获取Message Message m = Message.obtain(); // 记住Runnale,等消息获得执行时回调 m.callback = r; return m; } 不管哪种方式发送,最终消息队列MessageQueue只接受到了消息对象Message。而将消息加入到消息队列,最终通过enqueueMessage()加入。 在将消息加入消息队列时,有时需要提供延迟信息delayTime,以期未来多久后执行,这个值存于 uptimeMillis。 123456789private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { // Message.target 记住 Handler 以明确是由哪一个Handler来处理这个消息的 msg.target = this; if (mAsynchronous) { msg.setAsynchronous(true); } // 消息入队 return queue.enqueueMessage(msg, uptimeMillis); } 之后,等待Looper轮询从消息队列中读取消息进行处理。见Looper.loop()。 123456789101112131415161718192021222324252627public static void loop() { // 拿到Looper final Looper me = myLooper(); if (me == null) { // 没调用prepare初始化Looper,报错 throw new RuntimeException(\"No Looper; Looper.prepare() wasn't called on this thread.\"); } // 拿到消息队列 final MessageQueue queue = me.mQueue; ...... for (;;) { // 从消息队列取出下一个信息 Message msg = queue.next(); if (msg == null) { // 消息为空,返回 return; } ....... try { // 分发消息到Handler msg.target.dispatchMessage(msg); end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis(); } // 消息回收,放入缓存池 msg.recycleUnchecked(); } Looper从MessageQueue里取出Message,Message.target则是具体的Hander,Handler.dispatchMessage()将触发具体分配逻辑。此后,将Message回收,放入缓存池。 123456789101112131415public void dispatchMessage(Message msg) { if (msg.callback != null) { // 这个情况说明了本次消息为Runnable,触发Runnable.run() handleCallback(msg); } else { if (mCallback != null) { // 指定了Handler的mCallback if (mCallback.handleMessage(msg)) { return; } } // 普通消息处理 handleMessage(msg); }} Handler分配消息分三种情况: 可以通过Handler发送Runnable消息到消息队列,因此handleCallback()处理这种情况 可以给Handler设置Callback,当分配消息给Handler时,Callback可以优先处理此消息,如果Callback.handleMessage()返回了true,不再执行Handler.handleMessage() Handler.handleMessage()处理具体逻辑 回收Message则是通过Message.recycleUnchecked()。 123456789101112131415void recycleUnchecked() { // 这里是将Message各种属性重置操作 ...... synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { // 缓存池还能装下,回收到缓存池 // 下面操作将此Message加入到缓存池头部 next = sPool; sPool = this; sPoolSize++; } } } 通过上面的分析,Handler的运行如下图: image Handler 从缓存池获取Message,发送到MessageQueue Looper不断从MessageQueue读取消息,通过Message.target.dispatchMessage()触发Handler处理逻辑 回收Message到缓存池 Java端与Native端建立连接 实际上,不仅仅是Java端存在Handler机制,在Native端同样存在Handler机制。他们通过MessageQueue建立了连接。 一般来说,Looper通过prepare()进行初始化。 12345678private static void prepare(boolean quitAllowed) { // 保证Looper在线程唯一 if (sThreadLocal.get() != null) { throw new RuntimeException(\"Only one Looper may be created per thread\"); } // 将Looper放入ThreadLocal sThreadLocal.set(new Looper(quitAllowed)); } 在实例化Looper时,需要确保Looper在线程里是唯一的。Handler知道自己的具体Looper对象,而Looper运行在具体的线程里并在此线程里处理消息。这也是为什么Looper能达到切换线程的目的。Looper线程唯一需要ThreadLocal来确保,ThreadLocal的原理,简单来说Thread里有类型为ThreadLocalMap的成员threadLocals,通过ThreadLocal能将相应对象放入threadLocals里通过K/V存储,如此能保证变量在线程范围内存储,其中Key为ThreadLocal< T > 。 12345678910111213private Looper(boolean quitAllowed) { // 初始化MessageQueue mQueue = new MessageQueue(quitAllowed); // 记住当前线程 mThread = Thread.currentThread(); } MessageQueue(boolean quitAllowed) { mQuitAllowed = quitAllowed; // 与Native建立连接 mPtr = nativeInit(); } 在MessageQueue创建时,通过native方法nativeInit()与Native端建立了连接,mPtr为long型变量,存储一个地址。方法实现文件位于frameworks/base/core/jni/android_os_MessageQueue.cpp 1234567891011121314151617181920212223static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) { NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue(); if (!nativeMessageQueue) { jniThrowRuntimeException(env, \"Unable to allocate native queue\"); return 0; } nativeMessageQueue->incStrong(env); // 返回给Java层的mPtr, NativeMessageQueue地址值 return reinterpret_cast<jlong>(nativeMessageQueue);}NativeMessageQueue::NativeMessageQueue() : mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) { mLooper = Looper::getForThread(); // 检查Looper 是否创建 if (mLooper == NULL) { mLooper = new Looper(false); // 确保Looper唯一 Looper::setForThread(mLooper); }} 在Native端创建了NativeMessageQueue,同样也创建了Native端的Looper。在创建NativeMessageQueue后,将它的地址值返回给了Java层MessageQueue.mPtr。实际上,Native端Looper实例化时做了更多事情。Nativ端Looper文件位于system/core/libutils/Looper.cpp。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546Looper::Looper(bool allowNonCallbacks) : mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false), mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false), mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) { // 添加到epoll的文件描述符,线程唤醒事件的fd mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); LOG_ALWAYS_FATAL_IF(mWakeEventFd < 0, \"Could not make wake event fd: %s\", strerror(errno)); AutoMutex _l(mLock); rebuildEpollLocked();}void Looper::rebuildEpollLocked() { ..... // Allocate the new epoll instance and register the wake pipe. // 创建epolle实例,并注册wake管道 mEpollFd = epoll_create(EPOLL_SIZE_HINT); LOG_ALWAYS_FATAL_IF(mEpollFd < 0, \"Could not create epoll instance: %s\", strerror(errno)); struct epoll_event eventItem; // 清空,把未使用的数据区域进行置0操作 memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union // 监听可读事件 eventItem.events = EPOLLIN; // 设置作为唤醒评判的fd eventItem.data.fd = mWakeEventFd; // 将唤醒事件(mWakeEventFd)添加到epoll实例,意为放置一个唤醒机制 int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem); LOG_ALWAYS_FATAL_IF(result != 0, \"Could not add wake event fd to epoll instance: %s\", strerror(errno)); // 添加各种事件的fd到epoll实例,如键盘、传感器输入等 for (size_t i = 0; i < mRequests.size(); i++) { const Request& request = mRequests.valueAt(i); struct epoll_event eventItem; request.initEventItem(&eventItem); int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, request.fd, & eventItem); if (epollResult < 0) { ALOGE(\"Error adding epoll events for fd %d while rebuilding epoll set: %s\", request.fd, strerror(errno)); } }} 如何理解epoll机制? 文件、socket、pipe(管道)等可以进行I/O操作的对象可以视为流。既然是I/O操作,则有read端读入数据,有write端写入数据。但是两端并不知道对方进行操作的时机。而epoll则能观察到哪个流发生了了I/O事件,并进行通知。 这个过程,就好比你在等快递,但你不知道快递什么时候来,那这时你可以去睡觉,因为你知道快递送来时一定会打个电话叫醒你,让你拿快递,接着做你想的事情。 epoll有效地降低了CPU的使用,在线程空间时令其休眠,等有事件到来时再讲它唤醒。 在知道了epoll之后,再来看上面的代码,就可以理解了。在Native端创建Looper时,会创建用来唤醒线程的fd —— mWakeEventFd,创建epoll实例并注册管道,清空管道数据,监听可读事件。当有数据写入mWakeEventFd描述的文件时,epoll能监听到此事件,并通知将目标线程唤醒。 在Java端MessageQueue.mPrt存储了Native端NativeMassageQueue的地址,可以利用NativeMassageQueue享用此机制。 发送数据的具体过程 Handler发送消息时,最终通过MessageQueue.enqueueMessage向消息队列中插入消息,下面为具体代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051boolean enqueueMessage(Message msg, long when) { ...... synchronized (this) { ...... // 记录消息处理的时间 msg.when = when; Message p = mMessages; // 唤醒线程的标志位 boolean needWake; if (p == null || when == 0 || when < p.when) { // 这里三种情况: // 1、目标消息队列是空队列 // 2、插入的消息处理时间等于0 // 3、插入的消息处理时间小于保存在消息队列头的消息处理时间 // 这三种情况都插入列表头 msg.next = p; mMessages = msg; // mBlocked 表示当前线程是否睡眠 needWake = mBlocked; } else { // 这里则说明消息处理时间大于消息列表头的处理时间,因此需要找到合适的插入位置 needWake = mBlocked && p.target == null && msg.isAsynchronous(); Message prev; // 这里的循环是找到消息的插入位置 for (;;) { prev = p; p = p.next; // 到链表尾,或处理时间早于p的时间 if (p == null || when < p.when) { break; } if (needWake && p.isAsynchronous()) { // 如果插入的消息在目标队列中间,是不需要检查改变线程唤醒状态的 needWake = false; } } // 插入到消息队列 msg.next = p; prev.next = msg; } if (needWake) { // 唤醒线程 nativeWake(mPtr); } } return true; } 消息队列里的消息也是以链表形式存储,存储顺序则按照处理的时间顺序。那么在向消息队列里插入数据时,存在四种情况: 目标消息队列是空队列 插入的消息处理时间等于0 插入的消息处理时间小于保存在消息队列头的消息处理时间 插入的消息处理时间大于消息队列头的消息处理时间 前三种情况,将消息插入消息队列头的位置,在这种情况下,因为不能保证当前消息是否达到了可以处理的状态,且如果此时线程是睡眠的,则需要调用nativeWake()将其线程唤醒。后一种情况,则需要找到消息的插入位置,因不影响线程状态而需要改变线程状态。插入消息如图: image mPtr保存了NativeMessageQueue的地址,所以Native可以知道具体操作的NativeMessageQueue,当前用它来唤醒线程,实际调用链为MessageQueue.cpp.nativeWake()到MessageQueue.cpp.wake()到Looper.cpp.wake()。 123456789101112131415void Looper::wake() {#if DEBUG_POLL_AND_WAKE ALOGD(\"%p ~ wake\", this);#endif uint64_t inc = 1; // 向管道写入一个新数据,这样管道因为发生了IO事件被唤醒 ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t))); if (nWrite != sizeof(uint64_t)) { if (errno != EAGAIN) { LOG_ALWAYS_FATAL(\"Could not write wake signal to fd %d: %s\", mWakeEventFd, strerror(errno)); } }} 实现也简单,向mWakeEventFd文件里写入一个数据,根据epoll机制监听到此次I/O事件,将线程唤醒。 消息读取 Looper不断从MessageQueue读取消息进行处理,通过MessageQueue.next()进行读取。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110Message next() { final long ptr = mPtr; if (ptr == 0) { // 获取NativeMessageQueue地址失败,无法正常使用epoll机制 return null; } // 用来保存注册到消息队列中的空闲消息处理器(IdleHandler)的个数 int pendingIdleHandlerCount = -1; // 如果这个变量等于0,表示即便消息队列中没有新的消息需要处理,当前 // 线程也不要进入睡眠等待状态。如果值等于-1,那么就表示当消息队列中没有新的消息 // 需要处理时,当前线程需要无限地处于休眠等待状态,直到它被其它线程唤醒为止 int nextPollTimeoutMillis = 0; for (;;) { ...... // 检查当前线程的消息队列中是否有新的消息需要处理,尝试进入休眠 nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // 当前时间 final long now = SystemClock.uptimeMillis(); Message prevMsg = null; // mMessages 表示当前线程需要处理的消息 Message msg = mMessages; if (msg != null && msg.target == null) { // 找到有效的Message do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } if (msg != null) { /** * 检查当前时间和消息要被处理的时间,如果小于当前时间,说明马上要进行处理 */ if (now < msg.when) { // 还没达到下一个消息需要被处理的时间,计算需要休眠的时间 nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else { // 有消息需要处理 // 不要进入休眠 mBlocked = false; if (prevMsg != null) { prevMsg.next = msg.next; } else { // 指向下一个需要处理的消息 mMessages = msg.next; } msg.next = null; if (DEBUG) Log.v(TAG, \"Returning message: \" + msg); msg.markInUse(); return msg; } } else { // 没有更多消息,休眠时间无限 nextPollTimeoutMillis = -1; } ...... if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { // 获取IdleHandler数 pendingIdleHandlerCount = mIdleHandlers.size(); } if (pendingIdleHandlerCount <= 0) { // 没有IdleHandler需要处理,可直接进入休眠 mBlocked = true; continue; } if (mPendingIdleHandlers == null) { mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; } mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); } // 如果没有更多要进行处理的消息,在休眠之前,发送线程空闲消息给已注册到消息队列中的IdleHandler对象来处理 for (int i = 0; i < pendingIdleHandlerCount; i++) { final IdleHandler idler = mPendingIdleHandlers[i]; mPendingIdleHandlers[i] = null; // release the reference to the handler boolean keep = false; try { // 处理对应逻辑,并由自己决定是否保持激活状态 keep = idler.queueIdle(); } catch (Throwable t) { Log.wtf(TAG, \"IdleHandler threw exception\", t); } if (!keep) { // 不需要存活,移除 synchronized (this) { mIdleHandlers.remove(idler); } } } // 重置IdleHandler数量 pendingIdleHandlerCount = 0; /** * 这里置0,表示下一次循环不能马上进入休眠状态,因为IdleHandler在处理事件的时间里, * 有可能有新的消息发送来过来,需要重新检查。 */ nextPollTimeoutMillis = 0; } } 分为两种情况处理: 取到消息Message时: 需要查看当前时间是否达到了Message处理的时间,如果达到了则返回,改变mMessages指向下一消息。如果没达到,则计算要达到处理的时间,还需要休眠多久,并进行休眠。 没有更多Message时: 当消息队列里没有消息时,则会检查是否有IdleHandler需要处理。在Handler机制里,允许添加一些事件,在线程空闲时进行处理,表现为IdleHandler,可以通过MessageQueue.addIdleHandler添加。当有IdleHandler需要处理,则在IdleHandler处理完后,线程不能马上进入休眠状态,在此期间可能已有新消息加入消息队列,需要重新做检查。如果没有IdleHandler,则可以进入休眠。 线程休眠调用链为NativeMessageQueue.nativePollOnce()到NativeMessageQueue.pollOnce()到Looper.pollOnce()到Looper.pollInner()。 12345678910111213141516171819202122232425int Looper::pollInner(int timeoutMillis) {...... // 这个是用来监听实例化时创建的epoll实例的文件描述符的IO读写事件 struct epoll_event eventItems[EPOLL_MAX_EVENTS]; // 如果没有事件,进入休眠,timeoutMillis为休眠事件 int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis); ...... /** * 检测是哪一个文件描述符发生了IO读写事件 */ for (int i = 0; i < eventCount; i++) { int fd = eventItems[i].data.fd; uint32_t epollEvents = eventItems[i].events; if (fd == mWakeEventFd) { if (epollEvents & EPOLLIN) { // 如果文件描述符为mWakeEventFd,并且读写事件类型为EPOLLIN,说明 // 当前线程所关联的一个管道被写入了一个新的数据 // 唤醒 awoken(); } } ...... }} Java层提供了线程休眠时间timeoutMillis,通过epoll_wait()让线程进行休眠。当线程被唤醒后,查看文件描述符,如果为mWakeEventFd并且为I/O事件,则说明当前线程所关联的一个管道被写入了一个新的数据,通过awoken()处理。当前线程已是唤醒状态,awoken()则是将管道中的数据读出达到清理目的,但并不关心数据什么。核心目的是唤醒线程。 总结 Handler机制更具体的原理如图: image Looper通过prepare()创建,借助ThreadLocal保证线程唯一,如果没有进行prepare(),调用Loop()会抛出异常; Looper在实例化时创建MessageQueue,MessageQueue与NativeMessageQueue建立连接,NativeMessageQueue存储地址存于MessageQueue.mPtr。Native端也建立了Handler机制,使用epoll机制。Java端借由NativeMessageQueue能达到使用epoll机制的目的; 从Message缓存里获取Message,缓存为链表存储,从头出取出,并且Message在回收时也是插入头部。如果存缓存里取不到,则新建; Handler向MessageQueue插入消息,如果消息插入消息队列头部,需要唤醒线程;如果插入消息队列中,无需改变线程状态; Looper.loop() 不断从消息队列获取消息,消息队列获取消息时会出现两种情况。如果取到消息,但没达到处理时间,则让线程休眠;如果没有更多消息,则在处理IdleHandler事后,在考虑让线程进入休眠; Message达到了可处理状态,则有Handler处理,处理时考虑三种情况,消息内容为Runnable时、设置了Handle.Callback时、普通消息时,对应调用为Message.callback.run() 、 Callback.handleMessage()、Handler.handleMessage(); 从Handler机制里,epoll可以简单理解为,当Handler机制没有消息要处理时,让线程进入休眠,当Handler机制有消息要处理时,将线程唤起。通过Native端监听mWakeEventFd的I/O事件实现。 疑问点Handler如何保证运行在目标线程 Looper在实例化时通过ThreadLocal保证线程唯一。Looper运行在目标线程中,接收Handler发送的消息并进行处理。Message创建时与具体的Handler进行了关联,因此能知道由哪一个Handler进行相应。 Handler容易造成内存泄漏的原因 Message.target存有Handler的引用,以知道自身由哪一个Handler来处理。因此,当Handler为非静态内部类、或持有关键对象的其它表现形式时(如Activity常表现为Context),就引用了其它外部对象。当Message得不到处理时,被Handler持有的外部对象会一直处于内存泄漏状态。 loop()为什么不会阻塞,CPU为什么不会忙等 通过epoll机制监听文件I/O时间,在有Message需要处理时,写入数据以唤醒线程;在没有Message要处理时,让线程进入休眠状态。 MessageQueue如何存储 以链表存储,MessageQueue.mMessages指向头节点。 Message如何缓存 以链表缓存,取出时从头部取出,回收时插入头部。 什么是线程空闲消息 Handler提供的一种机制,允许添加IdleHandler事件。并在没有更多Message要处理,要进入休眠前,让IdleHandler做具体事情,也就是线程空间时处理的事件。 子线程如何使用Handler机制 只要保证在子线程先执行Looper.prepare()再使用Looper.Loop()即可,但实际应用场景不多。顺便提一句,主线程初始化Looper操作在ActivityTread.main()里触发,简单了解即可。","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"},{"name":"Handler","slug":"Handler","permalink":"http://sorgs.cn/tags/Handler/"}]},{"title":"Android每日一问笔记-哪些 Context调用 startActivity 需要设置NEW_TASK","date":"2019-08-05T12:58:49.000Z","path":"post/3427/","text":"基于https://www.wanandroid.com每日一问的笔记,做一些整理,方便自己进行查看和记忆。 原文链接:https://www.wanandroid.com/wenda/show/8697以及nanchen的文章 使用非 Activity 的 startActivity()的时候,都需要指定Intent.FLAG_ACTIVITY_NEW_TASK,如果没有指定,直接进行操作则会直接抛出异常 使用 applicationContext 来做 startActivity() 操作,却没有指定任何的 FLAG,但是,在 8.0 的手机上,你一定会惊讶的发现,我们并没有等到意料内的崩溃日志,而且跳转也是非常正常,这不由得和我们印象中必须加 FLAG 的结论大相径庭。然后再拿一个 9.0 的手机来尝试,马上就出现了上面的崩溃 123456789101112131415161718192021222324252627282930313233343536373839404142434445//SDK 26@Overridepublic void startActivity(Intent intent, Bundle options) { warnIfCallingFromSystemProcess(); // Calling start activity from outside an activity without FLAG_ACTIVITY_NEW_TASK is // generally not allowed, except if the caller specifies the task id the activity should // be launched in. if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0 && options != null && ActivityOptions.fromBundle(options).getLaunchTaskId() == -1) { throw new AndroidRuntimeException( \"Calling startActivity() from outside of an Activity \" + \" context requires the FLAG_ACTIVITY_NEW_TASK flag.\" + \" Is this really what you want?\"); } mMainThread.getInstrumentation().execStartActivity( getOuterContext(), mMainThread.getApplicationThread(), null, (Activity) null, intent, -1, options);}//SDK 28@Overridepublic void startActivity(Intent intent, Bundle options) { warnIfCallingFromSystemProcess(); // Calling start activity from outside an activity without FLAG_ACTIVITY_NEW_TASK is // generally not allowed, except if the caller specifies the task id the activity should // be launched in. A bug was existed between N and O-MR1 which allowed this to work. We // maintain this for backwards compatibility. final int targetSdkVersion = getApplicationInfo().targetSdkVersion; if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0 && (targetSdkVersion < Build.VERSION_CODES.N || targetSdkVersion >= Build.VERSION_CODES.P) && (options == null || ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) { throw new AndroidRuntimeException( \"Calling startActivity() from outside of an Activity \" + \" context requires the FLAG_ACTIVITY_NEW_TASK flag.\" + \" Is this really what you want?\"); } mMainThread.getInstrumentation().execStartActivity( getOuterContext(), mMainThread.getApplicationThread(), null, (Activity) null, intent, -1, options);} 使用 Context.startActivity() 的时候是一定要加上 FLAG_ACTIVITY_NEW_TASK 的,但是在 Android N 到 O-MR1,即 24~27 之间却出现了 bug,即使没有加也会正确跳转。 非 Activity 调用 startActivity() 的时候,我们这个 options通常是 null 的,所以在 24~27 之间的时候,误把判断条件 options == null 写成了options != null 导致进不去 if,从而不会抛出异常。","tags":[{"name":"每日一问笔记","slug":"每日一问笔记","permalink":"http://sorgs.cn/tags/每日一问笔记/"},{"name":"面试","slug":"面试","permalink":"http://sorgs.cn/tags/面试/"},{"name":"笔记","slug":"笔记","permalink":"http://sorgs.cn/tags/笔记/"}]},{"title":"利用Retrofit+RxJava简单封装网络请求库","date":"2019-08-01T08:13:35.000Z","path":"post/15217/","text":"网络请求在移动端是极为常见和重要,随处可见。为此,为了避免到处使用增加内存和性能,以及方便使用和解耦,进行网络库的简单封装。 特点 解耦:对下面使用的网络请求框架和上层网络进行解耦。方便底层可以根据业务要求换更网络请求网络也不影响到上层业务逻辑。 方便:对使用的场景极为方便,仅仅5行左右代码,即可完成一次网络请求以及数据处理。 解放:解放繁琐的线程切换,错误处理和判断,数据处理,Json的转换等,使调用方不必考虑与业务逻辑无关的事情。 透明:调用方对调用的函数使用起来简单,便于理解 Retrofit简介 Retrofit,一个远近闻名的网络框架,它是由Square公司开源的。Square公司,是我们的老熟人了,很多框架都是他开源的,比如OkHttp,picasso,leakcanary等等。他们公司的很多开源库,几乎已经成为现在开发Android APP的标配。 简单来说,Retrofit其实是底层还是用的OkHttp来进行网络请求的,只不过他包装了一下,使得开发者在使用访问网络的时候更加方便简单高效。 一句话总结:Retrofit将接口动态生成实现类,该接口定义了请求方式+路径+参数等注解,Retrofit可以方便得从注解中获取这些参数,然后拼接起来,通过OkHttp访问网络。 Retrofit简单使用引入依赖 引入retrofit2以及需要转化使用的Gson 12implementation 'com.squareup.retrofit2:retrofit:2.3.0'implementation 'com.squareup.retrofit2:converter-gson:2.3.0' API的interface接口 需要先定义出注解的接口 1234567891011interface Request { companion object { val HOST = \"http://42.157.129.91/\" } @POST(\"user/sorgs\") fun getSorgs(@Query(\"id\") id: String): Observable<ResponseData<JavaBean>> @POST(\"user/sorgs\") fun getCallTest(@Query(\"id\") id: String): Call<ResponseData<JavaBean>>} 定义出静态字符串HOST,用来限定请求的服务器,即BaseUrl 定义出接口方法,@POST用于指定请求的方式,包括POST,GET等。 方法需要定义出返回值以及接受的参数 创建Retrofit实例 通过构造者方式,创建出Retrofit实例 baseUrl即传入服务器地址 addConverterFactory为转化工厂,我们需要将获取的Json直接通过Gson转化为Bean对象 123456// 初始化Retrofitval request = Retrofit.Builder() .client(client) .baseUrl(Request.HOST) .addConverterFactory(GsonConverterFactory.create()) .build() API接口转换成实例 将刚定义的interface文件引入转化为实例 1val callApi = request.create(Request::class.java) 进行网络请求 直接调用interface定义的请求方法,调用enqueue方法进行回调Callback返回请求成功以及失败 123456789callApi.getCallTest(\"1\").enqueue(object : Callback<ResponseData<JavaBean>> { override fun onFailure(call: Call<ResponseData<JavaBean>>, t: Throwable) { Log.e(\"sorgs\", \"btnTest3 e:${t.message}\") } override fun onResponse(call: Call<ResponseData<JavaBean>>, responseData: Response<ResponseData<JavaBean>>) { Log.i(\"sorgs\", \"btnTest3 JavaBean:${(responseData.body()?.data as JavaBean).name}\") }}) 封装网络库 上诉简单使用仅仅是demo如此写,如果放到实际业务中,还需考虑request的创建单例化,日志拦截查看,线程切换,数据处理以及错误处理 应用依赖单例建立NetWorkManager 通过单例构建retrofit等 123456private var retrofit: Retrofit? = null//单例获取NetWorkManagerval instant: NetWorkManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { NetWorkManager() }val request: Request? by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { retrofit?.create(Request::class.java)} 进行初始化 1234567891011121314151617181920212223242526/** * 初始化必要对象和参数 * @param baseUrl 基础连接 */fun init(baseUrl: String) { val logging = HttpLoggingInterceptor() logging.level = if (BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BODY } else { HttpLoggingInterceptor.Level.NONE } /* val logging2 = HttpLoggingInterceptor() logging2.level = HttpLoggingInterceptor.Level.HEADERS*/ // 初始化okhttp val client = OkHttpClient.Builder() //.addInterceptor(logging2) .addInterceptor(logging) .build() // 初始化Retrofit retrofit = Retrofit.Builder() .client(client) .baseUrl(baseUrl) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build()} 在初始化时候,构建HttpLoggingInterceptor,用于日志拦截。并利用BuildConfig.DEBUG进行区分,因为不希望在正式包里面也暴露出请求信息 日志拦截分为NONE、BASIC、HEADERS以及BODY,一般来说使用BASIC以及BODY,为了方便调试和查看,使用BODY更加方便。 同时addInterceptor是可以添加多个的,可以同时把BASIC和BODY添加上进行打印输出。通过client()构建到retrofit中。 retrofit的构造,同时加入addCallAdapterFactory(RxJava2CallAdapterFactory.create()),直接把RxJava引入,方便错误处理,数据处理,线程切换等 接口返回数据封装 服务器返回的数据,一般都是有严格的格式,分为code、msg和data。真实数据包含在data中,code和msg是用来进行判断这次请求的结果,这些判断我们就需要在底层直接处理好,所以直接封装起来。 123456789101112131415/** * description: 接口返回数据封装. * {code:0,data:\"\",msg:\"\"} * code:接口返回的code 一定不能为空 * data:接口返回具体的数据结果 可能为空 * msg:message 可用来返回接口的说明 可能为空 * * @author Sorgs. * Created date: 2019/7/30. */data class ResponseData<T>( var code: Int, var data: T?, var msg: String?) 错误处理 错误处理分为了服务器异常和本地异常 服务器异常包括404,500等 本地异常情况更多,包括网络错误,连接异常等 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110/** * description: 非服务器产生的异常,比如本地无无网络请求,Json数据解析错误等等. * @author Sorgs. * Created date: 2019/7/30. */class ErrorResumeFunction<T> : Function<Throwable, ObservableSource<out ResponseData<T>>> { override fun apply(throwable: Throwable): ObservableSource<out ResponseData<T>> { return Observable.error(CustomException.handleException(throwable)) }}/** * description: 自定义异常处理,包括解析异常等其他异常. * * @author Sorgs. * Created date: 2019/7/30. */object CustomException { /** * 未知错误 */ const val UNKNOWN = 1000 /** * 解析错误 */ const val PARSE_ERROR = 1001 /** * 网络错误 */ const val NETWORK_ERROR = 1002 /** * 协议错误 */ const val HTTP_ERROR = 1003 fun handleException(e: Throwable): LocalException { val ex: LocalException if (e is JsonParseException || e is JSONException || e is ParseException ) { //解析错误 ex = LocalException(PARSE_ERROR, e.message) return ex } else if (e is ConnectException) { //网络错误 ex = LocalException(NETWORK_ERROR, e.message) return ex } else if (e is UnknownHostException || e is SocketTimeoutException) { //连接错误 ex = LocalException(NETWORK_ERROR, e.message) return ex } else { //未知错误 ex = LocalException(UNKNOWN, e.message) return ex } }}``` - 把异常进行封装成自己的异常处理。提前把Json转化出错,网络错误,连接异常处理出来- 再利用继承ResponseFunction进行处理服务器异常处理,然后把异常进行封装。正确的数据发送到下游```java/** * description: 服务其返回的数据解析 * 正常服务器返回数据和服务器可能返回的exception. * @author Sorgs. * Created date: 2019/7/30. */class ResponseFunction<T> : Function<ResponseData<T>, ObservableSource<T>> { override fun apply(tResponseData: ResponseData<T>): ObservableSource<T> { val code = tResponseData.code val message = tResponseData.msg return if (code == 200) { Observable.just(tResponseData.data) } else { Observable.error(LocalException(code, message)) } }}/** * description: 异常处理. * * @author Sorgs. * Created date: 2019/7/30. */class LocalException : Exception { var code: Int = 0 var displayMessage: String? = null constructor(code: Int, displayMessage: String?) { this.code = code this.displayMessage = displayMessage } constructor(code: Int, message: String, displayMessage: String?) : super(message) { this.code = code this.displayMessage = displayMessage }} 线程切换 网络请求是需要进行放到子线程进行处理防止阻塞主线程,等待网络请求结果回来之后,再进行转化到UI线程处理数据 定义切线接口,然后进行线程切换 123456789101112131415161718192021222324252627282930313233343536373839404142434445/** * description: 切换线程的线程定义 * @author Sorgs. * Created date: 2019/7/30. */interface BaseSchedulerProvider { fun computation(): Scheduler fun io(): Scheduler fun ui(): Scheduler fun <T> applySchedulers(): ObservableTransformer<T, T>}/** * description: 完成处理线程切换. * @author Sorgs. * Created date: 2019/7/30. */class SchedulerProvider : BaseSchedulerProvider { override fun computation(): Scheduler { return Schedulers.computation() } override fun io(): Scheduler { return Schedulers.io() } override fun ui(): Scheduler { return AndroidSchedulers.mainThread() } override fun <T> applySchedulers(): ObservableTransformer<T, T> { return ObservableTransformer { observable -> observable.subscribeOn(io()).observeOn(ui()) } } companion object { val schedulerProvider: SchedulerProvider by lazy( mode = LazyThreadSafetyMode.SYNCHRONIZED) { SchedulerProvider() } }} 调用方使用123456789NetWorkManager.request ?.getSorgs(\"1\") ?.compose(ResponseTransformer.handleResult()) ?.compose(SchedulerProvider.schedulerProvider.applySchedulers()) ?.subscribe({ javabean -> Log.i(\"sorgs\", \"btnTest1 name:${javabean?.name}\") }, { t -> Log.e(\"sorgs\", \"btnTest1 e:${(t as LocalException).displayMessage}\") }) 直接利用subscribe返回得到处理数据后的结果,拿到数据直接进行业务逻辑代码边写。对于错误,直接将服务器错误和本地异常都抛出来,由调用方选择进行处理 结语 封装成库的好处就是方便调用者,利用最简单的方式进行复杂而常用的操作。 同时对接口透明,对实现封装,实现对调用者最大的友好 demo:https://github.com/sorgs/Network","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"},{"name":"网络请求","slug":"网络请求","permalink":"http://sorgs.cn/tags/网络请求/"},{"name":"Retorfit","slug":"Retorfit","permalink":"http://sorgs.cn/tags/Retorfit/"},{"name":"Rxjava","slug":"Rxjava","permalink":"http://sorgs.cn/tags/Rxjava/"}]},{"title":"Android每日一问笔记-Looper.loop为什么不会阻塞掉UI线程?","date":"2019-07-27T09:32:03.000Z","path":"post/35154/","text":"基于https://www.wanandroid.com每日一问的笔记,做一些整理,方便自己进行查看和记忆。原文链接:https://www.wanandroid.com/wenda/show/8685 Android中为什么主线程不会因为Looper.loop()里的死循环卡死? 这里涉及线程,先说说说进程/线程 进程:每个app运行时前首先创建一个进程,该进程是由Zygote fork出来的,用于承载App上运行的各种Activity/Service等组件。进程对于上层应用来说是完全透明的,这也是google有意为之,让App程序都是运行在Android Runtime。大多数情况一个App就运行在一个进程中,除非在AndroidManifest.xml中配置Android:process属性,或通过native代码fork进程。 线程:线程对应用来说非常常见,比如每次new Thread().start都会创建一个新的线程。该线程与App所在进程之间资源共享,从Linux角度来说进程与线程除了是否共享资源外,并没有本质的区别,都是一个task_struct结构体,在CPU看来进程或线程无非就是一段可执行的代码,CPU采用CFS调度算法,保证每个task都尽可能公平的享有CPU时间片。 有了这么准备,再说说死循环问题: 对于线程既然是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。 真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死 没看见哪里有相关代码为这个死循环准备了一个新线程去运转? 事实上,会在进入死循环之前便创建了新binder线程,在代码ActivityThread.main()中: 1234567891011121314public static void main(String[] args) { .... //创建Looper和MessageQueue对象,用于处理主线程的消息 Looper.prepareMainLooper(); //创建ActivityThread对象 ActivityThread thread = new ActivityThread(); //建立Binder通道 (创建新线程) thread.attach(false); Looper.loop(); //消息循环运行 throw new RuntimeException(\"Main thread loop unexpectedly exited\"); } thread.attach(false);便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程,具体过程可查看 startService流程分析,这里不展开说,简单说Binder用于进程间通信,采用C/S架构。关于binder感兴趣的朋友,可查看我回答的另一个知乎问题:为什么Android要采用Binder作为IPC机制? - Gityuan的回答 另外,ActivityThread实际上并非线程,不像HandlerThread类,ActivityThread并没有真正继承Thread类,只是往往运行在主线程,该人以线程的感觉,其实承载ActivityThread的主线程就是由Zygote fork而创建的进程。 主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,详情见Android消息机制1-Handler(Java层),此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。 Activity的生命周期是怎么实现在死循环体外能够执行起来的? ActivityThread的内部类H继承于Handler,通过handler消息机制,简单说Handler机制用于同一个进程的线程间通信。 Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施:在H.handleMessage(msg)方法中,根据接收到不同的msg,执行相应的生命周期。 比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法; 再比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。 上述过程,我只挑核心逻辑讲,真正该过程远比这复杂。 主线程的消息又是哪来的呢?当然是App进程中的其他线程通过Handler发送给主线程,请看接下来的内容: 最后,从进程与线程间通信的角度,通过一张图加深大家对App运行过程的理解: image system_server进程是系统进程,java framework框架的核心载体,里面运行了大量的系统服务,比如这里提供ApplicationThreadProxy(简称ATP),ActivityManagerService(简称AMS),这个两个服务都运行在system_server进程的不同线程中,由于ATP和AMS都是基于IBinder接口,都是binder线程,binder线程的创建与销毁都是由binder驱动来决定的。 App进程则是我们常说的应用程序,主线程主要负责Activity/Service等组件的生命周期以及UI相关操作都运行在这个线程; 另外,每个App进程中至少会有两个binder线程 ApplicationThread(简称AT)和ActivityManagerProxy(简称AMP),除了图中画的线程,其中还有很多线程,比如signal catcher线程等,这里就不一一列举。 Binder用于不同进程之间通信,由一个进程的Binder客户端向另一个进程的服务端发送事务,比如图中线程2向线程4发送事务;而handler用于同一个进程中不同线程的通信,比如图中线程4向主线程发送消息。 结合图说说Activity生命周期,比如暂停Activity,流程如下: 线程1的AMS中调用线程2的ATP;(由于同一个进程的线程间资源共享,可以相互直接调用,但需要注意多线程并发问题) 线程2通过binder传输到App进程的线程4; 线程4通过handler消息机制,将暂停Activity的消息发送给主线程; 主线程在looper.loop()中循环遍历消息,当收到暂停Activity的消息时,便将消息分发给ActivityThread.H.handleMessage()方法,再经过方法的调用,最后便会调用到Activity.onPause(),当onPause()处理完后,继续循环loop下去。","tags":[{"name":"每日一问笔记","slug":"每日一问笔记","permalink":"http://sorgs.cn/tags/每日一问笔记/"},{"name":"面试","slug":"面试","permalink":"http://sorgs.cn/tags/面试/"},{"name":"笔记","slug":"笔记","permalink":"http://sorgs.cn/tags/笔记/"}]},{"title":"Android每日一问笔记-Handler简述","date":"2019-07-26T11:52:08.000Z","path":"post/5343/","text":"基于每日一问的笔记,做一些整理,方便自己进行查看和记忆。nanchen的文章 Handler 的简单使用1234567891011121314151617181920212223242526272829303132333435363738394041424344override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main3) // 请求网络 subThread.start()}override fun onDestroy() { subThread.interrupt() super.onDestroy()}private val handler by lazy(LazyThreadSafetyMode.NONE) { MyHandler() }private val subThread by lazy(LazyThreadSafetyMode.NONE) { SubThread(handler) }private class MyHandler : Handler() { override fun handleMessage(msg: Message) { super.handleMessage(msg) // 主线程处理逻辑,一般这里需要使用弱引用持有 Activity 实例,以免内存泄漏 }}private class SubThread(val handler: Handler) : Thread() { override fun run() { super.run() // 耗时操作 比如做网络请求 // 网络请求完毕,咱们就得哗哗哗通知 UI 刷新了,直接直接考虑 Handler 处理,其他方案暂时不做考虑 // 第一种方法,一般这个 data 是请求结果解析的内容 handler.obtainMessage(1,data).sendToTarget() // 第二种方法 val message = Message.obtain() // 尽量使用 Message.obtain() 初始化 message.what = 1 message.obj = data // 一般这个 data 是请求结果解析的内容 handler.sendMessage(message) // 第三种方法 handler.post(object : Thread() { override fun run() { super.run() // 处理更新操作 } }) }} 上述代码非常简单,因为网络请求是一个耗时任务,所以我们新开了一个线程,并在网络请求结束解析完毕后通过 Handler 来通知主线程去更新 UI,简单采用了 3 种方式,细心的小伙伴可能会发现,其实第一种和第二种方法是一样的。就是利用 Handler 来发送了一个携带了内容 Message 对象,值得一提的是:我们应该尽可能地使用 Message.obtain() 而不是 new Message() 进行 Message 的初始化,主要是 Message.obtain() 可以减少内存的申请 123456789101112131415161718public boolean sendMessageAtTime(Message msg, long uptimeMillis) { MessageQueue queue = mQueue; if (queue == null) { RuntimeException e = new RuntimeException( this + \" sendMessageAtTime() called with no mQueue\"); Log.w(\"Looper\", e.getMessage(), e); return false; } return enqueueMessage(queue, msg, uptimeMillis);}private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { msg.target = this; if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis);} 代码出现了一个 MessageQueue,并且最终调用了 MessageQueue#enqueueMessage方法进行消息的入队 MessageQueue MessageQueue 就是消息队列,即存放多条消息 Message 的容器,它采用的是单向链表数据结构,而非队列。它的 next() 指向链表的下一个 Message 元素。从入队消息 enqueueMessage() 的实现来看,它的主要操作其实就是单链表的插入操作. 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889boolean enqueueMessage(Message msg, long when) { // ... 省略一些检查代码 synchronized (this) { // ... 省略一些检查代码 msg.markInUse(); msg.when = when; Message p = mMessages; boolean needWake; if (p == null || when == 0 || when < p.when) { // New head, wake up the event queue if blocked. msg.next = p; mMessages = msg; needWake = mBlocked; } else { // Inserted within the middle of the queue. Usually we don't have to wake // up the event queue unless there is a barrier at the head of the queue // and the message is the earliest asynchronous message in the queue. needWake = mBlocked && p.target == null && msg.isAsynchronous(); Message prev; for (;;) { prev = p; p = p.next; if (p == null || when < p.when) { break; } if (needWake && p.isAsynchronous()) { needWake = false; } } msg.next = p; // invariant: p == prev.next prev.next = msg; } // We can assume mPtr != 0 because mQuitting is false. if (needWake) { nativeWake(mPtr); } } return true;}Message next() { // ... int nextPollTimeoutMillis = 0; for (;;) { // ... nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // Try to retrieve the next message. Return if found. final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages; if (msg != null && msg.target == null) { // Stalled by a barrier. Find the next asynchronous message in the queue. do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } if (msg != null) { if (now < msg.when) { // Next message is not ready. Set a timeout to wake up when it is ready. nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else { // Got a message. mBlocked = false; if (prevMsg != null) { prevMsg.next = msg.next; } else { mMessages = msg.next; } msg.next = null; if (DEBUG) Log.v(TAG, \"Returning message: \" + msg); msg.markInUse(); return msg; } } else { // No more messages. nextPollTimeoutMillis = -1; } //... } //... // While calling an idle handler, a new message could have been delivered // so go back and look again for a pending message without waiting. nextPollTimeoutMillis = 0; }} next() 方法其实很长,不过我们仅仅贴了极少的一部分,可以看到,里面不过是有一个for (;;)的无限循环,循环体内部调用了一个 nativePollOnce(long, int) 方法。这是一个 Native 方法,实际作用是通过 Native 层的 MessageQueue 阻塞当前调用栈线程 nextPollTimeoutMillis 毫秒的时间。 下面是 nextPollTimeoutMillis 取值的不同情况的阻塞表现: 小于 0,一直阻塞,直到被唤醒; 等于 0,不会阻塞; 大于 0,最长阻塞 nextPollTimeoutMillis 毫秒,期间如被唤醒会立即返回。 可以看到,最开始 nextPollTimeoutMillis 的初始化值是 0,所以不会阻塞,会直接去取 Message 对象,如果没有取到 Message 对象数据,则直接会把 nextPollTimeoutMillis 置为 -1,此时满足小于 0 的条件,会被一直阻塞,直到其他地方调用另外一个 Native 方法 nativeWake(long) 进行唤醒。如果取到值的话,会直接把得到的 Message 对象进行返回。 nativeWake(long) 方法在前面的 MessageQueue#enqueueMessage 方法有个调用,调用时机是在 MessageQueue 入队消息的过程中 Handler 发送了 Message,消息用MessageQueue进行存储,使用MessageQueue#enqueueMessage 方法进行入队,使用MessageQueue#next方法进行轮训消息。这就不免抛出了一个问题,MessageQueue#next 方法是谁调用的?没错,就是 Looper。Looper Looper 在 Android 的消息机制中扮演着消息循环的角色,具体来说就是它会不停地从 MessageQueue 通过 next() 查看是否有新消息,如果有新消息就立刻处理,否则就任由 MessageQueue 阻塞在那里。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException(\"No Looper; Looper.prepare() wasn't called on this thread.\"); } // ... for (;;) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } //... try { // 分发消息给 handler 处理 msg.target.dispatchMessage(msg); dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; } finally { // ... } // ... }}public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); }}private static void handleCallback(Message message) { message.callback.run();}public static @Nullable Looper myLooper() { return sThreadLocal.get();}static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); 先会通过 myLooper() 方法得到 Looper 对象,如果这个 Looper 返回为空的话,则直接抛出异常。否则进入到一个 for (;;) 循环中,调用 MessageQueue#next() 方法进行轮训获取 Message 对象,如果获取的 Message 对象为空,则直接退出 loop() 方法。否则直接通过 msg.target拿到 Handler 对象,并调用 Handler#dispatchMessage() 方法。 如果 Message 设置了 callback 则,直接调用 message.callback.run(),否则判断是否初始化了 mCallbackThreadLocal ThreadLocal 是用来存储指定线程的数据的,当某些数据的作用域是该指定线程并且该数据需要贯穿该线程的所有执行过程时就可以使用 ThreadnLocal 存储数据,当某线程使用 ThreadnLocal 存储数据后,只有该线程可以读取到存储的数据,除此线程之外的其他线程是没办法读取到该数据的。 举个栗子: 1234567891011121314151617181920212223ThreadLocal<Boolean> local = new ThreadLocal<>();// 设置初始值为true.local.set(true);Boolean bool = local.get();Logger.i(\"MainThread读取的值为:\" + bool);new Thread() { @Override public void run() { Boolean bool = local.get(); Logger.i(\"SubThread读取的值为:\" + bool); // 设置值为false. local.set(false); }}.start():// 主线程睡1秒,确保上方子线程执行完毕再执行下面的代码。Thread.sleep(1000);Boolean newBool = local.get();Logger.i(\"MainThread读取的新值为:\" + newBool); 第一条 Log 无可置疑,因为设置了值为 true,因为打印结果没什么好说的。对于第二条 Log,根据上方介绍,某线程使用 ThreadLocal 存储的数据,只能被该线程读取,因此第二条 Log 的结果是:null。紧接着在子线程中设置了 ThreadLocal 的值为 false,然后第三条 Log 将被打印,原理同上,子线程中设置了 ThreadLocal 的值并不影响主线程的数据,所以打印是 true。 实验结果证实:就算是同一个 ThreadLocal 对象,任一线程对其的 set() 和 get() 方法的操作都是相互独立互不影响的。 Looper.myLooper()12345678910public static void prepare() { prepare(true);}private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException(\"Only one Looper may be created per thread\"); } sThreadLocal.set(new Looper(quitAllowed));} 这就是在子线程中使用 Handler 前,必须要调用 Looper.prepare() 的原因。 可能你会疑问,我在主线程使用的时候,没有要求 Looper.prepare() 呀。原来,我们在 ActivityThread 中,有去显示调用 Looper.prepareMainLooper(): 1234567891011public static void main(String[] args) { // ... Looper.prepareMainLooper(); // ... if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); } //... Looper.loop(); // ... } 我们看看 Looper.prepareMainLooper(): 123456789public static void prepareMainLooper() { prepare(false); synchronized (Looper.class) { if (sMainLooper != null) { throw new IllegalStateException(\"The main Looper has already been prepared.\"); } sMainLooper = myLooper(); }}","tags":[{"name":"每日一问笔记","slug":"每日一问笔记","permalink":"http://sorgs.cn/tags/每日一问笔记/"},{"name":"面试","slug":"面试","permalink":"http://sorgs.cn/tags/面试/"},{"name":"笔记","slug":"笔记","permalink":"http://sorgs.cn/tags/笔记/"}]},{"title":"Android每日一问笔记-View中的getContext一定返回的是Activity对象吗?","date":"2019-07-25T11:29:13.000Z","path":"post/39723/","text":"基于https://www.wanandroid.com每日一问的笔记,做一些整理,方便自己进行查看和记忆。 原文链接:https://www.wanandroid.com/wenda/show/8626以及nanchen的文章 不一定是 那么,在什么场景下不是呢: 除了自己手动传不是Activity的Context进去之外,还有一种情况,就是:当使用AppCompatActivity 时。 我们都知道,在这个Activity里的原生控件(如TextView, ImageView等等),当在LayoutInflater中把xml解析成View的时候,最终会经过AppCompatViewInflater的createView方法: 把这些原生控件都变成AppCompatXXX一类的!比如TextView的话,就会变成AppCompatTextView, ImageView会变成AppCompatImageView 。 当然了,这些AppCompat开头的,都是继承于被转换的那个对象的。 那重点就在这些AppCompat开头的控件了,随便打开一个他们源码,比如AppCompatImageView 打开之后会看到: 当它们调用父类的构造方法时,调用了TintContextWrappe 看这个方法的名字, wrap很明显就是包装的意思嘛,点进去wrap方法看,还会看到首先调用了shouldWrap方法: 检查一下这个context应不应该被包装。 如果方法返回true, 会创建一个TintContextWrapper对象(把Context传进去),然后返回,那么,这时候,当我们调用这个View的getContext方法,自然就不是Activity了,而是它传进去的TintContextWrapper。 那么,究竟什么情况下,shouldWrap方法会返回true呢(Context会被包装), 点开看下源码: 如果它已经被包装过了,那么就不需要继续包装,即返回false了。 如果没有被包装过,并且Build.VERSION.SDK_INT<21(也就是5.0之前的版本),就会返回true。 得出结论: 当运行在5.0系统版本以下的手机,并且Activity是继承自AppCompatActivity的,那么View的getConext方法,返回的就不是Activity而是TintContextWrapper. 首先,显而易见这个问题有不少陷阱,比如这个View是我们自己构造出来的,那肯定它的getContext()返回的是我们构造它的时候传入的 Context 类型。 但是View.getContext()它也可能返回的是TintContextWrapper 直接继承 Activity 的 Activity 构造出来的View.getContext()返回的是当前 Activity。但是:当 View 的 Activity 是继承自 AppCompatActivity,并且在 5.0 以下版本的手机上,View.getContext() 得到的并非是 Activity,而是 TintContextWrapper。 image Activity.setContentView() 看看Activity.setContentView()方法。不过是直接调用 Window 的实现类 PhoneWindow 的 setContentView() 方法 1234public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar();} 看看 PhoneWindow 的 setContentView() 是怎样的 12345678910111213141516171819202122232425@Overridepublic void setContentView(int layoutResID) { // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else { mLayoutInflater.inflate(layoutResID, mContentParent); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } mContentParentExplicitlySet = true;} 假如我们没有FEATURE_CONTENT_TRANSITIONS标记的话,我们直接通过mLayoutInflater.inflate()加载出来。这个如果有 mLayoutInflater 的是在PhoneWindow 的构造方法中被初始化的。而 PhoneWindow 的初始化是在 Activity的attach() 方法中: 1234567891011121314151617final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window, ActivityConfigCallback activityConfigCallback) { attachBaseContext(context); mFragments.attachHost(null /*parent*/); mWindow = new PhoneWindow(this, window, activityConfigCallback); mWindow.setWindowControllerCallback(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this); // 此处省略部分代码...} 所以 PhoneWindow 的 Context 实际上就是 Activity 本身 回到我们前面分析的 PhoneWindow 的 setContentView() 方法,如果有 FEATURE_CONTENT_TRANSITIONS 标记,我们直接调用了一个 transitionTo() 方法: 123456789101112131415161718192021222324252627private void transitionTo(Scene scene) { if (mContentScene == null) { scene.enter(); } else { mTransitionManager.transitionTo(scene); } mContentScene = scene;}public void enter() { // Apply layout change, if any if (mLayoutId > 0 || mLayout != null) { // empty out parent container before adding to it getSceneRoot().removeAllViews(); if (mLayoutId > 0) { LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot); } else { mSceneRoot.addView(mLayout); } } // Notify next scene that it is entering. Subclasses may override to configure scene. if (mEnterAction != null) { mEnterAction.run(); } setCurrentScene(mSceneRoot, this);} 还是通过这个 mContext 的 LayoutInflater 去 inflate 的布局。这个 mContext 初始化的地方是: 12345678910111213141516public static Scene getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) { SparseArray<Scene> scenes = (SparseArray<Scene>) sceneRoot.getTag( com.android.internal.R.id.scene_layoutid_cache); if (scenes == null) { scenes = new SparseArray<Scene>(); sceneRoot.setTagInternal(com.android.internal.R.id.scene_layoutid_cache, scenes); } Scene scene = scenes.get(layoutId); if (scene != null) { return scene; } else { scene = new Scene(sceneRoot, layoutId, context); scenes.put(layoutId, scene); return scene; }} 即 Context 来源于我们外面传入的 getContext(),这个 getContext() 返回的就是初始化的 Context 也就是 Activity 本身。 AppCompatActivity.setContentView() AppCompatActivity 的 setContentView() 实现。这个 mDelegate 实际上是一个代理类,由 AppCompatDelegate 根据不同的 SDK 版本生成不同的实际执行类,就是代理类的兼容模式:123456789101112131415161718192021222324252627282930313233343536373839public void setContentView(@LayoutRes int layoutResID) { this.getDelegate().setContentView(layoutResID);}@NonNullpublic AppCompatDelegate getDelegate() { if (this.mDelegate == null) { this.mDelegate = AppCompatDelegate.create(this, this); } return this.mDelegate;}/** * Create a {@link android.support.v7.app.AppCompatDelegate} to use with {@code activity}. * * @param callback An optional callback for AppCompat specific events */public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) { return create(activity, activity.getWindow(), callback);}private static AppCompatDelegate create(Context context, Window window, AppCompatCallback callback) { final int sdk = Build.VERSION.SDK_INT; if (BuildCompat.isAtLeastN()) { return new AppCompatDelegateImplN(context, window, callback); } else if (sdk >= 23) { return new AppCompatDelegateImplV23(context, window, callback); } else if (sdk >= 14) { return new AppCompatDelegateImplV14(context, window, callback); } else if (sdk >= 11) { return new AppCompatDelegateImplV11(context, window, callback); } else { return new AppCompatDelegateImplV9(context, window, callback); }} 简单总结 之所以能得到上面的结论,是因为我们在 AppCompatActivity 里面的 layout.xml 文件里面使用原生控件,比如 TextView、ImageView 等等,当在 LayoutInflater 中把 XML 解析成 View 的时候,最终会经过 AppCompatViewInflater 的 createView() 方法,这个方法会把这些原生的控件都变成 AppCompatXXX 一类。 包含了: RatingBar CheckedTextView MultiAutoCompleteTextView TextView ImageButton SeekBar Spinner RadioButton ImageView AutoCompleteTextView CheckBox EditText Button 那么重点肯定就是在 AppCompat这些开头的控件了,随便打开一个源码.可以看到,关键是super(TintContextWrapper.wrap(context), attrs, defStyleAttr);这行代码。shouldWrap() 这个方法返回为 true 的时候,就会采用了 TintContextWrapper 这个对象来包裹了我们的 Context。如果是 5.0 以前,并且没有包装的话,就会直接返回 true。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(TintContextWrapper.wrap(context), attrs, defStyleAttr); this.mBackgroundTintHelper = new AppCompatBackgroundHelper(this); this.mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr); this.mTextHelper = new AppCompatTextHelper(this); this.mTextHelper.loadFromAttributes(attrs, defStyleAttr); this.mTextHelper.applyCompoundDrawablesTints();}public static Context wrap(@NonNull Context context) { if (shouldWrap(context)) { Object var1 = CACHE_LOCK; synchronized(CACHE_LOCK) { if (sCache == null) { sCache = new ArrayList(); } else { int i; WeakReference ref; for(i = sCache.size() - 1; i >= 0; --i) { ref = (WeakReference)sCache.get(i); if (ref == null || ref.get() == null) { sCache.remove(i); } } for(i = sCache.size() - 1; i >= 0; --i) { ref = (WeakReference)sCache.get(i); TintContextWrapper wrapper = ref != null ? (TintContextWrapper)ref.get() : null; if (wrapper != null && wrapper.getBaseContext() == context) { return wrapper; } } } TintContextWrapper wrapper = new TintContextWrapper(context); sCache.add(new WeakReference(wrapper)); return wrapper; } } else { return context; }}private static boolean shouldWrap(@NonNull Context context) { if (!(context instanceof TintContextWrapper) && !(context.getResources() instanceof TintResources) && !(context.getResources() instanceof VectorEnabledTintResources)) { return VERSION.SDK_INT < 21 || VectorEnabledTintResources.shouldBeUsed(); } else { return false; }} 当运行在 5.0 系统版本以下的手机,并且 Activity 是继承自 AppCompatActivity 的,那么View 的 getConext() 方法,返回的就不是 Activity 而是 TintContextWrapper。 其它情况么 上面讲述了两种非 Activity 的情况: 直接构造 View 的时候传入的不是 Activity; 使用 AppCompatActivity 并且运行在 5.0 以下的手机上,XML 里面的 View 的 getContext() 方法返回的是 TintContextWrapper。 实际上,View.getContext() 和 inflate 这个 View 的 LayoutInflater 息息相关,比如 Activity 的 setContentView() 里面的 LayoutInflater 就是它本身,所以该 layoutRes 里面的 View.getContext() 返回的就是 Activity。但在使用 AppCompatActivity 的时候,值得关注的是, layoutRes 里面的原生 View 会被自动转换为 AppCompatXXX,而这个转换在 5.0 以下的手机系统中,会把 Context 转换为其包装类 TintThemeWrapper,所以在这样的情况下的 View.getContext() 返回是 TintThemeWrapper。","tags":[{"name":"每日一问笔记","slug":"每日一问笔记","permalink":"http://sorgs.cn/tags/每日一问笔记/"},{"name":"面试","slug":"面试","permalink":"http://sorgs.cn/tags/面试/"},{"name":"笔记","slug":"笔记","permalink":"http://sorgs.cn/tags/笔记/"}]},{"title":"Android每日一问笔记-对于事件分发,嵌套滚动的了解","date":"2019-07-16T09:31:01.000Z","path":"post/35340/","text":"基于https://www.wanandroid.com每日一问的笔记,做一些整理,方便自己进行查看和记忆。 原文链接:https://www.wanandroid.com/wenda/show/8463以及nanchen的文章 事件分发原理 事件分发,其实就是一个责任链的变种,这个责任链,是一个设计模式。 在Android中,当最顶层的View收到事件之后,会一级一级地往下传,在每一级View中,它们各自都有权利去处理(也就是拦截)这个事件,如果这次的事件传到了最底层的View,也没能处理的话,就会从这个最底层的View一级一级地向上传回去。嵌套滚动 这个嵌套滚动,说的应该不是普通的嵌套滚动(比如ScrollView套ListView),而是说NestedScrollingParent和NestedScrollingChild,这两个东西,出来挺久了,可能好多同学还是觉得有点陌生,但我们在很多场景下,已经在不识不觉中使用它了,比如说CoordinatorLayout,它是一个NestedScrollingParent,还有RecycleView,它是一个NestedScrollingChild。一个最常见的效果:列表向上滚动,ToolBar收起,反之,当列表向下滚动时,ToolBar随着列表的滚动出现。这个效果,用NestedScrolling来实现,可以非常简单。嵌套滚动原理 它的原理,很简单:在NestedScrollingChild滚动过程中,它和NestedScrollingParent会一直”保持通讯”,比如: 当Child滚动之前,会通知Parent:”我要开始滚动啦,你看你要不要做点什么”。 当Child在滚动的时候,也会每次通知Parent:”我这次消费了xxx,你看你还要做什么”。 当Child滚动完成,Parent也会收到通知:”我滚动完成了”。 除了手指触摸滚动的,还有惯性滚动,但原理和流程是一样的。 至于为什么嵌套滚动有必要存在,我觉得有以下几个原因: 减少工作量,比如说一些看似很复杂滚动效果,在使用NestedScrolling机制之后,就变得简单起来了。 降低耦合度,在NestedScrolling机制出现之前,很多与子View有滚动交互的ViewGroup,大部分处理滚动的代码,都堆积这个ViewGroup中。而推出了NestedScrolling之后,这个滚动的子View,由被动方,变成了主动方(滚动的状态都是由这个子View去决定,不再需要ViewGroup去主动判断)。 增加灵活性,CoordinatorLayout的强大,相信同学们都体会到了,它可以通过设置各种不同的Behavior,来定制它的交互效果。 简单介绍 View 的时间分发机制 当然,这里也可以简单地提一下,基本的流程就是下面的伪代码。 123456789public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; if (onInterceptTouchEvent(ev)) { consume = onTouchEvent(ev); }else{ consume = child.dispatchTouchEvent(ev); } return consume;} 当一个 ViewGroup 接收到一个事件的时候,首先会调用 dispatchTouchEvent() 方法进行事件分发,如果 onInterceptTouchEvent() 返回 true,则代表当前 View 会拦截事件,则直接回调 onTouchEvent() 方法进行事件处理。如果不拦截,则直接回调子 View 的 dispatchTouchEvent() 方法,如此反复,一直到最里面的子 View。 当一个点击事件产生后,它的传递过程遵循以下顺序:Activity => Window => View,即事件总是先传递给 Activity,Activity 再传递给 Window,最后 Window 再传递给顶层 DocorView,然后遵循上面的方式一直在最里层 View。 而处理事件则从最里层 View 不断回传给自己的外层 View,如果一直没有 View 进行处理,则直接会回传到 Activity 中。1onTouchEvent() 返回 true 代表自己要处理。 既然都提了这么一点,也就突然想给出一些结论,参考自 Android 开发艺术探索: 同一个事件序列是指从手指接触屏幕(ACTION_DOWN)的那一刻起,到手指离开屏幕(ACTION_UP)的那一刻结束,中间含不定数量的 ACTION_MOVE 事件。 某个 View 一旦决定拦截事件,那么这一个事件序列都只能由它处理,并且它的 onInterceptTouchEvent() 方法也不会再调用。换句话说,比如一个 ViewGroup 里面有数个子 View,一旦 ACTION_DOWN 事件从 Activity 传到这个 ViewGroup 被其拦截,则后续的 MOVE 和 UP 等事件也不会传递到里面的子 View 中。 如果一个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件,即 onTouchEvent() 返回为 false,那么同一事件序列中的其他事件也不会再交给它处理,直接会调用其父 View 的 onTouchEvent()。 如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent() 并不会被调用,并且当然 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。 ViewGroup 默认不拦截事件,View 没有 onInterceptTouchEvent() 方法,一旦有事件传递给它,则直接会调用 onTouchEvent(),并且起默认都会消耗掉事件。除非它是不可点击的(即 clickable 和 longClickable 均为 false)。View 的 longClickable 默认都为 false,而 clickable 分情况,比如 Button 默认为 true,TextView 默认为 false。 View 的 enable 属性不会影响 onTouchEvent() 的默认返回值,哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable 有一个为 true,那么它的 onTouchEvent() 就会返回 true。 requestDisallowInterceptTouchEvent() 可以在子元素中干预父元素的事件分发过程,但是无法干预 ACTION_DOWN 事件。 事件优先顺序:setOnTouchListener() => onTouchEvent() => onClickListener()处理自定义 View 中的滑动冲突— 对于大多数 Android 开发来说,处理滑动冲突好像很难,但实战一下又发现,好像也挺简单,因为这个实际上是有套路可循的。基本就两种方案:外部拦截法 && 内部拦截法外部拦截法 所谓外部拦截法,顾名思义,就是直接在父容器中直接拦截掉我们的滑动事件,让其不能进入到子元素中,这似乎和我们 RecyclerView 嵌套 RecyclerView 时禁用内部 RecyclerView 滑动有那么一丝相似之处,就是内部不处理就完事儿了。但细细品来又完全不一样,这里的外部拦截法会让内部元素根本就收不到滑动事件。 这种方法明显非常适合我们上面讲的事件分发机制。我们在接收 ACTION_MOVE 事件的时候,直接通过使 onInterceptTouchEvent() 方法返回 true 来直接拦截掉事件就可以了,伪代码想必大家也知道了: 12345678override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { ev?.run { if (action == MotionEvent.ACTION_MOVE && 父容器需要点击事件){ return true } } return super.onInterceptTouchEvent(ev)} 代码很简单,我们仅仅需要在事件ACTION_MOVE时去处理我们的逻辑就好了,当满足我们的逻辑的时候,就拦截掉 ACTION_MOVE 事件给自己处理。 至于为什么不去拦截 ACTION_DOWN 和 ACTION_UP,想必大家也清楚了。上面说了,如果拦截了 ACTION_DOWN 事件,那后续的 ACTION_MOVE、ACTION_UP 等其它事件均不会在调用 onInterceptTouchEvent() 方法,会直接交给当前容器处理。而如果我们拦截掉 ACTION_UP 的话,肯定会导致子元素的点击事件无法被处理,因为大家肯定都知道一个点击事件从 ACTION_DOWN 开始,从 ACTION_UP 结束,二者缺一不可。内部拦截法 内部拦截法相对外部拦截法会复杂一些,所以我们通常来说,都更加推荐用外部拦截法进行处理。不过,内部拦截法依然有着它非常重要的地位,具体情况有可能会遇到。 内部拦截法的话,需要 requestDisallowInterceptTouchEvent() 方法的支持,这个方法是干什么的呢?顾名思义,请求是否不允许拦截事件,其接收一个 boolean 参数,表示是否不允许拦截。 我们直接重写子元素的 dispatchTouchEvent() 方法,得到伪代码如下: 12345678910111213override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { ev?.run { when(action){ MotionEvent.ACTION_DOWN -> parent.requestDisallowInterceptTouchEvent(true) MotionEvent.ACTION_MOVE ->{ if(满足需要让外部容器拦截事件){ parent.requestDisallowInterceptTouchEvent(false) } } } } return super.dispatchTouchEvent(ev)} 我们给父容器的 requestDisallowInterceptTouchEvent() 传递的参数代表是否不允许其拦截事件,当参数为 true 的时候代表不允许拦截,为 false 的时候代表拦截。所以看起来和外部拦截法也就如出一辙了。 不过仅仅有这点修改还不够,我们通过前面的理论基础知道,当我们的父容器拦截掉 ACTION_DOWN 事件的时候,所有的事件都无法再传递到子元素中,自然也就不会调用上面我们写的 dispatchTouchEvent() 方法了。所以我们在内部拦截法的时候还需要重写父容器的 onInterceptTouchEvent() 方法。12345678 override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { ev?.run { if (action == MotionEvent.ACTION_DOWN){ return false } } return super.onInterceptTouchEvent(ev)}","tags":[{"name":"每日一问笔记","slug":"每日一问笔记","permalink":"http://sorgs.cn/tags/每日一问笔记/"},{"name":"面试","slug":"面试","permalink":"http://sorgs.cn/tags/面试/"},{"name":"笔记","slug":"笔记","permalink":"http://sorgs.cn/tags/笔记/"}]},{"title":"Android中JNI调用第三方so以及头文件方式","date":"2019-05-19T13:05:57.000Z","path":"post/7510/","text":"引言有时候我们在android开发JNI的时候,会涉及到引用第三方的so和头文件引用。现在网上也有相应的资料,但是还是感觉不全和描述不清晰。这里进行整理一些,方便大家参考。 准备工作 NDK,进行JNI开发,Android studio中的NDK肯定是需要配好的。需要注意一点的是,如果上比较新的NDK版本的话,在toolchains目录会少几种,需要去下载比较旧的版本把缺失的放进去。原因大概是Google已经放弃哪几种了。这个主要是针对比较老的工程会遇得到,也会有报错信息,搜一下很容易就知道了,就不展开说了。 cMake和cpp。一般来说进行了JNI开发了,这些应该是有了,不再细说。只说下目录,cpp可以建一个cpp文件夹放在main文件夹下面,cMake需要放在app目录下面。详情目录结构可以参考Demo。 build.gradle 首先是在defaultConfig闭包类添加如下内容。我这边是生成了armeabi-v7a的格式,如需要其他格式,自行添加即可。 123456externalNativeBuild { cmake { cppFlags "" abiFilters 'armeabi-v7a' }} 在android闭包下面,即最大的闭包下面添加 12345678910111213externalNativeBuild { cmake { path "CMakeLists.txt" version "3.10.2" }}sourceSets { main { // let gradle pack the shared library into apk jniLibs.srcDirs = ['src/main/jniLibs'] }} 文件放置放置 so文件:在main目录下面建立jniLibs文件夹。然后在下面在建立armeabi-v7a文件夹,把相应的so文件放到里面。需要注意的是,在自己需要生成什么类型的so,就需要建立什么类的文件夹,然后拷入相应类型第三方so文件。 头文件:在cpp目录下面建立include文件夹,放入第三方头文件即可。 cMake编写1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465# For more information about using CMake with Android Studio, read the# documentation: https://d.android.com/studio/projects/add-native-code.html# Sets the minimum version of CMake required to build the native library.cmake_minimum_required(VERSION 3.4.1)# Creates and names a library, sets it as either STATIC# or SHARED, and provides the relative paths to its source code.# You can define multiple libraries, and CMake builds them for you.# Gradle automatically packages shared libraries with your APK.add_library( # Sets the name of the library. native-lib # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). src/main/cpp/native-lib.cpp)#动态方式加载 STATIC:表示静态的.a的库 SHARED:表示.so的库。add_library(gmpfprojectorfocusmanager_hidl SHARED IMPORTED)add_library(utils SHARED IMPORTED)add_library(hidlbase SHARED IMPORTED)add_library(hwbinder SHARED IMPORTED)add_library(hidltransport SHARED IMPORTED)add_library(hidlmemory SHARED IMPORTED)#设置要连接的so的相对路径 ${CMAKE_SOURCE_DIR}:表示CMake.txt的当前文件夹路径 ${ANDROID_ABI}:编译时会自动根据CPU架构去选择相应的库set_target_properties(gmpfprojectorfocusmanager_hidl PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libgmpfprojectorfocusmanager_hidl.so)set_target_properties(utils PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libutils.so)set_target_properties(hidlbase PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libhidlbase.so)set_target_properties(hwbinder PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libhwbinder.so)set_target_properties(hidltransport PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libhidltransport.so)set_target_properties(hidlmemory PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libhidlmemory.so)#添加第三方头文件target_include_directories(native-lib PRIVATE ${CMAKE_SOURCE_DIR}/src/main/cpp/include)# Searches for a specified prebuilt library and stores the path as a# variable. Because CMake includes system libraries in the search path by# default, you only need to specify the name of the public NDK library# you want to add. CMake verifies that the library exists before# completing its build.find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log)# Specifies libraries CMake should link to your target library. You# can link multiple libraries, such as libraries you define in this# build script, prebuilt third-party libraries, or system libraries.target_link_libraries( # Specifies the target library. native-lib # Links the target library to the log library gmpfprojectorfocusmanager_hidl utils hidlbase hwbinder hidltransport hidlmemory # included in the NDK. ${log-lib}) add_library:这里主要是依赖第三方的so方式,每个so都要写一句。第一个参数是so的文件。例如libutils.so,则需要填写utils;第二个参数为STATIC:表示静态的.a的库或者SHARED:表示.so的库;第三个参数固定IMPORTED set_target_properties:链接so的路径。第一个参数依然是so的名字;第二个参数填写PROPERTIES即可;第三个填写IMPORTED_LOCATION即可;第四个则需要填写so的路径,需要注意的是会根据自己的需要生成so的类型去查找相应类型的so。 target_include_directories:添加第三方头文件。第一个参数填写native-lib;第二个参数PRIVATE;第三个参数即头文件的文件夹路径。 target_link_libraries:最后需要在这里把第三方so名字加入即可。 引用 在自己的cpp里面就只直接通过include引用第三的文件夹了,以及调用第三方的so文件 结语 建议按照这样的路径来放置,防止出现问题。 demo已经放到了github上面,可以进行参考配置。https://github.com/sorgs/NDKTest","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"},{"name":"so","slug":"so","permalink":"http://sorgs.cn/tags/so/"},{"name":"NDK","slug":"NDK","permalink":"http://sorgs.cn/tags/NDK/"},{"name":"JNI","slug":"JNI","permalink":"http://sorgs.cn/tags/JNI/"}]},{"title":"github发布和维护属于自己的基础工程远程库","date":"2019-03-24T10:34:24.000Z","path":"post/9217/","text":"引言自己动手搭建一个属于自己的远程基础仓库 不管是开发新项目亦或者是自己写demo练练手之类的。都需要建立工程,然后开始拷贝工具类,然后在啪啦啪啦引用必须的三方库,建立Base基类等等。 搞了一段时间之后,觉得实在是太麻烦了,为什么我不建立一个基础工程。然后做成一个远程库,每次建立新的工程之后,直接就引用这个库。一下子自己熟手的工具类,Base基类,甚至常用一些第三方库都OK了。 在平时写代码的时候,也注意收集,比较顺手的东西,直接放到基础库当中去,以后对新工程简直太方便了。 这里只说简单说下流程遇到坑, 建立基础库 建立工程,再new一个module,选择android Library,然后开始编写和搭建自己想放到基础工程的东西。 发布到github上面 在github上面首页点击release-create a new releases(后续发布新版本点击draft a new release),相当于是打一个tag。在Tag version写上版本号,比如 V1.0之类的。下面可以自己随便写点描述。 上https://jitpack.io/ ,输入自己的git仓库,点击look up即可。选择自己发布的版本,点击get it即可。 然后在自己新建工程引用就可以开心使用了。 鉴于网上很多教程,这里不再细说过程,可以直接到https://www.jianshu.com/p/49ea4fa47037 问题 这里想简单说下自己遇到的一些问题 既然是自己建立的基础工程,就不需要app目录等其他module之类的 一定不要在build.gradle里面配置优化压缩等1234567891011121314151617buildTypes { release { //Zipalign优化 zipAlignEnabled true //去除无用资源 shrinkResources true //签名 signingConfig signingConfigs.release //混淆 minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug { //签名 signingConfig signingConfigs.release } } 这些是不需要的。尤其是最优化之类的,很容易造成的问题就是,打aar包没有部分代码打进去,因为在优化之后,没有被调用的函数和类是会被忽略的,而工具类就很容易被优化处理了。混淆的也是不太需要配置,如果一定要配置混淆,则需要注意,把混淆中压缩优化等去除掉。 结语 自己简单的搭建了一个基础工程,目前里面包含了一些常用的第三方库,BaseActivity,BaseFragment等。还有一些常用工具类等,比如Log,Toast等 欢迎大家一起维护和优化,https://github.com/sorgs/project 。如果能够任何地方能够帮助到您,希望可以给个start鼓励鼓励。感谢!","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"},{"name":"基础工程","slug":"基础工程","permalink":"http://sorgs.cn/tags/基础工程/"},{"name":"远程库","slug":"远程库","permalink":"http://sorgs.cn/tags/远程库/"}]},{"title":"整理系类-Java基础-Java关键字汇总","date":"2019-01-02T15:03:09.000Z","path":"post/60878/","text":"整理系类-Java基础 整理于YCBlogs 常见的关键字 用于定义数据类型的关键字 class interface byte short int long float double char boolean void 用于定义数据类型值的关键字 true false null 用于定义流程控制的关键字 if else switch case default while do for break continue return 用于定义访问权限修饰符的关键字 private protected public 用于定义类,函数,变量修饰符的关键字 abstract final static synchronized 用于定义类与类之间关系的关键字 extends implements 用于定义建立实例及引用实例,判断实例的关键字 new this super instanceof 用于异常处理的关键字 try catch finally throw throws 用于包的关键字 package import 其他修饰符关键字 native strictfp transient volatile assert 关键字的作用说明 break 用在 switch 或者循环语句中,表示中断结束的意思,跳出循环直接可以结束该语句 continue 用在循环语句中,表示中断结束的意思,不过跟 break 有区别,它是退出本次循环后继续执行下一次循环 return 常用功能是结束一个方法(退出一个方法),跳转到上层调用的方法 interface 接口的意思,用来定义接口。 static 静态修饰符,被修饰后成员被该类所有的对象所共有。也可以通过类名调用 private 权限修饰符,可以修饰成员变量和成员方法,被修饰的成员只能在本类中被访问。隐藏具体实现细节,提供对外公共访问方法,提高安全性 this 当成员变量和局部变量名称一样时,需要用 this 修饰,谁调用这个方法,那么该方法的内部的this就代表谁,如果不适用 this ,那么局部变量隐藏了成员变量 super 代表的是父类存储空间的标识(可以理解成父类的引用,可以操作父类的成员) final 由于继承中有一个方法重写的现象,而有时候我们不想让子类去重写父类的方法.这对这种情况java就给我们提供了一个关键字: final。可以修饰类,变量,成员方法。 被修饰类不能被继承; 被修饰的方法不能被重写; 被修饰的变量不能被重新赋值,因为这个量其实是一个常量。 修饰基本数据类型,指的是值不能被改变; 修饰引用数据类型,指的是地址值不能被改变 finally 被finally控制的语句体一定会执行;特殊情况:在执行到finally之前jvm退出了(比如System.exit(0)) finally的作用: 用于释放资源,在IO流操作和数据库操作中会见到 abstract 抽象的意思,用来修饰抽象类与抽象方法 abstract 不能和哪些关键字共存? private:冲突,被private修饰的方法不能被子类继承,就不能被重写,而我们的抽象方法还需要被子类重写 final:冲突,被final修饰的方法,不能被子类重写,而我们的抽象方法还需要被子类重写 static:无意义,因为被static修饰的方法可以通过类名直接访问,但是我们的抽象方法没有方法体,所以这样访问没有意思 extends 继承的意思,通过它可以类与类之间产生继承关系。 implements 实现的意思,通过它可以让类与接口之间产生实现关系。 instanceof 测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据 重要关键字说明 instanceof instanceof是Java的一个二元操作符,和==,>,<是同一类东西。由于它是由字母组成的,所以也是Java的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据。 final,finally,finalize有什么不同? final可以修饰类,方法,变量 final修饰类代表类不可以继承拓展 final修饰变量表示变量不可以修改 final修饰方法表示方法不可以被重写 finally则是Java保证重点代码一定要被执行的一种机制 可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC连接、保证 unlock 锁等动作。 finalize 是基础类 java.lang.Object的一个方法 它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9开始被标记为 deprecated。 final 关键字深入理解 可以将方法或者类声明为 final,这样就可以明确告知别人,这些行为是不许修改的。 如果你关注过 Java 核心类库的定义或源码, 有没有发现java.lang 包下面的很多类,相当一部分都被声明成为final class?在第三方类库的一些基础类中同样如此,这可以有效避免 API 使用者更改基础功能,某种程度上,这是保证平台安全的必要手段。 使用 final 修饰参数或者变量,也可以清楚地避免意外赋值导致的编程错误,甚至,有人明确推荐将所有方法参数、本地变量、成员变量声明成 final。 final 变量产生了某种程度的不可变(immutable)的效果,所以,可以用于保护只读数据,尤其是在并发编程中,因为明确地不能再赋值 final 变量,有利于减少额外的同步开销,也可以省去一些防御性拷贝的必要。 static 可以用来修饰:成员变量,成员方法,代码块,内部类等。具体如下所示 修饰成员变量和成员方法 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。 被static 声明的成员变量属于静态成员变量,静态变量存放在Java内存区域的方法区。 静态代码块 静态代码块定义在类中方法外,静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法) 该类不管创建多少对象,静态代码块只执行一次. 静态内部类(static修饰类的话只能修饰内部类) 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围内,但是静态内部类却没有。没有这个引用就意味着:1.它的创建是不需要依赖外围类的创建。2.它不能使用任何外围类的非static成员变量和方法。 静态导包(用来导入类中的静态资源,1.5之后的新特性): 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。","tags":[{"name":"Java基础","slug":"Java基础","permalink":"http://sorgs.cn/tags/Java基础/"},{"name":"Java关键字","slug":"Java关键字","permalink":"http://sorgs.cn/tags/Java关键字/"},{"name":"整理转载","slug":"整理转载","permalink":"http://sorgs.cn/tags/整理转载/"}]},{"title":"kotlin配合dagger2出现的问题","date":"2018-10-14T05:12:03.000Z","path":"post/2500/","text":"最近没事玩玩kotlin,随便学习了一波dagger2,打配合使用下,但是出现了些问题,记录出来 问题 Unresolved reference: DaggerAddFavoriteComponent Compilation error. See log for more details Caused by: org.gradle.api.GradleException: Compilation error. See log for more details org.gradle.api.tasks.TaskExecutionException: Execution failed for task ‘:app:compileDebugKotlin’.配置 kotlin插件 File->Settings->Plugins image 记住版本号 1.2.71 project的build.gradle 1234567891011 buildscript { ext.kotlin_version = '1.2.71' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" }} app目录build.gradle1234567891011121314 apply plugin: 'kotlin-android'apply plugin: 'kotlin-android-extensions'apply plugin: 'kotlin-kapt'kapt { generateStubs = true}...dependencies {implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" //一定需要用annotationProcessor annotationProcessor "com.google.dagger:dagger-compiler:2.15" implementation 'com.google.dagger:dagger:2.15'}","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"},{"name":"kotlin","slug":"kotlin","permalink":"http://sorgs.cn/tags/kotlin/"},{"name":"dagger2","slug":"dagger2","permalink":"http://sorgs.cn/tags/dagger2/"}]},{"title":"自定义签到的步骤View","date":"2018-08-19T07:46:52.000Z","path":"post/388/","text":"*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 引言涉及到一个签到的步骤view 需求:以七天为周天,执行当天签到需要一个动画效果;签到前灰色,签到后变为绿色;每天加的分数不一定,第三天和第七天加的比较多,分数签到完成为橙色,有up标签。 效果图: - 分析 首先是把该绘制的东西绘制到画布上,这点没什么好说,上一遍博客差不多说了怎么去绘制。 先根据数据绘制出静态的东西。把未签到的东西全部绘制完毕。 然后开始绘制动画。处理动画的方式,利用postInvalidate()引起重绘,每次画一点点的橙色进度,后面部分绘制为未签到的灰色。每次更新增加一点点橙色的进度,这样在快速的情况下,就是一个连续的动画效果 封装状态bean1234public StepBean(int state, int number) { this.state = state; this.number = number;} state:封装了3个状态,代表已完成签到,当前进行的签到,和未签到 number:封装添加的分数 初始化 把一些具体的画笔,资源文件等初始化出来12345678//已经完成的iconmCompleteIcon = ContextCompat.getDrawable(getContext(), R.drawable.ic_sign_finish);//正在进行的iconmAttentionIcon = ContextCompat.getDrawable(getContext(), R.drawable.ic_sign_unfinish);//未完成的iconmDefaultIcon = ContextCompat.getDrawable(getContext(), R.drawable.ic_sign_unfinish);//UP的iconmUpIcon = ContextCompat.getDrawable(getContext(), R.drawable.ic_sign_up); 初始化一些paint就不再介绍,这里就说下初始化Drawable文件,利用ContextCompat.getDrawable()把资源文件引入。因为未签到和当前签到都是属于还没有签到,所以都是展示没有签到的图标。 测量 onMeasure():这里没有太多操作,仅仅把值设置下 1setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); onSizeChanged():这里操作就多了一些,主要是需要确定下来图标绘制的位置,已经线段的位置。(这里的说是线段,其实就是当矩形来绘制),注释已经写了很清楚,这里不多做说明了 12345678910111213141516//图标的中中心Y点mCenterY = CalcUtils.dp2px(getContext(), 28f) + mIconHeight / 2;//获取左上方Y的位置,获取该点的意义是为了方便画矩形左上的Y位置mLeftY = mCenterY - (mCompletedLineHeight / 2);//获取右下方Y的位置,获取该点的意义是为了方便画矩形右下的Y位置mRightY = mCenterY + mCompletedLineHeight / 2;//计算图标中心点mCircleCenterPointPositionList.clear();//第一个点距离父控件左边14.5dpfloat size = mIconWeight / 2 + CalcUtils.dp2px(getContext(), 14.5f);mCircleCenterPointPositionList.add(size);for (int i = 1; i < mStepNum; i++) { //从第二个点开始,每个点距离上一个点为图标的宽度加上线段的23dp的长度 size = size + mIconWeight + mLineWeight; mCircleCenterPointPositionList.add(size);} 传值这里对外界暴露了一个方法,传入封装好的bean的List12345678910111213141516/** * 设置流程步数 * * @param stepsBeanList 流程步数 */public void setStepNum(List<StepBean> stepsBeanList) { if (stepsBeanList == null) { return; } mStepBeanList = stepsBeanList; mStepNum = mStepBeanList.size(); //找出最大的两个值的位置 mMax = CalcUtils.findMax(stepsBeanList); //引起重绘 postInvalidate();} 值传递进来之后调用postInvalidate()方法,引起重绘,调用draw()方法,进行再次绘制。并且把List里面的最大两个值的位置找出来,在后面方便设置UP标志。1234567891011121314151617181920212223242526/** * 寻到最大两个值的位置 */public static int[] findMax(List<StepBean> steps) { int[] value = new int[2]; int[] position = new int[2]; int temValue; int temPosition; for (int i = 0; i < steps.size(); i++) { if (steps.get(i).getNumber() > value[1]) { //比较出大的放到value[0]中 value[1] = steps.get(i).getNumber(); position[1] = i; } if (value[1] > value[0]) { //把最大的放到value[0]中,交换位置 temValue = value[0]; value[0] = value[1]; value[1] = temValue; temPosition = position[0]; position[0] = position[1]; position[1] = temPosition; } } return position;} 寻找最大值,我的想法是通过一次循环找出来,采用一个数组存储的方式,时间复杂度为O(n)。可能方法并非最优,如果有更好的方式的欢迎指教~! 绘制 绘制我这里分为了两步,第一步,是传入值之后,便绘制出签到之前的View,也就是静态的。然后提供一个方法暴露出去,待调用的时候开始执行签到动画,完成动态的绘制 绘制签到之前的View12345if (isAnimation) { drawSign(canvas);} else { drawUnSign(canvas);} 在onDraw()方法里面,我使用isAnimation,默认为false,调用绘制未签到状态的View,待调用执行动画方法时候为true,执行另一个方法。 绘制线段123456789101112131415161718192021222324252627282930313233343536//绘制线段float preComplectedXPosition = mCircleCenterPointPositionList.get(i) + mIconWeight / 2;if (i != mCircleCenterPointPositionList.size() - 1) { //最后一条不需要绘制 if (mStepBeanList.get(i + 1).getState() == StepBean.STEP_COMPLETED) { //下一个是已完成,当前才需要绘制绿色 canvas.drawRect(preComplectedXPosition, mLeftY, preComplectedXPosition + mLineWeight, mRightY, mCompletedPaint); } else { //其余绘制灰色 canvas.drawRect(preComplectedXPosition, mLeftY, preComplectedXPosition + mLineWeight, mRightY, mUnCompletedPaint); }}``` 我们在mCircleCenterPointPositionList里面存储了签到每个步骤的图标中心点X坐标。那么就拿出来,进行绘制。 - 线段是比图标少一个的,那么可以少画第一条或者少画最后一条(相对图标)。我采取的是最后一条不绘制。那么每条线段就在每个步骤图标的后面,获取到图标的中线点X坐标,加上图标宽度的一般,就是该线段的X坐标。其余的根据已经固定的Y坐标和线段长度绘制便可。这里主要是根据当前状态,不是已经签到了,则绘制为灰色,已经签到才绘制为绿色。 - 绘制图标``` Java//绘制图标float currentComplectedXPosition = mCircleCenterPointPositionList.get(i);Rect rect = new Rect((int) (currentComplectedXPosition - mIconWeight / 2), (int) (mCenterY - mIconHeight / 2), (int) (currentComplectedXPosition + mIconWeight / 2), (int) (mCenterY + mIconHeight / 2));StepBean stepsBean = mStepBeanList.get(i);if (stepsBean.getState() == StepBean.STEP_UNDO) { mDefaultIcon.setBounds(rect); mDefaultIcon.draw(canvas);} else if (stepsBean.getState() == StepBean.STEP_CURRENT) { mAttentionIcon.setBounds(rect); mAttentionIcon.draw(canvas);} else if (stepsBean.getState() == StepBean.STEP_COMPLETED) { mCompleteIcon.setBounds(rect); mCompleteIcon.draw(canvas);} 对于图标的绘制,也是非常简单的计算,既然已经获取到了每个图标的中心X坐标,那么根据图标的大小计算出左上角和右下角,然后根据state绘制即可。 绘制分数123456789101112131415161718//绘制增加的分数数目if (stepsBean.getState() == StepBean.STEP_COMPLETED) { //已经完成了 if (i == mMax[0] || i == mMax[1]) { //是up的需要橙色 mTextNumberPaint.setColor(mCurrentTextColor); } else { //普通完成的颜色 mTextNumberPaint.setColor(mCompletedLineColor); }} else { //还没签到的,颜色均为灰色 mTextNumberPaint.setColor(mUnCompletedLineColor);}canvas.drawText(\"+\" + stepsBean.getNumber(), currentComplectedXPosition + CalcUtils.dp2px(getContext(), 2f), mCenterY - mIconHeight / 2 - CalcUtils.dp2px(getContext(), 0.5f), mTextNumberPaint); 对于分数,就依附在每个图标的上方,根据设计师给的标注,找出文本的左下角坐标(默认文本绘制是文本的左下角坐标)绘制。注意的是,要根据找出最大两个值的位置,如果是较大的两个,最需要为橙色 绘制UP图标1234567891011//绘制UPif (i == mMax[0] || i == mMax[1]) { //需要UP才进行绘制 Rect rectUp = new Rect((int) (currentComplectedXPosition - mUpWeight / 2), (int) (mCenterY - mIconHeight / 2 - CalcUtils.dp2px(getContext(), 8f) - mUpHeight), (int) (currentComplectedXPosition + mUpWeight / 2), (int) (mCenterY - mIconHeight / 2 - CalcUtils.dp2px(getContext(), 8f))); mUpIcon.setBounds(rectUp); mUpIcon.draw(canvas);} Up图标的绘制依附在增加的分数上面,也是根据较大两个值的位置绘制,计算出左上角和右下角进行绘制。 静态绘制完毕,就已经展示出来了未签到状态的View。123456789101112/** * 执行签到动画 * * @param position 执行的位置 */public void startSignAnimation(int position) { //线条从灰色变为绿色 isAnimation = true; mPosition = position; //引起重绘 postInvalidate();} 我这里暴露出执行动画的方法,将要执行动画的位置传入。(这里要传位置是因为后台数据所致,也是可以根据state位置自行找出)。将isAnimation赋值true,调用postInvalidate(),再次调用Drwa()方法进行绘制。 绘制线段动画 12345678910111213141516171819202122232425//绘制线段float preComplectedXPosition = mCircleCenterPointPositionList.get(i) + mIconWeight / 2;if (i != mCircleCenterPointPositionList.size() - 1) { //最后一条不需要绘制 if (mStepBeanList.get(i + 1).getState() == StepBean.STEP_COMPLETED) { //下一个是已完成,当前才需要绘制绿色 canvas.drawRect(preComplectedXPosition, mLeftY, preComplectedXPosition + mLineWeight, mRightY, mCompletedPaint); } else { //其余绘制灰色 //当前位置执行动画 if (i == mPosition - 1) { //绿色开始绘制的地方, float endX = preComplectedXPosition + mAnimationWeight * (mCount / ANIMATION_INTERVAL); //绘制绿色 canvas.drawRect(preComplectedXPosition, mLeftY, endX, mRightY, mCompletedPaint); //绘制灰色 canvas.drawRect(endX, mLeftY, preComplectedXPosition + mLineWeight, mRightY, mUnCompletedPaint); } else { canvas.drawRect(preComplectedXPosition, mLeftY, preComplectedXPosition + mLineWeight, mRightY, mUnCompletedPaint); } }} 对于未签到和已经签到的和上面的绘制没有太多变,仅仅在当前签到位置执行动画效果 定义mCount为整个动画执行分段的次数记录;ANIMATION_INTERVAL为每次动画执行的时间间隔,暂定10ms;mAnimationWeight为每次间隔中增加的长度。然后每次用根据是分度绘制的第几次算出绘制橙色的长度,然后根据线段长度减去这段长度算出灰色的长度,进行绘制。 绘制图标,文字,up动画 12345if (i == mPosition && mCount == ANIMATION_TIME) { //当前需要绘制成绿色了 mCompleteIcon.setBounds(rect); mCompleteIcon.draw(canvas);} 对于这部分的绘制,变化不太多,因为需求是线段动画执行完毕,就把文本、图标变为绿色,如果是较大两个值的地方,则变为橙色(这部分代码没有太多粘贴,详情请见demo项目) 计算动画执行的次数12345678910//记录重绘次数mCount = mCount + ANIMATION_INTERVAL;if (mCount <= ANIMATION_TIME) { //引起重绘 postInvalidate();} else { //重绘完成 isAnimation = false; mCount = 0;} 维护了一个mCount,记录动画分段执行的次数,当值达到了要求的动画执行时间,变停止重绘,否则,调用postInvalidate()进行重绘,增加mCount的值。 调用 在activity或者dialog等里面封装List,调用12345678910111213141516171819private void initData() { mStepBeans.add(new StepBean(StepBean.STEP_COMPLETED, 2)); mStepBeans.add(new StepBean(StepBean.STEP_COMPLETED, 4)); mStepBeans.add(new StepBean(StepBean.STEP_CURRENT, 10)); mStepBeans.add(new StepBean(StepBean.STEP_UNDO, 2)); mStepBeans.add(new StepBean(StepBean.STEP_UNDO, 4)); mStepBeans.add(new StepBean(StepBean.STEP_UNDO, 4)); mStepBeans.add(new StepBean(StepBean.STEP_UNDO, 30)); mStepView.setStepNum(mStepBeans);}private void initListener() { mTvSign.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mStepView.startSignAnimation(2); } });} 结语 这部分的自定义还是很简单,主要是对待动画处理上。之前拿到这个需求,完全不知道怎么去完成动画效果,请教之后才明白,就是不停的引起重绘完成。在原理上是每次多绘制一部分,但在视觉上因为快速(低于16ms)形成了动画(或许还有其他方式)。比如歌词同步也是差不多是这个原理。 该部分自定义View很简单,但是我感觉到自己通过不断的学习慢慢在了解到更多的方式,欢迎各位尝试! 代码已经放在GitHub,如果有帮助到您,希望不要忘记点颗小星星。https://github.com/sorgs/StepView","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"},{"name":"自定义View","slug":"自定义View","permalink":"http://sorgs.cn/tags/自定义View/"},{"name":"动画","slug":"动画","permalink":"http://sorgs.cn/tags/动画/"}]},{"title":"一步步自定义一个封面选择框","date":"2018-05-28T13:59:04.000Z","path":"post/45123/","text":"*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 引言很多时候我们拍摄视频用户是竖屏拍摄,但是一个视频的封面需要一个16:9的图片,并且允许用户自己选择,于是做了一个简单的自定义View,进行展示封面选择。 先看看引入到项目的效果: - 自定义View的准备 首先来说自定义View就是进行绘制,绘制肯定会需要确定大小,位置以及绘制的内容。对应的既是onMeasure()、onLayout()和onDraw()来看一张自定义View的流程图,对照图进行编写,变会轻松很多。 本次主要是在滑动监听上做功夫进行绘制,即onTouchEvent() 分析1.当看到这自定义View的时候,我也是一脸懵逼。然后慢慢的思考。先绘制一个东西上去,让它跟着手指动起来就好,于是有了这样的代码 123456789101112131415161718public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: moveX = event.getX(); moveY = event.getY(); break; case MotionEvent.ACTION_MOVE: setTranslationX(getX() + (event.getX() - moveX)); setTranslationY(getY() + (event.getY() - moveY)); break; case MotionEvent.ACTION_UP: break; case MotionEvent.ACTION_CANCEL: break; } return true; } 2.动起来之后,我们继续再看目标,可以发现,我们只需要在Y轴上移动,并且是在一个长矩形中间滑动一个小的矩形。于是就绘制一个长矩形,和一个小矩形,修改onTouchEvent()中的方法,删除和x轴相关的代码。绘制矩形使用drawRect函数,传入两个点的坐标(左上角和右下角)和画笔。 1canvas.drawRect(mC1X1, mC1Y1, mC1X2, mC1Y2, mChildPaint); 3.这一步也不算很难。继续绘制,发现背景需要绘制一张图片,这个时候需要在调用这个View的地方传递一张Bitmap过来。于是有了这样的代码: 12345678910/** * 设置图片 */public void setData(Bitmap bitmap) { if (bitmap == null) { throw new RuntimeException(\"bitmap can't null\"); } mParentBg = bitmap; invalidate();} invalidate()进行重新绘制,调用之后,流程进行onDraw(),调用绘制bitmap的函数 12345// 指定图片绘制区域Rect src = new Rect(0, 0, mParentBg.getWidth(), mParentBg.getHeight());// 指定图片在屏幕上显示的区域Rect dst = new Rect(mPX1, mPY1, mPX2, mPY2);canvas.drawBitmap(mParentBg, src, dst, null); 先简单说明一下,mParentBg即为Bitmap对象,先获取要绘制的背景图片的大小,这里当然是把整个背景图绘制进行,然后显示的位置,即为矩形的位置,依然是左上角和右下角。我们不需要给图片着色,所以paint传null即可。 完成这几部之后,觉得很不错,大功告成,这篇文章到此为止了。问题来了,本想中间的选择矩形绘制为透明的,长矩形即父控件矩形绘制一个半透明的。但是发现,根本没有作用,绘制透明的就好像没有绘制一样。 于是,又开始认真思考。中间选择部分是透明的,也就是相当于没有绘制。那么就把父控件分为两个变化的子控件矩形,根据中间选择区域的变化,调整上下两个子控件矩形的大小。 编码1.初始化。根据上面的分析之后,开始编码。首先是要定义一些初始化的东西,于是在构造函数中调用init: 12345678private void init(Context context) { mContext = context; mChildPaint = new Paint(); mChildPaint.setColor(context.getResources().getColor(R.color.colorT)); mChildPaint.setStyle(Paint.Style.FILL);} 定义了绘制两个子控件的画笔,设置了抗锯齿和半透明带黑色蒙层的颜色 2.大小确定。根据流程肯定是测量出自定义View的大小: 12345678910protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mParentHeight = MeasureSpec.getSize(heightMeasureSpec); mScreenWidth = MeasureSpec.getSize(widthMeasureSpec); //父控件宽度 16:9的宽 mParentWidth = mParentHeight * 9 / 16f; //选中区域高度 mChildHeight = mParentWidth * 9 / 16f; initCalc();} 我们首先获取到了view的宽高,然后去进行父控件的宽高和子控件的宽高 首先来说,父控件的高度肯定就是整个view的高度,而宽度的话,因为要求是16:9,所以根据高度计算出宽度 有了父控件的宽度,也便有了中间选择矩形的宽度,也是要求16:9,所以根据比例计算出来高度大小 获取到一些宽高之后,便马上进行对坐标点的计算。首先来看一张图 这是我优化之前的一张草图。首先来说,绘制矩形只需要知道左上角和右下角的坐标。 如图,第一个状态,是子控件1是0,也就是左上角即为父控件的左上角,右下角即为父控件的左上角,而这个状态中,子控件2呈现最大的状态,左上角的仅仅为父控件的减去一个选择区域的大小,右下角即为父控件的右下角。 中间的状态进行变化,子控件1的左上角和子控件2的右下角始终和父控件一样,不进行变化。而这里只有上下滑动,所以,变化的仅仅为Y轴上面。根据Android的坐标系来说,Y轴向下为正方向。也就是子控件1的右下角的Y坐标和子控件2的左上角的Y坐标进行加减手指滑动的距离,然后进行重绘,即可达到绘制效果。 第三个状态便是当滑到最底部的时候,原理和第一个状态类似,子控件1达到最大,左下角的坐标仅仅减去选择区域的高度;子控件2为0,左上角为父控件的左下角,右下角为父控件的右下角。 搞清楚这些之后,开始计算初始化的坐标点: 1234567891011121314151617181920212223242526272829303132333435 /** * 坐标点的计算 * X轴基本不变,变化的是Y轴 */ private void initCalc() { //计算父控件的位置点 mPX1 = (int) (mScreenWidth / 2f - mParentWidth / 2f); mPY1 = 0; mPX2 = (int) (mScreenWidth / 2f + mParentWidth / 2f); mPY2 = (int) (mParentHeight); //刚开始子控件1的位置点 mC1X1 = mPX1; mC1Y1 = mPY1; mC1X2 = mPX2; mC1Y2 = mPY1; //刚开始子控件2的位置点 mC2X1 = mPX1; mC2Y1 = (int) (mChildHeight); mC2X2 = mPX2; mC2Y2 = mPY2; }``` - 值得注意的是,我们需要把控件摆放到屏幕中间,所以,左上角的X便是屏幕宽度除以2减去计算出来的父控件宽度除以2。右下角同理是加上父控件宽度除以2。3.位置确定。坐标计算完毕,进行设置view的大小:``` Java protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); setMeasuredDimension((int) mParentWidth, (int) mParentHeight); } 这里调用setMeasuredDimension()将我们计算好的宽高设置上去 5.绘制。因为我们这边已经通过坐标来进行了位置的确定,所以直接调用onDraw()进行绘制: 123456789101112131415161718protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制父控件 // 指定图片绘制区域 Rect src = new Rect(0, 0, mParentBg.getWidth(), mParentBg.getHeight()); // 指定图片在屏幕上显示的区域 Rect dst = new Rect(mPX1, mPY1, mPX2, mPY2); canvas.drawBitmap(mParentBg, src, dst, null); //绘制子控件1 canvas.drawRect(mC1X1, mC1Y1, mC1X2, mC1Y2, mChildPaint); //绘制子控件2 canvas.drawRect(mC2X1, mC2Y1, mC2X2, mC2Y2, mChildPaint);} 这里的基本都在分析的时候已经进行了说明,没有太多要说的地方。mParentBg即为传入Bitmap对象。 6.划动事件。要进行划动了,心里开始莫名的紧张,这部分是最不好进行控制。还是先看图: MotionEvent.ACTION_DOWN: 首先我们肯定是有一个拖拽范围的。因为我们只能拖选择框的地方才能有效。所以,图上右边紫色我写了可选中范围。 在X轴上面,没的说,很好办,就在父控件的宽度内。 但是在Y轴上面的话,需要动态根据选择框的位置进行变化了。 一开始,我进行判断划动的时候,写死了区域。不在这个区域直接不进行划动监听。这样也是可以做到,但是效果并不好。第一点,当手指从不可选中区域划入到可选中的时候,这样会响应事件,表现出来的便是选择区域突然跳到手指最开始落下的地方(从上往下划动);第二点,从可选中区域划动到不可选中区域,这样不会响应时间,效果表现出来好像划不动一样。 12345678910//手指按下//记录按下的距离float beginY = event.getY();if (beginY < mC1Y2) { //起始点在选择框上部,不做反应 return false;} else if (beginY > mC1Y2 + mChildHeight) { //起始点在选择框下部,不做反应 return false;} - 我们记录手指按下的Y坐标,进行判断,如果小于了子控件1的右下角Y坐标,说明按下的时候在选择框的上面,那么不做反应,针对上述第一点。如果按下距离在子控件2的右下角加上一个可选取与的高度,那么说明按下的点再选择框的下部,那已经超出了可选范围,也不做反应。 说完了可拖拽区域,现在来看一下划动的距离变化。其实这部分在分析的时候也说了,主要是就是加减手指移动的距离便可。 要计算距离,这里需要减去一个按下的距离和可选区域的上边框的差值,否则,选择框会跳一下,然后以上边框为基准线进行改变,这显然不是我们想要的结果: 12//记录按下的位置和选择区域的上边距的差mDistanceY = beginY - mC1Y2; MotionEvent.ACTION_MOVE: 我们手指移动的距离为event.getY() - beginY。而我们实际要计算的是画在图中右边一点的实际移动的改变值,便是event.getY() - mDistanceY。event.getY()是指距离父控件的上边距,减去之前算好的mDistanceY,便可以比较精确得出实际移动的距离。可能还是会有疑问,为什么这样计算出来的距离会多一部分手指按下的距离和可选区域上边距的距离。因为我们这边是改变的子控件1和2的大小,子控件1是以右下角的Y,也可以理解为可选区域上边这根线绘制。如果不算手指和可选区域上边距的距离,那么效果就是划动起来,可选区域就会跳一下,然后以可选区域上边这根线在划动,而手指按下的时候明明和上边是有一定距离的。这部分可以尝试去掉进行感受。(感觉文字功底不好,有点扯不清,逃~)简单来说,反正就是要记录下手指按下距离可选区域上边的距离,在移动完之后,手指还是要距离可选区域同样的距离。嗯,就是这样,喵。 这样就完工了。等等,不急。我们的可选区域肯定是不能划出父控件哒。也就是说可选区域上面不能划出父控件的上面,下面也不能划出父控件的下面的。 往上划动。需要判断下子控件1右下角的Y不能小于0,即不能小于了父控件的Y,超过了则保持状态一。 往下划动。需要判断下子控件1右下角的Y不能大于父控件高度减去一个可选区域的高度,超过了则保持状态三。 12345678910111213//往上滑动if (mC1Y2 < 0) { //防止顶部超过出 //子控件1为0 mC1Y2 = 0; //子控件2为最大 mC2Y1 = (int) (mSelectHeight);} else if (mC1Y2 > mParentHeight - mSelectHeight) { //防止底部超过 //子控件1为最大 mC1Y2 = (int) (mParentHeight - mSelectHeight); //子控件2为0 mC2Y1 = (int) (mParentHeight); 最后执行一下重绘。invalidate() 最后来一个完整的onTouchEvent()的代码,其实上诉已经说完了 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162public boolean onTouchEvent(MotionEvent event) { //有效触控范围(X轴,Y轴另外判断) if (mC1X1 <= event.getRawX() && event.getRawX() <= mC1X1 + mParentWidth) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //手指按下 //记录按下的距离 float beginY = event.getY(); if (beginY < mC1Y2) { //起始点在选择框上部,不做反应 return false; } else if (beginY > mC1Y2 + mSelectHeight) { //起始点在选择框下部,不做反应 return false; } //记录按下的位置和选择区域的上边距的差 mDistanceY = beginY - mC1Y2; break; case MotionEvent.ACTION_MOVE: //mC1Y1和mC2Y2始终不变 //更改子控件坐标 mC1Y2 = (int) (event.getY() - mDistanceY); mC2Y1 = (int) (event.getY() - mDistanceY + mSelectHeight); //往上滑动 if (mC1Y2 < 0) { //防止顶部超过出 //子控件1为0 mC1Y2 = 0; //子控件2为最大 mC2Y1 = (int) (mSelectHeight); } else if (mC1Y2 > mParentHeight - mSelectHeight) { //防止底部超过 //子控件1为最大 mC1Y2 = (int) (mParentHeight - mSelectHeight); //子控件2为0 mC2Y1 = (int) (mParentHeight); } //重新绘制 invalidate(); break; case MotionEvent.ACTION_UP: //手指抬起 break; case MotionEvent.ACTION_CANCEL: //事件取消 break; default: break; } } return true;} 7.后续处理。后续处理的话,和项目不一样就不一样的,我们项目是把可选区域的坐标绝对值给后台,后台截取。demo里面是利用Android的截图,然后传去可选区域的坐标截取出来,但是这样分辨率肯定比较低,不太适合做封面。然后就是截取的话,需要注意下有个状态栏高度。(这里有个小坑的地方,就是Android截图系统只有能一张,需要重新加载才能获取新的截图,因为项目没有用到这个,所以没有深入研究,如果有知道的,麻烦赐教,感谢) 123456789101112131415161718192021222324252627public Bitmap getBitmap(Activity activity) { View screenView = activity.getWindow().getDecorView(); screenView.setDrawingCacheEnabled(true); screenView.buildDrawingCache(); //获取屏幕整张图 Bitmap bitmap = screenView.getDrawingCache(); //截图指定部分 if (bitmap != null) { bitmap = Bitmap.createBitmap(bitmap, mC1X1, mC1Y2 + getStatusBarHeight(), (int) mParentWidth, (int) mSelectHeight); } invalidate(); return bitmap;}/** * 获取状态栏高度 */private int getStatusBarHeight() { int result = 0; int resourceId = mContext.getResources().getIdentifier(\"status_bar_height\", \"dimen\", \"android\"); if (resourceId > 0) { result = mContext.getResources().getDimensionPixelSize(resourceId); } return result;} 结语 demo已经放到了github:https://github.com/sorgs/DragView上面了,如果能帮到您的话,还麻烦动动小指头给个小星星,万分感谢了! 本人才疏学浅,仅仅是一个还差一个多月才毕业的应届生,写的比较简单,请大家见谅。如果有什么纰漏和不对的地方,感谢指出。 对自定义View安利一个学习的地方:http://www.gcssloop.com/customview/CustomViewIndex/,GcsSloop大佬的系列,很受教! 最后就是我最想说的。其实很多东西看起来很复杂,但是慢慢静下心去做还是可以做出来的。虽然这个很简单,但是我们老大说让我研究下的时候,我也是一脸懵逼啊。心里想,这,我怎么能做得出来。反正研究嘛,做不出来还有老大撑腰。就是就一步一步来尝试,先让动起来,然后再慢慢靠近需求,最后优化。最终发现还是弄出来了。写这篇博客的主要目的就是给自己和大家说这个道理,不畏惧,一步步来!","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"},{"name":"自定义View","slug":"自定义View","permalink":"http://sorgs.cn/tags/自定义View/"},{"name":"969","slug":"969","permalink":"http://sorgs.cn/tags/969/"},{"name":"滑动事件","slug":"滑动事件","permalink":"http://sorgs.cn/tags/滑动事件/"}]},{"title":"JAVA编程思想读书笔记-2","date":"2018-01-26T06:39:16.000Z","path":"post/45310/","text":"第一章对象导论(1.7-1.13) 1.7伴随多态的可互换对象 在处理类型的层次结构时,经常想把一个对象不当做它所属的特定类型来对待,而是将其当做其基类的对象来对待。 这样的代码是不会受添加新类型的影响,而且添加新类型是扩展一个面向对象程序以便处理新情况的最常用方式。 在java中,动态绑定是默认行为,不需要添加额外的关键字来实现多态。 1.8单根继承结构 在单根继承结构中的所有对象都具有一个共用接口,所以他们归根结底到底都是相同的基本类型。 单根接口保证所有对象都具备某些功能。 对象都可以很容易地在堆上创建。 1.9容器 不同容器提供了不同类型的接口和外部行为。 不同的容器对于某些操作具有不同的效率。 除非确切知道所要处理的对象的类型,否则向下转型几乎是不安全的。 1.10对象的创建和生命期 将对象置于堆栈(它们有时候被称为自动变量或限域变量)或静态存储区内来实现。这种方式将存储空间分配和释放置于优先考虑的位置,某些情况下这样控制非常有价值。但是也牺牲了灵活性。 第二种方式是在被称为堆的内存池中动态地创建对象。这种方式中,知道运行时才知道需要多少对象,它们生命周期如何,以及它们的具体类型是什么。 java完全采用了动态内存分配方式,每当想要创建新对象时,就要使用new关键字来构建此对象的动态实例。 1.11异常处理:处理错误 异常处理将错误处理直接置于编程语言中,有时甚至置于操作系统中。异常是一种对象,它从出错地点被“抛出”,并被专门设计用来处理特定类型错误的相应的异常处理器“捕获”。 异常提供了一种从错误状态进行可靠恢复的途径。 异常处理不是面向对象的特征。异常处理在面向对象语言之前就已经存在了。 1.12并发编程 在程序中,这些彼此独立运行的部分称之为线程,上述概念被称为“并发”。 通常,线程只是一种为单一处理器分配执行时间的手段。 1.13Java与Internet 它解决了万维网上的程序设计问题。","tags":[{"name":"JAVA学习","slug":"JAVA学习","permalink":"http://sorgs.cn/tags/JAVA学习/"}]},{"title":"JAVA编程思想读书笔记_1","date":"2018-01-25T07:08:25.000Z","path":"post/4506/","text":"第一章对象导论(1.1-1.6) 1.1抽象过程 程序员必须建立起在机器模型(位于“解空间”内,这是你对问题建模的地方,例如计算机)和实际待解决问题的模型(位于“解空间”内,这是问题存在的地方,例如一项物业)之前的关联。 面向对象五个基本特征: 万物皆对象 程序是对象的集合,它们通过发消息来告知彼此所要做的。 每个对象都有自己的由其他对象所构成的存储。 每个对象都拥有其类型。 某一特定的内类型的所有对象都可以接受同样的消息。 更加简洁的描述:对象具有状态、行为和标识。这意味着每一个对象都可以拥有内部数据(它们给出了该对象的状态)和方法(它们产生行为),并且每一个对象都可以唯一地与其他对象区分开来,具体来说,就是每一个对象在内存中都有一个唯一的地址。 1.2每个对象都有一个接口 具有相同的特征和行为的对象所归属的类的一部分。 在程序执行期间具有相同特征(数据元素)和行为(功能)的对象集合,所以一个类实际上就是一个数据类型。 面向对象程序设计的挑战之一,就是在问题空间的元素和解空间的对象之间创建一对一的映射。 每个对象都只能满足某些请求,这些请求由对象的接口(interface)所定义,决定接口的便是类型。 为了向对象发送消息,需要声明对象的名称,并以圆点连接一个消息请求。 1.3每个对象都提供服务 高内聚是软件设计的基本质量要求之一,这意味着一个软件构建(例如一个对象,当然它也有可能是指一个方法或一个对象库)的各个方面“组合”得很好。 1.4被隐藏的具体实现 访问控制的第一个存在原因就是让客户端程序员无法触及他们也不应该触及的部分——这些部分对数据类型的内部操作来说是必需的,但并不是用户解决特定问题所需要的接口的一部分。 访问控制的第二个存在原因就是允许库设计者可以改变类内部的工作方式不用担心会影响到客户端程序员。 Java用三个关键字在类的内部设定边界:public、private、protected。 还有一种默认访问权限,通常称为包访问权限,类可以访问在同一个包(库构件)中的其它类的成员,但是在包之外,这些成员如同指定了private一样。 1.5复用具体实现 复用是面向对象程序设计所提供的最了不起的优点之一。 1.6 继承 一个基类包含其所有导出类型所共享的特性和行为。可以创建一个基类来表示系统中某些对象的核心概念,从基类类型中导出其他类型,来表示此核心可以被实现的各种不同方式。 导出类与基类具有相同的类型。 想要覆盖某个方法,可以直接在导出类中创建该方法的新定义即可。 判断是否继承,就是要确定是否可以用is-a来描述来描述类之间的关系,并使之具有实际意义。","tags":[{"name":"JAVA学习","slug":"JAVA学习","permalink":"http://sorgs.cn/tags/JAVA学习/"}]},{"title":"Android动态图片选择的一种简单实现方式","date":"2018-01-20T12:20:08.000Z","path":"post/3403/","text":"很久没有更新博客了,以后还是决定每个月来更新一遍。本次到来的是一个常用的场景,比如我们在发朋友圈的时候,我们可以选择多张照片,也可以删除之前选择的,但是最多一般会有个上限,达到上限之后一般添加的就消失了。这里给出一个简单的实现思路。 效果图 我们还是先看看效果图 首先是没有图片的时候 然后我们选择两张图片 选可以点图片右上交的×删除一张 最后是选择6张,继续添加标志消失(我这里上限是6张,后面读者可以更新代码自己设置上限) 代码实现 这里简单的提供一种实现方式 我这里是利用RecyclerView来实现,接下来具体看下 首先是主布局文件,其实就是一个RecyclerView 123456789101112131415161718192021222324252627282930313233343536373839404142 <android.support.v7.widget.RecyclerView android:id=\"@+id/rl_repair\" android:layout_width=\"match_parent\" android:layout_height=\"250dp\"> </android.support.v7.widget.RecyclerView>``` - 然后主要逻辑代码在adapter,我们先看看adapter的布局 ``` xml<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" android:layout_width=\"100dp\" android:layout_height=\"100dp\" android:layout_margin=\"10dp\"> <ImageView android:id=\"@+id/iv_add\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:scaleType=\"centerCrop\" android:src=\"@mipmap/bg_scan\"/> <ImageView android:id=\"@+id/iv_photo\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:scaleType=\"centerCrop\"/> <ImageView android:id=\"@+id/iv_remove\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_alignParentRight=\"true\" android:paddingBottom=\"5dp\" android:paddingLeft=\"5dp\" android:scaleType=\"centerCrop\" android:src=\"@mipmap/ic_delete\" android:visibility=\"visible\"/></RelativeLayout> 这部分代码没有什么要说的,主要是用RelativeLayout里面包裹3个ImageView,一个用图片右上角显示的删除,一个是添加新图片,还有一个是用来展示选择的图片 接下来是adapter的逻辑部分代码了 12345678910111213141516171819202122@Overridepublic void onBindViewHolder(ViewHolder holder, int position) { if (mList.size() >= MAX_SIZE) { //最多6张 holder.ivAdd.setVisibility(View.GONE); holder.ivRemove.setVisibility(View.GONE); } else { holder.ivPhoto.setVisibility(View.VISIBLE); holder.ivPhoto.setVisibility(View.VISIBLE); holder.ivRemove.setVisibility(View.VISIBLE); } if (getItemViewType(position) == TYPE_ADD) { holder.ivRemove.setVisibility(View.GONE); holder.ivPhoto.setVisibility(View.GONE); } else { holder.ivRemove.setVisibility(View.VISIBLE); holder.ivAdd.setVisibility(View.GONE); holder.ivPhoto.setVisibility(View.VISIBLE); holder.ivPhoto.setImageBitmap(BitmapFactory.decodeFile(mList.get(position))); }} 不难看出,这里利用setVisibility来进行操作,更具数据来显示那些或者隐藏哪些。当我们的数目大于等于6的时候,我们就需要删除和添加隐藏起来。 别忘了,我们一开始就需要有添加的图片,所以我们的size应该是 1234@Overridepublic int getItemCount() { return mList.size() + 1;} 数目应该要多加一个,然后应该注意到了我们的getItemViewType()方法了,这里我们进入看下 12345678@Overridepublic int getItemViewType(int position) { if (position == getItemCount() - 1) { return TYPE_ADD; } else { return TYPE_PIC; }} 没错,这里就是用来区别是那种类型的方法,我们这里有两个类型 12private static final int TYPE_ADD = 1;private static final int TYPE_PIC = 2; 然后我们根据传入的postion来区分,如果position==我们的总数-1,那就说明我们这里显示添加图片,否则就显示展示的图片。为什么要减一个的原因就是最后总是要显示添加,除非达到了上限。然后回到我们的onBindViewHolder()代码中,就很简单了,当需要展示添加的时候,就把展示图片和删除的GONE,当需要展示图片的时候,就需要把添加隐藏,其余的展示出来 然后我们还需要监听点击事件,用来增加或者是删除 123456789101112131415private OnItemClickListener itemClickListener;public interface OnItemClickListener { /** * 继续添加图片接口 */ void onItemAddClick(); /** * 删除已经添加的图片接口 * * @param position 删除的position */ void onItemRemoveClick(int position);} 写一个点击事件的接口,两个方法,一个是添加,一个删除 然后注册监听 12345678910111213141516171819202122@Overridepublic ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (context == null) { context = parent.getContext(); } final ViewHolder viewHolder = new ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_photo, parent, false)); viewHolder.ivAdd.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { itemClickListener.onItemAddClick(); } }); viewHolder.ivRemove.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { itemClickListener.onItemRemoveClick(viewHolder.getAdapterPosition()); } }); return viewHolder;} 在构造函数当中把接口进行注册 1234 public TakePhotoAdapter(List<String> mList, OnItemClickListener itemClickListener) { this.mList = mList; this.itemClickListener = itemClickListener;} 最后就是我们的页面的逻辑代码,先把adapter的接口引入implementsTakePhotoAdapter.OnItemClickListener 然后就是两个接口中的方法 12345678910111213@Overridepublic void onItemAddClick() { //添加照片 mTakePhoto = getTakePhoto(); mTakePhoto.onPickFromCapture(configCompress());}@Overridepublic void onItemRemoveClick(int position) { //删除照片 mPhotoList.remove(position); mTakePhotoAdapter.notifyDataSetChanged();} 可以看到每次对图片操作了,我们需要进行刷新。这里图片的添加可以不是我们的重点,大家可以去用第三方库,我这里使用的是TakePhoto这个库。 然后我们这边对RecyclerView进行注册就OK了 123rlRepair.setLayoutManager(new GridLayoutManager(this, 3));mTakePhotoAdapter = new TakePhotoAdapter(mPhotoList, this);rlRepair.setAdapter(mTakePhotoAdapter); 这里用的是GridLayoutManager,并设置每行3个,读者可以更具情况自己设置 差不多就是这么多内容了,很简单的东西。如果有不对或者更好的方式,欢迎指教","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"},{"name":"动态选择图片","slug":"动态选择图片","permalink":"http://sorgs.cn/tags/动态选择图片/"},{"name":"朋友圈","slug":"朋友圈","permalink":"http://sorgs.cn/tags/朋友圈/"}]},{"title":"关于MediaRecorder中的setAudioEncoder和setOutputFormat","date":"2017-11-18T13:44:12.000Z","path":"post/20591/","text":"很久没有更新博客了,最近实习一直挺忙的。最近做的项目有关使用了录音类MediaRecorder。其中有个setAudioEncoder设置编解码器和setOutputFormat和输出格式。不太明白这之间有什么约束,但是总觉得不可能是随便设置的但是Google怎么都搜不到这方面相关的,于是去稍微了解了下编码解码器的区别以及它的输出容器。这里自己记录下,也方便大家有个了解。 MediaRecorder.OutputFormat先看看包含的格式(总计8个) AAC_ADTS .aac AMR_NB .3gp AMR_WB .3gp DEFAULT MPEG_2_TS .ts MPEG_4 .mp4 .m4a RAW_AMR(此常数在API级别16中已被弃用) .3gp THREE_GPP .3gp WEBM .ogg mkv MediaRecorder.AudioEncoder先看看包含的编解码器(总计7个) AAC(AAC低复杂度(AAC-LC)音频编解码器) AAC_ELD(增强型低延迟AAC(AAC-ELD)音频编解码器) AMR_NB(AMR(窄带)音频编解码器) AMR_WB(AMR(宽带)音频编解码器) DEFAULT HE_AAC(高效率AAC(HE-AAC)音频编解码器) VORBIS(Ogg Vorbis音频编解码器)这就介绍完了,到这里我们可以关闭网页了。放下砖,让我慢慢说,我们稍微深入的去看看这些编解码的东西 AAC 采用了全新的算法进行编码,更加高效,具有更高的“性价比”。 优点:相对于mp3,AAC格式的音质更佳,文件更小。 缺点:AAC属于有损压缩的格式。 其设计目标是替代原有MP3编码标准,在与MP3在相似的码率下希望质量优于MP3。这一目标已达到并且由ISO和IEC标准组织标准化在MPEG-2和MPEG-4中。 支持的文件类型/容器格式 •3GPP .3gp •MPEG-4 .mp4 .m4a •ADTS原始AAC .aac(在Android 3.1+中解码,在Android 4.0+中编码,不支持ADIF) ADTS(Audio Data Transport Stream):这种格式的特征是它有一个同步的字的比特流,解码器可以在这个流中任何开始位置开始。 DAIF:模拟数据交换模式 •MPEG-TS .ts (not seekable,Android 3.0+) 然后还有大致3个版本 AAC_LC AAC低复杂度(AAC_LC)音频编解码器 设计用于数字电视,AAC_LC用于存储空间和计算能力有限的情况。 AAC-LC是充分利用心理声学原理,对人类对音频信号的感知存在不相干性和统计冗余的特性,最大程度的减少用于表达信号的比特数据,实现音频信号快速有效地压缩,而不再追求输出信号和原始信号相似度。 重要技术点 支持从8到48 kHz的标准采样率的单声道/立体声/ 5.0 / 5.1内容。 HE_AAC 高效率AAC(HE-AAC)音频编解码器 分为两个版本 HE_AACV1(编码器 Android4.1+) 支持从8到48 kHz的标准采样率的单声道/立体声/ 5.0 / 5.1内容。 HE_AACV2(增强的AAC+) 支持从8到48 kHz的标准采样率的立体声/ 5.0 / 5.1内容。 对比与AAC_LC 同等音频,音频文件体积(低码率下比较明显):AAC_LC > HE_AAC 算法复杂度:AAC_LC < HE_AAC 更加详细的性能对比 AAC_ELD 增强型低延迟AAC(AAC-ELD)音频编解码器 编码器:(Android 4.1+) 解码器:(Android 4.1+) 支持从16到48 kHz的标准采样率的单声道/立体声内容 能提供跟CD一样的音频质量,让用户获得无与伦比的通信体验。是唯一被广泛采用的全高清语音技术。 AMR_NB AMR(窄带)音频编解码器 主要用于第三代移动通信 W-CDMA 系统中 AMR-NB 支持八种速率模式。使其以更加智能的方式解决信源和信道编码的速率分配问题,根据无线信道和传输状况来自适应地选择一种编码模式进行传输,使得无线资源的配置与利用更加灵活有效。 模式 0(4.75kbit/s) 模式 1(5.15kbit/s) 模式 2(5.90kbit/s) 模式 3(6.70kbit/s) 模式 4(7.40kbit/s) 模式 5(7.95kbit/s) 模式 6(10.2kbit/s) 模式 7(12.2kbit/s) 在8kHz采样时为4.75至12.2 kbps 支持的文件类型/容器格式 3GPP .3gp AMR_WB AMR(宽带)音频编解码器 作为第三代移动通信系统使用的语音编解码算法 AMR-WB 音频带宽在 50Hz-7000Hz,相对于 200Hz-3400Hz 为宽带,支持九种速率模式 模式 0(6.60kbit/s) 模式 1(8.85kbit/s) 模式 2(12.65kbit/s) 模式 3(14.25kbit/s) 模式 4(15.85kbit/s) 模式 5(18.25kbit/s) 模式 6(19.85kbit/s) 模式 7(23.05kbit/s) 模式 8(23.85kbit/s) 采用的是代数码激励线性预测编码(Algebraic Code ExcitedLinear Prediction,简称 ACELP),其已被 3GPP选定为GSM和3G无线W-CDMA的宽带编码器,并将应用于IP电话、第三代移动通信、ISDN 宽带电话、ISDN 可视电话和电视会议等领域,这标志着无线和有线业务第一次采用同样的编码器。 9个速率从6.60 kbit / s到23.85 kbit / s采样@ 16kHz 支持的文件类型/容器格式 3GPP .3gp AMR_WB和AMR_NB更多详情 VORBIS Ogg Vorbis音频编解码器。 Ogg Vorbis是一种新的音频压缩格式,类似于MP3等现有的音乐格式。 它是完全免费、开放和没有专利限制的。 支持多声道。 更低的码率和文件体积。 Ogg Vorbis文件的扩展名是.ogg。 现在创建的OGG文件可以在未来的任何播放器上播放,因此,这种文件格式可以不断地进行大小和音质的改良,而不影响旧有的编码器或播放器。 支持的文件类型/容器格式 •Ogg .ogg •Matroska .mkv (Android 4.0+) Matroska是一种新的多媒体封装格式,它可将多种不同编码的视频及16条以上不同格式的音频和不同语言的字幕流封装到一个Matroska Media文件当中。也是其中一种开放源代码的多媒体封装格式。总结说明 通过以上整理,我们在使用mediaRecord的时候,就不会盲目去设置AudioEncoder和OutputFormat了,而是根据实际情况来使用。 设置的支持的文件类型/容器格式请参考Google官方文档。 整理的笔记如果错误的地方,请一起交流讨论共同进步,谢谢。 以上资料均来自网络整理,如有侵权请告知。","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"},{"name":"MediaRecorder","slug":"MediaRecorder","permalink":"http://sorgs.cn/tags/MediaRecorder/"},{"name":"MediaRecorder.setAudioEncoder","slug":"MediaRecorder-setAudioEncoder","permalink":"http://sorgs.cn/tags/MediaRecorder-setAudioEncoder/"},{"name":"MediaRecorder.setOutputFormat","slug":"MediaRecorder-setOutputFormat","permalink":"http://sorgs.cn/tags/MediaRecorder-setOutputFormat/"},{"name":"setAudioEncoder and setOutputFormat","slug":"setAudioEncoder-and-setOutputFormat","permalink":"http://sorgs.cn/tags/setAudioEncoder-and-setOutputFormat/"}]},{"title":"第三方登录之支付宝登录","date":"2017-08-06T13:02:00.000Z","path":"post/12778/","text":"公司一个需求让做一个支付宝的第三方登录,注意,是登录不是支付。也很简单,这里我自己记录下大家没有说的问题。 首先,支付宝登录和其他第三方登录不太一样,相比麻烦一点。一般第三方登录我们用shareSDK就好,但是支付宝不行。查看官方文档,大部分就是去讲什么支付,没有怎么说怎么登陆。 而且官方文档还有少许错误 首先是我们先请求后台,给我们一个验证信息,我这里使用的网络请求框架是android-async-http。 然后获取的信息类似这样 然后调用支付宝的函数这个函数必须是异步调用,获取到用户信息之后,在利用handler发送到主线程进行登录 大概就是怎么多内容了。还是很基础的东西而已","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"}]},{"title":"属性动画的研究——多级菜单展开","date":"2017-07-26T08:33:11.000Z","path":"post/29596/","text":"很久都没有更新博客了,之前一直忙着参加挑战杯,然后就是期末考试,再然后在室友的乱带节奏下准备找实习。现在找到了一份实习工作,老大还在给新项目打框架,让我先研究下动画,说是后面会用到,就忙里偷闲玩demo 是看imooc上面的一个大神的课程-http://www.imooc.com/learn/263。讲的很nice,然后我就继续深入了一点点,完善了demo而已。效果图 分析 首先是采用了属性动画的方式 在x和y上面做手脚去变化坐标 1234567PropertyValuesHolder Y, X; //设置动画 Y = PropertyValuesHolder.ofFloat(\"translationY\", y); X = PropertyValuesHolder.ofFloat(\"translationX\", x); //添加动画集合 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(imageViewList.get(i), X, Y); 然后是两个动画一起 使用抖动效果(使用了BounceInterpolator)[可能gif有些看不出来]12//为控件增加自由落体动画效果 animator.setInterpolator(new BounceInterpolator()); 计算坐标我们的展开是一个半圆的扇形,那么肯定就是利用数学函数来进行计算,如图 从上面到下面,x是从最大到0,y是从0到最大每一个点的坐标就是根据圆心角来计算123456//需要扩散的角度 以180度为例 float angle = (float) (Math.PI * 180 / 180); //计算偏移的x,y坐标 x = (float) (n * Math.sin(angle / (res.length - 1) * count)); y = (float) (n * Math.cos(angle / (res.length - 1) * count)); 完整代码 布局 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748<?xml version=\"1.0\" encoding=\"utf-8\"?><RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:gravity=\"center\"> <ImageView android:id=\"@+id/iv_h\" style=\"@style/ImageView\" android:src=\"@mipmap/h\"/> <ImageView android:id=\"@+id/iv_g\" style=\"@style/ImageView\" android:src=\"@mipmap/g\"/> <ImageView android:id=\"@+id/iv_f\" style=\"@style/ImageView\" android:src=\"@mipmap/f\"/> <ImageView android:id=\"@+id/iv_e\" style=\"@style/ImageView\" android:src=\"@mipmap/e\"/> <ImageView android:id=\"@+id/iv_d\" style=\"@style/ImageView\" android:src=\"@mipmap/d\"/> <ImageView android:id=\"@+id/iv_c\" style=\"@style/ImageView\" android:src=\"@mipmap/c\"/> <ImageView android:id=\"@+id/iv_b\" style=\"@style/ImageView\" android:src=\"@mipmap/b\"/> <ImageView android:id=\"@+id/iv_a\" style=\"@style/ImageView\" android:src=\"@mipmap/a\"/></RelativeLayout> 抽取的属性 123456<style name=\"ImageView\"> <item name=\"android:layout_width\">50dp</item> <item name=\"android:layout_height\">50dp</item> <item name=\"android:paddingLeft\">5dp</item> <item name=\"android:paddingTop\">5dp</item></style> 逻辑 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121package com.sorgs.animtest;import android.animation.ObjectAnimator;import android.animation.PropertyValuesHolder;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.view.animation.BounceInterpolator;import android.widget.ImageView;import android.widget.Toast;import java.util.ArrayList;import java.util.List;public class MainActivity extends AppCompatActivity implements View.OnClickListener { private int[] res = {R.id.iv_a, R.id.iv_b, R.id.iv_c, R.id.iv_d, R.id.iv_e, R.id.iv_f, R.id.iv_g, R.id.iv_h}; private List<ImageView> imageViewList = new ArrayList<>(); /** * 菜单打开或者关闭的标志 */ private boolean flag = true; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); for (int re : res) { //循环添加每个控件 ImageView imageView = (ImageView) findViewById(re); //为每个控件添加点击事件 imageView.setOnClickListener(this); //将每个控件添加到List中 imageViewList.add(imageView); } } @Override public void onClick(View view) { switch (view.getId()) { case R.id.iv_a: if (flag) { startAnim(); } else { emdAnim(); } break; default: //点击其他按钮,弹出toast Toast.makeText(getApplication(), \"click\" + view.getId(), Toast.LENGTH_SHORT).show(); break; } } /** * 关闭菜单动画 */ private void emdAnim() { for (int i = 1; i < res.length; i++) { PropertyValuesHolder Y, X; //X Y 都回归原点 Y = PropertyValuesHolder.ofFloat(\"translationY\", 0); X = PropertyValuesHolder.ofFloat(\"translationX\", 0); ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(imageViewList.get(i), X, Y); //设置动画执行时间 animator.setDuration(500); //每个控件之间的延时,形成每个按钮依次出现 animator.setStartDelay(i * 300); //为控件增加自由落体动画效果 animator.setInterpolator(new BounceInterpolator()); //执行动画 animator.start(); //重置flag flag = true; } } /** * 打开菜单动画 */ private void startAnim() { //扩散的距离,获取控件的高度的2倍 float x, y, n = imageViewList.get(0).getMeasuredHeight() * 2; for (int i = 1; i < res.length; i++) { int count = res.length - i; //需要扩散的角度 以180度为例 float angle = (float) (Math.PI * 180 / 180); //计算偏移的x,y坐标 x = (float) (n * Math.sin(angle / (res.length - 1) * count)); y = (float) (n * Math.cos(angle / (res.length - 1) * count)); PropertyValuesHolder Y, X; //设置动画 Y = PropertyValuesHolder.ofFloat(\"translationY\", y); X = PropertyValuesHolder.ofFloat(\"translationX\", x); //添加动画集合 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(imageViewList.get(i), X, Y); //设置动画执行时间 animator.setDuration(500); //每个控件之间的延时,形成每个按钮依次出现 animator.setStartDelay(i * 300); //为控件增加自由落体动画效果 animator.setInterpolator(new BounceInterpolator()); //执行动画 animator.start(); //重置flag flag = false; } }}","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"}]},{"title":"Sorgs天气app开发","date":"2017-05-20T06:02:18.000Z","path":"post/50314/","text":"看完了郭霖大神我第一行代码(第二版),最后书上那个案例,我也用来了实现了一下。修改了一点 #主要是完成了天气的更新和生活的建议 修改后台为3个小时更新 修改进入app首先根据定位来决定当地的天气,不再是手动选择 侧边栏可以选择查看中国不同城市的天气,点击还可以根据定位回到当前地区 做了一点简单的美化,当然背景图片还是必应的图片,每天更新 #代码就不贴了,先看看效果图 第一个图本来是需要这些权限的,是为了后面接入广告准备的 求下载啊!!! 最后还是要给出github地址,本项目开源","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"}]},{"title":"Android开发中屏幕的适配问题 px pd sp之间的转换工具","date":"2017-03-28T02:31:28.000Z","path":"post/32369/","text":"在Android开发中,美工妹子给我的图片都是px的单位,但是这个但是这个单位在程序中并不好,不能够自动适配。所以需要我们手动转换为dp。 #关于DP,PD,SPPPI = Pixels per inch,每英寸上的像素数,即 “像素密度” ppi的运算方式是:PPI = √(长度像素数² + 宽度像素数²) / 屏幕对角线英寸数 dp:Density-independent pixels,以160PPI屏幕为标准,则1dp=1px, dp和px的换算公式 :dp*ppi/160 = px。比如1dp x 320ppi/160 = 2px。 sp:Scale-independent pixels,它是安卓的字体单位,以160PPI屏幕为标准,当字体大小为 100%时, 1sp=1px。 sp 与 px 的换算公式:sp*ppi/160 = px 得出: px = dp*ppi/160dp = px / (ppi / 160) px = sp*ppi/160sp = px / (ppi / 160) dp ≈ sp #程序所以我就需要按计算机计算咯。但是我怎么可能手动呢,于是我就写了一个C艹的可以执行文件。帮助计算。都是些垃圾代码。不敢私藏,拿出来分享,有需要改进的就随便改。(改了给我说说,一起用) 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394#include<stdlib.h>#include<iostream>using namespace std;void PX_PD(float b) { while (true) { float a; cout << \"请输入px:\"; cin >> a; cout << \"dp = \" << a / b << endl; }}void PD_PX(float b) { while (true) { float a; cout << \"请输入pd:\"; cin >> a; cout << \"dx = \" << a * b << endl; }}int main() { cout << \"------------------------------------------------------------------------------------------------------------\" << endl; cout << \"author:sorgs.如有需要改进的地方欢迎提出QQ:1042746391\" << endl << \" 由于开发Android程序员,美工妹子给的图片的单位和程序单位不一致,所以产生需要转换。(所有数据基于标准)\" << endl; cout << \"dp是虚拟像素,在不同的像素密度的设备上会自动适配,这里就采用标准的就OK。(sp和dp基本一样)\" << endl; cout << \"在mdpi分辨率,像素密度为160,1dp=1px\" << endl; cout << \"在hdpi分辨率,像素密度为240,1dp=1.5px\" << endl; cout << \"在xhdpi分辨率,像素密度为320,1dp=2px\" << endl; cout << \"在xxhdpi分辨率,像素密度为480,1dp=3px\" << endl; cout << \"计算公式:1dp*像素密度/160 = 实际像素数\" << endl; cout << \"------------------------------------------------------------------------------------------------------------\" << endl; number3: cout << \"请输入需要转化的形式:1(px->dp) 2(dp->px)\" << endl; int i; cin >> i; if (i == 1) { number1: cout << \"请输入需要计算的分辨率: 1(mdpi) 2(hdpi) 3(xhdpi) 4(xxhdpi)\" << endl; int j; cin >> j; if (j == 1) { PX_PD(1); } if (j == 2) { PX_PD(1.5); } if (j == 3) { PX_PD(2); } if (j == 4) { PX_PD(3); } else { cout << \"貌似输入有误呢\" << endl; goto number1; } }if (i == 2) { number2: cout << \"请输入需要计算的分辨率: 1(mdpi) 2(hdpi) 3(xhdpi) 4(xxhdpi)\" << endl; int j; cin >> j; if (j == 1) { PX_PD(1); } if (j == 2) { PX_PD(1.5); } if (j == 3) { PX_PD(2); } if (j == 4) { PX_PD(3); } else { cout << \"貌似输入有误呢\" << endl; goto number2; } } else { cout << \"输入有误哦!\" << endl; goto number3; }} 最后放上源码地址:https://github.com/sorgs/DP_PX.git","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"}]},{"title":"Android studio 报错 error opening trace file: Permission denied (13)","date":"2017-03-04T05:46:37.000Z","path":"post/36822/","text":"关于这个报错,其实是Android 4.1(16)在Android studio 2.3产生的。 具体原因是因为Android 6.0之后的动态申请权限。很明显,这句话的意思是权限不足。在Android studio 升级到了2.3之后,4.1的模拟器本来不需要动态权限的,估计是个bug吧(个人猜测),因为5.1是完美运行的。 关于动态申请权限,这里就不再赘述,百度一搜一大把。反正养成动态申请权限的习惯是好多 。这里给出一个大神写的动态申请权限的demo。 https://github.com/Android-Mu/Android6.0Authority.git 具方法很多的,参考这demo就很不错了,写的很具体。 谨以此记录我调试一天的bug。 顺便放上我的拙劣代码,见笑了!(部分代码,SD卡的权限) 123456789101112131415161718192021222324252627282930313233/** * 检查权限 */ private void CheckPermission() { if (ContextCompat.checkSelfPermission(SplashActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { //权限不足就需要去申请,上下文,需要申请的权限,请求码(唯一就行) ActivityCompat.requestPermissions(SplashActivity.this , new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); } else { downloadApk(); } } /** * 回调的权限请求结果,是否同意都会调用 * * @param requestCode 请求码 * @param permissions 申请的权限 * @param grantResults 结果 */ @Override public void onRequestPermissionsResult(int requestCode , @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == 1) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { downloadApk(); } else { ToastUtil.show(getApplicationContext(), \"权限不足,不能更新,下次开启请允许权限,如没有弹出,请到设置中心开启权限\"); enterHome(); } } }","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"}]},{"title":"Windows网络编程-简单的多线程聊天室","date":"2016-11-25T13:05:20.000Z","path":"post/6290/","text":"实验室系统:Windows10实验室IDE:VS2012 创建MFC文件项目文件->新建->项目 然后根据图片进行操作 注意的地方:1.取消union库2.勾选上Windows套接字,让系统自动帮我们生成3.选择Dlog 添加控件点开工具箱 建立两个主框,一个用来接收数据,一个用来发送数据 再在主框里面建立3个edit,一个用来显示发送来的数据,一个用来显示自己发送的数据,下面那个用来显示要发送的数据 最后删掉原有,并加一个按钮为发送,添加一个ip控制 编写代码加载套接字数据库先手到我们的cpp中,因为我们之前选择了Windows套接字库,所以会给我们生成好 我们也可以让系统提示我们是否加载失败 1234567CWinApp::InitInstance(); //去加载套接字库,需要包含一个Afxsock.h的头文件,就不需要去链接套接字库 if (!AfxSocketInit()) { AfxMessageBox(\"Load socket\"); //利用AfxMessageBox弹出提示 return FALSE; } 接着我们去查看是包含了头文件 看到这个说明没有问题然后我们去头文件中去定义。 然后我们去写初始化的函数体 123456789101112131415161718192021222324252627BOOL CSorgsDlg::InitSocket(){ //套接字本身的初始化 //指定地址族,类型(给予UDT的数据包套接字),0(系统自己选择合适的协议) msocket = socket(AF_INET,SOCK_DGRAM,0);//msocket:私有权限的套接字描述符 if (INVALID_SOCKET == msocket) //判断套接字是否创建失败 { MessageBox(\"create socket false\"); return FALSE; } //作为接收端,需要绑定端口和地址上 SOCKADDR_IN acceptSock; //定义地址结构体的变量 acceptSock.sin_family = AF_INET; //地址族 acceptSock.sin_port = htons(6000);//设定端口 用htons转换 acceptSock.sin_addr.S_un.S_addr = htonl(INADDR_ANY); //接受发送到本地任何IP地址的数据 htonl转换 //进行绑定 int retval = 0; //定义整理变量用来判断 bind(msocket,(SOCKADDR *)&acceptSock,sizeof(SOCKADDR)); if (SOCKET_ERROR == retval) { closesocket(msocket); //关闭套接字 MessageBox(\"bind false\"); return FALSE; } return TRUE; } 写完了我们的函数,我们需要去设置加载它的地方。 在这个函数中去调用我么的初始化套接字函数1BOOL CSorgsChatDlg::OnInitDialog() 接收端现在开始接收端的程序,为解决CreateThread中的LPVOID只能传递一个参数值的问题,我们首先去头文件穿件一个一个结构体 123456//定义结构体,解决CreateThread中的LPVOID只能传递一个参数值的问题 struct RECVPARAM { SOCKET sock;//定义一个套接字类型的变量 HWND hwnd;//定义一个窗口类型的变量 }; 定义套接字的指针接下来去定义套接字的指针和串口句柄(现在刚刚调用初始化套接字函数的下面) 12345678RECVPARAM *mRecvParam = new RECVPARAM; //定义一个指针 mRecvParam->sock = msocket; //初始化我们创建的套接字 mRecvParam->hwnd = m_hWnd; //初始化我们窗口 mhWnd里面保存了和这个类相关的窗口的句柄 //调用CteateThread常见线程 //NULL,0:和调用线程使用一样的大小,线程函数的地址,(强转)参数,创建的标记(一旦创建立即运行),线程ID HANDLE mThread = CreateThread(NULL,0,Threadpro,(LPVOID)mRecvParam,0,NULL); CloseHandle(mThread); //将线程句柄关闭 同时递减线程类和对象的使用基数 为了使用完全使用面向对象的方式,我们去头文件进行定义 123456//当创建线程的时候。运行时代码需要去调用这个线程函数从而启动线程 //而我们为了不设置为全局函数设置为CSorgsChatDlg类的成员函数(完全面向对象的思想编程) //而要想调用这个成员函数,必须去定义一个CSorgsChatDlg的对象 //对于运行时代码来说,并不知道要定义那个对象或者说不知道怎么去定义 //所以我们就像这个成员函数设置为静态 static DWORD WINAPI Threadpro(LPVOID mlpvpid); //定义为静态,不属于那一个对象,只属于这个类本身 Threadpro然后开始编写Threadpro函数体 发送消息的函数然后就是去编写发送消息的函数先去我们的MFC界面双击发送按钮,生成点击事件按钮并右键属性,去知道我们的显示发送和显示接收数据的ID号 123456789101112131415161718192021222324252627void CSorgsDlg::OnBnClickedButton1() { DWORD dwIP; ((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);//IP控件的ID SOCKADDR_IN ToSock;//定义地址结构体变量 ToSock.sin_family = AF_INET; //地址族 ToSock.sin_port = htons(6000);//设定端口 用htons转换 ToSock.sin_addr.S_un.S_addr= htonl(dwIP); CString strSend; GetDlgItemText(IDC_EDIT4,strSend);//发送框里面ID,获取里面的内容 //套接字,发送的buffer,长度(多发送一个字节),标记,地址结构体的指针,地址结构体的长度 sendto(msocket,strSend,strSend.GetLength()+1,0,(SOCKADDR*)&ToSock,sizeof(SOCKADDR));//发送 SetDlgItemText(IDC_EDIT4,\"\");//发送之后将发送框的内容置空 //显示自己发送框的数据设置 CString strto; GetDlgItemText(IDC_EDIT3,strto);//获取文本,ID号,存放数据的地方 strto +=\"\\r\\n\"; //增加换行 strto +=\"帅帅的自己说:\"; strto += strSend; SetDlgItemText(IDC_EDIT3,strto);//将数据放回编辑框 } 消息响应然后我们在去编写消息响应函数在头文件中写入函数声明 消息映射然后去编写消息映射 然后去写函数体 12345678910111213141516//消息响应函数 //LRESULT,32位整形数,常常用于回调函数 LRESULT CSorgsDlg::MRecvData(WPARAM wParam,LPARAM lParam){ CString str = (char*)lParam; CString strTemp;//接收久的数据 GetDlgItemText(IDC_EDIT2,strTemp);//获取显示发送框ID号,存放数据的地方 str += \"\\r\\n\"; //增加换行 str +=strTemp; SetDlgItemText(IDC_EDIT2,str);//将数据放回编辑框 return TRUE; } 自此,我们代码编写完毕 优化控件使显示框分行右键属性把Multiline设置为true 按钮回车发送和不显示按钮右键按钮属性 测试这里我们使用127.0.0.1回环地址进行测试 这样就说明没有问题了","tags":[{"name":"Windows网络编程","slug":"Windows网络编程","permalink":"http://sorgs.cn/tags/Windows网络编程/"},{"name":"聊天室","slug":"聊天室","permalink":"http://sorgs.cn/tags/聊天室/"},{"name":"多线程","slug":"多线程","permalink":"http://sorgs.cn/tags/多线程/"}]},{"title":"Android两个android两个activity之间相互传递数据之装备选择(书上案例)","date":"2016-11-21T03:00:58.000Z","path":"post/6825/","text":"这个是书上的一个案例,我将其完善了一点而已 xml显示创建一个xml的主界面 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152<?xml version=\"1.0\" encoding=\"utf-8\"?><LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" xmlns:tools=\"http://schemas.android.com/tools\" android:id=\"@+id/activity_main\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:orientation=\"vertical\" android:gravity=\"center\" tools:context=\"sorgs.com.selectequipment.MainActivity\"> <ImageView android:id=\"@+id/pet_imgv\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_centerHorizontal=\"true\" android:layout_marginBottom=\"5dp\" android:layout_marginTop=\"30dp\" android:src=\"@drawable/body\"/> <TextView android:id=\"@+id/pet_dialog_tv\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_centerHorizontal=\"true\" android:layout_marginBottom=\"25dp\" android:gravity=\"center\" android:text=\"主人,快给小宝宝购买装备吧\"/> <TableLayout android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:layout_gravity=\"center\" android:layout_marginBottom=\"20dp\"> <TableRow android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\"> <TextView android:layout_width=\"0dip\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:text=\"生命值:\" android:textColor=\"@android:color/black\" android:textSize=\"14sp\"/> <ProgressBar android:id=\"@+id/progressBar1\" style=\"?android:attr/progressBarStyleHorizontal\" android:layout_width=\"0dip\" android:layout_height=\"wrap_content\" android:layout_gravity=\"center\" android:layout_weight=\"2\"/> <TextView android:id=\"@+id/tv_life_progress\" android:layout_width=\"0dip\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:text=\"0\" android:gravity=\"center\" android:textColor=\"#000000\" /> </TableRow> <TableRow android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\"> <TextView android:layout_width=\"0dip\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:text=\"攻击力:\" android:textColor=\"@android:color/black\" android:textSize=\"14sp\"/> <ProgressBar android:id=\"@+id/progressBar2\" style=\"?android:attr/progressBarStyleHorizontal\" android:layout_width=\"0dip\" android:layout_height=\"wrap_content\" android:layout_gravity=\"center\" android:layout_weight=\"2\"/> <TextView android:id=\"@+id/tv_attack_progress\" android:layout_width=\"0dip\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:text=\"0\" android:gravity=\"center\" android:textColor=\"#000000\" /> </TableRow> <TableRow android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\"> <TextView android:layout_width=\"0dip\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:text=\"敏捷:\" android:textColor=\"@android:color/black\" android:textSize=\"14sp\"/> <ProgressBar android:id=\"@+id/progressBar3\" style=\"?android:attr/progressBarStyleHorizontal\" android:layout_width=\"0dip\" android:layout_height=\"wrap_content\" android:layout_gravity=\"center\" android:layout_weight=\"2\"/> <TextView android:id=\"@+id/tv_speed_progress\" android:layout_width=\"0dip\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:text=\"0\" android:gravity=\"center\" android:textColor=\"#000000\" /> </TableRow> </TableLayout> <RelativeLayout android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\"> <Button android:id=\"@+id/btn_master\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_alignParentLeft=\"true\" android:layout_alignParentTop=\"true\" android:onClick=\"click1\" android:drawablePadding=\"3dp\" android:text=\"主人购买装备\" android:textSize=\"14sp\"/> <Button android:id=\"@+id/btn_baby\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_alignParentRight=\"true\" android:layout_alignParentTop=\"true\" android:onClick=\"click2\" android:drawablePadding=\"3dp\" android:text=\"小宝宝购买装备\" android:textSize=\"14sp\"/> </RelativeLayout></LinearLayout> 然后在创建一个购买装备购买的页面 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950<?xml version=\"1.0\" encoding=\"utf-8\"?><RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" android:id=\"@+id/r1\" android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:orientation=\"vertical\"> <View android:layout_width=\"30dp\" android:layout_height=\"30dp\" android:layout_centerVertical=\"true\" android:layout_alignParentLeft=\"true\"/> <TextView android:id=\"@+id/tv_name\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_centerVertical=\"true\" android:layout_marginLeft=\"60dp\" android:text=\"商品名称\"/> <LinearLayout android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_centerInParent=\"true\" android:orientation=\"vertical\"> <TextView android:id=\"@+id/tv_life\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:textSize=\"13sp\" android:text=\"生命值\"/> <TextView android:id=\"@+id/tv_attack\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:textSize=\"13sp\" android:text=\"攻击力\"/> <TextView android:id=\"@+id/tv_speed\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:textSize=\"13sp\" android:text=\"速度\"/> </LinearLayout></RelativeLayout> JAVA然后创建一个Itemfnfo类,用来封装装备信息 接着写代码 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859package sorgs.com.domain;import java.io.Serializable;/** * Created by Administrator on 2016/11/20. */public class ItemInfo implements Serializable{ private String name; private int acctack; private int life; private int speed; public ItemInfo(String name, int acctack, int life, int speed){ this.name = name; this.acctack = acctack; this.life = life; this.speed = speed; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAcctack() { return acctack; } public void setAcctack(int acctack) { this.acctack = acctack; } public int getLife() { return life; } public void setLife(int life) { this.life = life; } public int getSpeed() { return speed; } public void setSpeed(int speed) { this.speed = speed; } @Override public String toString() { return \"[\" + \"name='\" + name + \", acctack=\" + acctack + \", life=\" + life + \", speed=\" + speed + \"]\"; }} 然后创建一个shopactivity,用来展示主人装备信息的 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748package sorgs.com.selectequipment.sorgs.com;import android.app.Activity;import android.content.Intent;import android.os.Bundle;import android.view.View;import android.widget.TextView;import sorgs.com.domain.ItemInfo;import sorgs.com.selectequipment.R;/** * Created by Administrator on 2016/11/20. */public class ShopActivity extends Activity implements View.OnClickListener { private ItemInfo itemInfo; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_shop); itemInfo = new ItemInfo(\"金剑\",100,20,20); findViewById(R.id.r1).setOnClickListener(this); TextView mLifeTV = (TextView) findViewById(R.id.tv_life); TextView mNameTV = (TextView) findViewById(R.id.tv_name); TextView mSpeedTV = (TextView) findViewById(R.id.tv_speed); TextView mAttackTV = (TextView) findViewById(R.id.tv_attack); //TextView 显示字符串,这里传入int值编译不会报错,运行会出错 mLifeTV.setText(\"生命值+\"+itemInfo.getLife()); mNameTV.setText(itemInfo.getName()); mSpeedTV.setText(\"敏捷度+\"+itemInfo.getSpeed()); mAttackTV.setText(\"攻击力+\"+itemInfo.getAcctack()); } @Override public void onClick(View view) { switch (view.getId()){ case R.id.r1: Intent intent = new Intent(); intent.putExtra(\"equipment\",itemInfo); setResult(1,intent); //设置请求码 finish(); break; } }} 再建立一个shopactivity2,用来放宝宝购买装备 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748package sorgs.com.selectequipment.sorgs.com;import android.app.Activity;import android.content.Intent;import android.os.Bundle;import android.view.View;import android.widget.TextView;import sorgs.com.domain.ItemInfo;import sorgs.com.selectequipment.R;/** * Created by Administrator on 2016/11/20. */public class ShopActivity2 extends Activity implements View.OnClickListener{ private ItemInfo itemInfo; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_shop); itemInfo = new ItemInfo(\"银剑\",50,10,10); findViewById(R.id.r1).setOnClickListener(this); TextView mLifeTV = (TextView) findViewById(R.id.tv_life); TextView mNameTV = (TextView) findViewById(R.id.tv_name); TextView mSpeedTV = (TextView) findViewById(R.id.tv_speed); TextView mAttackTV = (TextView) findViewById(R.id.tv_attack); //TextView 显示字符串,这里传入int值编译不会报错,运行会出错 mLifeTV.setText(\"生命值+\"+itemInfo.getLife()); mNameTV.setText(itemInfo.getName()); mSpeedTV.setText(\"敏捷度+\"+itemInfo.getSpeed()); mAttackTV.setText(\"攻击力+\"+itemInfo.getAcctack()); } @Override public void onClick(View view) { switch (view.getId()){ case R.id.r1: Intent intent = new Intent(); intent.putExtra(\"equipment\",itemInfo); setResult(2,intent); //设置请求码 finish(); break; } }} 最后编写我们的主函数 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101package sorgs.com.selectequipment;import android.content.Intent;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.widget.ProgressBar;import android.widget.TextView;import sorgs.com.domain.ItemInfo;import sorgs.com.selectequipment.sorgs.com.ShopActivity;import sorgs.com.selectequipment.sorgs.com.ShopActivity2;public class MainActivity extends AppCompatActivity { private ProgressBar mProgressBar1; private ProgressBar mProgressBar2; private ProgressBar mProgressBar3; private TextView mLifeTV; private TextView mAttackTV; private TextView mSpeedTV; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mLifeTV = (TextView) findViewById(R.id.tv_life_progress); mAttackTV = (TextView) findViewById(R.id.tv_attack_progress); mSpeedTV = (TextView) findViewById(R.id.tv_speed_progress); initProgress(); //初始化进度条 } private void initProgress() { mProgressBar1 = (ProgressBar) findViewById(R.id.progressBar1); mProgressBar2 = (ProgressBar) findViewById(R.id.progressBar2); mProgressBar3 = (ProgressBar) findViewById(R.id.progressBar3); mProgressBar1.setMax(1000); //设置最大的值1000 mProgressBar2.setMax(1000); mProgressBar3.setMax(1000); } public void click1(View view){ //开启新的activity并且想获取他的返回值 Intent intent = new Intent(this, ShopActivity.class); startActivityForResult(intent,1); //返回请求结果,请求码为1 } public void click2(View view){ //开启新的activity并且想获取他的返回值 Intent intent = new Intent(this, ShopActivity2.class); startActivityForResult(intent,2); //返回请求结果,请求码为2 } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {//获取ShopActivity的装备信息 super.onActivityResult(requestCode, resultCode, data); if (data != null){ if (resultCode==1){ //判断结果码是否等于1,等于1为主人添加装备 if (requestCode == 1){ ItemInfo info = (ItemInfo) data.getSerializableExtra(\"equipment\"); updateProgress(info); //更新ProgressBar的值 } } else if (requestCode == 2){//判断结果码是否等于2,等于2为宝宝添加装备, if (requestCode == 2){ ItemInfo info = (ItemInfo) data.getSerializableExtra(\"equipment\"); updateProgress2(info); //更新ProgressBar的值 } } } } private void updateProgress2(ItemInfo info) { int progress1 = mProgressBar1.getProgress(); int progress2 = mProgressBar2.getProgress(); int progress3 = mProgressBar3.getProgress(); mProgressBar1.setProgress(progress1+info.getLife()); mProgressBar2.setProgress(progress2+info.getAcctack()); mProgressBar3.setProgress(progress3+info.getSpeed()); mLifeTV.setText(mProgressBar1.getProgress()+\"\"); mAttackTV.setText(mProgressBar2.getProgress()+\"\"); mSpeedTV.setText(mProgressBar3.getProgress()+\"\"); } private void updateProgress(ItemInfo info) { //更新ProgressBar的值 int progress1 = mProgressBar1.getProgress(); int progress2 = mProgressBar2.getProgress(); int progress3 = mProgressBar3.getProgress(); mProgressBar1.setProgress(progress1+info.getLife()); mProgressBar2.setProgress(progress2+info.getAcctack()); mProgressBar3.setProgress(progress3+info.getSpeed()); mLifeTV.setText(mProgressBar1.getProgress()+\"\"); mAttackTV.setText(mProgressBar2.getProgress()+\"\"); mSpeedTV.setText(mProgressBar3.getProgress()+\"\"); }} 配置最后一步,去配置清单里面去配置一下 效果","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"}]},{"title":"Android两个android两个activity之间相互传递数据","date":"2016-11-21T02:34:39.000Z","path":"post/50165/","text":"这个案例是书上的习题,我发生来了一点改变而已 xml:这是第一个xml,很简单的两个tv和ed加一个btn 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667<?xml version=\"1.0\" encoding=\"utf-8\"?><RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" xmlns:tools=\"http://schemas.android.com/tools\" android:id=\"@+id/activity_main\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:orientation=\"vertical\" tools:context=\"sorgs.com.datepass.MainActivity\"> <LinearLayout android:id=\"@+id/regisrt_username\" android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:layout_centerHorizontal=\"true\" android:layout_marginLeft=\"10dp\" android:layout_marginRight=\"10dp\" android:layout_marginTop=\"22dp\" android:orientation=\"horizontal\"> <TextView android:layout_width=\"80dp\" android:layout_height=\"wrap_content\" android:gravity=\"right\" android:paddingRight=\"5dp\" android:text=\"用户名:\"/> <EditText android:id=\"@+id/et_name\" android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:hint=\"请输入您的姓名\" android:textSize=\"14dp\"/> </LinearLayout> <LinearLayout android:id=\"@+id/regisrt_userage\" android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:layout_below=\"@+id/regisrt_username\" android:layout_centerHorizontal=\"true\" android:layout_marginLeft=\"10dp\" android:layout_marginRight=\"10dp\" android:layout_marginTop=\"5dp\" android:orientation=\"horizontal\"> <TextView android:layout_width=\"80dp\" android:layout_height=\"wrap_content\" android:gravity=\"right\" android:paddingRight=\"5dp\" android:text=\"用户名:\"/> <EditText android:id=\"@+id/et_age\" android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:hint=\"请输入您的年龄\" android:textSize=\"14dp\"/> </LinearLayout> <Button android:id=\"@+id/btn_send\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_below=\"@+id/regisrt_userage\" android:layout_centerHorizontal=\"true\" android:layout_marginTop=\"24dp\" android:text=\"发送\"/></RelativeLayout> 再来看看第二个,就显示第一个页面传过来的数据 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748<?xml version=\"1.0\" encoding=\"utf-8\"?><RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:orientation=\"vertical\"> <LinearLayout android:id=\"@+id/text\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:orientation=\"horizontal\"> <TextView android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:text=\"恭喜您,\" android:textSize=\"20dp\"/> <TextView android:id=\"@+id/tv_1\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:textSize=\"20dp\"/> <TextView android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:text=\"来到这个世界:\" android:textSize=\"20dp\"/> <TextView android:id=\"@+id/tv_2\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:textSize=\"20dp\"/> </LinearLayout> <Button android:id=\"@+id/btn_return\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_below=\"@+id/text\" android:layout_centerHorizontal=\"true\" android:layout_marginTop=\"24dp\" android:text=\"返回\"/></RelativeLayout> JAVA:接下来就是java的代码 12345678910111213141516171819202122232425262728293031323334353637383940414243package sorgs.com.datepass;import android.content.Intent;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;public class MainActivity extends AppCompatActivity { private Button btn1; private EditText etname; private EditText etage; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn1 = (Button) findViewById(R.id.btn_send); etage = (EditText) findViewById(R.id.et_age); etname = (EditText) findViewById(R.id.et_name); btn1.setOnClickListener(new View.OnClickListener() { //用户点击按钮提交数据 @Override public void onClick(View view) { pssDate(); } }); } public void pssDate(){ Intent intent = new Intent(this,MainActivity2.class);//创建Intent对象,启动MainActivity2 intent.putExtra(\"name\",etname.getText().toString().trim()); //将数据存入Intent对象 intent.putExtra(\"age\",etage.getText().toString().trim()); startActivity(intent); finish(); }} 然后就是第二个页面的java了 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950package sorgs.com.datepass;import android.app.Activity;import android.content.Intent;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.TextView;/** * Created by Administrator on 2016/11/20. */public class MainActivity2 extends Activity { private TextView tv1; private TextView tv2; private Button btn2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_date); tv1 = (TextView) findViewById(R.id.tv_1); tv2 = (TextView) findViewById(R.id.tv_2); btn2 = (Button) findViewById(R.id.btn_return); Intent intent1 = getIntent();//获取Intent对象 //取出对key中的值 String name = intent1.getStringExtra(\"name\"); String age = intent1.getStringExtra(\"age\"); //设置到对的控件中 tv1.setText(name + \"!\"); tv2.setText(age + \"年。\"); btn2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { updata(); } }); } private void updata() { Intent intent2 = new Intent(this,MainActivity.class); startActivity(intent2); finish(); }} 配置第二个页面做了一个跳转回第一个页面的处理 最后一定记得在配置里面配置一下 来看看效果","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"}]},{"title":"android隐式意图开启系统照相机","date":"2016-11-20T11:21:24.000Z","path":"post/21426/","text":"由于书上是转到另一个页面,我是用真机,所以直接打开相机 先是123456789101112131415161718<?xml version=\"1.0\" encoding=\"utf-8\"?><RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" xmlns:tools=\"http://schemas.android.com/tools\" android:id=\"@+id/activity_main\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" tools:context=\"sorgs.com.opencamera.MainActivity\"> <!--layout_centerHorizontal将控件置于水平方向的中心位置 layout_centerVertical让这个相对布局,处于它父控件的垂直方向的中心--> <Button android:id=\"@+id/openCamera\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_centerHorizontal=\"true\" android:layout_centerVertical=\"true\" android:text=\"打开相机\" /></RelativeLayout> 然后是java 12345678910111213141516171819202122232425262728package sorgs.com.opencamera;import android.content.Intent;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.widget.Button;public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = (Button) findViewById(R.id.openCamera); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(); intent.setAction(\"android.media.action.IMAGE_CAPTUER\"); intent.addCategory(\"android.intent.category.DEFAULT\"); startActivity(intent); } }); }} 最后需要再配置清单里弄一下 1234567891011<activity android:name=\".MainActivity\"> <intent-filter> <action android:name=\"android.intent.action.MAIN\" /> <category android:name=\"android.intent.category.LAUNCHER\" /> <action android:name=\"androd.media.action.IMAGE_CAPTURE\"/> <category android:name=\"android.intent.category.DEFAULT\" /> </intent-filter> </activity>","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"}]},{"title":"Android逆向基础笔记—Android NDK开发4之Android studio NDK自动编译","date":"2016-11-17T08:41:09.000Z","path":"post/16045/","text":"这部分就是最后的部分了,为什么要写Android studio呢。大家知道,eclipse 到了现在,已经不被Google支持了,所以现在最好的开发就是利用Android studio。虽然说,网上有很多类似的教程了。但是我都一一试过了,并不是太详细,还有些少许错误。所以,我在这里写出详细的过程,大家笑笑就好。但是新生我这个劝一句,最好做一遍,这个很重要。我们用Android studio创建一个app工程,我这建立的是17。切换到project模式。 然后这个工程上右键,选择open module settings然后配置环境 设置好了之后,我们去看看是否设置成功呢 确认之后,就该我们去写代码了。为了方便,我直接把hello world的textview改为动了一下 之后我们在这里创建一个类 之后写下如下代码 之后build一下工程 然后去查看时候生成这个文件 如果有的话Terminal输入指令:cd app/build/intermediates/classes/debug 再输入:javah -jni sorgs.com.hellondk.NDKtest 看到这个文件,就说明OK了。然后在src/main下新建文件夹jni,把生成的.h文件移到jni文件夹下面去,新建一个c类随便取一个名字 一定不要选这个 写上这些代码 123456#include \"sorgs_com_hellondk_NDKtest.h\" JNIEXPORT jstring JNICALL Java_sorgs_com_hellondk_NDKtest_getString (JNIEnv *env, jobject obj){ return (*env)->NewStringUTF(env,\"you are successful!\"); } 然后我们还需要去gradle.properties文件末尾添加android.useDeprecatedNdk=true 再然后 然后在app文件下得build.gradle ->defaultConfig括号内添加如下代码 123ndk { moduleName \"test\" //生成的so名字,一定要和So的名称一致,这里就是test) abiFilters \"armeabi\", \"armeabi-v7a\", \"x86\" //输出指定三种abi体系结构下的so库,随便写一个就行,主要是看模拟器。也可以都写上} 然后再去主函数写代码了 然后build一下,去尝试运行看看效果","tags":[{"name":"Android逆向基础","slug":"Android逆向基础","permalink":"http://sorgs.cn/tags/Android逆向基础/"},{"name":"ndk","slug":"ndk","permalink":"http://sorgs.cn/tags/ndk/"},{"name":"gcc","slug":"gcc","permalink":"http://sorgs.cn/tags/gcc/"},{"name":"sdk","slug":"sdk","permalink":"http://sorgs.cn/tags/sdk/"}]},{"title":"Android逆向基础笔记—Android NDK开发3之使用ndk-build工具手动编译","date":"2016-11-17T08:35:10.000Z","path":"post/31148/","text":"做这个之前,必须把android.bat的环境配置进去 然后我们使用android list看看Android SDK种所有的SDK版本 我在这里选择了Android-17输入如下命令1android create project -n NDKtest -p NDTtest -t android-17 -k com.sorgs.NDKtest -a MyActiviry 这个命令可以根据默认Activity文件名自动生成java文件,并生成AndroidMenifest.xml 之后我们在跟目录下建立一个文件夹jni。然后把C文件放进去。然后开始编写Android.mk这里说明一下ndk-build使用Android.mk和Application.mk作为脚本文件Application.mk是可选的,是用来描述原生程序本身用到的一些特性。Android.mk文件是工程的编译脚本,描述了编译原生程序所需的编译选项、头文件、源文件以及依赖库所以我们这里暂时只需要编写Android.mk 12345678<span style=\"font-size:14px;color:#ff9900;\"><strong>LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_ARM_MODE := arm LOCAL_MODULE := NDKtest LOCAL_SRC_FILES := NDKtest.c include $(BUILD_EXECUTABLE)</strong></span> 然后把它也放到jni里面 然后我们到NDKtest的目录下,输入ndk-build之后等待命令的完成。完成之后会在libs/armeabi等一系列的文件夹里生成NDKtest可执行文件。 然后我们把文件push到手机中去 想要运行它,就的给它权限。 使用之前的 adb shell /data/NDKtest命令或者在adb shell里面使用./NDKtest都可以! 总结:在这篇里面,难度不算太大。重要的是要会只要配置好环境,会一些基本的Linux命令就是OK的。剩下的就是多去思考了。比如我在成功之前,失败了很多次。善于思考才能解决问题。最后给出下载的地方,大家可以参考:链接:http://pan.baidu.com/s/1hsHjRik 密码:uqvh","tags":[{"name":"Android逆向基础","slug":"Android逆向基础","permalink":"http://sorgs.cn/tags/Android逆向基础/"},{"name":"ndk","slug":"ndk","permalink":"http://sorgs.cn/tags/ndk/"},{"name":"gcc","slug":"gcc","permalink":"http://sorgs.cn/tags/gcc/"},{"name":"sdk","slug":"sdk","permalink":"http://sorgs.cn/tags/sdk/"}]},{"title":"Android逆向基础笔记—Android NDK开发1环境的配置及介绍","date":"2016-11-17T08:29:38.000Z","path":"post/3280/","text":"有句话,叫做开发的能力决定逆向的能力。为了更好的去研究so,我整理了非虫大侠的书的NDK开发。把书中的老版本更新一下,并把不清楚的地方搞清楚。写一个NDK系类的基础教程。如有不对的地方,还请大神扶正。虽然看起来这些很简单,但是实际动手去做 分别为:1.环境的配置2.利用gcc编译器(交叉工具链)手动编译和Linux Ubuntu系统下的交叉工具链手动编译3.使用ndk-build工具手动编译和.Android studio NDK编译 一.使用的系统 Windows10工具:java version “1.8.0_60” NDK:android-ndk-r13 Android studio 1.5.0 SDKJava的话,直接百度即可NDK和Android studio给一个下的地方:一个安卓工具集合的网站:http://androiddevtools.cn/ 使用的系统Linux ubuntu-16.04工具:java version “1.8.0_60” NDK:android-ndk-r13 SDK 二.配置环境 参照我的方式把SDK,JAVA,DNK的环境配置带环境变量中去。 成功的效果图: java的 其实Windows的环境是很好装的,只要是Linux,真是各种百度。关于Java的话,请参考这里:http://blog.csdn.net/qq_24349189/article/details/53000869然后就是NDK环境首先还是去给的网址把包下下来,然后我们放到Linux下面,使用1sorgs@sorgs-VirtualBox:$ sudo unzip android-ndk-r13-linux-x86_64.zip 之后把环境配置进去使用1sorgs@sorgs-VirtualBox:/home/tools$ sudo gedit /etc/profile 写上这个 然后执行1sorgs@sorgs-VirtualBox:/home/tools$ sudo source /etc/profile 使环境变量生效之后我们来看看是否成功可以输入make和ndk-build 已经成功","tags":[{"name":"Android逆向基础","slug":"Android逆向基础","permalink":"http://sorgs.cn/tags/Android逆向基础/"},{"name":"ndk","slug":"ndk","permalink":"http://sorgs.cn/tags/ndk/"},{"name":"gcc","slug":"gcc","permalink":"http://sorgs.cn/tags/gcc/"},{"name":"linux","slug":"linux","permalink":"http://sorgs.cn/tags/linux/"}]},{"title":"Android逆向基础笔记—Android NDK开发2之Windows下的gcc手动编译(交叉连编译)和利Linux Ubuntu系统下的交叉工具链手动编译","date":"2016-11-17T08:18:25.000Z","path":"post/33150/","text":"一、交叉工具链这些工具都在NDK的路径下:E:\\Android\\android-ndk-r13\\toolchains\\arm-linux-androideabi-4.9\\prebuilt\\windows-x86_64\\bin这些工具的前缀均为arm-linux-androideabi,可以直接使用他们来编写NDK原生程序Windows和Linux平台使用的gcc都是一样的,命令参数也是一样的:arm-linux-androideabi-addr2line //将程序地址转换为文件名和行号arm-linux-androideabi-ar // 建立、修改、提取归档文件arm-linux-androideabi-as //gas汇编器arm-linux-androideabi-c++ //工具链中arm-linux-androideabi-g++.exe的一个拷贝arm-linux-androideabi-c++filt //连接器使用它过滤符号,防止重载函数冲突arm-linux-androideabi-cpp //C++程序编译工具arm-linux-androideabi-g++ //C++程序编译工具arm-linux-androideabi-gcc-4.9.x //工具链中arm-linux-androideabi-gcc.exe的一个拷贝arm-linux-androideabi-gcc //C程序编译工具arm-linux-androideabi-gcov //程序覆盖度测量工具,记录代码的执行路径arm-linux-androideabi-gdb //调试工具arm-linux-androideabi-gprof //程序性能测量工具arm-linux-androideabi-ld //连接器,用于生成可执行程序arm-linux-androideabi-nm //列出目标文件中的符号arm-linux-androideabi-objcopy //复制目标文件中的内容到另一种类型的目标文件中arm-linux-androideabi-objdump //输出目标文件的信息arm-linux-androideabi-ranlib //产生归档文件索引,并将其保存到这个归档文件中arm-linux-androideabi-readelf //显示elf格式可执行文件的信息arm-linux-androideabi-run //ARM程序模拟器arm-linux-androideabi-size //列出目标文件每一段的大小及总体的大小arm-linux-androideabi-strings //输出目标文件的可打印字符串arm-linux-androideabi-strip //去除目标文件中的符号信息以上摘录之《Android软件安全与逆向分析》 二、编写C++程序我这里习惯使用VS2012,大家可以按自己的习惯。记事本都可以。 1234#include <stdio.h> void main(){ printf(\"Hello,you are successful !\"); } 三、编写makefile 注意:1.斜杠的方向(千万不要弄反了,这个很重要!!!) 2.把我makefile中 的//以及后面的内容删除! 3.一定记得先配置环境最后给这个makefile文档 1234567891011121314151617181920212223242526272829303132NDK_ROOT=E:/Android/android-ndk-r13 TOOLCHAINS_ROOT=$(NDK_ROOT)/toolchains/arm-linux-androideabi-4.9/prebuilt/windows-x86_64 TOOLCHAINS_PREFIX=$(TOOLCHAINS_ROOT)/bin/arm-linux-androideabi TOOLCHAINS_INCLUDE=$(TOOLCHAINS_ROOT)/lib/gcc/arm-linux-androideabi/4.9.x/include-fixed PLATFORM_ROOT=$(NDK_ROOT)/platforms/android-17/arch-arm PLATFORM_INCLUDE=$(PLATFORM_ROOT)/usr/include PLATFORM_LIB=$(PLATFORM_ROOT)/usr/lib MODULE_NAME=HelloNDK RM=del FLAGS=-I$(TOOLCHAINS_INCLUDE) \\ -I$(PLATFORM_INCLUDE) \\ -L$(PLATFORM_LIB) \\ -nostdlib \\ -lgcc \\ -Bdynamic \\ -lc OBJS=$(MODULE_NAME).o \\ $(PLATFORM_LIB)/crtbegin_dynamic.o \\ $(PLATFORM_LIB)/crtend_android.o all: $(TOOLCHAINS_PREFIX)-gcc $(FLAGS) -c $(MODULE_NAME).c -o $(MODULE_NAME).o $(TOOLCHAINS_PREFIX)-gcc $(FLAGS) $(OBJS) -o $(MODULE_NAME) clean: $(RM) *.o install: adb push $(MODULE_NAME) /data/local/ adb shell chmod 755 /data/local/$(MODULE_NAME) 四、编译我们把这些东西弄好了放到桌面上的文件夹gccNDKtest。然后cmd命令打开这个文件夹然后make看到是这个样子,就说明没有问题了 接下来,我们需要看效果,就需要一个Android的手机或者模拟器了。依次输入 make installadb shell /data/local/HelloNDK 就可以看到效果图了 五、Linux和Windows差不多,重点是环境需要配置好我直接把Windows里面的C放进去,再编写makefilemakefile如下 1234567891011121314151617181920212223242526272829303132NDK_ROOT=/home/tools/android-ndk-r13 TOOLCHAINS_ROOT=$(NDK_ROOT)/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64 TOOLCHAINS_PREFIX=$(TOOLCHAINS_ROOT)/bin/arm-linux-androideabi TOOLCHAINS_INCLUDE=$(TOOLCHAINS_ROOT)/lib/gcc/arm-linux-androideabi/4.9.x/include-fixed PLATFORM_ROOT=$(NDK_ROOT)/platforms/android-17/arch-arm PLATFORM_INCLUDE=$(PLATFORM_ROOT)/usr/include PLATFORM_LIB=$(PLATFORM_ROOT)/usr/lib MODULE_NAME=HelloNDK RM=rm -rf FLAGS=-I$(TOOLCHAINS_INCLUDE) \\ -I$(PLATFORM_INCLUDE) \\ -L$(PLATFORM_LIB) \\ -nostdlib \\ -lgcc \\ -Bdynamic \\ -lc OBJS=$(MODULE_NAME).o \\ $(PLATFORM_LIB)/crtbegin_dynamic.o \\ $(PLATFORM_LIB)/crtend_android.o all: $(TOOLCHAINS_PREFIX)-gcc $(FLAGS) -c $(MODULE_NAME).c -o $(MODULE_NAME).o $(TOOLCHAINS_PREFIX)-gcc $(FLAGS) $(OBJS) -o $(MODULE_NAME) clean: $(RM) *.o install: adb push $(MODULE_NAME) /data/local/ adb shell chmod 755 /data/local/$(MODULE_NAME) 然后放到Linux下面去,到这个文件的目录下make就OK的。命令是 sorgs@sorgs-VirtualBox:/home/tools/gccNDKtest$ sudo make 这个就说明编译成功了。因为我这Linux是虚拟机,所以不好用手机真是去测试所以手续的测试就在Windows上测试的,效果和上面的一样。 六、总结虽然看起来这很简单,但是实际上自己不去动手,永远不知道这点点东西来的多么艰辛,各种百度查。其实这个还遗留了一个问题。本来说好的NDK用gcc编译,是可以编译C++的,但是我写了一个C++。不管怎么修改makefile都要报错。这个问题我查了很多资料,都没有结果。我在想等下,有空了,去问问我们的老师,看看能不能给出答案。如果可以的话,再回来更新,编写一个C++的代码尝试编译。然后就是Linux,我的天啊。我之前都没有学过这个,然后为了写出来来尝试,才开始研究。各种报错,各种重装。你不去动手,永远不知道这里面的辛酸和晚上连续几天熬夜到12点的汗水以及成功之后的喜悦。所以说,不要看着简单,要实际去做做!链接:http://pan.baidu.com/s/1boNj4IF 密码:w4ec","tags":[{"name":"Android逆向基础","slug":"Android逆向基础","permalink":"http://sorgs.cn/tags/Android逆向基础/"},{"name":"ndk","slug":"ndk","permalink":"http://sorgs.cn/tags/ndk/"},{"name":"gcc","slug":"gcc","permalink":"http://sorgs.cn/tags/gcc/"}]},{"title":"Linux ubuntu的vbox和本机Windows文件共享以及Linux中的java环境配置","date":"2016-11-17T08:12:37.000Z","path":"post/33593/","text":"实验环境:本机Windows10Vbox5.1.8 r111374Linux:ubuntu 一、共享文件设置安装就不用说明了,安装完了需要再Linux把增强工具装好安装完Linux之后,在Windows本机的E盘新建一个share文件夹之后在VBOX里面设置挂在这个文件夹 然后打开Linux在终端里面输入命令:12sudo mkdir /home/shared //新建一个文件夹sudo mount -t vboxsf share /home/shared //把Windows的share 文件夹加载到shared里来需要注意的是:1.shared和share不一样。不然会有如下错误:/sbin/mount.vboxsf: mounting failed with the error: Protocol error2.每次开机都需要输入这命令3.不想每次开机都输的话。我们接下来这样操作。执行这个命令:1sorgs@sorgs-VirtualBox:~$ sudo nano /etc/rc.local 然后在最后一行加上:1mount -t vboxsf share /home/shared 意思就是开机就执行这句话然后我们去放一个文件到本机的share里面,去Linux看看时候加载进去了 这就说明加载进去了 二、java环境配置设置之前需要先删一下系统原来自带的,不管有没有1root@sorgs-VirtualBox:/home/sorgs# apt-get purge openjdk-\\* 确认:Y,等待一段时间后,卸载完成!然后更具自己的系统去官网下一个jdk.gzhttp://www.oracle.com/technetwork/java/javase/downloads/index.html我是选择在本机上下好了,通过共享弄进去的。个人觉得方便。然后提取出来1root@sorgs-VirtualBox:/mnt/shared# tar -xvf jdk-8u112-linux-64.gz -C /usr/local 之后在使用 VI 打开 /etc/profile 文件,如下命令:1root@sorgs-VirtualBox:/mnt/shared# vi /etc/profile 在文件的最后位置,写入 Java 的环境变量1234JAVA_HOME=/usr/local/jdk1.8.0_112PATH=$PATH:$HOME/bin:$JAVA_HOME/binexport JAVA_HOMEexport PATH esc返回,输入 :wq 保存文件。再分别输入一下命令123456root@sorgs-VirtualBox:/mnt/shared# update-alternatives --install "/usr/bin/java" "java" "/usr/local/jdk1.8.0_112/bin/java" 1root@sorgs-VirtualBox:/mnt/shared# update-alternatives --install "/usr/bin/javac" "javac" "/usr/local/jdk1.8.0_112/bin/javac" 1root@sorgs-VirtualBox:/mnt/shared# update-alternatives --install "/usr/bin/javaws" "javaws" "/usr/local/jdk1.8.0_112/bin/javaws" 1root@sorgs-VirtualBox:/mnt/shared# update-alternatives --set java /usr/local/jdk1.8.0_112/bin/javaroot@sorgs-VirtualBox:/mnt/shared# update-alternatives --set javac /usr/local/jdk1.8.0_112/bin/javacroot@sorgs-VirtualBox:/mnt/shared# update-alternatives --set javaws /usr/local/jdk1.8.0_112/bin/javaws 最后我们来测试看看:java -version 这就说明已经配置成功了!","tags":[{"name":"linux","slug":"linux","permalink":"http://sorgs.cn/tags/linux/"},{"name":"windows","slug":"windows","permalink":"http://sorgs.cn/tags/windows/"},{"name":"java","slug":"java","permalink":"http://sorgs.cn/tags/java/"},{"name":"文件共享","slug":"文件共享","permalink":"http://sorgs.cn/tags/文件共享/"}]},{"title":"Android逆向实例笔记—在so里对游戏的修改","date":"2016-11-17T08:04:57.000Z","path":"post/47975/","text":"这里还是利用鬼哥的提供的样本,天天消联盟 这里我就不玩了,直接AK看看。这是一个移动的支付,直接搜索OnBillingFinish来看看源码。 我们从这里就很轻松知道了,关键就这这个paramInt。如果等于102或者104或者1001,我们就购买成功。也就是说,我们在这if之前,给paramInt一个值也就是OK的。 像这样修改就是OK的。我们今天的任务是从so来看看,那么继续跟入。我们看到在if里面,有调用PopStar。我们过去看看。在这个类的开始,有这个,也是个关键,先记住。 说明,这里是调用的xinxin的so。我们接着往下看看。看到了关键的方法 这里看到了去掉的so,接下来我们去IDA里面看看了。直接搜索我们刚刚调用。双击过去。 找到关键的地方。 然后双击过去看看。 这里我们就可以修改,鬼哥的教程也是在这里修改的。可以直接把A41EE 这句改为mov R0,#FF也是可以的(我没有实验哈,有兴趣的可以自己去试试,理论上是可以的哈)我们继续往下看上面有个get去获取那我们进去看看F5之后,我们可以看到反正是返回了一个值,这个值就是我们要的。我们可以在这里动手脚 这里是一种方式,然后我们还看到有set,就是去设置金币。那我们去看看同样F5之后,看到返回的东西 接下来使用无名侠的SH依然使用仅加载的方式。 改为255之后,我们使用两次道具之后 然后就一直保持这个数据了。说明我们修改成功的","tags":[{"name":"Android逆向实例","slug":"Android逆向实例","permalink":"http://sorgs.cn/tags/Android逆向实例/"},{"name":"so修改","slug":"so修改","permalink":"http://sorgs.cn/tags/so修改/"},{"name":"IDA","slug":"IDA","permalink":"http://sorgs.cn/tags/IDA/"},{"name":"反编译","slug":"反编译","permalink":"http://sorgs.cn/tags/反编译/"},{"name":"游戏","slug":"游戏","permalink":"http://sorgs.cn/tags/游戏/"}]},{"title":"Android逆向实例笔记—初入so并还原分析出代码","date":"2016-11-17T07:55:37.000Z","path":"post/64124/","text":"很久没有出基础教程了,这里做一个鬼哥的so的作业吧。很基础的东西,算是教学帖子吧。大牛路过吧。这里感谢鬼哥的apk和无名侠的软件 首先我们打开鬼哥提供的apk看看。 其余没有发现什么,我们直接AK来看看吧。 直接看看Java的代码 1234567protected void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); setContentView(2130903040); this.tview = ((TextView)findViewById(2131230720)); this.tview.setText(JniGg.ggPrintHello() + JniGg.VipLevel(5)); } 我们发现往so里传入了一个很重要的参数5,这点是个关键。我们先记住。接下来我们打开so的位置,拖入IDA了。 右键,打开文件路径-》打开文件位置,复制到桌面建一个test的文件夹。拖入IDA看看。 这里,我喜欢先看看输出。(看个人习惯吧) 这里有个viplevel对吧,很明显了。我们双击过去 几种等级都出来了,这个时候我们按下空格切换视图 这里就非常的清晰了,对吧。(这里还是需要懂得一些arm汇编知识的) 我们仔细分析下下。然后我写在了图上 这里就是和输入的数字有关我整理出来的是这样的:gold vip 1silvery vip 2copper vip 3normal user other分析完了,我们就知道该怎么修改了那么方法一:修改samli,就是我们之前看到的那个穿进去的参数了。 这样汇编就OK了,但是我们今天是对so进行操作。方法二:PUSH {R3,LR}这个不能动,那我们就只有动第一个比较了。剩下我们试试,so helper(无名侠大哥的作品)拖进去,然后仅加载 这里最重点是指令长度了。其实我们利用00000C44减去00000C42就知道是2了。这里已经改为了赋值,那么下面那个比较就没有用了,直接nop掉。 然后点击编译,去替换掉之前的so文件,记得名字要改为原来那个。 方法三就是把所有的比较改为Nop,就是执行每个比较,直接到后面的gold vip。这个大家就自己尝试一下下。其实还有几种方法,有点思路,但是没去研究。最后给出还原出来的函数(没完全还原,只还原这部分)","tags":[{"name":"Android逆向实例","slug":"Android逆向实例","permalink":"http://sorgs.cn/tags/Android逆向实例/"},{"name":"so修改","slug":"so修改","permalink":"http://sorgs.cn/tags/so修改/"},{"name":"破解","slug":"破解","permalink":"http://sorgs.cn/tags/破解/"},{"name":"IDA","slug":"IDA","permalink":"http://sorgs.cn/tags/IDA/"},{"name":"反编译","slug":"反编译","permalink":"http://sorgs.cn/tags/反编译/"}]},{"title":"Android逆向实例笔记—手游中的内购破解(火柴人联盟最新版1.9.2 BB弹 )","date":"2016-11-16T14:53:25.000Z","path":"post/2575/","text":"最近学到了一些内购的破解方式,就来试试手。然后找个了比较火爆的游戏BB弹,找个个没壳的就来练习。这些东西都是大神写烂了的东西了,我这里只是写出我自己找不到方法的时候的思路。勿笑。 一、BB弹BB弹的话比较简单,我们首先弄到模拟器上看看是什么支付。 我们发现支付宝和话费都可以。那说明我们有很多种方法去破解内购了。我们的目的就是取消就为购买! 我们先用支付宝的 该图为引用的。 我们直接搜索0x1771 把它改为0x1771 -> :sswitch_0 就行了。 然后再来试试话费的,我们直接所搜索paysuccess 分别点进去看看 1234567891011121314151617181920212223242526272829303132333435363738394041424344.class public interface abstract Lcn/egame/terminal/paysdk/EgamePayListener; .super Ljava/lang/Object; .source \"EgamePayListener.java\" # virtual methods .method public abstract payCancel(Ljava/util/Map;)V .annotation system Ldalvik/annotation/Signature; value = { \"(\", \"Ljava/util/Map\", \"<\", \"Ljava/lang/String;\", \"Ljava/lang/String;\", \">;)V\" } .end annotation .end method .method public abstract payFailed(Ljava/util/Map;I)V .annotation system Ldalvik/annotation/Signature; value = { \"(\", \"Ljava/util/Map\", \"<\", \"Ljava/lang/String;\", \"Ljava/lang/String;\", \">;I)V\" } .end annotation .end method .method public abstract paySuccess(Ljava/util/Map;)V .annotation system Ldalvik/annotation/Signature; value = { \"(\", \"Ljava/util/Map\", \"<\", \"Ljava/lang/String;\", \"Ljava/lang/String;\", \">;)V\" } .end annotation .end method 显然看不出太多的信息第二处 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129.class Lcom/zplay/bbtan/plug/EgamePlug$2$1; .super Ljava/lang/Object; .source \"EgamePlug.java\" # interfaces .implements Lcn/egame/terminal/paysdk/EgamePayListener; # annotations .annotation system Ldalvik/annotation/EnclosingMethod; value = Lcom/zplay/bbtan/plug/EgamePlug$2;->run()V .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x0 name = null .end annotation # instance fields .field final synthetic this$1:Lcom/zplay/bbtan/plug/EgamePlug$2; # direct methods .method constructor <init>(Lcom/zplay/bbtan/plug/EgamePlug$2;)V .locals 0 .prologue .line 1 iput-object p1, p0, Lcom/zplay/bbtan/plug/EgamePlug$2$1;->this$1:Lcom/zplay/bbtan/plug/EgamePlug$2; .line 79 invoke-direct {p0}, Ljava/lang/Object;-><init>()V return-void .end method # virtual methods .method public paySuccess(Ljava/util/Map;)V .locals 2 .param p1, \"params\" # Ljava/util/Map; .prologue .line 90 sget-object v0, Lcom/dubo/android/JniMsgType;->RechargeFail:Lcom/dubo/android/JniMsgType; invoke-virtual {v0}, Lcom/dubo/android/JniMsgType;->ordinal()I move-result v0 iget-object v1, p0, Lcom/zplay/bbtan/plug/EgamePlug$2$1;->this$1:Lcom/zplay/bbtan/plug/EgamePlug$2; # getter for: Lcom/zplay/bbtan/plug/EgamePlug$2;->this$0:Lcom/zplay/bbtan/plug/EgamePlug; invoke-static {v1}, Lcom/zplay/bbtan/plug/EgamePlug$2;->access$0(Lcom/zplay/bbtan/plug/EgamePlug$2;)Lcom/zplay/bbtan/plug/EgamePlug; move-result-object v1 # getter for: Lcom/zplay/bbtan/plug/EgamePlug;->_sku:Ljava/lang/String; invoke-static {v1}, Lcom/zplay/bbtan/plug/EgamePlug;->access$1(Lcom/zplay/bbtan/plug/EgamePlug;)Ljava/lang/String; move-result-object v1 invoke-static {v0, v1}, Lcom/dubo/android/PlatformMessage;->SendPlatformMessage(ILjava/lang/String;)V .line 91 return-void .end method .method public payFailed(Ljava/util/Map;I)V .locals 2 .param p1, \"params\" # Ljava/util/Map; .param p2, \"errorInt\" # I .prologue .line 86 sget-object v0, Lcom/dubo/android/JniMsgType;->RechargeFail:Lcom/dubo/android/JniMsgType; invoke-virtual {v0}, Lcom/dubo/android/JniMsgType;->ordinal()I move-result v0 iget-object v1, p0, Lcom/zplay/bbtan/plug/EgamePlug$2$1;->this$1:Lcom/zplay/bbtan/plug/EgamePlug$2; # getter for: Lcom/zplay/bbtan/plug/EgamePlug$2;->this$0:Lcom/zplay/bbtan/plug/EgamePlug; invoke-static {v1}, Lcom/zplay/bbtan/plug/EgamePlug$2;->access$0(Lcom/zplay/bbtan/plug/EgamePlug$2;)Lcom/zplay/bbtan/plug/EgamePlug; move-result-object v1 # getter for: Lcom/zplay/bbtan/plug/EgamePlug;->_sku:Ljava/lang/String; invoke-static {v1}, Lcom/zplay/bbtan/plug/EgamePlug;->access$1(Lcom/zplay/bbtan/plug/EgamePlug;)Ljava/lang/String; move-result-object v1 invoke-static {v0, v1}, Lcom/dubo/android/PlatformMessage;->SendPlatformMessage(ILjava/lang/String;)V .line 87 return-void .end method .method public payCancel(Ljava/util/Map;)V .locals 2 .param p1, \"params\" # Ljava/util/Map; .prologue .line 82 sget-object v0, Lcom/dubo/android/JniMsgType;->Recharge:Lcom/dubo/android/JniMsgType; invoke-virtual {v0}, Lcom/dubo/android/JniMsgType;->ordinal()I move-result v0 iget-object v1, p0, Lcom/zplay/bbtan/plug/EgamePlug$2$1;->this$1:Lcom/zplay/bbtan/plug/EgamePlug$2; # getter for: Lcom/zplay/bbtan/plug/EgamePlug$2;->this$0:Lcom/zplay/bbtan/plug/EgamePlug; invoke-static {v1}, Lcom/zplay/bbtan/plug/EgamePlug$2;->access$0(Lcom/zplay/bbtan/plug/EgamePlug$2;)Lcom/zplay/bbtan/plug/EgamePlug; move-result-object v1 # getter for: Lcom/zplay/bbtan/plug/EgamePlug;->_sku:Ljava/lang/String; invoke-static {v1}, Lcom/zplay/bbtan/plug/EgamePlug;->access$1(Lcom/zplay/bbtan/plug/EgamePlug;)Ljava/lang/String; move-result-object v1 invoke-static {v0, v1}, Lcom/dubo/android/PlatformMessage;->SendPlatformMessage(ILjava/lang/String;)V .line 83 return-void .end method 这里就很关键了。这个看起来眼花的话,我们看看Java的 我们惊奇的发现,这几个成功,取消和失败的代码只有一处不同。 方法一:那么我们就可以把取消的RechargeFail换成Recharge就OK了。 方法二:我们可以把paySuccess和payCancel的函数名调换。也就说说,我们点了取消,却调用的是paySuccess里面的代码。OK。BB弹就简单的破解了。 二、火柴人联盟破解BB弹之后,我本以为内购破解起来很简单。也很好玩,然后逛吾爱的时候,看到一篇破解火柴人的帖子。我也就去下了个官方版本去试试破解。(版本比帖子的高,帖子地址:http://www.52pojie.cn/thread-522841-1-1.html) 这不是重点,重点是。这个游戏在模拟器上打不开。我也不知道为什么,直接反编译。 发现了这个游戏的购买方式很多,移动,电信,联通,支付宝都有。然后还是按照之前破解BB弹的方式去破解。 却发现根本不行。把取消的RechargeFail换成Recharge,不行 把支付宝的代码换掉也不行。 于是认真参考了刚刚那个帖子,发现很多代码已经被原作者改了。那就只有自己研究了 这里就是重点了,我来说说我自己的思路。后来发现这个游戏坑爹的只能移动购买。 首先是住入口去看看然后在这个函数去看看移动购买的函数。 然后点过去看看,发现信息不是太多。 12345678910111213141516171819202122232425262728293031323334353637.method private payInYidong()V .locals 6 .prologue const/4 v1, 0x1 .line 735 sget v0, Lcom/DBGame/DiabloLOL/DiabloLOL;->sCMCC_OPEN:I if-nez v0, :cond_0 .line 736 const-string v0, \"\\u6b63\\u5728\\u5904\\u7406,\\u8bf7\\u7a0d\\u540e.....\" invoke-static {v0}, Lcom/DBGame/Common/BLHelper;->showShieldLayer(Ljava/lang/String;)V .line 738 :cond_0 iget-object v0, p0, Lcom/DBGame/DiabloLOL/DiabloLOL;->PAY_CODE_MM:[Ljava/lang/String; iget v2, p0, Lcom/DBGame/DiabloLOL/DiabloLOL;->mPayIndex:I aget-object v3, v0, v2 const/4 v4, 0x0 iget-object v5, p0, Lcom/DBGame/DiabloLOL/DiabloLOL;->payCallback:Lcn/cmgame/billing/api/GameInterface$IPayCallback; move-object v0, p0 move v2, v1 invoke-static/range {v0 .. v5}, Lcn/cmgame/billing/api/GameInterface;->doBilling(Landroid/content/Context;ZZLjava/lang/String;Ljava/lang/String;Lcn/cmgame/billing/api/GameInterface$IPayCallback;)V .line 739 return-void .end method 但是我们好像发现了payCallback这个东西。感觉又价值。我们搜索看看。 发现了两处,有用的是第一处的。于是我们过去看看 这个时候我们就看出来了 12345678910111213141516171819202122232425262728293031package com.DBGame.DiabloLOL; import cn.cmgame.billing.api.GameInterface.IPayCallback; import com.DBGame.Common.BLHelper; class DiabloLOL$4 implements GameInterface.IPayCallback { DiabloLOL$4(DiabloLOL paramDiabloLOL) {} public void onResult(int paramInt, String paramString, Object paramObject) { switch (paramInt) { default: new StringBuilder().append(\"购买道具:[\").append(this.this$0.PAY_NAME[DiabloLOL.access$800(this.this$0)]).append(\"] 取消!\").toString(); } for (;;) { BLHelper.closeShieldLayer(); return; if (!\"10\".equals(paramObject.toString())) { new StringBuilder().append(\"购买道具:[\").append(this.this$0.PAY_NAME[DiabloLOL.access$800(this.this$0)]).append(\"] 成功!\").toString(); BLHelper.purchaseComplete(this.this$0.PRO_ID_Str[DiabloLOL.access$800(this.this$0)], 1); continue; new StringBuilder().append(\"购买道具:[\").append(this.this$0.PAY_NAME[DiabloLOL.access$800(this.this$0)]).append(\"] 失败!\").toString(); } } } } 也就说packed-switch p1, :pswitch_data_0,然后pswitch_data_0就购买成功。 直接来个goto大发。OK了教程就到这里了,本破解只为学习交流。 提供样本BB弹:https://yunpan.cn/cMN8KD5h2IUg2 访问密码 9456火柴人去官网下就是了成本:BB弹:https://yunpan.cn/cM4jkAt4u7m5n 访问密码 5c1b火柴人:https://yunpan.cn/cMN89LIBLqjPq 访问密码 67a6","tags":[{"name":"Android逆向实例","slug":"Android逆向实例","permalink":"http://sorgs.cn/tags/Android逆向实例/"},{"name":"破解","slug":"破解","permalink":"http://sorgs.cn/tags/破解/"},{"name":"游戏","slug":"游戏","permalink":"http://sorgs.cn/tags/游戏/"},{"name":"支付","slug":"支付","permalink":"http://sorgs.cn/tags/支付/"}]},{"title":"Android逆向实例笔记—同步家教王及其升级版的破解","date":"2016-11-16T14:45:30.000Z","path":"post/40324/","text":"一朋友让我来破解下一软件,我拿来一看是这玩意。我以为很难,结果发现没壳。兴趣就来了,弄了一天,就弄出来了。这里把过程和思路分享一下。其实很简单,大神一看就知道。因为这个没加壳,只是加了混淆的。这算是我第一次破解玩玩整整的apk了。然后我们就开始吧。 一、工具这次我用的是AndroidKiller,感觉很不错的样子然后就是我每次都要用的蓝叠 二、同步家教王1.看情况老规矩,还是先拖蓝叠看看情况 随便输了123,出现了激活码错误。我们记下来 2.反编译这个就不多说了,前面说的够多了。直接拖进去,反编译就OK。 3.修改我们先去string.xml中没有信息,然后转码搜索激活码错误 我们跳过去,看看源码。右键,查看-查看源码 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051<span style=\"font-size:14px;\">package com.school.app.activity.login; import android.os.Handler; import android.os.Message; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import com.school.app.utils.SharedPreHandler; class LoginActivity$3 extends Handler { LoginActivity$3(LoginActivity paramLoginActivity, String paramString) {} public void handleMessage(Message paramMessage) { try { if (paramMessage.obj != null) { paramMessage = ((String)paramMessage.obj).split(\"&\"); if (paramMessage[0].equals(\"yes\")) { SharedPreHandler.getShared().setSharedPreKey(\"activation_code\", this.val$text); SharedPreHandler.getShared().setSharedPreKey(\"activation_deviceId\", LoginActivity.access$1(this.this$0)); SharedPreHandler.getShared().setSharedPreKey(\"activation_model\", LoginActivity.access$2(this.this$0)); if (paramMessage[1].equals(\"-1\")) {} for (paramMessage = \"激活成功\";; paramMessage = String.format(LoginActivity.access$3(this.this$0), new Object[] { paramMessage[1], paramMessage[2] })) { SharedPreHandler.getShared().setSharedPreKey(\"activation_msg\", paramMessage); LoginActivity.access$4(this.this$0); this.this$0.finish(); return; } } } return; } catch (Exception paramMessage) { paramMessage.printStackTrace(); this.this$0.title.setText(\"激活码错误\"); this.this$0.back.setVisibility(0); this.this$0.exit.setVisibility(0); this.this$0.yes.setVisibility(8); this.this$0.clear.setVisibility(8); this.this$0.edit.setVisibility(8); } } } </span> 我们可以很容易的看到激活码出错的字样。再来分析一下java代码。我们发现激活码错误并没有跳转到这里,但是之前的代码却有个 :cond_1跳转这里来。所以我们明显知道是加了混淆的。那我们只有从激活成功去看看下手了。再次搜索 发现有两处刚刚这出看过了,我们去看看另一处的源码 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596package com.school.app.service; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.widget.Toast; import com.school.app.activity.login.LoginActivity; import com.school.app.utils.CommTool; import com.school.app.utils.SharedPreHandler; public class TimeCountService extends Service { private static final long sMinute = 1L; private Handler mHandler = new Handler() { public void handleMessage(Message paramAnonymousMessage) { try { if ((paramAnonymousMessage.obj != null) && (((String)paramAnonymousMessage.obj).contains(\"stop\"))) { SharedPreHandler.getShared().setSharedPreKey(\"activation_code\", \"\"); SharedPreHandler.getShared().setSharedPreKey(\"activation_msg\", \"\"); paramAnonymousMessage = new Intent(); paramAnonymousMessage.setFlags(268435456); paramAnonymousMessage.setClass(TimeCountService.this, LoginActivity.class); TimeCountService.this.startActivity(paramAnonymousMessage); } return; } catch (Exception paramAnonymousMessage) { paramAnonymousMessage.printStackTrace(); } } }; private MyReceiver myReceiver; private long time; private void requestLoginInfo() { if (CommTool.isNetworkAvailable(this)) { String str = SharedPreHandler.getShared().getSharedStrPreKey(\"activation_code\", \"\"); CommTool.getActivationCode(SharedPreHandler.getShared().getSharedStrPreKey(\"activation_deviceId\", \"\"), SharedPreHandler.getShared().getSharedStrPreKey(\"activation_model\", \"\"), str, this.mHandler); } } private void stopTimeCountService() { Intent localIntent = new Intent(); localIntent.setClass(this, TimeCountService.class); stopService(localIntent); } public IBinder onBind(Intent paramIntent) { return null; } public void onCreate() { super.onCreate(); } public void onDestroy() { super.onDestroy(); if (this.myReceiver != null) { unregisterReceiver(this.myReceiver); } } public int onStartCommand(Intent paramIntent, int paramInt1, int paramInt2) { String str = SharedPreHandler.getShared().getSharedStrPreKey(\"activation_msg\", \"\"); if ((!str.equals(\"\")) && (!str.equals(\"激活成功\"))) { Toast.makeText(this, str, 1).show(); } requestLoginInfo(); return super.onStartCommand(paramIntent, paramInt1, paramInt2); } class MyReceiver extends BroadcastReceiver { MyReceiver() {} public void onReceive(Context paramContext, Intent paramIntent) {} } } 这里的话,我们还可以清晰看到激活的只是一个判断,然后Toast出来的,那我们去修改前面那一处的跳转试试。在研究一下之前的源码,发现很混乱。但是明确知道,有个地方会跳转来验证。的确很混乱,我研究了半天,才在这个地方破解出来。这个不写出出来这个方法,因为我自己也不是太清楚。我是自己不清楚,就绝不误人子弟的,这里大家可以自行尝试。这里给大家就说另一个简单点的。我想起来wnagzihxain大神写的移动恶意APP分析的心得分享于是想到了这个思路。从入口去看看 其实这个工具挺好的,直接把入口写这了,都不用我们去AndroidManifest.xml里面找了好吧,直接点过。看看源码 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152package com.school.app.activity; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import com.school.app.activity.login.LoginActivity; import com.school.app.service.TimeCountService; import com.school.app.utils.SharedPreHandler; public class MainActivity extends FragmentActivity { private void startCountTimeService() { Intent localIntent = new Intent(); localIntent.setClass(this, TimeCountService.class); startService(localIntent); } public boolean isActivationCode() { if (SharedPreHandler.getShared().getSharedStrPreKey(\"activation_code\", \"\").equals(\"\")) { Intent localIntent = new Intent(); localIntent.setClass(this, LoginActivity.class); startActivityForResult(localIntent, 10085); return false; } startCountTimeService(); return true; } protected void onActivityResult(int paramInt1, int paramInt2, Intent paramIntent) { if ((paramInt1 == 10085) && (paramInt2 == 10086)) { finish(); } super.onActivityResult(paramInt1, paramInt2, paramIntent); } protected void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); setContentView(2130903040); isActivationCode(); } protected void onRestart() { super.onRestart(); } } 仔细一看,这不太简单了么。大概意思就是跳过去验证。这个时候,我们想到了,如果我们不去验证,不就完了么。有了思路,就直接上手,找到地方。 4.验证这里就不多说了,修改完了保存。打开就直接没有验证了。 三、学习平台(综合版)这个怎么说呢,算我是投机取巧吧。因为就上上面那个的升级版。更复杂。我估计要是没有前面那个,后面这个我也许还不能弄出来呢。害羞同样找到跳转验证的地方,给修改掉。 还是改为nez就OK了 此处,没有太多的技术含量。写这个的主要目的一是记记自己第一次破解完整的apk,然后就是写点换个思路的方式。 最后给出两个apk的下载地址: https://yunpan.cn/cMeIDvXGmzBjP 访问密码 1603 https://yunpan.cn/cMeIpjDyebQPw 访问密码 2eae 最后需要这两个破解的app的话,就去吾爱搜索吧,我把破解好的在吾爱发过帖子。","tags":[{"name":"Android逆向实例","slug":"Android逆向实例","permalink":"http://sorgs.cn/tags/Android逆向实例/"},{"name":"破解","slug":"破解","permalink":"http://sorgs.cn/tags/破解/"},{"name":"反编译","slug":"反编译","permalink":"http://sorgs.cn/tags/反编译/"},{"name":"同步学习","slug":"同步学习","permalink":"http://sorgs.cn/tags/同步学习/"},{"name":"AndroidKiller","slug":"AndroidKiller","permalink":"http://sorgs.cn/tags/AndroidKiller/"}]},{"title":"Android逆向基础笔记—巧用蓝叠和Android Studio进行动态调试","date":"2016-11-16T14:36:33.000Z","path":"post/15334/","text":"我们知道很多apk光是静态调试时远远满足不了我们对apk的分析,这个时候,我们就需要来一波静态调试。此处为个人笔记,也为入门小白引路,这里就不看结果了,主要是教大家怎么结合调试。起因是昨天刚看了动态调试的方法,我就想来试试。结果电脑上只有Android studio。好吧,那我就百度了Android studio的调试。然后用模拟器,结果那个模拟器太恼火了,慢死人,还卡。(学生党,只能苦逼用低配笔记本)然后身边唯一一根数据线居然罢工。没办法,后来突然想起来,蓝叠模拟器很快了,可不可以试试。于是就有了下文。好了,不废话了,进入正题。 一、工具毫无疑问:Android studio(附带SDK,这个不多说) 蓝叠然后有个大家可能大家不知道的东西 smalidea 这个百度一大堆。我就不多说了,自己去下一个就是了然后一个反编译的工具 jeb apkIDE AndroidKiller等等,都可以我用的是jeb 二、环境1java那些环境我想这个不应该说了,然后就是adb的环境,最好是加到环境配置中去,方便,省事。 2smalidea 这个是配置到Android studio的,配置了才能看smali文件具体步骤: 然后选择你本地smalidea文件,这里说一句,最好不要有中文路径。也行不一定会出问题,但是不一定不会出问题。 这个样子就是OK了一般会重启一下Android studio 三、准备这部分是反编译出文件,然后导入到Android studio中去。我这里用的jeb反编译的,各位请用自己喜欢的,都可以哈。 看到反编译成功,我们就去把文件提取出来这里最好是也是不要放中文路径最好接来下比较重要,一步一步操作。 然后选择我们放进去的反编译的文件夹 然后点那个三角 添加一个remote名字随便取,端口号为8700 之后我们打开cmd。进行adb的安装adb install -r apk的位置\\apk全名 看到succe就知道成功了,蓝叠也会有提示的(一直默认蓝叠是开启的,没开的我就无语了) 然后进去调试状态adb shell am start -D -n packageName/ActivityName 蓝叠也会这样显示 说明进入了调试状态,这个时候千万不要去点Force Close 四、进入调试进入调试之前我们还需要去monitor看看我们是否端口号给正确了。 点击我们需要调试的apk包名,把8700端口给它然后我们就可以调试了这个时候我们需要去源码看看我们在哪里下断点了。这里就不多说了我直接点断点了。在这行代码前面点一下,就断好了 然后点上面那个就可以进行调试了 下面是我们的信息地方,我们可以点加,进行添加寄存器 最后就可以进行调试,看看寄存器的值的变化了。F7是进入方法,F8是单步。","tags":[{"name":"Android逆向基础","slug":"Android逆向基础","permalink":"http://sorgs.cn/tags/Android逆向基础/"},{"name":"破解","slug":"破解","permalink":"http://sorgs.cn/tags/破解/"},{"name":"反编译","slug":"反编译","permalink":"http://sorgs.cn/tags/反编译/"},{"name":"动态调试","slug":"动态调试","permalink":"http://sorgs.cn/tags/动态调试/"},{"name":"调试","slug":"调试","permalink":"http://sorgs.cn/tags/调试/"}]},{"title":"Android逆向实例笔记—那些搜不到的中文怎么办","date":"2016-11-16T14:25:36.000Z","path":"post/9560/","text":"该crackme为吾爱培训教程的课后作业,简单到爆,大神请路过 1.源apk废话不多说,蓝叠看看错误提示。 记住我们的错误信息。 2.反编译首先去string.xml中 123456789101112131415161718192021222324<?xml version=\"1.0\" encoding=\"utf-8\"?> <resources> <string name=\"abc_action_mode_done\">Done</string> <string name=\"abc_action_bar_home_description\">Navigate home</string> <string name=\"abc_action_bar_up_description\">Navigate up</string> <string name=\"abc_action_menu_overflow_description\">More options</string> <string name=\"abc_toolbar_collapse_description\">Collapse</string> <string name=\"abc_action_bar_home_description_format\">%1$s, %2$s</string> <string name=\"abc_action_bar_home_subtitle_description_format\">%1$s, %2$s, %3$s</string> <string name=\"abc_searchview_description_search\">Search</string> <string name=\"abc_search_hint\">Search…</string> <string name=\"abc_searchview_description_query\">Search query</string> <string name=\"abc_searchview_description_clear\">Clear query</string> <string name=\"abc_searchview_description_submit\">Submit query</string> <string name=\"abc_searchview_description_voice\">Voice search</string> <string name=\"abc_activitychooserview_choose_application\">Choose an app</string> <string name=\"abc_activity_chooser_view_see_all\">See all</string> <string name=\"abc_shareactionprovider_share_with_application\">Share with %s</string> <string name=\"abc_shareactionprovider_share_with\">Share with</string> <string name=\"status_bar_notification_info_overflow\">999+</string> <string name=\"app_name\">CrackMe</string> <string name=\"hello_world\">Hello world!</string> <string name=\"action_settings\">Settings</string> </resources> 看吧,没有可用信息。那么我看就只有搜索了噻 什么情况。没有???懵逼了。这个时候我们就来试试unicode 把我们需要转换的写这里。 点后点击转换为unicode 最后一步,把转化好的,放到搜索内容去。 这个时候我们搜搜看看吧。 这下就出来了,双击过去。 然后往上找找跳转。 我们去看看关键的类源码。 我们可以看到,关键就是那个if<30,试想我们怎么可能大于30呢,所以可以直接修改为小于30。或者说,我们直接跳过这个if。 好吧,那我们有两种方法。 1).把ge改为le,改为小于 2)直接goto无条件跳转我们可以直接让goto去跳过这个if语句块 3.验证编译生成拖蓝叠 直接点击升级,然后重启 OK,已经成功破解。 apk下载 https://yunpan.cn/cMqFIzEa2y4UT 访问密码 93db 后记:这些都是很简单,很基础的apk。这个就是学习一下都不到中文怎么办。这里也就是自己做做笔记,很简单的东西。","tags":[{"name":"Android逆向实例","slug":"Android逆向实例","permalink":"http://sorgs.cn/tags/Android逆向实例/"},{"name":"转码","slug":"转码","permalink":"http://sorgs.cn/tags/转码/"}]},{"title":"A ndroid逆向基础笔记—Dalvik字节码小记_const/4 v2, 0x1","date":"2016-11-16T09:23:32.000Z","path":"post/10311/","text":"这几天认真研读了一下dalvik字节码,因为这个是重点,对以后的Android逆向分析很重要。我是学过汇编的,但是感觉还是不是太懂。这玩意也太乱了吧。然后我有些看不懂,就百度一阵,也没有结果。就自己研究了下下,把自己不懂的地方写出来。不对的地方,大家请指出。我主要是集中在赋值哪里。我开始没弄懂,后来才明白过来。const/4 v1, 0x1 这里大家应该知道 v1=1。但是真真正正想过为什么?也许很多人都知道,这里是写给不知道的。首先4代表4字节,那么就是4位的。所以呢 v1=04+1=1const/16 v2, 0x10 这里的话,16字节,那么16位对吧。所以v2 = 116+0 = 16const/16 v3, 0x28 16字节,16位。v3 = 2*16+8 = 40;这里就解释完了。后面给点dalvik的实例吧。123456789101112131415161718192021.local 4 //本地4个寄存器,也就是下面的v0,v1,v2,v3const/4 v2, 0x1 //4字节常量 v2=1const/16 v1, 0x10 //16字节常量 v1=16:local v1, \"length\":I //int length=v1if-nez v1,:cond_1 //如果v1不等于0,这跳转至cond_1:cond_0 //cond_0标签:goto_0 //goto_0标签return v2 //返回v2的值:cond_1 //开始执行cond_1标签代码const/4 v0,0x0 //4字节常量 v0=0:local v0, \"i\":I //int i=v0:goto_1 //开始执行goto_1标签代码if-lt v0, v1, :cond_2 //如果v0小于v1,则跳转至cond_2const/16 v3,0x28 //如果v0大于等于v1,则执行下面语句: 16字节常量v3=40if-le v1,v3, :cond_0 //如果v1小于等于v3,则跳转至cond_0,即返回v2的值const/4 v2, 0x0 //如果v1大于v3,则4字节常量v2=0goto:goto_0 //跳转至goto_0,即返回v2的值:cond_2 //cond_2标签xor-int/lit8 v1, v1, 0x3b //将第二个v1寄存器中的值与0x3b(59)进行异或运算,得到的值赋值给第一个v1寄存器中add-int/lit8 v0, v0, 0x1 //将第二个v0寄存器中的值加上0x1(1),所得的值放入第一个v0寄存器中goto:goto_1 //跳转值goto_1标签 翻译成java代码就是 123456789101112int v2 = 1; int v1 = 16; if (v1 != 0){ for (int v0 = 0; v0 < v1;){ v1 = v1 ^ 59; v0++; } if (v1 > 40){ v2 = 0; } } return v2;","tags":[{"name":"Android逆向基础","slug":"Android逆向基础","permalink":"http://sorgs.cn/tags/Android逆向基础/"}]},{"title":"Android逆向实例笔记—续力破解三个Android程序","date":"2016-11-16T09:03:08.000Z","path":"post/50997/","text":"这个首先感谢鱼C论坛的cbs大神,我是看了他的视频。自己再动手破解他给出这三个小程序。真心这样无私把技术分享给大家的人真的不多。再次感谢他。这里我就我自己破解的三个小程序自己做做笔记吧,方便自己以后查看,也方便刚刚入门的童鞋。这些都是些没啥技术含量的东西,大神请飘过。 一、认识新工具这里我先给出一个新的工具。jeb。给个我找的。分别有32和64的。https://yunpan.cn/cMuBpvug7qjc2 访问密码 da4a还是照例给个样图 二、Crackme031.查看原apk还是拖拽到蓝叠里面看看吧。 我们看到错误的提示是Bad boy。那就让我们开心的打开apkIDE吧 2.反编译反编译之后打开strings.xml。发现,没有Bad boy。1234567891011<?xml version=\"1.0\" encoding=\"utf-8\"?> <resources> <string name=\"app_name\">Android Crackme03 - [by deurus]</string> <string name=\"app_name2\">About Crackme03</string> <string name=\"textoPrueba2\">This is the third crackme of the Android collection crackmes, in this, the crackme take another phone values and with our name make something. For this reason, the crackme dont run in the emulator, only in the phone. Good luck for all!.</string> <string name=\"textodeurus\">by deurus [29-10-10] [Made in Basque Country]</string> <string name=\"imei\" /> <string name=\"temp\">Enter Name</string> <string name=\"labelserial\">Enter Serial</string> <string name=\"line\">--------------------------------------</string> </resources> 那我们就只能看看smali里面的东西了。搜索结果只有一处,还好。 双击过去,并往上找跳转。结果发现,这代码有混淆。没有办法了?这个时候就该我们的jeb上了。 点击File-Open,然后选择我们的Crackme03。然后点到Decompiled Java选项卡。双击左边的HelloAndroid。我们惊喜的发现这不是源码么? 其实不是哈。只是很类似了。而已。其实,这里的话,我们也可以不用jeb哈。直接用apkIDE带一个东西。 点击打开,选择打开。也是一样的 让我们来看看代码。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192package com.example.helloandroid; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.telephony.TelephonyManager; import android.view.View; import android.view.View$OnClickListener; import android.widget.TextView; import android.widget.Toast; public class HelloAndroid extends Activity { private View$OnClickListener pulsarBoton; private View$OnClickListener pulsarBotonabout; public HelloAndroid() { super(); this.pulsarBotonabout = new View$OnClickListener() { public void onClick(View v) { HelloAndroid.this.setContentView(2130903041); Intent v0 = new Intent(); v0.setClass(HelloAndroid.this, prueba2.class); HelloAndroid.this.startActivity(v0); HelloAndroid.this.finish(); } }; this.pulsarBoton = new View$OnClickListener() { public void onClick(View v) { String v10 = HelloAndroid.this.findViewById(2131034116).getText().toString(); int v11 = v10.length(); String v12 = \"\"; String v15 = HelloAndroid.this.findViewById(2131034118).getText().toString(); if(v11 >= 4) { goto label_29; } try { Toast.makeText(HelloAndroid.this.getApplicationContext(), \"Min 4 chars\", 1).show (); return; label_29: int v5; for(v5 = 0; v5 < v10.length(); ++v5) { v12 = String.valueOf(v12) + v10.charAt(v5); } v12 = String.valueOf(Integer.parseInt(v12.substring(0, 5)) ^ 438294); Object v8 = HelloAndroid.this.getSystemService(\"phone\"); String v6 = ((TelephonyManager)v8).getDeviceId(); String v16 = ((TelephonyManager)v8).getSimSerialNumber(); String v19 = v6.substring(0, 6); if(!String.valueOf(v12) + \"-\" + String.valueOf(((long)(Integer.parseInt(v19) ^ Integer .parseInt(v16.substring(0, 6))))) + \"-\" + v19.equals(v15)) { goto label_114; } Toast.makeText(HelloAndroid.this.getApplicationContext(), \"God boy\", 1).show(); return; label_114: Toast.makeText(HelloAndroid.this.getApplicationContext(), \"Bad boy \", 1).show(); return; } catch(Exception v22) { Toast.makeText(HelloAndroid.this.getApplicationContext(), \"Another Error Ocurred :(\" , 1).show(); return; } } }; } public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(2130903040); Object v3 = this.getSystemService(\"phone\"); String v2 = ((TelephonyManager)v3).getDeviceId(); new TextView(((Context)this)); this.findViewById(2131034112).setText(\"HardwareID 01: \" + v2); String v5 = ((TelephonyManager)v3).getSimSerialNumber(); new TextView(((Context)this)); this.findViewById(2131034113).setText(\"HardwareID 02: \" + v5); String v8 = v2.substring(0, 6); String v9 = v5.substring(0, 6); Integer.parseInt(v8); Integer.parseInt(v9); new TextView(((Context)this)); this.findViewById(2131034116).setText(\"\"); this.findViewById(2131034120).setOnClickListener(this.pulsarBotonabout); this.findViewById(2131034119).setOnClickListener(this.pulsarBoton); } } 我们很容易的发现Bad boy,上面有God boy。猜测就知道这就是正确信息。 那就让我们看看这代码。我们发现上面有个goto label_114; 就从这个就跳转到Bad boy。那么我们就得让它不跳转,对吧。大致知道那里之后回到apkIDE。我们往上找语句块,发现了 :cond_2那我们搜索这个。到了这里就是我们之前看到的那个跳转。 OK,我们果断改为nez,保存,生成。 3.验证拖拽到蓝叠,打开。不错,God boy和我们见面 这个我们就是KO了。 三、CrackMe-F1F21.查看原apk无需多说,进蓝叠 这不是是写的EditView,居然不是hint属性,表示无语。懂的就懂,不懂也不重要。这里扯远了,我们继续。既然有东西,我就懒得输了。直接验证 我们知道了错误代码就是Lisence Uncorrect.。 2.反汇编apkIDE,常规操作。照样,string.xml没有信息。只有smali里面搜索一波。 双击过去。找找跳转。这里给出代码 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272.class Lcom/mstar/test/LisenceCheck$1; .super Ljava/lang/Object; .source \"LisenceCheck.java\" # interfaces .implements Landroid/view/View$OnClickListener; # annotations .annotation system Ldalvik/annotation/EnclosingClass; value = Lcom/mstar/test/LisenceCheck; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x0 name = null .end annotation # instance fields .field final synthetic this$0:Lcom/mstar/test/LisenceCheck; # direct methods .method constructor <init>(Lcom/mstar/test/LisenceCheck;)V .locals 0 .prologue .line 1 iput-object p1, p0, Lcom/mstar/test/LisenceCheck$1;->this$0:Lcom/mstar/test/LisenceCheck; .line 51 invoke-direct {p0}, Ljava/lang/Object;-><init>()V return-void .end method # virtual methods .method public onClick(Landroid/view/View;)V .locals 10 .param p1, \"v\" # Landroid/view/View; .prologue const/4 v9, 0x0 const-string v8, \"\" .line 53 check-cast p1, Landroid/widget/Button; .end local p1 # \"v\":Landroid/view/View; iget-object v6, p0, Lcom/mstar/test/LisenceCheck$1;->this$0:Lcom/mstar/test/LisenceCheck; iget-object v6, v6, Lcom/mstar/test/LisenceCheck;->mbutton:Landroid/widget/Button; if-ne p1, v6, :cond_5 .line 55 new-instance v4, Ljava/lang/String; const-string v6, \"\" invoke-direct {v4, v8}, Ljava/lang/String;-><init>(Ljava/lang/String;)V .line 56 .local v4, \"s1\":Ljava/lang/String; iget-object v6, p0, Lcom/mstar/test/LisenceCheck$1;->this$0:Lcom/mstar/test/LisenceCheck; iget-object v6, v6, Lcom/mstar/test/LisenceCheck;->meditun:Landroid/widget/EditText; invoke-virtual {v6}, Landroid/widget/EditText;->getText()Landroid/text/Editable; move-result-object v6 invoke-interface {v6}, Landroid/text/Editable;->toString()Ljava/lang/String; move-result-object v4 .line 57 new-instance v5, Ljava/lang/String; const-string v6, \"\" invoke-direct {v5, v8}, Ljava/lang/String;-><init>(Ljava/lang/String;)V .line 58 .local v5, \"s2\":Ljava/lang/String; iget-object v6, p0, Lcom/mstar/test/LisenceCheck$1;->this$0:Lcom/mstar/test/LisenceCheck; iget-object v6, v6, Lcom/mstar/test/LisenceCheck;->meditsn:Landroid/widget/EditText; invoke-virtual {v6}, Landroid/widget/EditText;->getText()Landroid/text/Editable; move-result-object v6 invoke-interface {v6}, Landroid/text/Editable;->toString()Ljava/lang/String; move-result-object v5 .line 60 const/4 v1, 0x0 .local v1, \"i\":I const/4 v2, 0x0 .line 62 .local v2, \"k1\":I const/4 v1, 0x0 :goto_0 invoke-virtual {v4}, Ljava/lang/String;->length()I move-result v6 if-lt v1, v6, :cond_1 .line 69 :cond_0 xor-int/lit16 v2, v2, 0x5678 .line 72 const/4 v3, 0x0 .line 73 .local v3, \"k2\":I const/4 v1, 0x0 :goto_1 invoke-virtual {v5}, Ljava/lang/String;->length()I move-result v6 if-lt v1, v6, :cond_3 .line 78 xor-int/lit16 v3, v3, 0x1234 .line 80 if-ne v2, v3, :cond_4 .line 81 iget-object v6, p0, Lcom/mstar/test/LisenceCheck$1;->this$0:Lcom/mstar/test/LisenceCheck; invoke-virtual {v6}, Lcom/mstar/test/LisenceCheck;->getApplicationContext()Landroid/content/Context; move-result-object v6 const-string v7, \"Lisence Correct\\uff01\" invoke-static {v6, v7, v9}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast; move-result-object v6 invoke-virtual {v6}, Landroid/widget/Toast;->show()V .line 92 .end local v1 # \"i\":I .end local v2 # \"k1\":I .end local v3 # \"k2\":I .end local v4 # \"s1\":Ljava/lang/String; .end local v5 # \"s2\":Ljava/lang/String; :goto_2 return-void .line 64 .restart local v1 # \"i\":I .restart local v2 # \"k1\":I .restart local v4 # \"s1\":Ljava/lang/String; .restart local v5 # \"s2\":Ljava/lang/String; :cond_1 invoke-virtual {v4, v1}, Ljava/lang/String;->charAt(I)C move-result v0 .line 65 .local v0, \"ch\":C const/16 v6, 0x41 if-lt v0, v6, :cond_0 .line 66 const/16 v6, 0x5a if-le v0, v6, :cond_2 const/16 v6, 0x20 sub-int v6, v0, v6 int-to-char v0, v6 .line 67 :cond_2 add-int/2addr v2, v0 .line 62 add-int/lit8 v1, v1, 0x1 goto :goto_0 .line 74 .end local v0 # \"ch\":C .restart local v3 # \"k2\":I :cond_3 invoke-virtual {v5, v1}, Ljava/lang/String;->charAt(I)C move-result v0 .line 75 .restart local v0 # \"ch\":C const/16 v6, 0x30 sub-int v6, v0, v6 int-to-char v0, v6 .line 76 mul-int/lit8 v6, v3, 0xa add-int v3, v6, v0 .line 73 add-int/lit8 v1, v1, 0x1 goto :goto_1 .line 83 .end local v0 # \"ch\":C :cond_4 iget-object v6, p0, Lcom/mstar/test/LisenceCheck$1;->this$0:Lcom/mstar/test/LisenceCheck; invoke-virtual {v6}, Lcom/mstar/test/LisenceCheck;->getApplicationContext()Landroid/content/Context; move-result-object v6 const-string v7, \"Lisence Uncorrect\\uff01\" invoke-static {v6, v7, v9}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast; move-result-object v6 invoke-virtual {v6}, Landroid/widget/Toast;->show()V goto :goto_2 .line 88 .end local v1 # \"i\":I .end local v2 # \"k1\":I .end local v3 # \"k2\":I .end local v4 # \"s1\":Ljava/lang/String; .end local v5 # \"s2\":Ljava/lang/String; :cond_5 iget-object v6, p0, Lcom/mstar/test/LisenceCheck$1;->this$0:Lcom/mstar/test/LisenceCheck; iget-object v6, v6, Lcom/mstar/test/LisenceCheck;->meditun:Landroid/widget/EditText; const-string v7, \"\" invoke-virtual {v6, v8}, Landroid/widget/EditText;->setText(Ljava/lang/CharSequence;)V .line 89 iget-object v6, p0, Lcom/mstar/test/LisenceCheck$1;->this$0:Lcom/mstar/test/LisenceCheck; iget-object v6, v6, Lcom/mstar/test/LisenceCheck;->meditsn:Landroid/widget/EditText; const-string v7, \"\" invoke-virtual {v6, v8}, Landroid/widget/EditText;->setText(Ljava/lang/CharSequence;)V goto :goto_2 .end method 还是一片大乱,还是用jeb。 看我框出来的地方。很显然,如果前面不等于后面这一段,那么就跳转到下面去,Toast出来错误。OK,我们回到smali里面,去找这个跳转。其实大胆一点,我们直接可以在错误的上面看到:cond_4。然后搜素这个,但是我们还是稳一点,看看类似的源码,找找思路。搜索:cond_4,双击过去 ne就是等于,那我们改成等于就OK了。eq改上。 3.验证保存,生成apk。拖蓝叠。 又是我们熟悉又激动的正确Toast。 四、EX05011.原apk不废话,上蓝叠。看看错误提示 这里是直接不用输什么,直接来error– 2.反编译来看看我们的apkIDE怎么说。 不用多想。依然那个问题。string没信息。那我们就搜索error–看看 只有一处结果就是极好的,双击过去找跳转。显然没有什么可用信息。还是看看类源码吧。 123456789101112131415161718192021222324252627282930package irdc.ex05_01; import android.text.util.Linkify; import android.view.KeyEvent; import android.view.View; import android.view.View.OnKeyListener; import android.widget.TextView; import android.widget.Toast; class EX05_01$1 implements View.OnKeyListener { EX05_01$1(EX05_01 paramEX05_01) {} public boolean onKey(View paramView, int paramInt, KeyEvent paramKeyEvent) { if (\"gogo\".equals(\"11\")) { EX05_01.access$0(this.this$0).setText(\"gogo\"); Toast.makeText(this.this$0, \"right++\", 1).show(); } for (;;) { Linkify.addLinks(EX05_01.access$0(this.this$0), 7); return false; EX05_01.access$0(this.this$0).setText(\"gogo\"); Toast.makeText(this.this$0, \"error--\", 1).show(); } } } 这个很清晰明了。代码很少,而且可以看到思路的地方。 我们知道,意思就是“gogo”等于“11”才会跳转到正确的地方。但是怎么可能“gogo”等于“11”呢。这里我们就直接修改为不等于就OK了。回到apkIDE,找到跳转到错误的地方。 那么就直接把eqz改为nez。让它不等于 3.验证保存,生成,拖蓝叠。 打完收工!!! 这三个app都不是很难。但是主要的目的就是练练手,然后去理理思路。各位看官,看的开心就给个五星好评。 还是最后给出三个apk下载地址吧 https://yunpan.cn/cMu6crr4vXq5t 访问密码 21bc","tags":[{"name":"Android逆向实例","slug":"Android逆向实例","permalink":"http://sorgs.cn/tags/Android逆向实例/"}]},{"title":"Android逆向实例笔记—破解第一个Android程序_crackme02","date":"2016-11-16T08:46:17.000Z","path":"post/24104/","text":"本实例来源于《Android软件安全与逆向分析》这本书,作者是看雪的非虫,感谢提供这么好的书和实例。 一、工具干什么都得一个好工具对吧。 1.apkIDE反编译呢,我这里使用的是apkIDE(apk改之理),工具的话自己百度吧。个人不喜欢留一些不需要的东西在网盘里,难得整理,百度一大堆。我这里就给一个官网吧,免得有些童鞋找错了地方。现在这下下载网站一不小心,什么全家福都来了。好了,扯得有点远了。我用的就是最新版,我个人就喜欢最新版。(最新版可能有些问题)http://www.popotu.com/popo/apkide.html上个样图: 2.蓝叠只要是模拟器都可以,我个人觉得这个好用。随便找一个都行。官网:http://www.bluestacks.cn/ 样图: 二、查看源程序这里我们直接拖拽crackme02到蓝叠中,安装。 然后我们点开看看效果。 我们可以看到左上角有个程序未注册。我们随便输入字符,点击注册。发现Toast提示我们,无效用户名或注册码。此时,我们就该上我们的工具了 三、反编译1.工具的使用这里详细介绍一下apkIDE的使用。打开apkIDE 点击项目,然后点击打开apk,选择我们的crackme02。看输出框的进度,之后就可以进行我们的操作了。 反编译之后的文件目录 其中smali中存放的是反汇编的代码。res是所有的资源文件。都与开发目录一致。 2.strings.xml我们知道在开始的时候,我们一般会一些字符放到strings.xml文件中去。这里我就打开strings.xml文件。文件在:res-values-strings.xml 现在我们看看strings.xml中的内容 1234567891011121314151617<?xml version=\"1.0\" encoding=\"utf-8\"?> <resources> <string name=\"app_name\">Crackme0201</string> <string name=\"hello_world\">Hello world!</string> <string name=\"menu_settings\">Settings</string> <string name=\"title_activity_main\">crackme02</string> <string name=\"info\">Android程序破解演示实例</string> <string name=\"username\">用户名:</string> <string name=\"sn\">注册码:</string> <string name=\"register\">注 册</string> <string name=\"hint_username\">请输入用户名</string> <string name=\"hint_sn\">请输入16位的注册码</string> <string name=\"unregister\">程序未注册</string> <string name=\"registered\">程序已注册</string> <string name=\"unsuccessed\">无效用户名或注册码</string> <string name=\"successed\">恭喜您!注册成功</string> </resources> 我们可以很容易的看到,Toast提示我们错误的地方 3.public.xml我们知道每个字符都有唯一的int类型的索引值。于是我们打开strings.xml上面的public.xml文件。123456789101112131415161718192021222324252627282930<?xml version=\"1.0\" encoding=\"utf-8\"?> <resources> <public type=\"drawable\" name=\"ic_launcher\" id=\"0x7f020001\" /> <public type=\"drawable\" name=\"ic_action_search\" id=\"0x7f020000\" /> <public type=\"layout\" name=\"activity_main\" id=\"0x7f030000\" /> <public type=\"dimen\" name=\"padding_small\" id=\"0x7f040000\" /> <public type=\"dimen\" name=\"padding_medium\" id=\"0x7f040001\" /> <public type=\"dimen\" name=\"padding_large\" id=\"0x7f040002\" /> <public type=\"string\" name=\"app_name\" id=\"0x7f050000\" /> <public type=\"string\" name=\"hello_world\" id=\"0x7f050001\" /> <public type=\"string\" name=\"menu_settings\" id=\"0x7f050002\" /> <public type=\"string\" name=\"title_activity_main\" id=\"0x7f050003\" /> <public type=\"string\" name=\"info\" id=\"0x7f050004\" /> <public type=\"string\" name=\"username\" id=\"0x7f050005\" /> <public type=\"string\" name=\"sn\" id=\"0x7f050006\" /> <public type=\"string\" name=\"register\" id=\"0x7f050007\" /> <public type=\"string\" name=\"hint_username\" id=\"0x7f050008\" /> <public type=\"string\" name=\"hint_sn\" id=\"0x7f050009\" /> <public type=\"string\" name=\"unregister\" id=\"0x7f05000a\" /> <public type=\"string\" name=\"registered\" id=\"0x7f05000b\" /> <public type=\"string\" name=\"unsuccessed\" id=\"0x7f05000c\" /> <public type=\"string\" name=\"successed\" id=\"0x7f05000d\" /> <public type=\"style\" name=\"AppTheme\" id=\"0x7f060000\" /> <public type=\"menu\" name=\"activity_main\" id=\"0x7f070000\" /> <public type=\"id\" name=\"textView1\" id=\"0x7f080000\" /> <public type=\"id\" name=\"edit_username\" id=\"0x7f080001\" /> <public type=\"id\" name=\"edit_sn\" id=\"0x7f080002\" /> <public type=\"id\" name=\"button_register\" id=\"0x7f080003\" /> <public type=\"id\" name=\"menu_settings\" id=\"0x7f080004\" /> </resources> 找到我们需要的unsuccessed。如果觉得很难找,那么我们搜索功能就来了。在搜索内容里面写上unsuccessed,然后搜索范围选择选中的文件或文件夹,左边我们选择public.xml。然后点击搜索全部。 我们可以看到下面的搜索结果显示了出来,我们双击这一行,就会跳到我们所需要找的位置。 然后我们记住id:0x7f05000c。 3.smali这个时候我们还得使用我们的搜索。方法类似,不在赘述。我们要选择搜索smail 我们发现有两处。第一处const v1, 0x7f05000c于是我们双击过去。然后往上找跳转的地方。 move-result v0 if-nez v0, :cond_0 这里第一行代码返回的结果存到v0中去,第二行是对v0进行判断。如果值为0,就往下运行,也就是弹出未注册的地方。如果不为0,就跳转到cond_0处。 那么也就是这里如果跳转成功就会跳转,那么程序就是成功。 四、修改smali这里是nez,不等于0,那我们就修改为eqz,等于0。 重点中的重点,修改完之后,一定记得保存。不然可能编译错误或者,没有编译修改后的代码。 然后我们点击编译,编译生成apk 看输出框的进度和文件路径,我们就去查找我们的apk。一般来说就在原apk旁边。重新编译签名后的apk名字前面会加上ApkIDE_ 五、验证重新编译好的apk我们需要来验证一下是否成功。我们拖拽到蓝叠中,安装打开。 这里发现我们已经破解成功了。 最后给出apk的下载吧。https://yunpan.cn/cMuPerPjatc6S 访问密码 24ce说的有错误或者不对的地方欢迎指正讨论。","tags":[{"name":"Android逆向实例","slug":"Android逆向实例","permalink":"http://sorgs.cn/tags/Android逆向实例/"}]},{"title":"Android逆向基础笔记—初识逆向","date":"2016-11-16T08:24:13.000Z","path":"post/15339/","text":"(本笔记来源于吾爱以及吾爱坛友,加上本人自己的整理) 一.初识 APK、Dalvik字节码以及Smali1. apk是什么?apk实质上是一个zip压缩包,将apk后缀修改为zip,解压之后可以看到其内部结构: 2. apk 的组成assets: 资源目录1 assets 和 res 都是资源目录但有所区别: res 目录下的资源文件在编译时会自动生成索引文件(R.java),在Java代码中用R.xxx.yyy来引用;而asset目录下的资源文件不需要生成索引,在Java 代码中需要用AssetManager来访问; 一般来说,除了音频和视频资源(需要放在raw或asset下),使用Java开发的Android工程使用到的资源文件都会放在res下; 使用C++游戏引擎(或使用 Lua Unity3D等)的资源文件均需要放在 assets 下。 lib: so 库存放位置,一般由NDK编译得到,常见于使用游戏引擎或 JNI native调用的工程中 META-INF: 存放工程一些属性文件,例如 Manifest.MF res: 资源目录2 AndroidManifest.xml: Android工程的基础配置属性文件 classes.dex: Java代码编译得到的 Dalvik VM 能直接执行的文件 resources.arsc: 对res 目录下的资源的一个索引文件,保存了原工程中 strings.xml等文件内 apktool.yml - 重新打包必须文件 lib - native 动态库 so META-INF -签名 3. Dalvik字节码(重点来了)Dalvik 是 google 专门为 Android 操作系统设计的一个虚拟机,经过深度优化。虽然 Android 上的程序是使用java 来开发的,但是 Dalvik 和标准的 java 虚拟机 JVM 还是两回事。 Dalvik VM 是基于寄存器的,而 JVM 是基于栈的; Dalvik有专属的文件执行格式 dex (dalvik executable),而 JVM 则执行的是 java 字节码。 Dalvik VM 比 JVM 速度更快,占用空间更少。 通过 Dalvik 的字节码我们不能直接看到原来的逻辑代码,这时需要借助如 Apktool 或 dex2jar+jd-gui 工具来帮助查看。但是,我们最终修改 APK 需要操作的文件是 .smali 文件,而不是导出来的 Java 文件重新编译。 4. Smali(破解的重点。好吧还是重点)1)Smali,Baksmali分别是指安卓系统里的 Java 虚拟机(Dalvik)所使用的一种 dex 格式文件的汇编器,反汇编器。其语法是一种宽松式的 Jasmin/dedexer 语法,而且它实现了 .dex 格式所有功能(注解,调试信息,线路信息等) 当我们对 APK 文件进行反编译后,便会生成此类文件。在Davlik字节码中,寄存器都是32位的,能够支持任何类型,64位类(Long/Double)用2个寄存器表示;Dalvik字节码有两种类型:原始类型;引用类型(包括对象和数组) 2)原始类型:B—byte C—char D—double F—float I—int J—long S—short V—void Z—boolean [XXX—array Lxxx/yyy—object 这里解析下最后两项,数组的表示方式是:在基本类型前加上前中括号“[”,例如 int 数组和 float 数组分别表示为:[I、[F; 对象的表示则以 L 作为开头,格式是 LpackageName/objectName;(注意必须有个分号跟在最后),例如 String 对象在 smali 中为:Ljava/lang/String;其中 java/lang 对应 java.lang包,String 就是定义在该包中的一个对象。 内部类又如何在 smali 中:LpackageName/objectName$subObjectName;也就是在内部类前加“$”符号。 3)方法的定义Func-Name (Para-Type1Para-Type2Para-Type3…)Return-Type注意参数与参数之间没有任何分隔符,举例如下: A ()V 这就是void A()。 B (II)Z 这个则是boolean B(int, int)。 C (Z[I[ILjava/lang/String;J)Ljava/lang/String; 这是String C (boolean, int[], int[], String, long) 。 4)Smali基本语法.field private isFlag:z 定义变量 .method 方法 .parameter 方法参数 .prologue 方法开始 .line 123 此方法位于第123行 invoke-super 调用父函数 const/high16 v0, 0x7fo3 把0x7fo3赋值给v0 invoke-direct 调用函数 return-void 函数返回void .end method 函数结束 new-instance 创建实例 iput-object 对象赋值 iget-object 调用对象 invoke-static 调用静态函数 5)条件跳转分支“if-eq vA, vB, :cond**” 如果vA等于vB则跳转到:cond** “if-ne vA, vB, :cond**” 如果vA不等于vB则跳转到:cond** “if-lt vA, vB, :cond**” 如果vA小于vB则跳转到:cond** “if-ge vA, vB, :cond**” 如果vA大于等于vB则跳转到:cond** “if-gt vA, vB, :cond**” 如果vA大于vB则跳转到:cond** “if-le vA, vB, :cond**” 如果vA小于等于vB则跳转到:cond** “if-eqz vA, :cond**” 如果vA等于0则跳转到:cond** “if-nez vA, :cond**” 如果vA不等于0则跳转到:cond** “if-ltz vA, :cond**” 如果vA小于0则跳转到:cond** “if-gez vA, :cond**” 如果vA大于等于0则跳转到:cond** “if-gtz vA, :cond**” 如果vA大于0则跳转到:cond** “if-lez vA, :cond**” 如果vA小于等于0则跳转到:cond** 二.Smali 文件1. Smali中的包信息.class public Lcom/aaaaa; (它是com.aaaaa这个package下的一个类) .super Lcom/bbbbb; (继承自com.bbbbb这个类) .source “ccccc.java” (一个由ccccc.java编译得到的smali文件) 2. Smali中的声明1234567#annotations.annotation system Ldalvik/annotation/MemberClasses;value = {Lcom/aaa$qqq;,Lcom/aaa$www;}.end annotation //这个声明是内部类的声明:aaa这个类它有两个成员内部类——qqq和www,内部类将在后面小节中会有提及。 3.关于寄存器寄存器是什么意思呢?在 smali 里的所有操作都必须经过寄存器来进行:本地寄存器用 v 开头,数字结尾的符号来表示,如v0、v1、v2、… 参数寄存器则使用 p 开头,数字结尾的符号来表示,如p0、p1、p2、… 特别注意的是,p0 不一定是函数中的第一个参数,在非 static 函数中,p0 代指“this”,p1 表示函数的第一个参数,p2 代表函数中的第二个参数。 而在 static 函数中 p0 才对应第一个参数(因为 Java 的 static 方法中没有 this 方法。 4. 寄存器简单实例分析const/4 v0, 0x1 iput-boolean v0, p0, Lcom/aaa;->IsRegistered:Z 我们来分析一下上面的两句 smali 代码,首先它使用了 v0 本地寄存器,并把值 0x1 存到 v0 中,然后第二句用 iput-boolean 这个指令把 v0 中的值存放到 com.aaa.IsRegistered 这个成员变量中。 即相当于:this.IsRegistered= true;(上面说过,在非static函数中p0代表的是“this”,在这里就是com.aaa 实例)。 ## 5. Smali中的成员变量成员变量格式是:.field public/private [static] [final] varName:<类型>。 对于不同的成员变量也有不同的指令。 一般来说,获取的指令有:iget、sget、iget-boolean、sget-boolean、iget-object、sget-object等。 操作的指令有:iput、sput、iput-boolean、sput-boolean、iput-object、sput-object等。 没有“-object”后缀的表示操作的成员变量对象是基本数据类型,带“-object”表示操作的成员变量是对象类 型,特别地,boolean 类型则使用带“-boolean”的指令操作。 6. Smali成员变量指令简析1) 简析一sget-object v0, Lcom/aaa;->ID:Ljava/lang/String; sget-object就是用来获取变量值并保存到紧接着的参数的寄存器中,本例中,它获取ID这个String类型的成员变量并放到v0这个寄器中。 注意:前面需要该变量所属的类的类型,后面需要加一个冒号和该成员变量的类型,中间是“->”表示所属关系。 2) 简析二iget-object v0, p0, Lcom/aaa;->view:Lcom/aaa/view; 可以看到iget-object指令比sget-object多了一个参数,就是该变量所在类的实例,在这里就是p0即“this”。 获取array的话我们用aget和aget-object,指令使用和上述一致 3) 简析三(put指令的使用和get指令是统一的)12const/4 v3, 0x0sput-object v3, Lcom/aaa;->timer:Lcom/aaa/timer; 相当于:this.timer= null; 注意,这里因为是赋值object 所以是null 4) 简析四123.local v0, args:Landroid/os/Message;const/4 v1, 0x12iput v1, v0, Landroid/os/Message;->what:I 相当于:args.what = 18;(args 是 Message 的实例) 三.Smali函数分析1. Smali中函数的调用1)smali中的函数和成员变量一样也分为两种类型,分别为direct和virtual之分。direct method和virtualmethod的区别:简单来说,direct method 就是 private 函数,其余的 public 和 protected 函数都属于 virtual method。 所以在调用函数时,有invoke-direct,invoke-virtual,另外还有invoke-static、invoke-super以及invokeinterface等几种不同的指令。当然其实还有invoke-XXX/range 指令的,这是参数多于4个的时候调用的指令。 2)invoke-static:用于调用static函数,例如:1invoke-static {}, Lcom/aaa;->CheckSignature()Z 这里注意到 invoke-static 后面有一对大括号“{}”,其实是调用该方法的实例+参数列表,由于这个方法既不需参数也是static的,所以{}内为空 再看一个:12const-string v0, \"NDKLIB\"invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V 这个是调用 static void System.loadLibrary(String) 来加载 NDK 编译的 so 库用的方法,同样也是这里 v0 就是参数”NDKLIB”了。 3)invoke-super:调用父类方法用的指令,一般用于调用onCreate、onDestroy等方法。 4)invoke-direct:调用private函数:1invoke-direct {p0}, Landroid/app/TabActivity;->()V 这里init()就是定义在TabActivity中的一个private函数 5)invoke-virtual:用于调用 protected 或 public 函数,同样注意修改smali时不要错用 invoke-direct或 invoke-static:12sget-object v0, Lcom/dddd;->bbb:Lcom/ccc;invoke-virtual {v0, v1}, Lcom/ccc;->Messages(Ljava/lang/Object;)V v0是bbb:Lcom/ccc v1是传递给Messages方法的Ljava/lang/Object参数。 6)invoke-xxxxx/range:当方法的参数多于5个时(含5个),不能直接使用以上的指令,而是在后面加上“/range”,range表示范围,使用方法也有所不同:12invoke-direct/range {v0 .. v5}, Lcmb/pb/ui/PBContainerActivity;->h(ILjava/lang/CharSequence;Ljava/lang/String;Landroid/content/ 需要传递v0到v5一共6个参数,这时候大括号内的参数采用省略形式,且需要连续。 2. Smali中函数返回结果操作在Java代码中调用函数和返回函数结果可以用一条语句完成,而在Smali里则需要分开来完成,在使用上述指令后,如果调用的函数返回非void,那么还需要用到move-result(返回基本数据类型)和move-result-object(返回对象)指令:123const-string v0, \"Eric\"invoke-static {v0}, Lcmb/pbi;->t(Ljava/lang/String;)Ljava/lang/String;move-result-object v2 v2保存的就是调用t方法返回的String字符串。 3. Smali中函数实体分析–if函数分析12345678910111213.method private ifRegistered()Z.locals 2 //在这个函数中本地寄存器的个数.prologueconst/4 v0, 0x1 // v0赋值为1.local v0, tempFlag:Zif-eqz v0, :cond0 // 判断v0是否等于0,等于0则跳到cond0执行const/4 v1, 0x1 // 符合条件分支:goto_0 //标签return v1 //返回v1的值:cond_0 //标签const/4 v1, 0x0 // cond_0分支goto :goto0 //跳到goto0执行 即返回v1的值 这里可以改成return v1 也是一样的.end method","tags":[{"name":"Android逆向基础","slug":"Android逆向基础","permalink":"http://sorgs.cn/tags/Android逆向基础/"}]},{"title":"Android 最最最简单的浏览器代码","date":"2016-11-16T08:09:07.000Z","path":"post/12244/","text":"学了WebView之后,心血来潮,写了这个简易的浏览器。虽然很简单,但是也查了不少没学到东西。大神就忽略吧。这里分享出来,给需要的人参考参考。 首先是我们的xml123456789101112131415161718192021222324252627282930313233343536373839<?xml version=\"1.0\" encoding=\"utf-8\"?> <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:background=\"#F5F5DC\" android:orientation=\"vertical\"> <LinearLayout android:layout_width=\"wrap_content\" android:orientation=\"horizontal\" android:layout_height=\"wrap_content\"> <EditText android:id=\"@+id/text\" android:autoText=\"true\" android:singleLine=\"true\" android:selectAllOnFocus=\"true\" android:layout_marginTop=\"20dp\" android:layout_width=\"300dp\" android:layout_height=\"40dp\" android:textColor=\"#FFA500\" android:hint=\"@string/url\" android:layout_gravity=\"left|top\"/> <Button android:id=\"@+id/button\" android:layout_marginTop=\"20dp\" android:layout_width=\"60dp\" android:layout_height=\"40dp\" android:text=\"@string/next\" android:textColor=\"#FAEBD7\" android:layout_gravity=\"right|top\" /> </LinearLayout> <WebView android:id=\"@+id/webview\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\"/> </LinearLayout> 代码简单,就是LinearLayou里面再套一个LinearLayou,加上一个edittext和button。下面就是一个WebView。edittext里面有些属性虽然我写了,但是没感觉出来用处。android:autoText=”true”。自动补全,我感觉没有用处。android:singleLine=”true”这个呢就是单行显示,也就是说,有些网址很长,我们只显示一行就行了。这个为后面的实时显示网址有用的。android:selectAllOnFocus=”true”这个呢就是获取焦点,便于后面点edittext可以全选。 最后是重点啦,看看Java的代码。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116package com.sorgs.administrator.webview; import android.app.Activity; import android.app.ProgressDialog; import android.os.Bundle; import android.view.KeyEvent; import android.view.View; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; public class MainActivity extends Activity { private String url = null; private WebView webView; private ProgressDialog dialog; private EditText text; private Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toast.makeText(this,\"欢迎使用简易浏览器_by sorgs\",Toast.LENGTH_SHORT).show(); //弹出欢迎 init(); } private void init() { webView = (WebView) findViewById(R.id.webview); text = (EditText) findViewById(R.id.text); button = (Button) findViewById(R.id.button); webView.loadUrl(url); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String str = text.getText().toString(); //去获取text中输入的网址 url = \"http://\"+ str; webView.loadUrl(url); //设置到webView中去 } }); //覆盖WebView默认通过第三方或者是系统浏览器打开网页的行为,使网页可以再WebView中打开 webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { //返回值是true的时候控制网页在WebView中去打开,如果为false调用系统浏览器或者第三方浏览器打开 view.loadUrl(url); return true; }//WebViewClient帮助WebView去处理一些页面控制和请求通知 }); //启用支持javaScript WebSettings settings = webView.getSettings(); settings.setJavaScriptEnabled(true); //WebView加载页面优先使用缓存加载 settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); webView.setWebChromeClient(new WebChromeClient() { @Override public void onProgressChanged(WebView view, int newProgress) { //newProgress 1-100之间的整数 if (newProgress == 100) { //网页加载完毕,关闭ProgressDialog closeDialo(); } else { //网页正在加载,打开ProgressDialog openDialog(newProgress); text.setText(webView.getUrl()); //实时显示当前网址 text.requestFocus(); //把输入焦点放在调用这个方法的控件上 text.setSelectAllOnFocus(true); //点击之后就被全选 } } private void closeDialo() { if (dialog != null && dialog.isShowing()) { dialog.dismiss(); dialog = null; } } private void openDialog(int newProgress) { if (dialog == null) { dialog = new ProgressDialog(MainActivity.this); dialog.setTitle(\"加载中...\"); dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); dialog.setProgress(newProgress); dialog.show(); } else { dialog.setProgress(newProgress); } } }); } @Override //改写物理按键——返回的逻辑 public boolean onKeyDown(int keyCode, KeyEvent event) { if(keyCode == KeyEvent.KEYCODE_BACK){ if(webView.canGoBack()){ webView.goBack(); //返回上一页面 return true; }else { System.exit(0); } } return super.onKeyDown(keyCode,event); } } 代码中呢很多注释写的很详细了。这个就不在赘述了。 最后来几张效果图好啦。","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"}]},{"title":"自己第一个Android作品,Android计算器","date":"2016-11-16T00:40:48.000Z","path":"post/37302/","text":"写此时为了记录自己的开发历程,方便以后的查询。二则运算(能力有限,也没有想往更深处写),菜鸟级别,代码为参考慕课网上,听课代码,并加上自己的理解和参考的一些博客!有问题的地方欢迎指正,感激不尽!开发工具为Android studio。(第一次写博客,可读性估计很差) 废话不多说,代码轮上来(顺序按慕课老师讲解顺序) 1.先是xml(UI吧)  效果图: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291<EditText android:layout_width=\"fill_parent\" android:layout_marginTop=\"20dp\" android:layout_height=\"70dip\" android:id=\"@+id/et_input\" android:background=\"@color/white\" android:editable=\"false\" android:gravity=\"right|bottom\" /> <LinearLayout android:layout_width=\"fill_parent\" android:layout_height=\"wrap_content\" android:layout_marginTop=\"20dp\" android:orientation=\"horizontal\" android:gravity=\"center_horizontal\" > <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"C\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_clear\" /> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"DEL\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:layout_marginLeft=\"10dp\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_del\" /> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"÷\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:layout_marginLeft=\"10dp\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_divide\" /> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"×\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:layout_marginLeft=\"10dp\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_multiply\" /> </LinearLayout> <LinearLayout android:layout_width=\"fill_parent\" android:layout_height=\"wrap_content\" android:layout_marginTop=\"10dp\" android:orientation=\"horizontal\" android:gravity=\"center_horizontal\" > <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"7\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_7\" /> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"8\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:layout_marginLeft=\"10dp\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_8\" /> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"9\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:layout_marginLeft=\"10dp\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_9\" /> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"-\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:layout_marginLeft=\"10dp\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_minus\" /> </LinearLayout> <LinearLayout android:layout_width=\"fill_parent\" android:layout_height=\"wrap_content\" android:layout_marginTop=\"10dp\" android:orientation=\"horizontal\" android:gravity=\"center_horizontal\" > <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"4\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_4\" /> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"5\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:layout_marginLeft=\"10dp\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_5\" /> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"6\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:layout_marginLeft=\"10dp\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_6\" /> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"+\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:layout_marginLeft=\"10dp\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_plus\" /> </LinearLayout> <LinearLayout android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:orientation=\"horizontal\" android:gravity=\"center_horizontal\" android:layout_marginTop=\"10dp\"> <LinearLayout android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:orientation=\"vertical\"> <LinearLayout android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:orientation=\"horizontal\"> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"1\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_1\" /> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"2\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:textSize=\"20sp\" android:layout_marginLeft=\"10dp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_2\" /> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\"3\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:layout_marginLeft=\"10dp\" android:id=\"@+id/btn_3\" /> </LinearLayout> <LinearLayout android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:orientation=\"horizontal\" android:layout_marginTop=\"10dp\"> <Button android:layout_width=\"150dp\" android:layout_height=\"70dp\" android:text=\"0\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_0\" /> <Button android:layout_width=\"70dp\" android:layout_height=\"70dp\" android:text=\".\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:background=\"@drawable/white_select\" android:textSize=\"20sp\" android:layout_marginLeft=\"10dp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_point\" /> </LinearLayout> </LinearLayout> <Button android:layout_width=\"70dp\" android:layout_height=\"150dp\" android:text=\"=\" android:background=\"@drawable/orange_select\" android:layout_marginLeft=\"10dp\" android:paddingRight=\"10dp\" android:paddingBottom=\"10dp\" android:textSize=\"20sp\" android:gravity=\"right|bottom\" android:id=\"@+id/btn_equal\"/> </LinearLayout> <TextView android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_gravity=\"bottom|center\" android:editable=\"false\" android:text=\"能力有限,只能进行二则运算——by sorgs\" android:textSize=\"16dp\" android:textColor=\"@color/black\" android:layout_marginTop=\"30dp\" android:background=\"@color/sandybrown\" android:id=\"@+id/textureView\" /> xml里面注释不多,想必也不需太多解释,都是很简单布局和控件。其中有个背景颜色的问题需要详细说明 1.存放颜色 首先我们需要一个在value/下添加一个colors.xml文件用来存放我们需要的颜色不然你全用#xxxxxx得多累,而且还不还用颜色,这里我们可以百度,有配好的颜色的代码。我们直接复制到我们的代码中,然后引用。 建立xml为了在方便我们布局和颜色的搭配,也是在慕课老师那里学了一招。可以在res/drawable下面建立xml。 我们来依次看看这些文件都是怎么做的。 ###(1).ashend_bg.xml(几个_bg都是颜色不一样,意义差不多)目的是提供背景颜色,其实也可以不要颜色。直接在xml中添加颜色,这个主要是提供圆角 12345<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"> <corners android:radius=\"5dp\"/> <solid android:color=\"@color/beige\" /> </shape> 此处使用shape。(这里有更加完美的解释 http://www.cnblogs.com/cyanfei/archive/2012/07/27/2612023.html) (2).orange_select.xml(select也是颜色不一样,内容差不多)目的是为了按钮提供颜色和点击时会有另外一种颜色 12345678<span style=\"font-size:18px;\"><?xml version=\"1.0\" encoding=\"utf-8\"?> <selector xmlns:android=\"http://schemas.android.com/apk/res/android\"> <item android:drawable=\"@drawable/ashend_bg\" android:state_pressed=\"true\"/> <item android:drawable=\"@drawable/orange_bg\"/> </selector></span> 此处使用select(详细用法请参考http://blog.csdn.net/shakespeare001/article/details/7788400/) 2.然后是我们的Activity(代码都很简单,认真学了java都不会看不懂,而且注释很详细。只有几处需要解释下下的)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200package com.sorgs.administrator.caclulatordemo; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; public class MainActivity extends Activity implements OnClickListener{ ` Button btn_0;//0数字按钮 Button btn_1;//1数字按钮 Button btn_2;//2数字按钮 Button btn_3;//3数字按钮 Button btn_4;//4数字按钮 Button btn_5;//5数字按钮 Button btn_6;//6数字按钮 Button btn_7;//7数字按钮 Button btn_8;//8数字按钮 Button btn_9;//9数字按钮 Button btn_point;//小数点按钮 Button btn_clear;//清除按钮 Button btn_del;//删除按钮 Button btn_plus;//加好按钮 Button btn_minus;//减号按钮 Button btn_divide;//除号按钮 Button btn_multiply;//乘号按钮 Button btn_equle;//等于按钮 //以上建立按钮 EditText et_input;//显示输出内容的显示屏 boolean clear_flag;//清空标识,用于等号之后清空 @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.content_main);//控制xml为content_main btn_0 = (Button) findViewById(R.id.btn_0); btn_1 = (Button) findViewById(R.id.btn_1); btn_2 = (Button) findViewById(R.id.btn_2); btn_3 = (Button) findViewById(R.id.btn_3); btn_4 = (Button) findViewById(R.id.btn_4); btn_5 = (Button) findViewById(R.id.btn_5); btn_6 = (Button) findViewById(R.id.btn_6); btn_7 = (Button) findViewById(R.id.btn_7); btn_8 = (Button) findViewById(R.id.btn_8); btn_9 = (Button) findViewById(R.id.btn_9); btn_point = (Button) findViewById(R.id.btn_point); btn_del = (Button) findViewById(R.id.btn_del); btn_plus = (Button) findViewById(R.id.btn_plus); btn_clear = (Button) findViewById(R.id.btn_clear); btn_minus = (Button) findViewById(R.id.btn_minus); btn_multiply = (Button) findViewById(R.id.btn_multiply); btn_divide = (Button) findViewById(R.id.btn_divide); btn_equle = (Button) findViewById(R.id.btn_equal); //以上是实例化按钮 et_input = (EditText) findViewById(R.id.et_input);//实例化显示屏 btn_0.setOnClickListener(this); btn_1.setOnClickListener(this); btn_2.setOnClickListener(this); btn_3.setOnClickListener(this); btn_4.setOnClickListener(this); btn_5.setOnClickListener(this); btn_6.setOnClickListener(this); btn_7.setOnClickListener(this); btn_8.setOnClickListener(this); btn_9.setOnClickListener(this); btn_point.setOnClickListener(this); btn_del.setOnClickListener(this); btn_plus.setOnClickListener(this); btn_clear.setOnClickListener(this); btn_minus.setOnClickListener(this); btn_multiply.setOnClickListener(this); btn_divide.setOnClickListener(this); btn_equle.setOnClickListener(this); //以上设置按钮的点击事件 } @Override public void onClick(View v) { String str = et_input.getText().toString(); //取出显示屏内容 switch (v.getId()){ //判断点的是那个按钮 case R.id.btn_0: //建立数字0—9和. case R.id.btn_1: case R.id.btn_2: case R.id.btn_3: case R.id.btn_4: case R.id.btn_5: case R.id.btn_6: case R.id.btn_7: case R.id.btn_8: case R.id.btn_9: case R.id.btn_point: if(clear_flag){ // clear_flag =false; str = \"\"; //计算下一个时候,应将原来的设置为空 et_input.setText(\"\"); } et_input.setText(str+((Button)v).getText()); //将点击的文字添加到输入框里面(str原来输入框中内容) break; case R.id.btn_plus: //建立+-×÷ case R.id.btn_minus: case R.id.btn_multiply: case R.id.btn_divide: if(clear_flag){ clear_flag =false; str = \"\"; //计算下一个时候,应将原来的设置为空 et_input.setText(\"\"); } et_input.setText(str+\" \"+((Button)v).getText()+\" \");//将点击的运算符添加到输入框前后有“ ”用于区别 break; case R.id.btn_del: //建立删除 if(clear_flag){ clear_flag= false; str = \"\"; //计算下一个时候,应将原来的设置为空 et_input.setText(\"\"); }else if (str != null &&!str.equals(\"\")) { //如果显示屏里面不是NULL也不是空 et_input.setText(str.substring(0,str.length()-1)); //从后面长度减一 } break; case R.id.btn_clear: //建立清除 clear_flag = false; str = \"\"; //计算下一个时候,应将原来的设置为空 et_input.setText(\"\"); //将显示屏内容置空 break; case R.id.btn_equal: //建立等于 getResult(); //获取结算结果 break; } } //进行计算 private void getResult(){ String exp = et_input.getText().toString(); //取出显示屏内容并转化为String if (exp == null||exp.equals(\"\")){//如果内容为null和空,直接返回 return; } if(!exp.contains(\" \")){//如果不包含空格(运算符前面有空格),直接返回(比如点了数字,没有运算符) return; } if(clear_flag){ clear_flag = false; return; } clear_flag = true; double result = 0; //定义一个double的result=0 String s1 = exp.substring(0,exp.indexOf(' '));//截取运算符前面的字符 String op = exp.substring(exp.indexOf(' ')+1,exp.indexOf(' ')+2);//截取运算符 String s2 = exp.substring(exp.indexOf(' ')+3);//截取运算符后面的字符 if(!s1.equals(\"\")&&!s2.equals(\"\")){ //如果S1或者S2不为空 double d1 = Double.parseDouble(s1); //强制将S1转换为double类型 double d2 = Double.parseDouble(s2); //强制将S2转换为double类型 if(op.equals(\"+\")){ //如果op为四中情况的方案 result = d1+d2; }else if(op.equals(\"-\")){ result = d1-d2; }else if(op.equals(\"×\")){ result = d1*d2; }else if(op.equals(\"÷\")){ if(d2==0){ Toast.makeText(MainActivity.this, \"除数不能为0!!!\",Toast.LENGTH_LONG).show(); et_input.setText(\"0\"); }else{ result = d1/d2; } } if(!s1.contains(\".\")&&!s2.contains(\".\")&&!op.equals(\"÷\")){ //如果没有小数点则为int类型且op不为÷ int r = (int)result; //强制转换为int类型 et_input.setText(r+\"\"); }else{ //其中含有小数点,则输出double类型 et_input.setText(result+\"\"); } }else if(!s1.equals(\"\")&&s2.equals(\"\")){ //S1不为空,S2为空 double d1 = Double.parseDouble(s1); result = d1; Toast.makeText(MainActivity.this, \"不具备运算\",Toast.LENGTH_LONG).show(); et_input.setText(result+\"\"); //不进行计算,返回S1 }else if(s1.equals(\"\")&&!s2.equals(\"\")){ //S1为空,S2不为空 double d2 = Double.parseDouble(s2); if(op.equals(\"+\")){ result = 0+d2; }else if(op.equals(\"-\")){ result = 0-d2; }else if(op.equals(\"×\")){ result = 0; }else if(op.equals(\"÷\")){ result = 0; } if(!s2.contains(\".\")){ int r = (int)result; et_input.setText(r+\"\"); }else{ et_input.setText(result+\"\"); } }else{ et_input.setText(\"\"); } } } 这里采用消息显示的一个方法,个人觉得这样很好看。1Toast.makeText(MainActivity.this, \"除数不能为0!!!\",Toast.LENGTH_LONG).show(); 效果图: Toast.makeText的方法也不在赘述了,因为很多博客都有写(http://www.cnblogs.com/ycxyyzw/archive/2013/03/12/2955845.html)","tags":[{"name":"Android开发","slug":"Android开发","permalink":"http://sorgs.cn/tags/Android开发/"}]},{"title":"还是来个Hello吧","date":"2016-11-15T02:07:01.000Z","path":"post/52407/","text":"还是来个Hello吧       捣鼓了好几天,终于把我的博客给建立了起来在,真是心累啊。        从最最最开始的购买腾讯云的学生什么东东开始,发现WordPress不是太看看,这就很尴尬了,于是就发现装逼的hexo强力驱动还不多,还不花钱。就搞了过来。        从最最最开始的GitHub开始部署,结果国内访问太慢了,看到网上很多说还可以部署到coding上面,发现不是太好弄,于是再加上部署到七牛上。虽然确实很是麻烦,但是保证了数据的安全些。狡兔三窟嘛。        最后大家对hexo装逼部署不懂的,可以来问问我,我有时间一定耐心解答的        最后呢,我的CSDN的博客在这里。我会逐渐把我的博客搬过来的哈哈哈!!","tags":[{"name":"杂记","slug":"杂记","permalink":"http://sorgs.cn/tags/杂记/"}]}]
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。