background
Designing this BusUtils is actually doing ApiUtils Do it by hand, because they are basically the same way. I didn't want to compare them with EventBus of greenrobot before design, but there's always a need to compare them after design. So compare EventBus, the best EventBus in the industry, and you'll see that BusUtils that can't reach 300 in my area perform better than Ev.The entBus needs to be much higher, of course, all on the premise that BusUtils is practical and effective, and it's also a thread-safe event bus, which I've actually tested in a single test, don't boast, we talk about the data later, and you can download me if your little partner doesn't believe it.Source code can be compared, single test address: BusUtilsVsEventBusTest , Android test address: BusCompareActivity , BusUtils at AucFrame The role of the in is to pass in values from a module, which plays the following roles:
Its use is described below:
Use
To configure
Add the bus plug-in to build.gradle in the project root directory:
buildscript { dependencies { ... classpath 'com.blankj:bus-gradle-plugin:2.0' } }
Then use the plug-in in the application module:
apply plugin: "com.blankj.bus"
Add to your project AndroidUtilCode Dependency:
api "com.blankj:utilcode:1.25.0"
If you just want to import BusUtils, you need to copy the class yourself and put it in your project. Remember to copy ThreadUtils, and configure the SDL domain of bus in build.gradle under app as follows:
api { busUtilsClass "com.xxx.xxx.BusUtils" } android { ... }
You can guess that the default busUtilsClass is com.blankj.utilcode.util.BusUtils Ha.
If you turn on confusion, you also need to configure your BusUtils annotation method to prevent confusion, if you use it directly AndroidUtilCode If you do not need to configure it, I have already done it for you. Configure your own BusUtils to prevent confusion as follows:
-keepattributes *Annotation* -keepclassmembers class * { @com.xxx.xxx.BusUtils$Bus <methods>; }
Of course, if your project is open to confusion, introduce it in full AndroidUtilCode Yes, confusion can help you remove unused classes and methods.
Okay, plug-ins and dependencies are all configured, and the basic usage is described below.
Basic Use
public static final String TAG_NO_PARAM = "TagNoParam"; public static final String TAG_ONE_PARAM = "TagOneParam"; @BusUtils.Bus(tag = TAG_NO_PARAM) public void noParamFun() {/* Do something */} @BusUtils.Bus(tag = TAG_ONE_PARAM) public void oneParamFun(String param) {/* Do something */} @Override public void onStart() { super.onStart(); BusUtils.register(this); } @Override public void onStop() { super.onStop(); BusUtils.unregister(this); } BusUtils.post(TAG_NO_PARAM);// noParamFun() will receive BusUtils.post(TAG_ONE_PARAM, "param");// oneParamFun() will receive
EventBus is sure to be understood in a flash.
Advanced Use
Sticky event
Supports sticky events, which are sent first, then received at subscription time, consumed, and used in the same way as EventBus, by setting sticky = true in the @BusUtils.Bus annotation, as shown in the following example:
public static final String TAG_NO_PARAM_STICKY = "TagNoParamSticky"; @BusUtils.Bus(tag = TAG_NO_PARAM_STICKY, sticky = true) public void noParamStickyFun() {/* Do something */} BusUtils.postSticky(TAG_NO_PARAM_STICKY); BusUtils.register(xxx);// will invoke noParamStickyFun BusUtils.removeSticky(TAG_NO_PARAM_STICKY);// When u needn't use the sticky, remove it BusUtils.unregister(xxx);
Thread Switching
Thread switching uses the thread pool in ThreadUtils, which has a secure Cached thread pool along with MAIN, IO, CPU, CACHED, SINGLE thread pool. By default, it is POSTING for submitted threads, using threadMode = BusUtils.ThreadMode.xx in the @BusUtils.Bus comment.
Standard
If you want to make your tools comfortable to use, the specifications must be followed. The so-called irregularities are not square, otherwise there will be a lot of problems. The following specifications are recommended here:
- Classes and functions that hold events ensure that they are public.
- Since BusUtils is used for in-module calls, you can write a BusConfig class to hold all the bus Tag s in a module for easy discovery of users and callers.
- Tag s can also best have business module suffix names to prevent duplication, sticky for sticky types, and thread names for specific threads, such as update_avatar_sticky_main_info, which makes sense directly.
- If you can combine AucFrame To use it would be more standard.
- You need to keep the bean s for event transfers in BusUtils, otherwise you will fail to find the entity object after turning on confusion.
Now that you've finished using it, let's compare it to EventBus in terms of performance.
performance testing
First, define the events for both, because the comparison is how fast the events reach, so empty implementations can be done internally, as shown in the code below:
@Subscribe public void eventBusFun(String param) { } @BusUtils.Bus(tag = "busUtilsFun") public void busUtilsFun(String param) { }
BusUtils generates a mapping table of record tag s and method signatures from the @BusUtils.Bus annotation at compile time, since this is done at compile time, and here we do it by reflection.
@Before public void setUp() throws Exception { // This step was injected at AOP time, where the busUtilsFun event was injected by reflection with the same effect. ReflectUtils getInstance = ReflectUtils.reflect(BusUtils.class).method("getInstance"); getInstance.method("registerBus", "busUtilsFun", BusUtilsVsEventBusTest.class.getName(), "busUtilsFun", String.class.getName(), "param", false, "POSTING"); }
Complete the comparison by comparing the following tests:
- Register 10,000 subscribers and average 10 times
- Send * 1000000 times to a subscriber, averaging 10 times
- Send * 100,000 times to 100 subscribers, 10 times averaged
- Log off 10,000 subscribers, averaging 10 times
The test machine is as follows:
macOS: 2.2GHz Intel Core i7 16GB //One plus 6: Android 9 8GB
On Android, we've added EventBus's annotation processor to improve EventBus's efficiency and compare it to BusUtils in the best case.
Next, we write the template code for the test so that the code that compares the two can be directly inserted into the callback later, as follows:
/** * @param name The test function name passed in * @param sampleSize Number of samples * @param times Number of times per execution * @param callback Callback function for comparison */ private void compareWithEventBus(String name, int sampleSize, int times, CompareCallback callback) { long[][] dur = new long[2][sampleSize]; for (int i = 0; i < sampleSize; i++) { long cur = System.currentTimeMillis(); for (int j = 0; j < times; j++) { callback.runEventBus(); } dur[0][i] = System.currentTimeMillis() - cur; cur = System.currentTimeMillis(); for (int j = 0; j < times; j++) { callback.runBusUtils(); } dur[1][i] = System.currentTimeMillis() - cur; callback.restState(); } long eventBusAverageTime = 0; long busUtilsAverageTime = 0; for (int i = 0; i < sampleSize; i++) { eventBusAverageTime += dur[0][i]; busUtilsAverageTime += dur[1][i]; } System.out.println( name + "\nEventBusCostTime: " + eventBusAverageTime / sampleSize + "\nBusUtilsCostTime: " + busUtilsAverageTime / sampleSize ); } public interface CompareCallback { void runEventBus(); void runBusUtils(); void restState(); }
Let's do a one-to-one comparison test.
Register 10,000 subscribers and average 10 times
/** * Register 10,000 subscribers and average 10 times */ @Test public void compareRegister10000Times() { final List<BusUtilsVsEventBusTest> eventBusTests = new ArrayList<>(); final List<BusUtilsVsEventBusTest> busUtilsTests = new ArrayList<>(); compareWithEventBus("Register 10000 times.", 10, 10000, new CompareCallback() { @Override public void runEventBus() { BusUtilsVsEventBusTest test = new BusUtilsVsEventBusTest(); EventBus.getDefault().register(test); eventBusTests.add(test); } @Override public void runBusUtils() { BusUtilsVsEventBusTest test = new BusUtilsVsEventBusTest(); BusUtils.register(test); busUtilsTests.add(test); } @Override public void restState() { for (BusUtilsVsEventBusTest test : eventBusTests) { EventBus.getDefault().unregister(test); } eventBusTests.clear(); for (BusUtilsVsEventBusTest test : busUtilsTests) { BusUtils.unregister(test); } busUtilsTests.clear(); } }); } // MacOS Output: // Register 10000 times. // EventBusCostTime: 427 // BusUtilsCostTime: 41 // One plus 6 Output: // Register 10000 times. // EventBusCostTime: 1268 // BusUtilsCostTime: 399
Send * 1000000 times to a subscriber, averaging 10 times
/** * Send * 1000000 times to a subscriber, averaging 10 times */ @Test public void comparePostTo1Subscriber1000000Times() { comparePostTemplate("Post to 1 subscriber 1000000 times.", 1, 1000000); } // MacOS Output: // Post to 1 subscriber 1000000 times. // EventBusCostTime: 145 // BusUtilsCostTime: 33 // One plus 6 Output: // Post to 1 subscriber 1000000 times. // EventBusCostTime: 1247 // BusUtilsCostTime: 696 private void comparePostTemplate(String name, int subscribeNum, int postTimes) { final List<BusUtilsVsEventBusTest> tests = new ArrayList<>(); for (int i = 0; i < subscribeNum; i++) { BusUtilsVsEventBusTest test = new BusUtilsVsEventBusTest(); EventBus.getDefault().register(test); BusUtils.register(test); tests.add(test); } compareWithEventBus(name, 10, postTimes, new CompareCallback() { @Override public void runEventBus() { EventBus.getDefault().post("EventBus"); } @Override public void runBusUtils() { BusUtils.post("busUtilsFun", "BusUtils"); } @Override public void restState() { } }); for (BusUtilsVsEventBusTest test : tests) { EventBus.getDefault().unregister(test); BusUtils.unregister(test); } }
Send * 10,000 times to 100 subscribers, 10 times averaged
/** * Send * 100,000 times to 100 subscribers, 10 times averaged */ @Test public void comparePostTo100Subscribers10000Times() { comparePostTemplate("Post to 100 subscribers 100000 times.", 100, 100000); } // MacOS Output: // Post to 100 subscribers 100000 times. // EventBusCostTime: 139 // BusUtilsCostTime: 79 // One plus 6 Output: // Post to 100 subscribers 100000 times. // EventBusCostTime: 3092 // BusUtilsCostTime: 2900
Log off 10,000 subscribers, averaging 10 times
/** * Log off 10,000 subscribers, averaging 10 times */ @Test public void compareUnregister10000Times() { final List<BusUtilsVsEventBusTest> tests = new ArrayList<>(); for (int i = 0; i < 10000; i++) { BusUtilsVsEventBusTest test = new BusUtilsVsEventBusTest(); EventBus.getDefault().register(test); BusUtils.register(test); tests.add(test); } compareWithEventBus("Unregister 10000 times.", 10, 1, new CompareCallback() { @Override public void runEventBus() { for (BusUtilsVsEventBusTest test : tests) { EventBus.getDefault().unregister(test); } } @Override public void runBusUtils() { for (BusUtilsVsEventBusTest test : tests) { BusUtils.unregister(test); } } @Override public void restState() { for (BusUtilsVsEventBusTest test : tests) { EventBus.getDefault().register(test); BusUtils.register(test); } } }); for (BusUtilsVsEventBusTest test : tests) { EventBus.getDefault().unregister(test); BusUtils.unregister(test); } } // MacOS Output: // Unregister 10000 times. // EventBusCostTime: 231 // BusUtilsCostTime: 23 // One plus 6 Output: // Unregister 10000 times. // EventBusCostTime: 800 // BusUtilsCostTime: 199
conclusion
For easy observation, we generate a chart to compare the performance of the two:
The performance of four functions in MacOS and OnePlus6 is counted in the chart. From left to right are "BusUtils for MacOS", "EventBus for MacOS", "BusUtils for OnePlus6", "EventBus for OnePlus6". It can be found that BusUtils are more important than EventBus in registration and cancellation.Several times faster, BusUtils is much faster to send multiple events to a small number of subscribers than EventBus, and a little faster to send multiple events to multiple subscribers than EventBus.
Based on all the above, if you use event buses more frequently in your project, try replacing EventBus with my BusUtils to improve performance, or in new projects, you can use BusUtils with better performance directly.
Here's a summary of the benefits of BusUtils:
- BusUtils determines the only event through the event Tag, so the receive function supports no parameters or one parameter, whereas EventBus can only determine the specific recipient through the MessageEvent, and can only receive one parameter, even if it is just a notification, it also needs to define a MessageEvent, so BusUtils parameters are more flexible.
- When BusUtils is applied to a project, a list of u bus_u.json events is generated in the application after compilation, as shown in the list of events generated above:
{ "BusUtilsClass": "com.blankj.utilcode.util.BusUtils", "rightBus": { "noParamFun": "{ desc: com.blankj.utilcode.pkg.feature.bus.BusActivity#noParamFun(), threadMode: POSTING }", "oneParamFun": "{ desc: com.blankj.utilcode.pkg.feature.bus.BusActivity#oneParamFun(java.lang.String param), threadMode: POSTING }" }, "wrongBus": {} }
Modifying oneParamFun to two parameters ensures that the project does not crash because BusUtils is running, and that the api plug-in will make it compile later. The u bus_u.json file, shown below, prompts you that the number of parameters is incorrect:
{ "BusUtilsClass": "com.blankj.utilcode.util.BusUtils", "rightBus": { "noParamFun": "{ desc: com.blankj.utilcode.pkg.feature.bus.BusActivity#noParamFun(), threadMode: POSTING }", }, "wrongBus": { "oneParamFun": "{ desc: com.blankj.utilcode.pkg.feature.bus.BusActivity#oneParamFun(java.lang.String param, java.lang.String param1), threadMode: POSTING, paramSize: 2 }" }
Similarly, if two buses have the same Tag, they will also compile, but it will prompt you that there is a bus with the same Tag in your project.
So BusUtils is more friendly than EventBus.
- BusUtils has far fewer codes than EventBus. BusUtils only have 300 lines of source code, and EventBus 3000 lines is definitely more than just Ha.
- BusUtils perform better than EventBus.
principle
bus Plugin Principle Analysis
The source code for the bus plug-in is here: bus Plugin Source Port , the plug-in uses Gradle's transform ation to complete the injection of BusUtils.init(), which is analyzed in one step:
If you don't understand transforms, let's start with them. Simply put, transforms are designed for byte code insertion. The most common part is AOP (Face Oriented Programming). I won't be a popular science student, so I can search for them if I'm interested.
When it comes to byte code operations, there's something more knowledgeable. You can use javassist to get started quickly and easily. However, I chose a stronger and faster ASM. I won't go into details here. Interested ones can learn by themselves. ASM is also very simple. In fact, ASM ASM Bytecode Outline This plugin helps write quickly.
All functions annotated with @BusUtils.Bus are scanned by ASM, and the annotated values and function's parameter information are read and saved as follows:
@Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { className = name.replace("/", "."); super.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String funName, String desc, String signature, String[] exceptions) { if (cv == null) return null; MethodVisitor mv = cv.visitMethod(access, funName, desc, signature, exceptions); busInfo = null; mv = new AdviceAdapter(Opcodes.ASM5, mv, access, funName, desc) { @Override public AnnotationVisitor visitAnnotation(String desc1, boolean visible) { final AnnotationVisitor av = super.visitAnnotation(desc1, visible); if (("L" + mBusUtilsClass + "$Bus;").equals(desc1)) { busInfo = new BusInfo(className, funName); funParamDesc = desc.substring(1, desc.indexOf(")")); return new AnnotationVisitor(Opcodes.ASM5, av) { @Override public void visit(String name, Object value) {// Obtainable annotation values super.visit(name, value); if ("tag".equals(name)) { tag = (String) value; } else if ("sticky".equals(name) && (Boolean) value) { busInfo.sticky = true; } } @Override public void visitEnum(String name, String desc, String value) { super.visitEnum(name, desc, value); if ("threadMode".equals(name)) { busInfo.threadMode = value; } } }; } return av; } @Override public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) { super.visitLocalVariable(name, desc, signature, start, end, index);// Getting method parameter information if (busInfo != null && !funParamDesc.equals("")) { if ("this".equals(name)) { return; } funParamDesc = funParamDesc.substring(desc.length());// Every time a parameter is removed until it is "", then it is no longer a parameter busInfo.paramsInfo.add(new BusInfo.ParamsInfo(Type.getType(desc).getClassName(), name)); if (busInfo.isParamSizeNoMoreThanOne && busInfo.paramsInfo.size() > 1) { busInfo.isParamSizeNoMoreThanOne = false; } } } @Override public void visitEnd() { super.visitEnd(); if (busInfo != null) { List<BusInfo> infoList = mBusMap.get(tag); if (infoList == null) { infoList = new ArrayList<>(); mBusMap.put(tag, infoList); } else if (infoList.size() == 0) { mBusMap.put(tag, infoList); } else if (infoList.size() == 1) { BusInfo info0 = infoList.get(0); info0.isTagRepeat = true; busInfo.isTagRepeat = true; } else { busInfo.isTagRepeat = true; } infoList.add(busInfo); } } }; return mv; }
Then insert the scanned content into BusUtils.init(), such as the oneParamFun function mentioned above, and the code it eventually inserts is as follows:
private void init() { this.registerBus("TagOneParam", "com.blankj.bus.BusTest", "oneParamFun", "java.lang.String", "param", false, "POSTING"); }
Its ASM inserts the following code:
@Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { if (!"init".equals(name)) { return super.visitMethod(access, name, descriptor, signature, exceptions); } // Write to init() function if (cv == null) return null; MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions); mv = new AdviceAdapter(Opcodes.ASM5, mv, access, name, descriptor) { @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { return super.visitAnnotation(desc, visible); } @Override protected void onMethodEnter() { super.onMethodEnter(); } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode); for (Map.Entry<String, List<BusInfo>> busEntry : mBusMap.entrySet()) { List<BusInfo> infoList = busEntry.getValue(); if (infoList.size() != 1) continue; BusInfo busInfo = infoList.get(0); if (!busInfo.isParamSizeNoMoreThanOne) continue; mv.visitVarInsn(ALOAD, 0); mv.visitLdcInsn(busEntry.getKey()); mv.visitLdcInsn(busInfo.className); mv.visitLdcInsn(busInfo.funName); if (busInfo.paramsInfo.size() == 1) { mv.visitLdcInsn(busInfo.paramsInfo.get(0).className); mv.visitLdcInsn(busInfo.paramsInfo.get(0).name); } else { mv.visitLdcInsn(""); mv.visitLdcInsn(""); } mv.visitInsn(busInfo.sticky ? ICONST_1 : ICONST_0); mv.visitLdcInsn(busInfo.threadMode); mv.visitMethodInsn(INVOKESPECIAL, mBusUtilsClass, "registerBus", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)V", false); } } }; return mv; }
Principle Analysis of BusUtils
Next, look at the implementation of BusUtils.registerBus:
private void registerBus(String tag, String className, String funName, String paramType, String paramName, boolean sticky, String threadMode) { mTag_BusInfoMap.put(tag, new BusInfo(className, funName, paramType, paramName, sticky, threadMode)); }
Simply insert key as tag and value as an instance of BusInfo into the mTag_BusInfoMap to keep an event.
The next step is to use it. At the beginning, we all register first. The source code is as follows:
public static void register(final Object bus) { getInstance().registerInner(bus); } private void registerInner(final Object bus) { if (bus == null) return; String className = bus.getClass().getName(); synchronized (mClassName_BusesMap) { Set<Object> buses = mClassName_BusesMap.get(className); if (buses == null) { buses = new CopyOnWriteArraySet<>(); mClassName_BusesMap.put(className, buses); } buses.add(bus); } processSticky(bus); }
We get the class name of bus, then lock mClassName_BusesMap to insert it into the value set of mClassName_BusesMap. You can see that we have used the thread-safe CopyOnWriteArraySet set, and then we need to deal with whether we have subscribed to the sticky event processSticky before, so register hereThat's it.
Then post sends the event with the following source code:
public static void post(final String tag) { post(tag, NULL); } public static void post(final String tag, final Object arg) { getInstance().postInner(tag, arg); } private void postInner(final String tag, final Object arg) { postInner(tag, arg, false); } private void postInner(final String tag, final Object arg, final boolean sticky) { BusInfo busInfo = mTag_BusInfoMap.get(tag); if (busInfo == null) { Log.e(TAG, "The bus of tag <" + tag + "> is not exists."); return; } if (busInfo.method == null) { Method method = getMethodByBusInfo(busInfo); if (method == null) { return; } busInfo.method = method; } invokeMethod(tag, arg, busInfo, sticky); } private Method getMethodByBusInfo(BusInfo busInfo) { try { if ("".equals(busInfo.paramType)) { return Class.forName(busInfo.className).getDeclaredMethod(busInfo.funName); } else { return Class.forName(busInfo.className).getDeclaredMethod(busInfo.funName, Class.forName(busInfo.paramType)); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } return null; } private void invokeMethod(final String tag, final Object arg, final BusInfo busInfo, final boolean sticky) { Runnable runnable = new Runnable() { @Override public void run() { realInvokeMethod(tag, arg, busInfo, sticky); } }; switch (busInfo.threadMode) { case "MAIN": Utils.runOnUiThread(runnable); return; case "IO": ThreadUtils.getIoPool().execute(runnable); return; case "CPU": ThreadUtils.getCpuPool().execute(runnable); return; case "CACHED": ThreadUtils.getCachedPool().execute(runnable); return; case "SINGLE": ThreadUtils.getSinglePool().execute(runnable); return; default: runnable.run(); } } private void realInvokeMethod(final String tag, Object arg, BusInfo busInfo, boolean sticky) { Set<Object> buses = mClassName_BusesMap.get(busInfo.className); if (buses == null || buses.size() == 0) { if (!sticky) { Log.e(TAG, "The bus of tag <" + tag + "> was not registered before."); return; } else { return; } } try { if (arg == NULL) { for (Object bus : buses) { busInfo.method.invoke(bus); } } else { for (Object bus : buses) { busInfo.method.invoke(bus, arg); } } } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
You can see that there is still a lot of code, but don't worry, we're still simple. First, look for BusInfo with that tag in the mTag_BusInfoMap we injected before, and then output the error log and return it directly.
Then we find the method instance based on the obtained BusInfo. BusInfo will save the method in the instance for the first time, and then call it to take the method out of the instance directly.
Then we take out the thread information from BusInfo, and finally execute the reflection of the method in the thread, which is generally the case, and we need to analyze the source code for the details.
Finally unregister:
public static void unregister(final Object bus) { getInstance().unregisterInner(bus); } private void unregisterInner(final Object bus) { if (bus == null) return; String className = bus.getClass().getName(); synchronized (mClassName_BusesMap) { Set<Object> buses = mClassName_BusesMap.get(className); if (buses == null || !buses.contains(bus)) { Log.e(TAG, "The bus of <" + bus + "> was not registered before."); return; } buses.remove(bus); } }
unregister, in contrast to register, is removed from the value collection of mClassName_BusesMap, which also requires a lock on mClassName_BusesMap.