runtime 获取 SDK 的私有类信息

近期的一个项目里,我们自己开发的埋点模块和友盟 SDK 在兼容上发生了点问题,定位问题的过程中,想看看友盟 SDK 里有哪些类,但是友盟 SDK 只提供了 3 个 public header:

uimeng-sdk

办法其实也很简单,用 runtime 很容易 get 到当前项目里都有哪些类文件被编译进来了,即使打包成 .a .framework 的 SDK 也不例外

更新:runtime 的方式在文末,先说简单的方式:

一、获取 SDK 里的私有类名

使用 lipo + ar 命令:

cd UMMobClick.framework
// 拆分出单个架构
lipo UMMobClick   -thin arm64 -output UMMobClick_arm64

mkdir Objects
cd Objects

ar -x ../UMMobClick_arm64

以下是输出结果

DplusMobClick.o                 TException.o                    UMAggregatedValue.o
MobClick.o                      TMemoryBuffer.o                 UMCachedDB.o
MobClickApp.o                   TProtocolException.o            UMDeflated.o
MobClickConfig.o                TProtocolUtil.o                 UMEventMgr.o
MobClickEnvelope.o              TTransportException.o           UMGameLevel.o
MobClickEvent.o                 UMADplus.o                      UMHelper.o
MobClickGameAnalytics.o         UMANBaseEvent.o                 UMOpenUDID.o
MobClickInternal.o              UMANEkv.o                       UMPayloadBuild.o
MobClickJob.o                   UMANError.o                     UMProtocolData.o
MobClickLocation.o              UMANEvent.o                     UMUaDB.o
MobClickSession.o               UMANTerminate.o                 UMWorkDispatch.o
MobClickSocialAnalytics.o       UMANUtil.o                      UmengUncaughtExceptionHandler.o
MobClickSocialOperation.o       UMANWorker.o                    __.SYMDEF
MobClickUtility.o               UMANetWork.o                    umeng_analytics.o
TBinaryProtocol.o               UMAOCTools.o                    umeng_envelope.o

二、使用 LLDB 命令打印一个类的全部属性、方法

在上一步获取到类名的情况下,使用简单的一行 LLDB 命令,就可以获取到一个类的属性、方法,不用写一行代码,且打印内容也便于阅读:

在运行着的项目任意位置打断点,在控制台 po 需要打印的类名 + _shortMethodDescription:,eg:

 po [MobClickSession _shortMethodDescription]

输出:

<MobClickSession: 0x108788af8>:
in MobClickSession:
    Class Methods:
        + (void) profileSignInPUID:(id)arg1; (0x108755da6)
        + (void) profileSignInPUID:(id)arg1 withProvider:(id)arg2; (0x108755dc6)
        + (void) profileSignOff; (0x108755e21)
        + (void) signInPUID:(id)arg1 provider:(id)arg2; (0x108755c74)
        + (void) startWithAppkey:(id)arg1 reportPolicy:(int)arg2 channelId:(id)arg3; (0x1087563b1)
        + (void) startWithAppkey:(id)arg1; (0x108756994)
        + (id) sharedInstance; (0x108755b76)
    Properties:
        @property (nonatomic) BOOL observerRegistered;  (@synthesize observerRegistered = _observerRegistered;)
        @property (nonatomic) BOOL appInBackGround;  (@synthesize appInBackGround = _appInBackGround;)
        @property (nonatomic) BOOL appBeKilling;  (@synthesize appBeKilling = _appBeKilling;)
        @property (nonatomic) BOOL appCrashed;  (@synthesize appCrashed = _appCrashed;)
        @property (nonatomic) int sessionStatus;  (@synthesize sessionStatus = _sessionStatus;)
        @property (nonatomic) double lastLaunchTime;  (@synthesize lastLaunchTime = _lastLaunchTime;)
    Instance Methods:
        - (void) setObserverRegistered:(BOOL)arg1; (0x1087569be)
        - (void) setAppCrashed:(BOOL)arg1; (0x108756a1e)
        - (void) setAppBeKilling:(BOOL)arg1; (0x1087569fe)
        - (void) setSessionStatus:(int)arg1; (0x108756a3e)
        - (void) setLastLaunchTime:(double)arg1; (0x108756a60)
        - (void) beginSessionTime; (0x108755fd4)
        - (double) endSessionTime; (0x108756340)
        - (void) appActivate:(id)arg1; (0x108755e41)
        - (BOOL) observerRegistered; (0x1087569ae)
        - (void) appInactivate:(id)arg1; (0x108755ea0)
        - (void) ensureSessionLaunch; (0x10875638e)
        - (BOOL) appInBackGround; (0x1087569ce)
        - (void) setAppInBackGround:(BOOL)arg1; (0x1087569de)
        - (BOOL) appBeKilling; (0x1087569ee)
        - (BOOL) appCrashed; (0x108756a0e)
        - (int) sessionStatus; (0x108756a2e)
        - (double) lastLaunchTime; (0x108756a4e)
(NSObject ...)

当然使用 runtime 的方式,也可以实现以上需求,以下是探索过程:

三、使用 runtime 的方式获取 SDK 的私有类名

执行以下代码:

#import <objc/runtime.h>

- (void)showAllClassNameInProject {
    int allClasses = objc_getClassList(NULL,0);
    Class *classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * allClasses);
    allClasses = objc_getClassList(classes, allClasses);
    
    for (int i = 0; i < allClasses; i++) {
        Class clazz = classes[i];
        NSLog(@"当前项目中全部 class: %@", NSStringFromClass(clazz));
    }
    free(classes);
}

输出:

2017-09-09 14:42:20.248  当前项目中全部 class WKObject
2017-09-09 14:42:20.248  当前项目中全部 class WKNSURLRequest
2017-09-09 14:42:20.249  当前项目中全部 class WKNSURLAuthenticationChallenge
2017-09-09 14:42:20.249  当前项目中全部 class WKNSURL
2017-09-09 14:42:20.249  当前项目中全部 class WKNSString
2017-09-09 14:42:20.249  当前项目中全部 class WKNSError
2017-09-09 14:42:20.249  当前项目中全部 class JSExport
2017-09-09 14:42:20.249  当前项目中全部 class NSLeafProxy
2017-09-09 14:42:20.249  当前项目中全部 class NSProxy
2017-09-09 14:42:20.250  当前项目中全部 class _UITargetedProxy
2017-09-09 14:42:20.250  当前项目中全部 class _UIViewServiceReplyControlTrampoline
2017-09-09 14:42:20.250  当前项目中全部 class _UIViewServiceReplyAwaitingTrampoline
2017-09-09 14:42:20.290  当前项目中全部 class _UIViewServiceUIBehaviorProxy
2017-09-09 14:42:20.290  当前项目中全部 class _UIViewServiceImplicitAnimationDecodingProxy
2017-09-09 14:42:20.290  当前项目中全部 class _UIViewServiceImplicitAnimationEncodingProxy
……
// 以下省略 5000 多行

可以看到,把项目中导入的系统的 UIKit.framework Foundation.framework 都打出来了,如果我们再人肉排除 UI_UINS 打头的类:

   Class clazz = classes[i];
   NSString *className = NSStringFromClass(clazz);
   if (![className hasPrefix:@"UI"] && ![className hasPrefix:@"_UI"] && ![className hasPrefix:@"NS"]) {
       NSLog(@"当前项目中全部 class: %@", className);
   }

结果如下:

2017-09-09 15:06:35.995  当前项目中全部 class: WKObject
2017-09-09 15:06:35.995  当前项目中全部 class: WKNSURLRequest
2017-09-09 15:06:35.995  当前项目中全部 class: WKNSURLAuthenticationChallenge
2017-09-09 15:06:35.995  当前项目中全部 class: WKNSURL
2017-09-09 15:06:35.996  当前项目中全部 class: WKNSString
2017-09-09 15:06:35.996  当前项目中全部 class: WKNSError
2017-09-09 15:06:35.996  当前项目中全部 class: JSExport
2017-09-09 15:06:35.996  当前项目中全部 class: WebMainThreadInvoker
2017-09-09 15:06:35.996  当前项目中全部 class: BSZeroingWeakReferenceProxy
2017-09-09 15:06:35.996  当前项目中全部 class: __NSGenericDeallocHandler
// 省略 2000 多行

系统框架类名前缀也是五花八门,if else 的排除是不可能了,得换个思路。

那么,到底怎么区分一个类是系统的,还是用户自定义的呢?查了一下,还真没有办法区分,因为这个问题根本就不成立:
所谓系统的类,说白了就是苹果提供的 framework 里的类,也都是苹果自己自定义的。

中文搜不到,找了一圈在 stackoverflow 找到了这个:How to judge a class whether System's or Custom's?

简单点说就是,虽然区分不了什么系统不系统的类,但是 Framework 是分动态库和静态库的。而苹果开发的 Framework 都是以动态库的形式参与编译打包,只链接到 app 里,不会打包到 App 的 Bundle 中。但所有除了苹果以外的开发者,都只能以静态库 Framework 打包 SDK(上架),代码都会被打包到 App 的 Bundle 中,那么问题就简单了:

- (void)showCustomClassNameOnly {
    int allClasses = objc_getClassList(NULL,0);
    Class *classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * allClasses);
    allClasses = objc_getClassList(classes, allClasses);
    
    for (int i = 0; i < allClasses; i++) {
        Class clazz = classes[i];
        NSBundle *b = [NSBundle bundleForClass:clazz];
        if (b == [NSBundle mainBundle]) {
            NSLog(@"自定义 class: %@", NSStringFromClass(clazz));
        }
    }
    free(classes);
}

输出结果如下:

2017-09-09 14:43:27.291  自定义 class: UMTMemoryBuffer
2017-09-09 14:43:27.291  自定义 class: UMTProtocolUtil
2017-09-09 14:43:27.292  自定义 class: UMTBinaryProtocol
2017-09-09 14:43:27.292  自定义 class: UMTBinaryProtocolFactory
2017-09-09 14:43:27.292  自定义 class: MobClickApp
2017-09-09 14:43:27.292  自定义 class: MobClickUtility
2017-09-09 14:43:27.292  自定义 class: UMEventMgr
2017-09-09 14:43:27.293  自定义 class: MobClickSocialAnalytics
2017-09-09 14:43:27.293  自定义 class: MobClickSocialWeibo
2017-09-09 14:43:27.293  自定义 class: MobClick
2017-09-09 14:43:27.293  自定义 class: UMAnalyticsConfig
2017-09-09 14:43:27.294  自定义 class: UMANDeflated
2017-09-09 14:43:27.294  自定义 class: UmengUncaughtExceptionHandler
2017-09-09 14:43:27.330  自定义 class: MobClickEvent
2017-09-09 14:43:27.330  自定义 class: MobClickInternal
2017-09-09 14:43:27.330  自定义 class: UMANUtil
2017-09-09 14:43:27.331  自定义 class: MobClickConfig
2017-09-09 14:43:27.331  自定义 class: MobClickSession
2017-09-09 14:43:27.332  自定义 class: umeng_envelopeConstants
2017-09-09 14:43:27.332  自定义 class: UMEnvelope
2017-09-09 14:43:27.332  自定义 class: MobClickGameAnalytics
2017-09-09 14:43:27.333  自定义 class: MobClickLocation
// 省略

这样,排除 Demo 项目里的 AppDelegateViewController 这两个类,我们就能看到友盟 SDK 里的全部类名了。

Note

1、在 FLEX 源码中,发现了更简单的实现方式:

- (void)flexShowClassNames {
    unsigned int classNamesCount = 0;
    
    // 用 executablePath 获取当前 app image
    NSString *appImage = [NSBundle mainBundle].executablePath;

    // objc_copyClassNamesForImage 获取到的是 image 下的类,直接排除了系统的类
    const char **classNames = objc_copyClassNamesForImage([appImage UTF8String], &classNamesCount);
    if (classNames) {
        NSMutableArray *classNameStrings = [NSMutableArray array];
        for (unsigned int i = 0; i < classNamesCount; i++) {
            const char *className = classNames[i];
            NSString *classNameString = [NSString stringWithUTF8String:className];
            [classNameStrings addObject:classNameString];
        }
        NSArray *allClassNames = [classNameStrings sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
        NSLog(@"---%@", allClassNames);
        free(classNames);
    }
}

2、objc_getClassList 和 objc_copyClassList 用法区别

  • objc_getClassList 需要两次才能获取,较麻烦
    // int objc_getClassList(Class *buffer, int bufferCount) 获取已经注册的类
    // 第一个参数 buffer :已分配好内存空间的数组,
    // 第二个参数 bufferCount :数组中可存放元素的个数,返回值是注册的类的总数
    int allClasses = objc_getClassList(NULL,0);
    Class *classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * allClasses);
    allClasses = objc_getClassList(classes, allClasses);
  • objc_copyClassList 代码相对简单:
    // Class *objc_copyClassList(unsigned int *outCount)
    // 该函数的作用是获取所有已注册的类,和上述函数 objc_getClassList 参数传入 NULL 和  0 时效果一样
    unsigned int outCount;
    Class *classes = objc_copyClassList(&outCount);

四、使用 runtime 打印一个类的全部属性、方法

在获取到 SDK 私有类的文件名后,还需要进一步获取类的属性、方法、遵守的协议。

简单的调用几个 runtime 的 API 就可以做到 ,代码参见 Demo
,效果如下:

ivar[0] ----  B : _observerRegistered
ivar[1] ----  B : _appInBackGround
ivar[2] ----  B : _appBeKilling
ivar[3] ----  B : _appCrashed
ivar[4] ----  i : _sessionStatus
ivar[5] ----  d : _lastLaunchTime
instance method[0] ---- setObserverRegistered:
instance method[1] ---- setAppCrashed:
instance method[2] ---- setAppBeKilling:
instance method[3] ---- setSessionStatus:
instance method[4] ---- setLastLaunchTime:
instance method[5] ---- beginSessionTime
instance method[6] ---- endSessionTime
instance method[7] ---- appActivate:
instance method[8] ---- observerRegistered
instance method[9] ---- appInactivate:
instance method[10] ---- ensureSessionLaunch
instance method[11] ---- appInBackGround
instance method[12] ---- setAppInBackGround:
instance method[13] ---- appBeKilling
instance method[14] ---- appCrashed
instance method[15] ---- sessionStatus
instance method[16] ---- lastLaunchTime
class method[0] ---- profileSignInPUID:
class method[1] ---- profileSignInPUID:withProvider:
class method[2] ---- profileSignOff
class method[3] ---- signInPUID:provider:
class method[4] ---- startWithAppkey:reportPolicy:channelId:
class method[5] ---- startWithAppkey:
class method[6] ---- sharedInstance

Demo 戳这里

注:runtime 的方案,无法打印出 SDK 里的 Category 类

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,548评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,497评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,990评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,618评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,618评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,246评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,819评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,725评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,268评论 1 320
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,356评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,488评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,181评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,862评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,331评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,445评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,897评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,500评论 2 359

推荐阅读更多精彩内容