美文网首页
Xcode 使用 chisel 插件及 chisel 源码解读

Xcode 使用 chisel 插件及 chisel 源码解读

作者: who_young | 来源:发表于2020-01-03 20:03 被阅读0次

    LLDB 是 Xcode 中自带的一个调试工具 ,chisel 是 facebook 开源的一个 LLDB 命令的集合,它简化和扩展了 LLDB 的命令,使用方法在 chisel github 中介绍的也比较详细。我在这里简单介绍一下,并结合我的使用经验,做些说明。

    1. 安装 (安装工具:终端)

    1. 若未安装 homebrew , 则 先执行 /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 安装 homebrew
    2. brew update
    3. brew install chisel
    4. touch ~/.lldbinit
    5. open ~/.lldbinit
    6. command script import /usr/local/opt/chisel/libexec/fblldb.py 这条命令粘贴到 ~/.lldbinit 文件中
    7. 重启 Xcode 即可。
      提示: 若重启 Xcode 后,未生效,可以将 chisel 下载到本地,并将 command script import /path/to/fblldb.py 添加到 ~/.lldbinit 文件中。/path/to/fblldb.py 替换为 fblldb.py 所在的真实路径

    2. Command 介绍

    在 Xcode 控制台可以使用 help 命令查看所有支持的命令。 这里只介绍下面一些常用的命令:

    Command Description iOS OS X
    pviews 递归打印 key window 上的 View Yes Yes
    pvc 递归打印 key window 上的 View Controller Yes No
    visualize 在 Mac 的预览 APP 中打开 UIImage, CGImageRef, UIView, CALayer, NSData (of an image), UIColor, CIColor, or CGColorRef Yes No
    fv 在视图层级中,找到类名包含要搜索类名的所有 View Yes No
    fvc 在视图层级中,找到类名包含要搜索类名的所有 View Controller Yes No
    show/hide 显示或隐藏指定的 View 或者 Layer Yes Yes
    mask/unmask 在 View 或者 Layer 的上方添加/隐藏一个透明的蒙版 Yes No
    border/unborder 给 View 或者 Layer 添加/隐藏一个边框 Yes Yes
    caflush 重新渲染页面 Yes Yes
    bmessage 给类方法或实例方法添加一个断点,即使这个类没有实现该方法(父类实现了该方法) Yes Yes
    presponder 打印从指定对象开始的响应链 Yes Yes

    上述 Command 的实现源码在 /chisel/commands/目录下

    3. Command 使用例子

    使用 help <command> 可以查看具体使用方法
    pvc

    <RootTabBarController 0x7fd9fe80cc00>, state: appeared, view: <UILayoutContainerView 0x7fda00006390>
       | <BaseNavigationController 0x7fd9fd805e00>, state: appeared, view: <UILayoutContainerView 0x7fd9fd402df0>
       |    | <RootTableViewController 0x7fd9fd402a60>, state: disappeared, view: <UITableView 0x7fd9ff828200> not in the window
       |    | <EnumerateDemoViewController 0x7fda02001140>, state: appeared, view: <UIView 0x7fda0000d760>
    

    pviews

    <UIWindow: 0x7fd9fd602070; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x600002918ba0>; layer = <UIWindowLayer: 0x6000027602a0>>
       | <UITransitionView: 0x7fda0201a040; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x60000272ef00>>
       |    | <UIDropShadowView: 0x7fda02016630; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x60000272f040>>
       |    |    | <UILayoutContainerView: 0x7fda00006390; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x6000027592a0>>
       |    |    |    | <UITransitionView: 0x7fda00006f50; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x600002759300>>
      ......
       |    |    |    |    |    |    | <_UIVisualEffectContentView: 0x7fd9fd7052f0; frame = (0 0; 371 48); autoresize = W+H; tintColor = UIExtendedGrayColorSpace 1 1; layer = <CALayer: 0x600002765700>> disablesGroupFiltering
       |    |    |    |    |    | <UITabBarSwappableImageView: 0x7fda0200eaa0; frame = (174.5 4; 21 28); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x60000272ec40>>
       |    |    |    |    |    | <UITabBarButtonLabel: 0x7fda02401f50; frame = (158 34.5; 54.5 12); text = 'Downloads'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x60000047ce60>>
    

    pviews 打印的东西太多,"......" 省略了许多内容。想查找关心的 view,比较难查找,我们通过 help pviews 可以查看更详细的使用方法。

    help pviews

         Print the recursion description of <aView>.  Expects 'raw' input (see
         'help raw-input'.)
    
    Syntax: pviews
    Print the recursion description of <aView>.
    
    Arguments:
      <aView>; Type: UIView*/NSView*; The view to print the description of.
    
    Options:
      --up/-u ; Print only the hierarchy directly above the view, up to its window.
      --depth/-d <depth>; Type: int; Print only to a given depth. 0 indicates
      infinite depth.
      --window/-w <window>; Type: int; Specify the window to print a description
      of. Check which windows exist with "po (id)[[UIApplication sharedApplication]
      windows]".
      --short/-s ; Print a short description of the view
      --medium/-m ; Print a medium description of the view
    
    Syntax: pviews [--up] [--depth=depth] [--window=window] [--short] [--medium]
    <aView>
    
    This command is implemented as FBPrintViewHierarchyCommand in
    /Users/yanghu/chisel/commands/FBPrintCommands.py.
    

    我们加上 <aView>-m 参数
    pviews 0x7fda0000d760 -m

    <UIView: 0x7fda0000d760>
       | <UIButton: 0x7fda0000d8c0>
       |    | <UIButtonLabel: 0x7fd9fd40d0b0>
       | <UIButton: 0x7fda0000e7f0>
       |    | <UIButtonLabel: 0x7fd9fd40ce20>
       | <UIButton: 0x7fda0000ea90>
       |    | <UIButtonLabel: 0x7fd9fd40cb90>
       | <UIButton: 0x7fda0000ed30>
       |    | <UIButtonLabel: 0x7fd9fd40c6f0>
    

    visualize

    visualize 0x7fda0000e7f0
    
    visualize.png

    fvc enum

    0x7fda02001140 EnumerateDemoViewController
    

    fv UIButton

    0x7fda0000d8c0 UIButton
    0x7fd9fd40d0b0 UIButtonLabel
    0x7fda0000e7f0 UIButton
    0x7fd9fd40ce20 UIButtonLabel
    0x7fda0000ea90 UIButton
    0x7fd9fd40cb90 UIButtonLabel
    0x7fda0000ed30 UIButton
    0x7fd9fd40c6f0 UIButtonLabel
    0x7fda00013880 _UIButtonBarButton
    0x7fda000153e0 UIButtonLabel
    

    hide 0x7fda0000d8c0

    show 0x7fda0000d8c0

    caflush

    e (void)[0x7fda0000d8c0 setBackgroundColor:[UIColor redColor]]
    caflush
    

    border

    border 0x7fda0000e7f0 -c 'blue' -w 5
    caflush
    
    border.png

    unborder

    unborder 0x7fda0000e7f0
    caflush
    

    mask 0x7fda0000e7f0

    mask.png

    unmask 0x7fda0000e7f0

    presponder 0x7fda00015db0

    <UIButton: 0x7fda00015db0; frame = (40 80; 295 40); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x600002734660>>
       | <UIView: 0x7fda00006840; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x600002734500>>
       |    | <EnumerateDemoViewController: 0x7fda0000dc80>
       |    |    | <UIViewControllerWrapperView: 0x7fda02007ee0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x600002730340>>
       |    |    |    | <UINavigationTransitionView: 0x7fd9fd408ba0; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x600002755fc0>>
       |    |    |    |    | <UILayoutContainerView: 0x7fd9fd402df0; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x600002939800>; layer = <CALayer: 0x600002756820>>
       |    |    |    |    |    | <BaseNavigationController: 0x7fd9fd805e00>
       |    |    |    |    |    |    | <UIViewControllerWrapperView: 0x7fda02016e70; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x60000272ece0>>
       |    |    |    |    |    |    |    | <UITransitionView: 0x7fda00006f50; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x600002759300>>
       |    |    |    |    |    |    |    |    | <UILayoutContainerView: 0x7fda00006390; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x6000027592a0>>
       |    |    |    |    |    |    |    |    |    | <RootTabBarController: 0x7fd9fe80cc00>
       |    |    |    |    |    |    |    |    |    |    | <UIDropShadowView: 0x7fda02016630; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x60000272f040>>
       |    |    |    |    |    |    |    |    |    |    |    | <UITransitionView: 0x7fda0201a040; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x60000272ef00>>
       |    |    |    |    |    |    |    |    |    |    |    |    | <UIWindow: 0x7fd9fd602070; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x600002918ba0>; layer = <UIWindowLayer: 0x6000027602a0>>
       |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIWindowScene: 0x7fda02004a80; scene = <FBSSceneImpl: 0x600000960400; identifier: sceneID:com.yanghu.YHDailyDemo-default>; persistentIdentifier = B5525011-A1BC-40F3-AFB4-D8A8187B7102; activationState = UISceneActivationStateForegroundActive; settingsCanvas = <UIWindowScene: 0x7fda02004a80>; windows = (
        "<UIWindow: 0x7fd9fd602070; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x600002918ba0>; layer = <UIWindowLayer: 0x6000027602a0>>",
        "<UITextEffectsWindow: 0x7fda02109f60; frame = (0 0; 375 667); opaque = NO; autoresize = W+H; layer = <UIWindowLayer: 0x6000027708a0>>"
    )>
       |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIApplication: 0x7fd9fd600820>
       |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <AppDelegate: 0x600002774220>
    

    bmessage

    bmessage "-[EnumerateDemoViewController viewDidAppear:]"
    
    bmessage.png

    4. 自定义 Command

    1. 新建 Python 文件 , 例 /path/to/test.py
    2. 添加 script fblldb.loadCommandsInDirectory('/path/to/')~/.lldbinit 文件中。注意:/path/to/ 要使用绝对路径,否则会报错。
    3. 重启 Xcode 或者 在 Xcode 的控制台输入命令 command source ~/.lldbinit

    加载自定义 command 的过程,参考源码:fblldb.py : loadCommandsInDirectory()

    实例: 新建 Python 文件 : ~/lldbCustom/threadcheck.py , 修改 ~/.lldbinit 文件 :

    # ~/.lldbinit
    
    command script import ~/chisel/fblldb.py
    script fblldb.loadCommandsInDirectory('/Users/yanghu/lldbCustom/')
    

    添加 yhct 命令 ,打印当前线程; 添加 yhctm 命令 ,打印当前线程是否是主线程;

    #!/usr/bin/python
    
    # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
    #
    # This source code is licensed under the MIT license found in the
    # LICENSE file in the root directory of this source tree.
    
    import os
    
    import lldb
    import fblldbbase as fb
    
    def lldbcommands():
      return [
        YHCurrentThreadCheckCommand(),
        YHCurrentThreadMainCheckCommand()
      ]
    
    class YHCurrentThreadCheckCommand(fb.FBCommand):
      def name(self):
        return 'yhct'
    
      def description(self):
        return 'print current thread'
    
      def run(self, arguments, options):
        command = 'po [NSThread currentThread]'
        lldb.debugger.HandleCommand(command)
          
    class YHCurrentThreadMainCheckCommand(fb.FBCommand):
      def name(self):
        return 'yhctm'
    
      def description(self):
        return 'check current thread is or is not equal to main thread'
    
      def run(self, arguments, options):
        command = 'po [NSThread currentThread].isMainThread'
        lldb.debugger.HandleCommand(command)
    

    5. Command 源码解读 ( 以 visualize 为例)

    在 Xcode 控制台执行 help visualize 命令,控制台打印如下:

    Open a UIImage, CGImageRef, UIView, or CALayer in Preview.app on your Mac.
         Expects 'raw' input (see 'help raw-input'.)
    
    Syntax: visualize
    Open a UIImage, CGImageRef, UIView, or CALayer in Preview.app on your Mac.
    
    Arguments:
      <target>; Type: (id); The object to visualize.
    
    Syntax: visualize <target>
    
    This command is implemented as FBVisualizeCommand in
    /Users/yanghu/chisel/commands/FBVisualizationCommands.py.
    

    根据最后一行打印的路径,我们打开 FBVisualizationCommands.py 文件如下 ( "......" 省略了一些无关信息) :

    ......
    def _showImage(commandForImage):
      imageDirectory = '/tmp/xcode_debug_images/'
    
      imageName = time.strftime("%Y-%m-%d-%H-%M-%S", time.gmtime()) + ".png"
      imagePath = imageDirectory + imageName
    
      try:
        os.makedirs(imageDirectory)
      except OSError as e:
        if e.errno == errno.EEXIST and os.path.isdir(imageDirectory):
          pass
        else:
          raise
    
      toPNG = '(id)UIImagePNGRepresentation((id){})'.format(commandForImage)
      imageDataAddress = fb.evaluateExpressionValue(toPNG, tryAllThreads=True).GetValue()
      imageBytesStartAddress = fb.evaluateExpression('(void *)[(id)' + imageDataAddress + ' bytes]')
      imageBytesLength = fb.evaluateExpression('(NSUInteger)[(id)' + imageDataAddress + ' length]')
    
      address = int(imageBytesStartAddress, 16)
      length = int(imageBytesLength)
    
      if not (address or length):
        print('Could not get image data.')
        return
    
      process = lldb.debugger.GetSelectedTarget().GetProcess()
      error = lldb.SBError()
      mem = process.ReadMemory(address, length, error)
    
      if error is not None and str(error) != 'success':
        print(error)
      else:
        imgFile = open(imagePath, 'wb')
        imgFile.write(mem)
        imgFile.close()
        os.system('open ' + imagePath)
    
    ......
    
    def _showLayer(layer):
      layer = '(' + layer + ')'
      size = '((CGRect)[(id)' + layer + ' bounds]).size'
    
      width = float(fb.evaluateExpression('(CGFloat)(' + size + '.width)'))
      height = float(fb.evaluateExpression('(CGFloat)(' + size + '.height)'))
      if width == 0.0 or height == 0.0:
        print('Nothing to see here - the size of this element is {} x {}.'.format(width, height))
        return
    
      fb.evaluateEffect('UIGraphicsBeginImageContextWithOptions(' + size + ', NO, 0.0)')
      fb.evaluateEffect('[(id)' + layer + ' renderInContext:(void *)UIGraphicsGetCurrentContext()]')
    
      result = fb.evaluateExpressionValue('(UIImage *)UIGraphicsGetImageFromCurrentImageContext()')
      if result.GetError() is not None and str(result.GetError()) != 'success':
        print(result.GetError())
      else:
        image = result.GetValue()
        _showImage(image)
    
      fb.evaluateEffect('UIGraphicsEndImageContext()')
    
    ......
    
    def _visualize(target):
      target = fb.evaluateInputExpression(target)
    
      if fb.evaluateBooleanExpression('(unsigned long)CFGetTypeID((CFTypeRef)' + target + ') == (unsigned long)CGImageGetTypeID()'):
        _showImage('(id)[UIImage imageWithCGImage:' + target + ']')
      else:
        if objectHelpers.isKindOfClass(target, 'UIImage'):
          _showImage(target)
        elif objectHelpers.isKindOfClass(target, 'UIView'):
          _showLayer('[(id)' + target + ' layer]')
        elif objectHelpers.isKindOfClass(target, 'CALayer'):
          _showLayer(target)
        elif objectHelpers.isKindOfClass(target, 'UIColor') or objectHelpers.isKindOfClass(target, 'CIColor') or _colorIsCGColorRef(target):
          _showColor(target)
        elif objectHelpers.isKindOfClass(target, 'NSData'):
          if _dataIsImage(target):
            _showImage('(id)[UIImage imageWithData:' + target + ']')
          elif _dataIsString(target):
            print(fb.describeObject('[[NSString alloc] initWithData:' + target + ' encoding:4]'))
          else:
            print('Data isn\'t an image and isn\'t a string.')
        else:
          print('{} isn\'t supported. You can visualize UIImage, CGImageRef, UIView, CALayer, NSData, UIColor, CIColor, or CGColorRef.'.format(objectHelpers.className(target)))
    
    class FBVisualizeCommand(fb.FBCommand):
      def name(self):
        return 'visualize'
    
      def description(self):
        return 'Open a UIImage, CGImageRef, UIView, or CALayer in Preview.app on your Mac.'
    
      def args(self):
        return [ fb.FBCommandArgument(arg='target', type='(id)', help='The object to visualize.') ]
    
      def run(self, arguments, options):
        _visualize(arguments[0])
    

    在控制台输入 visualize 0x7fda0000e7f0 命令,会执行 FBVisualizeCommand 类的 run(self, arguments, options) 方法 , 该方法实现里调用 _visualize(target) 方法。
    _visualize(target) 方法的执行步骤如下:

    1. 确认传参符合要求 fb.evaluateInputExpression(target)
    2. 判断传参的类型 objectHelpers.isKindOfClass(target, 'class'), 不同类型执行不同的方法。该例中传参为一个 UIButton 对象,objectHelpers.isKindOfClass(target, 'UIView') 条件成立。因此执行 _showLayer('[(id)' + target + ' layer]') 方法。
    3. _showLayer(layer) 方法内,开启了一个图片上下文,在该上下文里绘制一张图片。绘制成功后,调用 _showImage(commandForImage) 方法。
    4. _showImage(commandForImage) 方法内,为图片创建一个临时存储路径,将图片流写入文件中 imgFile.write(mem) , 然后用 Mac 自带的预览工具打开该图片文件 os.system('open ' + imagePath)

    自定义 Command 源码解读

    自定义的 Command 需要添加脚本 script fblldb.loadCommandsInDirectory('/path/to/')~/.lldbinit 文件中, 该脚本中,重点调用了 fblldb.py 文件中的 loadCommandsInDirectory() 方法 ( "......" 省略了一些无关信息) :

    def loadCommandsInDirectory(commandsDirectory):
      for file in os.listdir(commandsDirectory):
        fileName, fileExtension = os.path.splitext(file)
        if fileExtension == '.py':
          module = imp.load_source(fileName, os.path.join(commandsDirectory, file))
    
          if hasattr(module, 'lldbinit'):
            module.lldbinit()
    
          if hasattr(module, 'lldbcommands'):
            module._loadedFunctions = {}
            for command in module.lldbcommands():
              loadCommand(module, command, commandsDirectory, fileName, fileExtension)
    
    def loadCommand(module, command, directory, filename, extension):
      func = makeRunCommand(command, os.path.join(directory, filename + extension))
      ......
      lldb.debugger.HandleCommand('script ' + functionName + ' = sys.modules[\'' + module.__name__ + '\']._loadedFunctions[\'' + key + '\']')
      lldb.debugger.HandleCommand('command script add --help "{help}" --function {function} {name}'.format(
        help=helpText.replace('"', '\\"'), # escape quotes
        function=functionName,
        name=name))
    
    def makeRunCommand(command, filename):
      def runCommand(debugger, input, exe_ctx, result, _):
        ......
        if validateArgsForCommand(args, command):
          command.run(args, options)
    
      runCommand.__doc__ = helpForCommand(command, filename)
      return runCommand
    

    该方法的执行步骤如下:

    1. 加载自定义 Command 文件 module = imp.load_source(fileName, os.path.join(commandsDirectory, file))
    2. 遍历自定义 Command 文件中 lldbcommands方法的类对象数组,对每个类对象调用 loadCommand(module, command, directory, filename, extension) 方法。
    3. loadCommand(module, command, directory, filename, extension) 方法内调用 func = makeRunCommand(command, os.path.join(directory, filename + extension)) 方法,获取具体执行命令的方法。 makeRunCommand(command, filename) 方法调用类对象的 run(self, arguments, options) 方法,返回执行命令的方法。
    4. 调用 lldb.debugger.HandleCommand() 执行命令

    相关文章

      网友评论

          本文标题:Xcode 使用 chisel 插件及 chisel 源码解读

          本文链接:https://www.haomeiwen.com/subject/uvdzoctx.html