我想搞个mac app 插件,仅仅为了Typroa插入几行预置文字

前提

为了更好的维持我的博客系统,想着之后的博客都发布于github.io, 那么如果你有搭建过github.io 那么相比都知道YAML, 通过MARK down 中插入YAML FORMAT 语言,我们可以控制博客的标题、评论等等。。那么简单来说就是我不想用Typora每次建立一个新blog,都需要繁琐的插入那一堆YAML,比如

title: position、anchorPoint、frame理解

tags: iOS

key: 107

# article_header:

# type: cover

# image:

# src: https://user-images.githubusercontent.com/8369671/80915045-153ff780-8d82-11ea-9acf-6ccbf2b05d9d.png

OK,所以首先呢我把Typora的所有文档看了一遍,没有入口可配置。那咋办?

想法也就从这诞生了,我能不能写个插件,在Typora 新建文件的时候,手动插入预置文字,接下来记录插件开发过程,成功不成功最后见分晓!

着手干(查资料)

通过查看资料,我们需要两个东西辅助,1.class-dump) 2. insert_dylib

 基本原理就是:

Mach-O 二进制文件Load Commands中的 LC_LOAD_DYLIB 标头告诉 macOS在执行期间要加载哪些动态库 (dylib)。所以我们只需要在二进制文件中添加一条LC_LOAD_DYLIB就可以。而insert_dylib工具已经为我们实现了添加的功能

接下来一个一个解释:

  1. class-dump

    通过名字我们大概能猜到这是一个什么工具,类似一个解释类的工具,确实他也是干这个的,正确安装之后,我们可以通过class-dump [option] Mach-o文件,输出mac app的暴露类的相关属性,OK,我们用的就是class-dump -H Mach-o文件 输出头文件,我们通过头文件查看mac app源头文件,然后找到需要hook的类进行hook(具体的安装可以点击超链接查看)

  2. insert_dylib

    这个工具,可以帮助我们插入一个dylib 到Mach-o二进制文件中,所以也就是说我们需要做一个framework然后通过这个库,嵌入到app的Mach-o二进制文件里

ok,工具都全乎了,接下来的时间,就是需要我们做一个framework,这个framework是用专门hook的动态库。

当然hook,Objective-C里就是runtime里的Swizzing Method搞定,这个iOS 开发应该都用过不少(常规技术)

  1. 具体实践

    1. new project - framework

      image-20220829140442928

      我们新建一个framework,用来做动态库

    2. 查看class-dump的类,hook 找到的类做功能

      image-20220829140836995

      cd 到 /Applications/Typora.app/Contents/MacOS/

      使用class-dump -H /Applications/Typora.app/Contents/MacOS/Typora -o /Users/haoyh02/Desktop/typora.h 输出头文件解析到桌面目标目录,如下

      image-20220829141211348

      ok,接下来我就需要分析头文件,找出自己需要hook的类,以及方法,当然这个过程才是最漫长的,而且是不断的尝试出来的。具体的分析我就不赘述了,就是看代码呗。

      最终我们找到了LibraryCommands 类,他大概就是一些文件命令处理,比如新建文件,当然我们hook的是这个类,但是具体文件处理则是Document这个类,集成于NSDocument。

      具体看代码

      //
      //  TyporaAutoRejectHook.m
      //  TyporaAutoReject
      //
      //  Created by 郝玉鸿 on 2022/8/26.
      //
            
      #import <Foundation/Foundation.h>
      #import <AppKit/AppKit.h>
      #import "TyporaAutoReject.h"
      #import <objc/runtime.h>
            
      void ty_hook(Class originClass, SEL originSelector, Class swizzClass, SEL swizzSelector) {
          Method originalMethod = class_getInstanceMethod(originClass, originSelector);
              Method swizzledMethod = class_getInstanceMethod(swizzClass, swizzSelector);
              if(originalMethod && swizzledMethod) {
                  method_exchangeImplementations(originalMethod, swizzledMethod);
              }
      }
            
      @interface Document : NSDocument
            
      @end
            
      @interface LibraryCommands: NSObject
            
      @property(retain) Document *document;
            
      @end
            
      @implementation NSObject (Typora)
            
      + (void)hookThunder{
          ty_hook(objc_getClass("LibraryCommands"), @selector(createFile:), [self class], @selector(hook_newDocument:));
      }
      - (void)hook_newDocument:(id)args {
          [self hook_newDocument:args];
                
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
              LibraryCommands *commans = (LibraryCommands *)self;
              Document *document = commans.document;
              NSString *injectString = @"---\ntitle: <xxxx> \ntags: [iOS] [MAC] [Plugin]> \nkey: xxx \n# article_header:\n# type: cover \n# image:\n # src: https://user-images.githubusercontent.com/8369671/80915045-153ff780-8d82-11ea-9acf-6ccbf2b05d9d.png \n---";
              [document setValue:injectString forKeyPath:@"content"];
                    
              [document writeToURL:document.fileURL ofType:document.fileType error:nil];
              [document performSelector:NSSelectorFromString(@"syncToClient")];
              [document performSelector:NSSelectorFromString(@"syncToSelf")];
          });
      }
            
      @end
            
      static void __attribute__((constructor)) initialize(void) {
          [NSObject hookThunder];
      }
            
      

      很简单,思路就是hook到新建文件的方法,然后2s之后,文件内插入一段预置文字。然后同步界面。

    3. copy framework的产出(编译,products/xxx.framework)到mac app 下的contents/MacOS

      OK,接下来我们需要build工程,得到products/xxx.framework 产物。xcode13 隐藏了products,我们只需要打开pbxproj文件, 修改mainGroup 和 productRefGroup 一样(本身也是一样的,我们只需要copy再次保存就好了),保存就可以出现,也可以到DerivedData里去找

      image-20220829141839812

      image-20220829141945528

      然后copy framework,到/Applications/Typora.app/Contents/ 下即可。

    4. 执行insert_dylib 命令

       ./insert_dylib --all-yes /Applications/Typora.app/Contents/MacOS/TyporaAutoReject.framework/TyporaAutoReject Typora_backup Typora 
      

      执行这个命令后,即可嵌入framework

    5. 重新打开应用

      一定要重新打开应用,否则不生效(这个其实不用想也是这样,毕竟我们是编译型程序)

    6. ok,当然这个步骤很麻烦,为了我们能更好的重复验证,我写了脚本执行,这些命令

            
      sudo rm -d -r /Applications/Typora.app/Contents/MacOS/TyporaAutoReject.framework
      sudo mv -f /Users/haoyh02/Library/Developer/Xcode/DerivedData/TyporaAutoReject-duhvxgpyrtuykugkepbmpmhciiyh/Build/Products/Debug/* /Applications/Typora.app/Contents/MacOS/
      ./insert_dylib --all-yes /Applications/Typora.app/Contents/MacOS/TyporaAutoReject.framework/TyporaAutoReject Typora_backup Typora
      

​ 至此,我们为Typora做的小插件,完全生效了。我们来看一下效果:

result

参考

安装class-dump

如何为macOS应用开发插件

insert_dylib

class-dump