Hi there 👋

Welcome to my blog

Android事件传递三部曲:本地广播LocalBroadcastManager

我们都知道Android的四大组件,分别是:Activity, Service,ContentProvider以及BroadcastReceiver,实际开发中前两者接触的更多一点,后面两个虽然不怎么常用但是偶尔也会接触到,今天我们要说的就和BroadcastReceiver有关,当我们想要去使用BroadcastReceiver会看到官方的提示:如果你不需要应用间的通信,可以考虑使用LocalBroadcastManager,会有更高的执行效率,因为它不涉及进程间通讯,而且不用担心普通广播可能产生的一些安全性问题, LocalBroadcastManager是何许人也,听着好像是普通广播的阉割版,实际使用上看,他们确实有些相似,只是LocalBroadcast不能实现跨进程,但当我们揭开它神秘面纱,你就会发现,它其实和普通的广播一点关系都没有,如果非得扯出点关系的话,那就是他们都借助了BroadcastReceiver这个类来担当receiver的角色, 基本使用 如果你之前有使用过普通的广播,你会发现在方法调用上,LocalBroadcastManager和普通的广播是一模一样,不同的LocalBroadCastManager的调用方不再是context,而是LocalBroadCastManager的实例,所以所有的逻辑都是在LocalBroadCastManager的掌控之内。首先,我们还是从最基本的使用场景出发,从最基本的使用方法开始跟踪,看它是怎么将消息传递的: public class Test extends Activity { private static final String ACTION = "simple_action"; private static final String DATA = "data"; BroadcastReceiver mReceiver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 新建一个receiver mReceiver = new MyReceiver(); // 注册receiver LocalBroadcastManager.getInstance(this) .registerReceiver(mReceiver, new IntentFilter(ACTION)); // 发送消息 Intent messageIntent = new Intent(ACTION); messageIntent.putExtra(DATA, "给xxx的一封信"); LocalBroadcastManager.getInstance(this).sendBroadcast(messageIntent); } @Override protected void onDestroy() { // 取消注册 LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); } class MyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // 处理消息 Log....

January 21, 2017 · 3 min · vistashao

Android事件传递三部曲:事件总线EventBus(上)

常用的事件传递方式包括:Handler、BroadcastReceiver、Interface 回调、事件总线EventBus,除去回调这种相对简单的多的方式我们不讨论,Handler的原理已经在之前分析过,接下来要分析的就是EventBus以及BroadcastReceiver,然后最后分析他们各自有优劣以及适用场景。今天的主角就是EventBus 因为整个分析下来,EventBus涉及到的内容还是很多的,所以我将其分成了两个部分,分作上下篇,其中上篇主要简单分析事件具体是怎么在EventBus中传递的,怎么从发布者的手中到达订阅者手中,在这个分析中,我们会特意跳过一些比较难的部分,只是快速地了解整个EventBus的架构;而在下篇中,将详细解释上篇中跳过的部分,将EventBus中的每个特性解释清楚。 基本使用 首先还是从最基本的使用场景出发,一步步来跟踪: public class Example { public void onCreate() { EventBus bus = EventBus.getDefault(); bus.register(this); bus.post("给xxx的一封信"); } @Subscribe public void subscribeEvent(String message) { Log.i("TAG", "收到:" + message); } public void onDestroy() { EventBus.unregister(this); } } 这就是EventBus的最简单的使用方式了,从获得一个EventBus实例开始,然后添加订阅,发送消息,然后订阅方收到消息进行处理,再到最后在合适的时机取消订阅,主要就是:订阅,发送消息,接受消息,取消订阅这四个步骤,再抽象一下就是两对方法:订阅和取消订阅,发送和接受,接下来分析的大致思路也都是按照依次进行, 今天我们就来看下,最主要的就是三行代码,加一个Subscribe注解的public方法,这个消息是怎么从转回来的。首先我们从第一行代码看起: public static EventBus getDefault() { if (defaultInstance == null) { synchronized (EventBus.class) { if (defaultInstance == null) { defaultInstance = new EventBus(); } } } return defaultInstance; } 没错,就是一个普通的单例,调用的就是默认的构造器,倒是看不出来有什么神奇之处,因为主要的处理都在它的构造器里:...

January 20, 2017 · 3 min · vistashao

Android事件传递三部曲:事件总线EventBus(下)

在上篇中,我们主要分析了EventBus的基本使用,从最基本的订阅和取消订阅,发送事件和接受事件开始,简单看了下EventBus的事件传递的过程,我们知道:在注册的过程中,EventBus将订阅者以及订阅方法保存到订阅列表中,当发送事件的时候,从订阅列表中取出符合要求的订阅信息,通过反射调用订阅者的订阅方法,至此完成事件的传递,但是在这过程中,我们还省去了很多部分,比如如何找到订阅者的订阅方法,最后订阅者的订阅方法执行在哪个线程,发送Sticky事件怎么处理,这些问题,我们将继续分析。 SubscriberMethodFinder 首先还是看它的构造方法 SubscriberMethodFinder(List<SubscriberInfoIndex> subscriberInfoIndexes, boolean strictMethodVerification, boolean ignoreGeneratedIndex) { // 这个我们在这里暂时不解释太多,后面用到的时候,我们在展开 this.subscriberInfoIndexes = subscriberInfoIndexes; // 是否开启方法严格检查,如果开启,后面在检查订阅方法找到不符规则的方法会直接抛出异常,否则不做任何处理。默认是false this.strictMethodVerification = strictMethodVerification; // 是否忽略EventBus AnnotationProcessor 生成的代码,后面用到的时候再提,同样默认是false this.ignoreGeneratedIndex = ignoreGeneratedIndex; } // 找到subscriber中所有的订阅方法 List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) { // 首先试图从缓存中读取,如果命中将直接返回 List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass); if (subscriberMethods != null) { return subscriberMethods; } // 根据配置的不同,选择不同的获取方法 if (ignoreGeneratedIndex) { // 忽略AnnotationProcessor生成的文件,直接通过反射获取 subscriberMethods = findUsingReflection(subscriberClass); } else { // 通过AnnotationProcessor生成的文件获取所有的订阅方法,效率更高 subscriberMethods = findUsingInfo(subscriberClass); } // 如果在subscriber中没有找到任何订阅方法,则抛出异常 if (subscriberMethods....

January 20, 2017 · 3 min · vistashao

三件事:Aar, Manifest和Activity-Alias

前几天在做需求的时候,接触了一些之间没有太了解的东西,于是今天找个机会写下来给大家分享一下,说不定大家以后也会有这种需求,可以提前了解一下,主要有三个点:aar文件的类型,Manifest文件的自动合并以及Activity-Alias 关于aar文件 为什么会存在这种文件格式,和jar文件有什么区别?先来句综述:aar文件是建立在jar文件的基础之上,aar是jar文件的一个变种。其实他们本质上没有什么区别,它们都是压缩包,只是能包含的内容不一样,aar包括的东西更多一些,jar也能包含资源文件,不过是文本资源和图片资源,不能包含Android平台下的drawable以及各种xml文件,aar相对来说更包容一些,aar文件结构其实更类似我们应用的apk安装包,我们通过Gradle compile进来的库也都是以aar文件引入进来的。 具体怎么生成一个aar文件以及引入本地aar文件,可以参考Android Developer的文档进行操作,因为官方的文档已经描述的很清楚了,所以我在这里也没必要在赘述什么,有兴趣的可以直接传送。传送门(自备梯子) Manifest 文件合并 刚才其实忘了说,aar文件中除了必要的class文件,还有一些必要的文件,其中就包括AndroidManifest文件,但是我们也知道,一个应用只能有一个Manifest文件,那当我们引入其他依赖库的Manifest文件是怎么处理的呢?没错,就是合并,最终所有的Manifest文件都会被合并成一个Manifest文件,那么我们要讨论的问题就出现了,按照什么顺序合并呢?合并出现冲突怎么解决呢?接下来慢慢解释: 合并优先级 优先级从高到低依次为: build.gradle 配置 app module 的Manifest文件,也就是我们主项目中的Manifest文件 其他依赖库的Manifest文件 下面这张图解释了优先级的作用 除了合并的优先级,还有一些潜在的合并规则也需要注意一下: 在 <manifest>节点中的属性都会已最高优先级指定的值为准,所以不会出现值不一样导致的冲突。这也解释了为什么我们只要在build.gradle文件中指定了versionCode或者versionName,那么项目的Manifest文件中配置的相应属性总是会被覆盖。 在 <uses-feature> 和<uses-library>节点中的android:required属性会在多个配置中采用或运算,也就是说,只有有一个配置设置了true,那这条属性就是true,表示这个feature或者library是必需的。 在<uses-sdk>中的属性,一般情况下,也是以最高优先级的配置为准,不过也有两种特殊情况: 如果低优先级的库的minSdkVersion更高,这时候需要处理一下冲突,可以使用 overrideLibrary来覆盖因入库的minSdkVersion,这样冲突是解决了,但是这样会存在隐患问题,一般第三方库的minSdkVersion用来表示最低可运行版本,你如果通过覆盖的方式解决了冲突,但是在低版本运行的时候,可能会出问题,所以更安全的解决方式就是调高自己的minSdkVersion或者换用其他库 低优先级的库的targetSdkVersion更低,表示这个库还没有为更高的版本做好适配的准备,这时候虽然合并Mainfest文件并并不会出现冲突,但是也会存在一些隐患问题,比如在targetSdkVersion低于15,并且声明了READ_CONTACTS的权限,那么在更高的版本上需要额外的增加READ_CALL_LOG权限,以让应用正常运行,而这件事,合并工具会自动帮我们完成。 <intent-filter> 标签不会被匹配,它们每一个都会被认为是独一无二的,然后一起加在相同的父标签里。 除了通过既定的规则,我们可以猜到最后合并的Manifest文件大概是什么样子,我们还可以直接在Android Studio中直观地实时看到合并以后的Manifest文件,就是在Manifest文件的底部有一个Tab可以看到Merged Manifest,在这里,我们可以清楚的看到我们最终的Manifest文件是什么结构,看有没有恶意的第三方库在我们的Manifest文件中动手脚。 除了这些基本的规则,我们还可以在Manifest文件中定义自己的规则,以防止冲突的出现或者当冲突出现的时候,用来解决冲突,因为具体规则比较多,所以就不在这里展开,大家都兴趣或者有需求的可以直接上Android Developer上看官方的文档,传送门(自备梯子) Activity-alias <activity android:name="me.shaohui.shareutil._ShareActivity" android:theme="@android:style/Theme.Translucent.NoTitleBar" /> <activity-alias android:name="$me.shaohui.shareutil.wxapi.WXEntryActivity" android:exported="true" android:targetActivity="me.shaohui.shareutil._ShareActivity"/> 大家通过字面意思就能看得出来,这个元素就是给TargetActivity属性所指定的Activity设定一个别名,之前一直不知道在什么情况需要给Activity指定别名,这次在做一个社会化分享库的时候,突然明白了Activity-Alias存在的意义了。 Android微信的分享SDK为了接受微信请求的回调以及返回值,需要在应用包名相应的目录下新建一个wxapi目录,并且在该目录下新增一个名为WXEntryActivity的Activity,处理好以后,会在发起微信请求以及请求完成以后,拉起这个Activity。这对我们使用者来说,单独为微信创建一个目录存放一个特定的Activity是个很蛋疼的一个操作,这样会打乱我们的项目结构,而且看起来也很是不优雅,苦思冥想许久,怎么能避免这样的尴尬,最后遇到了Activity-Alias这种解决方案,觉得简直就是碰到了亲人, 最后的解决方案就是大家前面看到的那段代码,我只需要在普通目录下定义我们接收微信回调的Activity,然后再给这个Activity定义一个me.shaohui.shareutil.wxapi.WXEntryActivity别名,这样就既满足了微信的调用需求,还不用给这个Activity进行特殊化处理,只要微信通过这个别名就能找到我们的目标Activity,我们的目标Activity可以随意按照我们既定的目录结构存放,不需要特殊处理,皆大欢喜。 Activity-Alias也有一些自己的属性,大部分都是和Activity节点的属性类似,只有一个targetActivity需要我们重点关注,它定义了这个别名是给哪个Activity设置的,而且要求指定的Activity必须在Activity-Alias之前被声明,否则最后应用安装的时候会出问题。 目前想到的就这些,以后再有新的想法再慢慢扩充,如果有哪写的不对的地方,欢迎大家指出,谢谢! 参考链接 https://developer.android.com/studio/projects/android-library.html https://developer.android.com/studio/build/manifest-merge.html

December 10, 2016 · 1 min · vistashao

临摹锤子便签--SmartisanNotes

因为前一段时间一直在做一个应用,其中有个类似生成长微博的需求,当时第一反应就是锤子便签,毕竟我还算是老罗的粉丝,对锤子便签的生成图片功能一直情有独钟,之前的想法是锤子把这个功能做成了API,点击生成图片的时候把文字和图片数据传上去,然后下载生成的图片,为什么会这么想呢,因为锤子便签是支持多平台的,Android,iOS以及Web端都支持生成图片的功能,最方便的方法就是利用API生成,但是最终的结果跟我想象的并不一样,锤子便签并没有调用API,这也让我想要偷懒直接使用他们的API生成图片的幻想破灭了,自己动手丰衣足食,于是有了SmartisanNotes这个项目。 整个项目下来花了差不多7、8个小时吧,总体感觉还不错,学到了很多东西,关于Canvas的,关于Path的,还有关于文件保存的,总之收获还是不小的,以后会尽量抽一些完整的时间做一些小项目,力求精致,不求大而全,认认真真写好每一行代码。 拍照保存 ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()); values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()); values.put(MediaStore.Images.Media.DATE_MODIFIED, System.currentTimeMillis()); imageFileUri = getActivity().getContentResolver() .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); 首先new一个 ContentValues 对象,设置这个对象的三个属性:DATE_ADDED, DATE_TAKEN, DATE_MODIFIED, 依次对象照片文件的新增日期,拍照日期和修改日期,然后在通过 ContentResolver 通过 insert 方法插入到 contentProvider 中,这样我们就获得了一个imageFileUri,然后通过一定的操作就可以通过Uri得到保存图片的具体路径。 文件保存 File resultDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) resultDir.mkdirs(); File result = new File(resultDir, System.currentTimeMillis() + ".jpg"); 通过 getExternalStoragePublicDirectory() 方法获得文件存储的具体文件夹,系统会根据传入的不同参数返回不同的文件夹,然后在文件夹中创建需要保存的文件,然后保存就OK了。 MediaScannerConnection.scanFile(mContext, new String[] { file.toString() }, null, new MediaScannerConnection.OnScanCompletedListener() { @Override public void onScanCompleted(String path, Uri uri) { Log.i("ExternalStorage", "Scanned " + path + ":"); Log....

October 23, 2016 · 1 min · vistashao