7.2 搭建tweak原型

在iOS 8中,Notes原始的阅览界面是这样的,如图7-2所示。

要在这个界面显示这条note的字数,在哪里显示比较好看呢?不知道你对iOS 6上的Notes有没有印象,那时的每条note都有一个居中显示的标题,如图7-3所示。

图7-2 iOS 8的Notes阅览界面

图7-3 iOS 6的Notes阅览界面

iOS 8把这个标题给去掉了,整个导航栏显得空空荡荡的,不如我们就把字数加在标题所在的位置,如图7-4所示。

效果还不赖!要把Notes改造成这样,需要做些什么工作呢?还记得第5章里说过,在iOS里你看见的每个东西都是一个对象吗?记住这个准则,一起来思考。

1)每条note都是一个对象,阅览界面涵盖了一条note的内容及修改时间等信息,这些信息都来源于这条note。阅览界面是一个view,可以通过nextResponder追溯到它的controller,用controller又可以访问note的相关数据,可以用于在刚刚进入阅览界面的时候初始化字数标题。

2)当编辑一条note时,阅览界面的右上方会出现一个“Done”的按钮,如图7-5所示。

点击“Done”之后,这条note被保存下来。这个现象说明一条note在编辑过程中是不会实时保存的,不然就不需要这个按钮了。而字数标题随着内容的编辑实时变化的效果是最好的,要达到这种效果,就需要一个实时监测note内容变化的方法,且要从这个方法里拿到当前的字数,实时更新标题——因为这种方法一般是定义在protocol里的,所以要留意各种protocol里有没有出现这类函数。

3)如果已经搞定了字数,要怎么把它放在导航栏上呢?阅览界面的controller想必是一个UIViewController的子类,而UIViewController有一个名为title的property,因此只要简简单单地调用setTitle:就可以了。

图7-4 加上字数之后的阅览界面

图7-5 阅览界面出现“Done”按钮

如果能解决上面3个问题,Characount for Notes的技术难点就算全部拿下。没有更多需要解释的了,我们开始动手吧~

7.2.1 定位Notes的可执行文件

在“/Applications/”下踅摸了一圈,没有名为“Notes.app”的文件夹。这种情况下,该怎么定位Notes所在的文件夹呢?还记得在dumpdecrypted章节里找App目录的小技巧吗?是的,就是用ps命令:先关掉所有的App,然后打开Notes。接着ssh到iOS中,用ps命令看看当前有哪些进程来自“/Applications/”,如下:


FunMaker-5:~ root# ps -e | grep /Applications/
 592 ??         0:37.70 /Applications/MobileMail.app/MobileMail
 761 ??         0:02.78 /Applications/MessagesNotificationViewService.app/MessagesNotificationViewService
 1807 ??         0:00.55 /private/var/db/stash/_.29LMeZ/Applications/MobileSafari.app/webbookmarksd
 2016 ??         0:05.23 /Applications/InCallService.app/InCallService
 2619 ??         0:02.66 /Applications/MobileSMS.app/MobileSMS
 2672 ??         0:01.20 /Applications/MobileNotes.app/MobileNotes
 2678 ttys000    0:00.01 grep /Applications/

其中,最可疑的当然就是MobileNotes了,怎么验证呢?kill掉它,看看已经打开的Notes会不会闪退,如下:


FunMaker-5:~ root# killall MobileNotes

Notes果然退出了,说明“/Applications/MobileNotes.app/MobileNotes”就是Notes的可执行文件,而且同时还知道了“/Applications/”下运行在后台的一些应用。把MobileNotes拷贝到OSX中,准备class-dump。

7.2.2 class-dump出MobileNotes的头文件

因为Notes不是从AppStore下载的,没有加壳,所以可以直接使用class-dump,如下:


snakeninnys-MacBook:~ snakeninny$ class-dump -S -s -H /Users/snakeninny/Code/iOSSystemBinaries/8.1_iPhone5/MobileNotes.app/MobileNotes -o /Users/snakeninny/Code/iOSPrivateHeaders/8.1/MobileNotes

一共有88个头文件,粗略扫一眼,看看能发现什么,如图7-6所示。

图7-6 class-dump头文件

看到图7-6中选中的文件了吗?是不是它,我们现在不知道,也不用急于猜测,结果马上就会揭晓了。

7.2.3 用Cycript找到阅览界面及其controller

百试不爽的recursiveDescription又要派上用场了,如下:


FunMaker-5:~ root# cycript -p MobileNotes
cy# ?expand
expand == true
cy# [[UIApp keyWindow] recursiveDescription]
@"<UIWindow: 0x17688db0; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x17689620>; layer = <UIWindowLayer: 0x17688fc0>>
   | <UILayoutContainerView: 0x175bb880; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x175bb900>>
   |    | <UILayoutContainerView: 0x17699350; frame = (0 0; 320 568); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x1769cf60>; layer = <CALayer: 0x17699530>>
   |    |    | <UINavigationTransitionView: 0x176564c0; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x17658ec0>>
   |    |    |    | <UIViewControllerWrapperView: 0x176d13b0; frame = (0 0; 320 568); layer = <CALayer: 0x176d1530>>
   |    |    |    |    | <UILayoutContainerView: 0x1769dd80; frame = (0 0; 320 568); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x176a16f0>; layer = <CALayer: 0x1769de00>>
   |    |    |    |    |    | <UINavigationTransitionView: 0x1769ebb0; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x1769ec40>>
   |    |    |    |    |    |    | <UIViewControllerWrapperView: 0x175109e0; frame = (0 0; 320 568); layer = <CALayer: 0x175109b0>>
   |    |    |    |    |    |    |    | <NotesBackgroundView: 0x175ee3e0; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x17510a70>; layer = <CALayer: 0x175ee580>>
   |    |    |    |    |    |    |    |    | <NotesTextureBackgroundView: 0x175ee5b0; frame = (0 0; 320 568); clipsToBounds = YES; layer = <CALayer: 0x175ee630>>
   |    |    |    |    |    |    |    |    |    | <NotesTextureView: 0x175ee940; frame = (0 -64; 320 640); layer = <CALayer: 0x175ee9c0>>
   |    |    |    |    |    |    |    |    | <NoteContentLayer: 0x176c5110; frame = (0 0; 320 568); layer = <CALayer: 0x176ca850>>
   |    |    |    |    |    |    |    |    |    | <UIView: 0x175f2130; frame = (16 0; 288 0); hidden = YES; layer = <CALayer: 0x175dd2b0>>
   |    |    |    |    |    |    |    |    |    | <NotesScrollView: 0x175f2a10; baseClass = UIScrollView; frame = (0 0; 320 568); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x175f1b70>; layer = <CALayer: 0x175f28d0>; contentOffset: {0, -64}; contentSize: {320, 460}>
   |    |    |    |    |    |    |    |    |    |    | <UIView: 0x175f09a0; frame = (0 0; 320 0); layer = <CALayer: 0x175f2790>>
   |    |    |    |    |    |    |    |    |    |    | <UIView: 0x175f27e0; frame = (0 0; 0 460); layer = <CALayer: 0x175f2850>>
   |    |    |    |    |    |    |    |    |    |    | <NoteDateLabel: 0x175f3400; baseClass = UILabel; frame = (69 5.5; 182 18); text = 'November 24, 2014, 20:44'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x175f3560>>
   |    |    |    |    |    |    |    |    |    |    | <NoteTextView: 0x175ee3e0; baseClass = _UICompatibilityTextView; frame = (6 28; 308 418); text = 'Secret'; clipsToBounds = YES; gestureRecognizers = <NSArray: 0x176c7ed0>; layer = <CALayer: 0x176d88e0>; contentOffset: {0, 0}; contentSize: {308, 52}>
……

果不其然有一个NoteTextView,且“Secret”就位于其中。持续调用nextResponder,找出它的controller,如下:


cy# [#0x175ee3e0 nextResponder]
#"<NotesScrollView: 0x17d307c0; baseClass = UIScrollView; frame = (0 0; 320 568); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x17e502a0>; layer = <CALayer: 0x17d30b60>; contentOffset: {0, -64}; contentSize: {320, 251}>"
cy# [#0x17d307c0 nextResponder]
#"<NoteContentLayer: 0x17e505b0; frame = (0 0; 320 568); layer = <CALayer: 0x17e50470>>"
cy# [#0x17e505b0 nextResponder]
#"<NotesBackgroundView: 0x17e52320; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x17d0c940>; layer = <CALayer: 0x17e522f0>>"
cy# [#0x17e52320 nextResponder]
#"<NotesDisplayController: 0x17edc340>"

好的,就是NoteDisplayController了。看看如下直接调用setTitle:能否改变阅览界面的标题:


cy# [#0x17edc340 setTitle:@"Characount = Character count"]

效果如图7-7所示。

图7-7 setTitle:的效果

没有任何问题,第一目标达成!

7.2.4 从NoteDisplayController找到当前note对象

趁热打铁,到NoteDisplayController.h里看看它的定义,如下:


@interface NotesDisplayController : UIViewController <NoteContentLayerDelegate, UIActionSheetDelegate, AFContextProvider, UIPopoverPresentationControllerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate, NotesQuickLookActivityItemDelegate, ScrollViewKeyboardResizerDelegate, NSUserActivityDelegate, NotesStateArchiving>
{
……
@property(nonatomic, getter=isVisible) BOOL visible; // @synthesize visible=_visible;
- (void)loadView;
@property(retain, nonatomic) NoteObject *note; // @synthesize note=_note;
……
}

文件的内容很多,通览之后,我们发现了一个NoteObject类型的属性。虽说一个note就是一个对象,但NoteObject的名称含义是不是也太明显了……在Cycript里把它打印出来看看,如下:


cy# [#0x17edc340 note]
#'<NoteObject: 0x176aa170> (entity: Note; id: 0x176a9040 <x-coredata://4B88CC7C-7A5F-4F15-9275-53C6D0ABE0C3/Note/p15> ; data: {\n    attachments =     (\n    );\n    author = nil;\n    body = "0x176a8b20 <x-coredata://4B88CC7C-7A5F-4F15-9275-53C6D0ABE0C3/NoteBody/p15>";\n    containsCJK = 0;\n    contentType = 0;\n    creationDate = "2014-11-24 05:00:59 +0000";\n    deletedFlag = 0;\n    externalFlags = 0;\n    externalSequenceNumber = 0;\n    externalServerIntId = "-4294967296";\n    guid = "781B6C87-2855-4512-8864-50618754333A";\n    integerId = 3865;\n    isBookkeepingEntry = 0;\n    modificationDate = "2014-11-24 12:44:08 +0000";\n    serverId = nil;\n    store = "0x175a2b60 <x-coredata://4B88CC7C-7A5F-4F15-9275-53C6D0ABE0C3/Store/p1>";\n    summary = nil;\n    title = Secret;\n})'

很明显,NoteObject就是当前显示的note,各个字段含义都比较清晰,现在去看看它的定义,如下:


@interface NoteObject : NSManagedObject
{
}
- (BOOL)belongsToCollection:(id)arg1;
@property(nonatomic) unsigned long long sequenceNumber;
- (BOOL)containsAttachments;
@property(retain, nonatomic) NSString *externalContentRef;
@property(retain, nonatomic) NSData *externalRepresentation;
@property(readonly, nonatomic) BOOL hasValidServerIntId;
@property(nonatomic) long long serverIntId;
@property(nonatomic) unsigned long long flags;
@property(readonly, nonatomic) NSURL *noteId;
@property(readonly, nonatomic) BOOL isBeingMarkedForDeletion;
@property(readonly, nonatomic) BOOL isMarkedForDeletion;
- (void)markForDeletion;
@property(nonatomic) BOOL isPlainText;
- (id)contentAsPlainTextPreservingNewlines;
@property(readonly, nonatomic) NSString *contentAsPlainText;
@property(retain, nonatomic) NSString *content;
// Remaining properties
@property(retain, nonatomic) NSSet *attachments; // @dynamic attachments;
@property(retain, nonatomic) NSString *author; // @dynamic author;
@property(retain, nonatomic) NoteBodyObject *body; // @dynamic body;
@property(retain, nonatomic) NSNumber *containsCJK; // @dynamic containsCJK;
@property(retain, nonatomic) NSNumber *contentType; // @dynamic contentType;
@property(retain, nonatomic) NSDate *creationDate; // @dynamic creationDate;
@property(retain, nonatomic) NSNumber *deletedFlag; // @dynamic deletedFlag;
@property(retain, nonatomic) NSNumber *externalFlags; // @dynamic externalFlags;
@property(retain, nonatomic) NSNumber *externalSequenceNumber; // @dynamic externalSequenceNumber;
@property(retain, nonatomic) NSNumber *externalServerIntId; // @dynamic externalServerIntId;
@property(readonly, retain, nonatomic) NSString *guid; // @dynamic guid;
@property(retain, nonatomic) NSNumber *integerId; // @dynamic integerId;
@property(retain, nonatomic) NSNumber *isBookkeepingEntry; // @dynamic isBookkeepingEntry;
@property(retain, nonatomic) NSDate *modificationDate; // @dynamic modificationDate;
@property(retain, nonatomic) NSString *serverId; // @dynamic serverId;
@property(retain, nonatomic) NoteStoreObject *store; // @dynamic store;
@property(retain, nonatomic) NSString *summary; // @dynamic summary;
@property(retain, nonatomic) NSString *title; // @dynamic title;
@end

非常好,这么多的property表明NoteObject是个非常标准的model。如何获取它的文字内容呢?在上面的代码中,看到了一个名为contentAsPlainText的property,像下面这样调用它看看是什么效果:


cy# [#0x176aa170 contentAsPlainText]
@"Secret"

为了进一步确认,改一下这条note的文字,再配一张图,如图7-8所示。

图7-8 重新编辑这条note

然后重新调用contentAsPlainText,如下:


cy# [#0x176aa170 contentAsPlainText]
@"bbs.iosre.com"

基本可以确定这个函数能够正确返回当前note的文字内容了,对它调用length就可以拿到这条note的文字个数,如下:


cy# [[#0x176aa170 contentAsPlainText] length]
13

还有最后一项任务,咱们快马加鞭,把它搞定!

7.2.5 找到实时监测note内容变化的方法

在本章开头部分已经提到,“实时监测note内容变化的方法一般是定义在protocol里的”。因为设置标题的函数,以及获取note对象的操作都是通过NotesDisplayController类完成的,所以如果能在这个类里找到一个符合条件的方法,那另两项操作就可以放在这个方法里完成了,可以极大地简化代码。打开NotesDisplayController.h,看看它实现了哪些协议,如下:


@interface NotesDisplayController:UIViewController<NoteContentLayerDelegate,UIActionSheetDelegate,AFContextProvider,UIPopoverPresentationControllerDelegate,UINavigationControllerDelegate,UIImagePickerControllerDelegate,NotesQuickLookActivityItemDelegate,ScrollViewKeyboardResizerDelegate,NSUserActivityDelegate,NotesStateArchiving>
……
@end

其中UIActionSheetDelegate、UIPopoverPresentationControllerDelegate、UINavigation-ControllerDelegate、UIImagePickerControllerDelegate都是公开协议,明显跟note内容的变化没关系,可以直接排除掉了。剩下的NoteContentLayerDelegate、AFContextProvider、NotesQu-ickLookActivityItemDelegate、ScrollViewKeyboardResizerDelegate、NSUserActivityDelegate和NotesStateArchiving都不能轻易放过,需要逐个排查。先看NoteContentLayerDelegate-Protocol.h,如下:


@protocol NoteContentLayerDelegate <NSObject>
- (BOOL)allowsAttachmentsInNoteContentLayer:(id)arg1;
- (BOOL)canInsertImagesInNoteContentLayer:(id)arg1;
- (void)insertImageInNoteContentLayer:(id)arg1;
- (BOOL)isNoteContentLayerVisible:(id)arg1;
- (BOOL)noteContentLayer:(id)arg1 acceptContentsFromPasteboard:(id)arg2;
- (BOOL)noteContentLayer:(id)arg1 acceptStringIncreasingContentLength:(id)arg2;
- (BOOL)noteContentLayer:(id)arg1 canHandleLongPressOnElement:(id)arg2;
- (void)noteContentLayer:(id)arg1 containsCJK:(BOOL)arg2;
- (void)noteContentLayer:(id)arg1 contentScrollViewWillBeginDragging:(id)arg2;
- (void)noteContentLayer:(id)arg1 didChangeContentSize:(struct CGSize)arg2;
- (void)noteContentLayer:(id)arg1 handleLongPressOnElement:(id)arg2 atPoint:(struct CGPoint)arg3;
- (void)noteContentLayer:(id)arg1 setEditing:(BOOL)arg2 nimated:(BOOL)arg3;
- (void)noteContentLayerContentDidChange:(id)arg1 updatedTitle:(BOOL)arg2;
- (BOOL)noteContentLayerShouldBeginEditing:(id)arg1;
@optional
- (void)noteContentLayerKeyboardDidHide:(id)arg1;
@end

其中,noteContentLayer:didChangeContentSize:和noteContentLayerContentDidChange:u-pdatedTitle:这两个方法有些可疑,编辑一条note时,这条note的内容和内容所占的尺寸都在实时变化,因此这两个方法确实有被实时调用的可能性。查看NotesDisplayController.h,这两个协议方法也都被实现了。为了确定它们有没有被实时调用,考虑用LLDB验证一下。

用LLDB附加MobileNotes,看看MobileNotes的ASLR偏移,如下:


(lldb) image list -o -f
[  0] 0x00035000 /private/var/db/stash/_.29LMeZ/Applications/MobileNotes.app/MobileNotes(0x0000000000039000)
[  1] 0x00197000 /Library/MobileSubstrate/MobileSubstrate.dylib (0x0000000000197000)
[  2] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/Frameworks/QuickLook.framework/QuickLook
……

ASLR偏移是0x35000。然后把MobileNotes拖进IDA,待初始分析完成后,查看[NotesDisplay-Controller noteContentLayer:didChangeContentSize:]和[NotesDisplayController noteContent-LayerContent-DidChange:updatedTitle:]的基地址,如图7-9和图7-10所示。

图7-9 [NotesDisplayController noteContentLayer:didChangeContentSize:]

图7-10 [NotesDisplayController noteContentLayerContentDidChange:updatedTitle:]

两者的基地址分别是0x16E70和0x1AEB8,因此断点地址分别是0x4BE70和0x4FEB8。下2个断点,然后随便打开一条note并编辑它,看看断点会不会停,如下:


(lldb) br s -a 0x4BE70
Breakpoint 1: where = MobileNotes`___lldb_unnamed_function382$$MobileNotes, address = 0x0004be70
(lldb) br s -a 0x4FEB8
Breakpoint 2: where = MobileNotes`___lldb_unnamed_function458$$MobileNotes, address = 0x0004feb8

你得到的结果肯定跟笔者的一模一样——2个断点都会被触发很多次!协议方法被调用,一般是因为方法名中提到的那个事件发生了;而那件事发生的对象,一般是协议方法的参数。在当前情况下,则表明发生了didChangeContentSize和ContentDidChange事件,而content本身很可能就是参数。接着来看看这两个函数的第一个参数是什么,如下:


(lldb) br com add 1
Enter your debugger command(s).  Type 'DONE' to end.
> po $r2
> c
> DONE
(lldb) br com add 2
Enter your debugger command(s).  Type 'DONE' to end.
> po $r2
> c
> DONE
(lldb) c

可以看到,输出中有很多的NoteContentLayer,如下:


Process 24577 resuming
Command #2 'c' continued the target.
<NoteContentLayer: 0x14ecdf50; frame = (0 0; 320 568); animations = { bounds.origin=<CABasicAnimation: 0x16fee090>; bounds.size=<CABasicAnimation: 0x16fee4a0>; position=<CABasicAnimation: 0x16fee500>; }; layer = <CALayer: 0x14eca900>>
Process 24577 resuming
Command #2 'c' continued the target.
<NoteContentLayer: 0x14ecdf50; frame = (0 0; 320 568); animations = { bounds.origin=<CABasicAnimation: 0x16fee090>; bounds.size=<CABasicAnimation: 0x16fee4a0>; position=<CABasicAnimation: 0x16fee500>; }; layer = <CALayer: 0x14eca900>>
Process 24577 resuming
Command #2 'c' continued the target.
<NoteContentLayer: 0x14ecdf50; frame = (0 0; 320 568); layer = <CALayer: 0x14eca900>>
Process 24577 resuming
Command #2 'c' continued the target.

既然能拿到NoteContentLayer,多半能够从中获取NoteContent。打开NoteContentLayer.h,看看它提供了些什么方法,如下:


@interface NoteContentLayer : UIView <NoteTextViewActionDelegate, Note TextViewLayoutDelegate, UITextViewDelegate>
……
@property(retain, nonatomic) NoteTextView *textView; // @synthesize textView=_textView;
……
@end

NoteContentLayer有一个NoteTextView的属性,而在本章的开头,用Cycript打印UI层次的时候,发现一条note的文字就是显示在NoteTextView之上的。更改一下断点命令,把NoteTextView打印出来看看,如下:


(lldb) br com add 1
Enter your debugger command(s).  Type 'DONE' to end.
> po [$r2 textView]    
> c
> DONE
(lldb) br com add 2
Enter your debugger command(s).  Type 'DONE' to end.
> po [$r2 textView]
> c
> DONE

然后继续编辑这条note,发现对note文字的改动全都体现在了LLDB的输出上,如下:


Process 24577 resuming
Command #2 'c' continued the target.
<NoteTextView: 0x15aace00; baseClass = _UICompatibilityTextView; frame = (6 28; 308 209); text = 'Secre'; clipsToBounds = YES; gestureRecognizers = <NSArray: 0x14eddfc0>; layer = <CALayer: 0x14ee7da0>; contentOffset: {0, 0}; contentSize: {308, 52}>
Process 24577 resuming
Command #2 'c' continued the target.
<NoteTextView: 0x15aace00; baseClass = _UICompatibilityTextView; frame = (6 28; 308 209); text = 'Secret'; clipsToBounds = YES; gestureRecognizers = <NSArray: 0x14eddfc0>; layer = <CALayer: 0x14ee7da0>; contentOffset: {0, 0}; contentSize: {308, 52}>

最后一个步骤,就是从NoteTextView拿到text。打开NoteTextView.h,如下:


@interface NoteTextView : _UICompatibilityTextView <UIGestureRecognizerDelegate>
{
    id <NoteTextViewActionDelegate> _actionDelegate;
    id <NoteTextViewLayoutDelegate> _layoutDelegate;
        ……
}
……
@property(nonatomic) __weak id <NoteTextViewActionDelegate> actionDelegate; // @synthesize actionDelegate=_actionDelegate;
……
@property(nonatomic) __weak id <NoteTextViewLayoutDelegate> layoutDelegate; // @synthesize layoutDelegate=_layoutDelegate;
……
@end

它的实现并不长,但里面唯一含有text字样的,是2个delegate,显然不会返回NSString对象。text不在它自己实现里,就一定在它的父类里,打开_UICompatibilityTextView.h,如下:


@interface _UICompatibilityTextView : UIScrollView <UITextLinkInteraction, UITextInput>
……
@property(nonatomic) int textAlignment;
@property(copy, nonatomic) NSString *text;- (BOOL)hasText;
@property(retain, nonatomic) UIColor *textColor;
@property(retain, nonatomic) UIFont *font;
@property(copy, nonatomic) NSAttributedString *attributedText;
……

原来text在这里。用LLDB做最后的确认,如下:


(lldb) br com add 1
Enter your debugger command(s).  Type 'DONE' to end.
> po [[$r2 textView] text]
> c
> DONE
(lldb) br com add 2
Enter your debugger command(s).  Type 'DONE' to end.
> po [[$r2 textView] text]
> c
> DONE
Secret 
Process 24577 resuming
Command #2 'c' continued the target.
Secret i
Process 24577 resuming
Command #2 'c' continued the target.

至此,我们成功找到了2个实时监测note内容变化的方法(随便选一个就好了,我们选第二个),并且可以拿到note的实时文本数据,早前设计的3个功能现在已经全部实现。不难吧?