Summary of basic principles of iOS

This paper focuses on how componentization communicates with each other

Componentized communication scheme

At present, the mainstream mainly has the following three ways:

  • 1.URLrouting
  • 2,target-action
  • 3,protocolmatching

URL routing

At present, most routing tools on iOS are based on URL matching, or based on naming conventions, using the Runtime method for dynamic invocation

The advantage of these dynamic schemes is that they are simple to implement, while the disadvantage is that they need to maintain string tables, or rely on naming conventions, and cannot expose all problems at compile time, requiring errors to be discovered at run time.

The URL routing mode is mainly MGJRouter represented by Mogujie

The realization idea is as follows:

  • Component modules are instantiated when the App starts, and then those components are directed toModuleManagerregisteredUrl, sometimes you don’t need to instantiate, use class registration
  • When component A needs to call component B, theModuleManagerPass the URL, and parameters follow the URL in GET mode, similar to openURL. The ModuleManager is then responsible for scheduling component B and finally completing the task.
/ / 1, a registered a URL MGJRouter. RegisterURLPattern (" app: / / home ") {(info) and in print (" info: Mgjrouter. openURL("app://home")Copy the code

Advantages of URL routing

  • Highly dynamic, suitable for apps that often carry out operational activities, such as e-commerce
  • Routing rules of multiple platforms can be centrally managed
  • Easy to adapt to URL schemes

Disadvantages of URl routing

  • There are limited ways to pass arguments, and no compiler can do parameter type checking, so all arguments are converted by string
  • Applies only to interface modules, not to generic modules
  • The format of the parameter is not clear, it is a flexible dictionary, and there needs to be a place to look up the format of the parameter.
  • Do not support the storyboard
  • Rely on string hard coding, difficult to manage, mushroom street to do a background management.
  • There is no guarantee that the module being used exists
  • The decoupling capability is limited, and the “registration”, “implementation” and “use” of URL must use the same character rules. Once any party makes modification, the code of other parties will become invalid, and it is difficult to reconstruct

In addition to CTMediator, there are the following tripartite frameworks

  • routable-ios
  • JLRoutes
  • HHRouter

target-action

This solution is based on the OC runtime/category feature to dynamically fetch modules, such as NSClassFromString to fetch classes and create instances, and NSInvocation to dynamically invoke methods via performSelector + NSInvocation

Its main representative framework is CASatwy’s CTMediator

The realization idea is as follows:

  • 1. Add a new interface to the route using the classification, and obtain the corresponding class in the interface through a string
  • 2. Create an instance through Runtime and call its methods dynamically
CTMediator{@objc func A_showHome()->UIViewController? { let params = [ kCTMediatorParamsKeySwiftTargetModuleName: "CJLBase_Example" ] if let vc = self.performTarget("A", action: "Extension_HomeViewController", params: params, shouldCacheTarget: false) as? UIViewController{return vc} return nil} //******* NSObject { @objc func Action_Extension_HomeViewController(_ params: [String: Any])->UIViewController{let home = HomeViewController() return home}} ******* CTMediator.sharedInstance().A_showHome() { self.navigationController? .pushViewController(vc, animated: true) }Copy the code

The reference relationship between its modules is shown in the figure below

advantages

  • usingclassificationYou can explicitly declare interfaces for compilation checks
  • implementationlightweight

disadvantages

  • Need to be inmediator 和 targetTo add each interface, modular code is more cumbersome
  • incategoryIs still introduced inHard coding of strings, internal use dictionary parameters, and URL routing to some extent also have the same problem
  • There is no guarantee that the module being used exists, and after target is modified, the user can only detect the error at run time
  • Too many Target classes may be created

CTMediator source code analysis

  • Called through the classificationperformTargetCame toCTMediatorThe concrete implementation of, namelyperformTarget:action:params:shouldCacheTarget:, mainly through the passed name, find the correspondingtarget 和 action
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget { if (targetName == nil || actionName == nil) { return nil; } // When used in swift, you need to pass in the target name of the corresponding project, Otherwise you will find the view controller nsstrings * swiftModuleName = params [kCTMediatorParamsKeySwiftTargetModuleName]; // generate target NSString *targetClassString = nil; If (swiftModuleName. Length > 0) {// Swift stringWithFormat:@"% @.target_ %@", swiftModuleName, targetName]; } else {//OC in target filename join targetClassString = [NSString stringWithFormat:@"Target_%@", targetName]; } / / the cache lookup target NSObject * target = [self safeFetchCachedTarget: targetClassString]; Target if (target == nil) {Class targetClass = NSClassFromString(targetClassString); Target = [[targetClass alloc] init]; NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName]; Sel action = NSSelectorFromString(actionString); If (target == nil) {// If (target == nil) {// If (target == nil) {// If (target == nil) {// If (target == nil) { In practice, you can give a fixed target in advance to be used at this time, Then process this request of [self NoTargetActionResponseWithTargetString: targetClassString selectorString: actionString originParams:params]; return nil; } / / whether to cache the if (shouldCacheTarget) {[self safeSetCachedTarget: target key: targetClassString]; } / / whether the response sel the if ([target respondsToSelector: action]) {/ / dynamic invocation methods return [self safePerformAction: the action target: the target params:params]; } else {// This is where the unresponsive request is handled. If there is no response, try calling the notFound method corresponding to target to handle SEL action = NSSelectorFromString(@"notFound:"); if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target params:params]; } else {// This is also where unresponsive requests are handled. If notFound is not present, this demo will return directly. In practice, you can use the fixed target above mentioned. [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params]; @synchronized (self) { [self.cachedTarget removeObjectForKey:targetClassString]; } return nil; }}}Copy the code
  • Enter thesafePerformAction:target:params:Realization, mainly throughinvocationforParameter passing + message forwarding
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params {// get method signature NSMethodSignature* methodSig = [target methodSignatureForSelector:action]; if(methodSig == nil) { return nil; Const char* retType = [methodSig methodReturnType]; //void type if (STRCMP (retType, @encode(void)) == 0) {... } / /... Omit other types of judgment}Copy the code

protocol class

Protocol matching is implemented as follows:

  • 1, will beprotocolAnd the correspondingclassforDictionary matching
  • 2, By usingprotocolTo obtainclassIn theDynamically creating an instance

A typical tripartite framework for Protocol is Alibaba’s BeeHive. BeeHive borrowed from the Spring Service, Apache DSO architecture concept, using AOP+ extension App life cycle API form, the business functions, basic functional modules in modular way to solve complex problems in large applications, and between modules in the form of Service call, complex problems will be segmtioned, Modularize services in an AOP manner.

BeeHive core ideas

  • 1. Calls between modules have changed from direct calls to corresponding modules to callsServiceAvoid direct dependence.
  • 2. The distribution of App life cycle will be coupled inAppDelegateEach module exists independently in the form of microapplications.

Here is an example (intended to use swift, but has some problems, temporarily use OC) :

******** 1. Register [[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]]; #import "bhService. h" id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];Copy the code

advantages

  • 1. Use the interface call to realize the type safety of parameter passing
  • 2. Directly use the protocol interface of the module without repeated encapsulation

disadvantages

  • 1. Create all objects with a framework in a different way, i.e. no external parameters are supported
  • 2, useOC runtimeCreate objects that do not support Swift
  • 3, just doprotocol 和 classDoes not support more complex creation methods and dependency injection
  • 4. It is not guaranteed that the protocol used must have a corresponding module, nor can it be directly determined whether a protocol can be used to obtain modules

In addition to BeeHive, there is Swinject

Register the BeeHive module

BeeHive manages each module mainly through the BHModuleManager. Only registered modules are managed in the BHModuleManager.

BeeHive provides three different invocation forms, static plist, dynamic registration, and annotation. Module and Service are not associated. Each Service Module can independently implement functions of Module or Service.

1. Annotation method Registration this method is mainly used for Annotation marking through BeeHiveMod macro

//***** use BeeHiveMod(ShopModule) //***** BeeHiveMod macrodefinition #define BeeHiveMod(name) \ class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name""; #define BeeHiveDATA(sectName) __attribute((used, Section ("__DATA,"#sectname" ")) //***** char * kShopModule_mod __attribute((used, section("__DATA,""BeehiveMods"" "))) = """ShopModule""";Copy the code

Here are a few things to say about __attribute

  • The first parameterusedUsed: modifies functions. Used means that functions are not optimized under Release even if they are not referenced. Without this modifier, unreferenced segments are removed from the Release environment linker.
  • Through the use of__attribute__((section("name")))To specify which paragraph. Data is used__attribute__((used))To prevent the linker from optimally removing unused segments and then injecting modules into__DATAIn the

Now that the Module has been stored in a special section of the Mach-O file, how do I get it?

  • Enter theBHReadConfigurationThe method is mainly throughMach-OLocate the stored data segment and place it in an array
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp) { NSMutableArray *configs = [NSMutableArray array]; unsigned long size = 0; Uintptr_t *memory = #ifndef __LP64__ (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size); #else const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp; uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size); #endif unsigned long counter = size/sizeof(void*); For (int idx = 0; idx < counter; ++idx){ char *string = (char*)memory[idx]; NSString *str = [NSString stringWithUTF8String:string]; if(! str)continue; BHLog(@"config = %@", str); if(str) [configs addObject:str]; } return configs; }Copy the code

2. Read the local Pilst file

  • First, you need to set up the path
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive"; / / optional, default is extracted. The bundle/BeeHive plistCopy the code

Create plist file, plist file format is also an array containing multiple dictionaries. There are two keys in the dictionary, one is @”moduleLevel” and the other is @”moduleClass”. Notice that the name of the root array is @” moduleClasses “.

  • Enter theloadLocalModulesThe method is mainly fromPlistYou take the array, and you add the array toBHModuleInfosIn the array.
// to initialize the context, load Modules and Services -(void)setContext:(BHContext *)context {_context = context; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self loadStaticServices]; [self loadStaticModules]; }); } 👇 // load modules - (void)loadStaticModules { And register with the BHModuleManager BHModuleInfos array [[BHModuleManager sharedManager] loadLocalModules]; // Register all modules and sort them internally by priority [[BHModuleManager sharedManager] registedAllModules]; } 👇 - (void)loadLocalModules {//plist file path NSString *plistPath = [[NSBundle mainBundle] pathForResource:[BHContext shareInstance].moduleConfigName ofType:@"plist"]; // Check if file exists if (! [[NSFileManager defaultManager] fileExistsAtPath:plistPath]) { return; } / / read the entire file] [@ "moduleClasses" : array NSDictionary * moduleList = [[NSDictionary alloc] initWithContentsOfFile: plistPath]; ModuleClasses key [[@"moduleClass":"aaa", @"moduleLevel": @"bbb"], [...]] NSArray<NSDictionary *> *modulesArray = [moduleList objectForKey:kModuleArrayKey]; NSMutableDictionary<NSString *, NSNumber *> *moduleInfoByClass = @{}.mutableCopy; / / to iterate through group [self BHModuleInfos enumerateObjectsUsingBlock: ^ (NSDictionary * _Nonnull obj, NSUInteger independence idx, BOOL * _Nonnull stop) { [moduleInfoByClass setObject:@1 forKey:[obj objectForKey:kModuleInfoNameKey]]; }]; [modulesArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if (! ModuleInfoByClass [[obj objectForKey: kModuleInfoNameKey]]) {/ / stored in BHModuleInfos [self. BHModuleInfos addObject: obj]; }}]; }Copy the code

3, load method registration

This method registers the Module class in the Load method

+ (void)load
{
    [BeeHive registerDynamicModule:[self class]];
}
Copy the code
  • Enter theregisterDynamicModuleimplementation
+ (void)registerDynamicModule:(Class)moduleClass { [[BHModuleManager sharedManager] registerDynamicModule:moduleClass]; } 👇 - (void) registerDynamicModule: (Class) moduleClass {[self registerDynamicModule: moduleClass shouldTriggerInitEvent:NO]; } 👇 - (void)registerDynamicModule:(Class)moduleClass shouldTriggerInitEvent:(BOOL)shouldTriggerInitEvent {[self addModuleFromObject:moduleClass shouldTriggerInitEvent:shouldTriggerInitEvent]; }Copy the code

Its underlying or the same as the first way, will eventually go addModuleFromObject: shouldTriggerInitEvent: method

  • The load method can also be usedBH_EXPORT_MODULEMacro instead of
#define BH_EXPORT_MODULE(isAsync) \ + (void)load { [BeeHive registerDynamicModule:[self class]]; } \ -(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue]; }Copy the code

The BH_EXPORT_MODULE macro can pass in a parameter that indicates whether the Module is loaded asynchronously, if YES, or synchronously if NO.

2. The BeeHive module event

BeeHive provides life cycle events for each module to interact with the BeeHive host environment and sense changes in the module’s life cycle.

BeeHive modules receive events. In the BHModuleManager, all events are defined as the BHModuleEventType enumeration. As shown below, there are two special events: BHMInitEvent and BHMTearDownEvent

Typedef NS_ENUM(NSInteger, BHModuleEventType) {// Set the Module BHMSetupEvent = 0, // Used to initialize the Module, such as the environment, // Used to remove the Module BHMTearDownEvent, BHMSplashEvent, BHMQuickActionEvent, BHMWillResignActiveEvent, BHMDidEnterBackgroundEvent, BHMWillEnterForegroundEvent, BHMDidBecomeActiveEvent, BHMWillTerminateEvent, BHMUnmountEvent, BHMOpenURLEvent, BHMDidReceiveMemoryWarningEvent, BHMDidFailToRegisterForRemoteNotificationsEvent, BHMDidRegisterForRemoteNotificationsEvent, BHMDidReceiveRemoteNotificationEvent, BHMDidReceiveLocalNotificationEvent, BHMWillPresentNotificationEvent, BHMDidReceiveNotificationResponseEvent, BHMWillContinueUserActivityEvent, BHMContinueUserActivityEvent, BHMDidFailToContinueUserActivityEvent, BHMDidUpdateUserActivityEvent, BHMHandleWatchKitExtensionRequestEvent, BHMDidCustomEvent = 1000 };Copy the code

There are three main types

  • 1.System events: Mainly refers toApplication life cycle events!

The general approach is to change the AppDelegate to inherit from BHAppDelegate

@interface TestAppDelegate : BHAppDelegate <UIApplicationDelegate>
Copy the code

2, application events: official flow chart, including modSetup, modInit, etc., can be used to code the setup and initialization of each plug-in module.

  • 3,Custom events

All of the above events can be handled by calling BHModuleManager triggerEvent:.

- (void)triggerEvent:(NSInteger)eventType { [self triggerEvent:eventType withCustomParam:nil]; } 👇 - (void)triggerEvent:(NSInteger)eventType withCustomParam:(NSDictionary *)customParam {[self handleModuleEvent:eventType forTarget:nil withCustomParam:customParam]; } 👇 #pragma mark-module protocol - (void)handleModuleEvent:(NSInteger)eventType ForTarget :(id<BHModuleProtocol>)target withCustomParam:(NSDictionary *)customParam {switch (eventType) {// initializes the event case BHMInitEvent: //special [self handleModulesInitEventForTarget:nil withCustomParam :customParam]; break; / / destructor event case BHMTearDownEvent: / / special [self handleModulesTearDownEventForTarget: nil withCustomParam: customParam]; break; Default: {NSString *selectorStr = [self.bhSelectorByevent objectForKey:@(eventType)]; [self handleModuleEvent:eventType forTarget:nil withSeletorStr:selectorStr andCustomParam:customParam]; } break; }}Copy the code

As you can see from the above code, with the exception of the BHMInitEvent initialization event and the BHMTearDownEvent removing Module event, All events are called handleModuleEvent: forTarget: withSeletorStr: andCustomParam: method, its internal implementation mainly traversal moduleInstances instance array, Call performSelector: withObject: method corresponding method call

- (void)handleModuleEvent:(NSInteger)eventType forTarget:(id<BHModuleProtocol>)target withSeletorStr:(NSString *)selectorStr andCustomParam:(NSDictionary *)customParam { BHContext *context = [BHContext shareInstance].copy; context.customParam = customParam; context.customEvent = eventType; if (! selectorStr.length) { selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)]; } SEL seletor = NSSelectorFromString(selectorStr); if (! seletor) { selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)]; seletor = NSSelectorFromString(selectorStr); } NSArray<id<BHModuleProtocol>> *moduleInstances; if (target) { moduleInstances = @[target]; } else { moduleInstances = [self.BHModulesByEvent objectForKey:@(eventType)]; } // Iterate through the moduleInstances array, Call performSelector: withObject: method corresponding method call [moduleInstances enumerateObjectsUsingBlock: ^ (id < BHModuleProtocol > moduleInstance, NSUInteger idx, BOOL * _Nonnull stop) { if ([moduleInstance respondsToSelector:seletor]) { #pragma clang diagnostic push #pragma clang Diagnostic ignored "-warc-performSelector -leaks" // Perform method calls [moduleInstance performSelector: Seletor withObject:context];  #pragma clang diagnostic pop [[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- %@", [moduleInstance class], NSStringFromSelector(seletor)]]; } }]; }Copy the code

Note: all modules here must be BHModuleProtocol compliant or they will not receive messages for these events.

3. Call the BeeHive module

In BeeHive, the BHServiceManager manages each Protocol. BHServiceManager manages only registered protocols.

There are three ways to register Protocol, which correspond to Module registration

1. Annotation registration

/ / * * * * * * 1, the Annotation by BeeHiveService macro marks BeeHiveService (HomeServiceProtocol BHViewController) / / * * * * * * 2, and # define a macro definition BeeHiveService(servicename,impl) \ class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ ""#servicename"" : ""#impl""}"; //****** Char * kHomeServiceProtocol_service __attribute((used, section("__DATA,""BeehiveServices"" "))) = "{ """HomeServiceProtocol""" : """BHViewController"""}";Copy the code

2. Read the local PList file

  • First, as with Module, you need to set the path first
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
Copy the code

Set the plist file

  • But also in thesetContextWhen the registeredservices
// Load services -(void)loadStaticServices {[BHServiceManager sharedManager]. EnableException = self.enableException; [[BHServiceManager sharedManager] registerLocalServices]; } 👇 - (void)registerLocalServices {NSString *serviceConfigName = [BHContext shareInstance].Serviceconfigname; / / the file path nsstrings * plistPath = [[NSBundle mainBundle] pathForResource: serviceConfigName ofType: @ "plist"]. if (! plistPath) { return; } NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath]; [self.lock lock]; For (NSDictionary *dict in serviceList) {NSString *protocolKey = [dict objectForKey:@"service"]; NSString *protocolImplClass = [dict objectForKey:@"impl"]; if (protocolKey.length > 0 && protocolImplClass.length > 0) { [self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}]; } } [self.lock unlock]; }Copy the code

3, load method registration

To register the Protocol in the Load method, register the Protocol by calling registerService:service: in BeeHive

+ (void)load { [[BeeHive shareInstance] registerService:@protocol(UserTrackServiceProtocol) service:[BHUserTrackViewController class]]; } 👇 - (void)registerService:(Protocol *)proto service:(Class) serviceClass {[[BHServiceManager sharedManager] registerService:proto implClass:serviceClass]; }Copy the code

At this point, the three methods are created

The Protocol for

Protocol differs from Module in that Protocol has one more method than Module that returns a Protocol instance object

- (id)createService:(Protocol *)proto; { return [[BHServiceManager sharedManager] createService:proto]; } 👇 - (id)createService:(Protocol *)service {return [self createService:service withServiceName:nil]; } 👇 - (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName {return [self createService:service withServiceName:serviceName shouldCache:YES]; } 👇 - (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName shouldCache:(BOOL)shouldCache {if (! serviceName.length) { serviceName = NSStringFromProtocol(service); } id implInstance = nil; // Check whether protocol is already registered if (! [self checkValidService:service]) { if (self.enableException) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ protocol does not been registed", NSStringFromProtocol(service)] userInfo:nil]; } } NSString *serviceStr = serviceName; // If there is a cache, Directly from the cache for the if (shouldCache) {id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName: serviceStr];  if (protocolImpl) { return protocolImpl; Class implClass = [self serviceImplClass:service]; if ([[implClass class] respondsToSelector:@selector(singleton)]) { if ([[implClass class] singleton]) { if ([[implClass Class] respondsToSelector:@selector(shareInstance)]) // Create a singleton. Else implInstance = [[implClass alloc] init]; If (shouldCache) {/ / cache the [[BHContext shareInstance] addServiceWithImplInstance: implInstance serviceName: serviceStr]; return implInstance; } else { return implInstance; } } } return [[implClass alloc] init]; }Copy the code

CreateService first checks if Protocol is registered. It then takes the corresponding Class from the dictionary and creates a singleton if the shareInstance method is implemented, or an instance if it is not. If singleton is implemented, we can further cache implInstance and serviceStr in the servicesByName dictionary of the BHContext. This can then be passed with context

  • Enter theserviceImplClassImplementation, from which you can see that protocol and classes are passedThe dictionaryThe binding,protocolAs akey.serviceImp(Class name) asvalue
- (Class)serviceImplClass:(Protocol *)service
{
    //通过字典将 协议 和 类 绑定,其中协议作为key,serviceImp(类的名字)作为value
    NSString *serviceImpl = [[self servicesDict] objectForKey:NSStringFromProtocol(service)];
    if (serviceImpl.length > 0) {
        return NSClassFromString(serviceImpl);
    }
    return nil;
}
Copy the code

Module & Protocol

Here is a brief summary:

  • forModule: Array storage
  • forProtocol: Through the dictionaryprotocolBind to the class,keyforprotocol.valueforserviceImpThe name of the class

Auxiliary class

  • BHConfigClass: is a singleton with one inside itNSMutableDictionaryThe type ofconfigProperty, which maintains some dynamic environment variables asBHContextSupplementary existence of
  • BHContextClass: is a singleton with two internal instancesNSMutableDictionaryProperties of, respectivelymodulesByName 和 servicesByName. This class is mainly used to store context information. For example, inapplication:didFinishLaunchingWithOptions:A large amount of context information can be initialized
// Save information [BHContext shareInstance]. Application = Application; [BHContext shareInstance].launchOptions = launchOptions; [BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive"; / / optional, default is extracted. The bundle/BeeHive plist [BHContext shareInstance]. ServiceConfigName = @ "BeeHive. Bundle/BHService";Copy the code
  • BHTimeProfilerClass: Profilers used to perform computationally timed performance
  • BHWatchDogClass: used to start a thread and listen for the main thread to block

Refer to the link

  • BeeHive – an elegant but still improving decoupling framework
  • BeeHive, an iOS module decouple practice