├── .gitignore
├── BSBacktraceLogger
└── BSBacktraceLogger
│ ├── BSBacktraceLogger.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── BSBacktraceLogger
│ ├── BSBacktraceLogger.h
│ ├── BSBacktraceLogger.m
│ └── Info.plist
├── Example
├── Example.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcuserdata
│ │ └── jiaxin.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
├── Example.xcworkspace
│ └── contents.xcworkspacedata
└── Example
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── BaseNavigationViewController.swift
│ ├── CrashTestViewController.swift
│ ├── Info.plist
│ ├── MySoldiers
│ └── ServerEnvironmentSoldier.swift
│ ├── NetworkTestViewController.swift
│ ├── PerformanceTestViewController.swift
│ ├── SanboxTestViewController.swift
│ ├── UIWebViewViewController.swift
│ ├── ViewController.swift
│ └── lufei.jpg
├── JXCaptain.podspec
├── JXCaptain.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── xcuserdata
│ └── jiaxin.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── JXCaptain
├── Info.plist
└── JXCaptain.h
├── LICENSE
├── README.md
├── Resources
└── Resource.bundle
│ ├── JXCaptain_icon_anr.png
│ ├── JXCaptain_icon_app_info.png
│ ├── JXCaptain_icon_app_user_defaults.png
│ ├── JXCaptain_icon_cpu.png
│ ├── JXCaptain_icon_crash.png
│ ├── JXCaptain_icon_fps.png
│ ├── JXCaptain_icon_h5.png
│ ├── JXCaptain_icon_memory.png
│ ├── JXCaptain_icon_network.png
│ ├── JXCaptain_icon_sanbox.png
│ └── JXCaptain_icon_shield.png
└── Sources
├── Captain.swift
├── CaptainDefines.swift
├── Manager
├── File
│ ├── ANRFileManager.swift
│ ├── CrashFileManager.swift
│ └── SoldierFileManager.swift
├── Image
│ └── ImageManager.swift
└── NetworkManager
│ └── NetworkManager.swift
├── Soldiers
├── ANR
│ ├── ANRMonitor.swift
│ ├── ANRPingThread.swift
│ └── ANRSoldier.swift
├── AppInfo
│ ├── AppInfoManager.swift
│ └── AppInfoSoldier.swift
├── CPU
│ ├── CPUMonitor.swift
│ └── CPUSoldier.swift
├── Crash
│ ├── CrashSignalExceptionHandler.swift
│ ├── CrashSoldier.swift
│ └── CrashUncaughtExceptionHandler.swift
├── FPS
│ ├── FPSMonitor.swift
│ └── FPSSoldier.swift
├── Memory
│ ├── MemoryMonitor.swift
│ └── MemorySoldier.swift
├── NetworkObserver
│ ├── JXCaptainURLProtocol.swift
│ └── NetworkObserverSoldier.swift
├── SanboxBrowser
│ └── SanboxBrowserSoldier.swift
├── UserDefaults
│ └── UserDefaultsSoldier.swift
└── WebsiteEntry
│ └── WebsiteEntrySoldier.swift
├── UI
├── ANR
│ ├── ANRDashboardViewController.swift
│ └── ANRListViewController.swift
├── AppInfo
│ └── AppInfoViewController.swift
├── CPU
│ └── CPUDashboardViewController.swift
├── Common
│ ├── DashboardCell.swift
│ ├── FileListViewController.swift
│ ├── MonitorConsoleLabel.swift
│ └── MonitorListWindow.swift
├── Crash
│ ├── CrashDashboardViewController.swift
│ └── CrashListViewController.swift
├── FPS
│ └── FPSDashboardViewController.swift
├── Main
│ ├── BaseTableViewController.swift
│ ├── BaseViewController.swift
│ ├── CaptainFloatingViewController.swift
│ ├── CaptainFloatingWindow.swift
│ └── SoldierListViewController.swift
├── Memory
│ └── MemoryDashboardViewController.swift
├── Model
│ ├── NetworkFlowDetailCellModel.swift
│ ├── NetworkFlowDetailSectionModel.swift
│ ├── NetworkFlowModel.swift
│ ├── SanboxModel.swift
│ ├── SoldierListSectionModel.swift
│ └── UserDefaultsKeyValueModel.swift
├── NetworkObserver
│ ├── NetworkFlowDetailViewController.swift
│ ├── NetworkFlowListViewController.swift
│ ├── NetworkFlowResponseDataDetailViewController.swift
│ └── NetworkObserverDashboardViewController.swift
├── SanboxBrowser
│ ├── ExcelView.swift
│ ├── JXDatabaseConnector.swift
│ ├── JXFileBrowserController.swift
│ ├── JXFilePreviewViewController.swift
│ ├── JXTableContentViewController.swift
│ ├── JXTableListViewController.swift
│ └── JXTextPreviewViewController.swift
├── UserDefaults
│ ├── UserDefaultsKeyValueDetailViewController.swift
│ └── UserDefaultsKeyValuesListViewController.swift
└── WebsiteEntry
│ ├── WebDetailViewController.swift
│ └── WebsiteEntryViewController.swift
└── UserDefaults+Access.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | # Package.pins
40 | # Package.resolved
41 | .build/
42 |
43 | # CocoaPods
44 | #
45 | # We recommend against adding the Pods directory to your .gitignore. However
46 | # you should judge for yourself, the pros and cons are mentioned at:
47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
48 | #
49 | # Pods/
50 |
51 | # Carthage
52 | #
53 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
54 | # Carthage/Checkouts
55 |
56 | Carthage/Build
57 |
58 | # fastlane
59 | #
60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
61 | # screenshots whenever they are needed.
62 | # For more information about the recommended setup visit:
63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
64 |
65 | fastlane/report.xml
66 | fastlane/Preview.html
67 | fastlane/screenshots/**/*.png
68 | fastlane/test_output
69 |
--------------------------------------------------------------------------------
/BSBacktraceLogger/BSBacktraceLogger/BSBacktraceLogger.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/BSBacktraceLogger/BSBacktraceLogger/BSBacktraceLogger/BSBacktraceLogger.h:
--------------------------------------------------------------------------------
1 | //
2 | // BSBacktraceLogger.h
3 | // BSBacktraceLogger
4 | //
5 | // Created by 张星宇 on 16/8/27.
6 | // Copyright © 2016年 bestswifter. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | #define BSLOG NSLog(@"%@",[BSBacktraceLogger bs_backtraceOfCurrentThread]);
12 | #define BSLOG_MAIN NSLog(@"%@",[BSBacktraceLogger bs_backtraceOfMainThread]);
13 | #define BSLOG_ALL NSLog(@"%@",[BSBacktraceLogger bs_backtraceOfAllThread]);
14 |
15 | @interface BSBacktraceLogger : NSObject
16 |
17 | + (NSString *)bs_backtraceOfAllThread;
18 | + (NSString *)bs_backtraceOfCurrentThread;
19 | + (NSString *)bs_backtraceOfMainThread;
20 | + (NSString *)bs_backtraceOfNSThread:(NSThread *)thread;
21 |
22 | @end
23 |
--------------------------------------------------------------------------------
/BSBacktraceLogger/BSBacktraceLogger/BSBacktraceLogger/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/xcuserdata/jiaxin.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Example.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 1
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Example/Example.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Example/Example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/8/20.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 | var window: UIWindow?
14 |
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | // Override point for customization after application launch.
18 |
19 | return true
20 | }
21 |
22 | func applicationWillResignActive(_ application: UIApplication) {
23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
25 | }
26 |
27 | func applicationDidEnterBackground(_ application: UIApplication) {
28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
30 | }
31 |
32 | func applicationWillEnterForeground(_ application: UIApplication) {
33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
34 | }
35 |
36 | func applicationDidBecomeActive(_ application: UIApplication) {
37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
38 | }
39 |
40 | func applicationWillTerminate(_ application: UIApplication) {
41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
42 | }
43 |
44 |
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/Example/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Example/Example/BaseNavigationViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseNavigationViewController.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/9/3.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class BaseNavigationViewController: UINavigationController {
12 |
13 | override var childForStatusBarStyle: UIViewController? {
14 | return topViewController
15 | }
16 |
17 | override var childForStatusBarHidden: UIViewController? {
18 | return topViewController
19 | }
20 |
21 | override func viewDidLoad() {
22 | super.viewDidLoad()
23 |
24 | // Do any additional setup after loading the view.
25 | }
26 |
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Example/Example/CrashTestViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CrashTestViewController.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/8/23.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class CrashTestViewController: UITableViewController {
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 |
16 | title = "Crash测试"
17 | tableView.tableFooterView = UIView()
18 | }
19 |
20 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
21 | if indexPath.row == 0 {
22 | let ocArray = NSArray()
23 | print(ocArray[1])
24 | }else if indexPath.row == 1 {
25 | let someVaule: Int? = nil
26 | print(someVaule!)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Example/Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | View controller-based stats bar appearance
38 |
39 | NSAppTransportSecurity
40 |
41 | NSAllowsArbitraryLoads
42 |
43 |
44 | UISupportedInterfaceOrientations~ipad
45 |
46 | UIInterfaceOrientationPortrait
47 | UIInterfaceOrientationPortraitUpsideDown
48 | UIInterfaceOrientationLandscapeLeft
49 | UIInterfaceOrientationLandscapeRight
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/Example/Example/MySoldiers/ServerEnvironmentSoldier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServerEnvironmentSoldier.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/8/23.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import JXCaptain
11 |
12 | class ServerEnvironmentSoldier: Soldier {
13 | public var name: String
14 | public var team: String
15 | public var icon: UIImage?
16 | public var contentView: UIView?
17 | public var hasNewEvent: Bool = false
18 |
19 | public init() {
20 | name = "线上环境"
21 | team = "业务工具"
22 | let myContentView = ServerEnvironmentContentView()
23 | myContentView.toggle.isOn = true
24 | myContentView.nameLabel.text = name
25 | contentView = myContentView
26 | }
27 |
28 | public func prepare() {
29 | }
30 |
31 | public func action(naviController: UINavigationController) {
32 | }
33 | }
34 |
35 | class ServerEnvironmentContentView: UIView {
36 | let toggle: UISwitch
37 | let nameLabel: UILabel
38 |
39 | override init(frame: CGRect) {
40 | toggle = UISwitch()
41 | nameLabel = UILabel()
42 | super.init(frame: frame)
43 |
44 | toggle.isOn = UserDefaults.standard.bool(forKey: "kServerEnvironment")
45 | toggle.translatesAutoresizingMaskIntoConstraints = false
46 | toggle.addTarget(self, action: #selector(toggleDidClick), for: .valueChanged)
47 | addSubview(toggle)
48 | toggle.topAnchor.constraint(equalTo: self.topAnchor, constant: 5).isActive = true
49 | toggle.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
50 |
51 | nameLabel.textColor = .gray
52 | nameLabel.font = .systemFont(ofSize: 12)
53 | nameLabel.textAlignment = .center
54 | nameLabel.translatesAutoresizingMaskIntoConstraints = false
55 | addSubview(nameLabel)
56 | nameLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
57 | nameLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
58 | nameLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
59 | }
60 |
61 | required init?(coder aDecoder: NSCoder) {
62 | fatalError("init(coder:) has not been implemented")
63 | }
64 |
65 | @objc func toggleDidClick() {
66 | UserDefaults.standard.set(toggle.isOn, forKey: "kServerEnvironment")
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Example/Example/NetworkTestViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkTestViewController.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/8/29.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class NetworkTestViewController: UITableViewController {
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 |
16 | title = "网络测试"
17 | tableView.tableFooterView = UIView()
18 | }
19 |
20 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
21 | if indexPath.row == 0 {
22 | let url = URL(string: "https://www.taobao.com/")!
23 | let request = URLRequest(url: url)
24 | NSURLConnection.sendAsynchronousRequest(request, queue: OperationQueue()) { (response, data, error) in
25 | if response != nil {
26 | print(response!)
27 | }
28 | }
29 | }else if indexPath.row == 1 {
30 | let url = URL(string: "https://www.jd.com/")!
31 | let request = URLRequest(url: url)
32 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
33 | if response != nil {
34 | print(response!)
35 | }
36 | }
37 | task.resume()
38 | }else if indexPath.row == 2 {
39 | let url = URL(string: "https://cn.bing.com/th?id=OIP.MehRXMQpJaV17DMm_MySxgHaFR&pid=Api&rs=1")!
40 | let request = URLRequest(url: url)
41 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
42 | if response != nil {
43 | print(response!)
44 | }
45 | }
46 | task.resume()
47 | }else if indexPath.row == 3 {
48 | let url = URL(string: "http://pic38.nipic.com/20140217/14150008_155520585000_2.gif")!
49 | let request = URLRequest(url: url)
50 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
51 | if response != nil {
52 | print(response!)
53 | }
54 | }
55 | task.resume()
56 | }else if indexPath.row == 4 {
57 | let url = URL(string: "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")!
58 | let request = URLRequest(url: url)
59 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
60 | if response != nil {
61 | print(response!)
62 | }
63 | }
64 | task.resume()
65 | }else if indexPath.row == 5 {
66 | let url = URL(string: "https://gitee.com/xiansanyee/codes/saix3etdh0bywm4pr9gzl38/raw?blob_name=News.mp3")!
67 | let request = URLRequest(url: url)
68 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
69 | if response != nil {
70 | print(response!)
71 | }
72 | }
73 | task.resume()
74 | }else if indexPath.row == 6 {
75 | let vc = UIWebViewViewController()
76 | navigationController?.pushViewController(vc, animated: true)
77 | }
78 |
79 | if indexPath.row != 6 {
80 | let alert = UIAlertController(title: nil, message: "发送成功", preferredStyle: .alert)
81 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil))
82 | present(alert, animated: true, completion: nil)
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Example/Example/PerformanceTestViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PerformanceTestViewController.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/8/28.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class PerformanceTestViewController: UITableViewController {
12 | var texts = [NSAttributedString]()
13 |
14 | override func viewDidLoad() {
15 | super.viewDidLoad()
16 |
17 | title = "性能测试"
18 | tableView.tableFooterView = UIView()
19 | }
20 |
21 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
22 | if indexPath.row == 0 {
23 | //卡顿
24 | Thread.sleep(forTimeInterval: 2)
25 | }else if indexPath.row == 1 {
26 | //低fps
27 | Thread.sleep(forTimeInterval: 0.5)
28 | }else if indexPath.row == 2 {
29 | //高cpu
30 | for number in 1..<10000000 {
31 | sqrt(pow(Double(number), Double(number)))
32 | }
33 | }else if indexPath.row == 3 {
34 | //高内存
35 | for index in 0..<100000 {
36 | texts.append(NSAttributedString(string: "\(index)"))
37 | }
38 | }else if indexPath.row == 4 {
39 | //高流量
40 | for _ in 0..<10 {
41 | let url = URL(string: "https://www.taobao.com/")!
42 | let request = URLRequest(url: url)
43 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
44 | if response != nil {
45 | print(response!)
46 | }
47 | }
48 | task.resume()
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Example/Example/SanboxTestViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SanboxTestViewController.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/8/23.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SanboxTestViewController: UITableViewController {
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 |
16 | title = "沙盒测试"
17 | tableView.tableFooterView = UIView()
18 | }
19 |
20 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
21 | guard let documentURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else {
22 | return
23 | }
24 | var toastText = "添加成功"
25 | if indexPath.row == 0 {
26 | let formatter = DateFormatter()
27 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
28 | formatter.timeZone = TimeZone.current
29 | let dateString = formatter.string(from: Date())
30 | let fileURL = documentURL.appendingPathComponent("\(dateString).txt")
31 | _ = try? dateString.write(to: fileURL, atomically: true, encoding: .utf8)
32 | do {
33 | try dateString.write(to: fileURL, atomically: true, encoding: .utf8)
34 | }catch (let error) {
35 | toastText = error.localizedDescription
36 | }
37 | }else if indexPath.row == 1 {
38 | let image = UIImage(named: "lufei.jpg")
39 | let imageURL = documentURL.appendingPathComponent("lufei.jpg")
40 | let result = FileManager.default.createFile(atPath: imageURL.path, contents: image?.jpegData(compressionQuality: 1), attributes: nil)
41 | if !result {
42 | toastText = "添加失败"
43 | }
44 | }else if indexPath.row == 2 {
45 | let image = UIImage(named: "icon_shield.png")
46 | let imageURL = documentURL.appendingPathComponent("icon_shield.png")
47 | let result = FileManager.default.createFile(atPath: imageURL.path, contents: image?.jpegData(compressionQuality: 1), attributes: nil)
48 | if !result {
49 | toastText = "添加失败"
50 | }
51 | }else if indexPath.row == 3 {
52 | let url = URL(string: "http://pic38.nipic.com/20140217/14150008_155520585000_2.gif")!
53 | let request = URLRequest(url: url)
54 | let semaphore = DispatchSemaphore(value: 0)
55 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
56 | if data != nil {
57 | if let documentURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) {
58 | let imageURL = documentURL.appendingPathComponent("lufei.gif")
59 | let result = FileManager.default.createFile(atPath: imageURL.path, contents: data, attributes: nil)
60 | if !result {
61 | toastText = "添加失败"
62 | }
63 | }
64 | }
65 | semaphore.signal()
66 | }
67 | task.resume()
68 | let waitResult = semaphore.wait(timeout: .distantFuture)
69 | print(waitResult)
70 | }else if indexPath.row == 4 {
71 | toastText = "正在下载"
72 | let url = URL(string: "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")!
73 | let request = URLRequest(url: url)
74 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
75 | if data != nil {
76 | if let documentURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) {
77 | let imageURL = documentURL.appendingPathComponent("temp.mp4")
78 | let result = FileManager.default.createFile(atPath: imageURL.path, contents: data, attributes: nil)
79 | DispatchQueue.main.async {
80 | if !result {
81 | self.showText("添加视频失败")
82 | }else {
83 | self.showText("添加视频成功")
84 | }
85 | }
86 | }
87 | }else {
88 | DispatchQueue.main.async {
89 | self.showText("添加视频失败")
90 | }
91 | }
92 | }
93 | task.resume()
94 | }else if indexPath.row == 5 {
95 | toastText = "正在下载"
96 | let url = URL(string: "https://gitee.com/xiansanyee/codes/saix3etdh0bywm4pr9gzl38/raw?blob_name=News.mp3")!
97 | let request = URLRequest(url: url)
98 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
99 | if data != nil {
100 | if let documentURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) {
101 | let imageURL = documentURL.appendingPathComponent("temp.mp3")
102 | let result = FileManager.default.createFile(atPath: imageURL.path, contents: data, attributes: nil)
103 | DispatchQueue.main.async {
104 | if !result {
105 | self.showText("添加音频失败")
106 | }else {
107 | self.showText("添加音频成功")
108 | }
109 | }
110 | }
111 | }else {
112 | DispatchQueue.main.async {
113 | self.showText("添加音频失败")
114 | }
115 | }
116 | }
117 | task.resume()
118 | }
119 | showText(toastText)
120 | }
121 |
122 | func showText(_ text: String) {
123 | if presentedViewController != nil {
124 | dismiss(animated: false) {
125 | let alert = UIAlertController(title: nil, message: text, preferredStyle: .alert)
126 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil))
127 | self.present(alert, animated: true, completion: nil)
128 | }
129 | }else {
130 | let alert = UIAlertController(title: nil, message: text, preferredStyle: .alert)
131 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil))
132 | self.present(alert, animated: true, completion: nil)
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/Example/Example/UIWebViewViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIWebViewViewController.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/9/6.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class UIWebViewViewController: UIViewController {
12 | var webView: UIWebView!
13 |
14 | override func viewDidLoad() {
15 | super.viewDidLoad()
16 |
17 | webView = UIWebView(frame: CGRect.zero)
18 | view.addSubview(webView)
19 |
20 | let url = URL(string: "https://www.baidu.com/")!
21 | let request = URLRequest(url: url)
22 | webView.loadRequest(request)
23 | }
24 |
25 | override func viewDidLayoutSubviews() {
26 | super.viewDidLayoutSubviews()
27 |
28 | webView.frame = view.bounds
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/Example/Example/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/8/20.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import JXCaptain
11 |
12 | class ViewController: UITableViewController {
13 |
14 | override func viewDidLoad() {
15 | super.viewDidLoad()
16 |
17 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "呼叫队长", style: .plain, target: self, action: #selector(naviRightItemDidClick))
18 | tableView.tableFooterView = UIView()
19 |
20 | //在prepare之前添加自定义Soldier
21 | Captain.default.enqueueSoldiers([ServerEnvironmentSoldier()])
22 | //在prepare之前赋值自定义Soldier闭包
23 | Captain.default.configSoldierClosure = { (soldier) in
24 | /*
25 | if let websiteEntry = soldier as? WebsiteEntrySoldier {
26 | //设置H5任意门默认网址
27 | websiteEntry.defaultWebsite = "https://www.baidu.com"
28 | //设置H5任意门自定义落地页面,比如项目有自定义WKWebView、有JS交互逻辑等
29 | websiteEntry.webDetailControllerClosure = { (website) in
30 | return UIViewController()
31 | }
32 | }
33 | */
34 | /*
35 | if let anr = soldier as? ANRSoldier {
36 | //设置卡顿时间阈值,单位秒
37 | anr.threshold = 2
38 | }
39 | */
40 | /*
41 | if let crash = soldier as? CrashSoldier {
42 | //自定义处理crash信息,可以存储到本地,下次打开app再传送到服务器记录
43 | crash.exceptionReceiveClosure = { (signal, exception, info) in
44 | if signal != nil {
45 | print("signal crash info:\(info ?? "")")
46 | }else if exception != nil {
47 | print("exception crash info:\(info ?? "")")
48 | }
49 | }
50 | }
51 | */
52 | }
53 |
54 | //最后再调用prepare
55 | Captain.default.prepare()
56 | }
57 |
58 | @objc func naviRightItemDidClick() {
59 | Captain.default.show()
60 | }
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/Example/Example/lufei.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Example/Example/lufei.jpg
--------------------------------------------------------------------------------
/JXCaptain.podspec:
--------------------------------------------------------------------------------
1 |
2 | Pod::Spec.new do |s|
3 | s.name = "JXCaptain"
4 | s.version = "0.0.13"
5 | s.summary = "一个应用调试工具箱"
6 | s.homepage = "https://github.com/pujiaxin33/JXCaptain"
7 | s.license = "MIT"
8 | s.author = { "pujiaxin33" => "317437084@qq.com" }
9 | s.platform = :ios, "9.0"
10 | s.swift_version = "5.0"
11 | s.source = { :git => "https://github.com/pujiaxin33/JXCaptain.git", :tag => "#{s.version}" }
12 | s.framework = "UIKit"
13 | s.source_files = "Sources", "Sources/**/*.{swift}"
14 | s.resource = 'Resources/Resource.bundle'
15 | s.requires_arc = true
16 |
17 | s.dependency 'BSBacktraceLogger'
18 | end
19 |
--------------------------------------------------------------------------------
/JXCaptain.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/JXCaptain.xcodeproj/xcuserdata/jiaxin.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | JXCaptain.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 2
11 |
12 | ResourceBundle.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 2
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/JXCaptain/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/JXCaptain/JXCaptain.h:
--------------------------------------------------------------------------------
1 | //
2 | // JXCaptain.h
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/20.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for JXCaptain.
12 | FOUNDATION_EXPORT double JXCaptainVersionNumber;
13 |
14 | //! Project version string for JXCaptain.
15 | FOUNDATION_EXPORT const unsigned char JXCaptainVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 暴走的鑫鑫
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | # JXCaptain
3 |
4 | 像美国队长一样威猛的应用调试工具箱!
5 |
6 | # 前言
7 |
8 | 每个稍有规模的APP都会有一些开发测试功能,比如切换服务器环境、浏览沙盒文件、查看页面帧率以及接口请求日志等。之前项目内部前前后后实现了一些功能,直到看到了滴滴开源的[DoraemonKit](https://github.com/didi/DoraemonKit)。被它完整的功能实现所折服,引入之后真的就拥有了强大的工具集合。在是否要引入`DoraemonKit`之际,我陷入了深深的思考。第一,项目几乎是纯Swift语言开发,并且尽量不再去引用OC的库。第二,内部已经实现了许多功能,稍加改造也能转正为人名服务。所以,就借鉴了`DoraemonKit`的思路和部分代码,重新实现了`JXCaptain`,一个强大的Swift编写的应用调试工具箱。
9 |
10 | 取名`Captain`队长的含义,就是把一个个功能比喻为战士,队长就是指挥这些战士的精神领袖。有了队长就能所向披靡,战无不胜!
11 |
12 | # 预览
13 |
14 | 
15 |
16 | # 功能模块
17 |
18 | ## 常用工具
19 |
20 | - **【App信息】** 快速查看手机信息,App 信息,权限信息的渠道,避免去手机设置查找或者查看项目源代码的麻烦;
21 | - **【沙盒浏览】** 沙盒目录浏览,支持图片、视频、sql数据库等文件预览,支持文件的共享;
22 | - **【Crash日志】** 捕获Crash,并将日志通过文件形式保存在沙盒里面;
23 | - **【H5任意门】** 开始输入H5地址进行网页功能验证;
24 | - **【UserDefauls】** 查看`UserDefauls.shared`记录的键值对数据;
25 |
26 | ## 性能检测
27 |
28 | - **【FPS】** 顶部状态栏实时显示当前的帧率;
29 | - **【内存】** 顶部状态栏实时显示当前的内存占用;
30 | - **【CPU】** 顶部状态栏实时显示当前的CPU使用率;
31 | - **【卡顿】** 当页面出现卡顿是,保存当时的堆栈信息,使用`BSBacktraceLogger`获取堆栈信息;
32 | - **【流量】** 监听APP和网页的接口调用,查看Header、Response等信息;
33 |
34 |
35 | # 自定义工具
36 |
37 | ## 实现`Soldier`协议
38 |
39 | ```Swift
40 | public class UserDefaultsSoldier: Soldier {
41 | public static let shared = UserDefaultsSoldier()
42 | //工具列表显示的名字
43 | public var name: String
44 | //归属于哪个团队?比如:常用工具、性能检测或者自定义团队名
45 | public var team: String
46 | //工具列表显示的icon
47 | public var icon: UIImage?
48 | //如果你不适用默认的name加icon的显示,直接创建新视图赋值给contentView即可
49 | public var contentView: UIView?
50 | //是否监听到了新的事件,比如Crash日志捕获到了新的Crash,将hasNewEvent设置为true,就会显示红点提示
51 | public var hasNewEvent: Bool = false
52 |
53 | public init() {
54 | name = "UserDefaults"
55 | team = "常用工具"
56 | icon = ImageManager.imageWithName("JXCaptain_icon_app_user_defaults")
57 | }
58 | //APP启动之后会调用该方法,用于启动当前工具的工作。比如帧率检测,调用prepare之后就开始检测帧率并显示到顶部状态了。
59 | public func prepare() { }
60 | //用点击了工具列表的Icon之后,就行页面的调整。一般工具都会有一个进行配置的Dashboard页面
61 | public func action(naviController: UINavigationController) {
62 | naviController.pushViewController(UserDefaultsKeyValuesListViewController(defaults: UserDefaults.standard), animated: true)
63 | }
64 | }
65 | ```
66 |
67 | ## 添加新的`Soldier`
68 |
69 | ```Swift
70 | Captain.default.enqueueSoldiers([ServerEnvironmentSoldier()])
71 | ```
72 |
73 | ## 配置已有的`Soldier`
74 |
75 | ```Swift
76 | Captain.default.configSoldierClosure = { (soldier) in
77 | if let websiteEntry = soldier as? WebsiteEntrySoldier {
78 | //设置H5任意门默认网址
79 | websiteEntry.defaultWebsite = "https://www.baidu.com"
80 | //设置H5任意门自定义落地页面,比如项目有自定义WKWebView、有JS交互逻辑等
81 | websiteEntry.webDetailControllerClosure = { (website) in
82 | return UIViewController()
83 | }
84 | }
85 | if let anr = soldier as? ANRSoldier {
86 | //设置卡顿时间阈值,单位秒
87 | anr.threshold = 2
88 | }
89 | if let crash = soldier as? CrashSoldier {
90 | //自定义处理crash信息,可以存储到本地,下次打开app再传送到服务器记录
91 | crash.exceptionReceiveClosure = { (signal, exception, info) in
92 | if signal != nil {
93 | print("signal crash info:\(info ?? "")")
94 | }else if exception != nil {
95 | print("exception crash info:\(info ?? "")")
96 | }
97 | }
98 | }
99 | }
100 | ```
101 |
102 | ## 启动工具
103 |
104 | ```Swift
105 | Captain.default.prepare()
106 | ```
107 |
108 | # `Captain`的显示与隐藏
109 |
110 | 可以监听摇一摇、或者在Window上面添加两指连击4次,就显示`Captain`。调用如下代码即可:
111 | ```Swift
112 | Captain.default.show()
113 | ```
114 |
115 | 隐藏`Captain`调用如下代码:
116 | ```Swift
117 | Captain.default.hide()
118 | ```
119 |
120 |
121 |
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/Resources/Resource.bundle/JXCaptain_icon_anr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_anr.png
--------------------------------------------------------------------------------
/Resources/Resource.bundle/JXCaptain_icon_app_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_app_info.png
--------------------------------------------------------------------------------
/Resources/Resource.bundle/JXCaptain_icon_app_user_defaults.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_app_user_defaults.png
--------------------------------------------------------------------------------
/Resources/Resource.bundle/JXCaptain_icon_cpu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_cpu.png
--------------------------------------------------------------------------------
/Resources/Resource.bundle/JXCaptain_icon_crash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_crash.png
--------------------------------------------------------------------------------
/Resources/Resource.bundle/JXCaptain_icon_fps.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_fps.png
--------------------------------------------------------------------------------
/Resources/Resource.bundle/JXCaptain_icon_h5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_h5.png
--------------------------------------------------------------------------------
/Resources/Resource.bundle/JXCaptain_icon_memory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_memory.png
--------------------------------------------------------------------------------
/Resources/Resource.bundle/JXCaptain_icon_network.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_network.png
--------------------------------------------------------------------------------
/Resources/Resource.bundle/JXCaptain_icon_sanbox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_sanbox.png
--------------------------------------------------------------------------------
/Resources/Resource.bundle/JXCaptain_icon_shield.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_shield.png
--------------------------------------------------------------------------------
/Sources/Captain.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Captain.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/20.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class Captain {
12 | public static let `default` = Captain()
13 | public var configSoldierClosure: ((Soldier)->())?
14 | public var screenEdgeInsets: UIEdgeInsets
15 | public var logoImage: UIImage
16 | internal var soldiers = [Soldier]()
17 | internal let floatingWindow = CaptainFloatingWindow(frame: CGRect.zero)
18 |
19 | init() {
20 | let defaultSoldiers: [Soldier] = [AppInfoSoldier(), SanboxBrowserSoldier(), CrashSoldier(), WebsiteEntrySoldier(), FPSSoldier(), MemorySoldier(), CPUSoldier(), ANRSoldier(), NetworkObserverSoldier.shared, UserDefaultsSoldier()]
21 | soldiers.append(contentsOf: defaultSoldiers)
22 | var topEdgeInset: CGFloat = 20
23 | var bottomEdgeInset: CGFloat = 12
24 | if #available(iOS 11.0, *) {
25 | let safeAreaInsets = floatingWindow.safeAreaInsets
26 | if safeAreaInsets.top > 0 {
27 | topEdgeInset = safeAreaInsets.top
28 | }
29 | if safeAreaInsets.bottom > 0 {
30 | bottomEdgeInset = safeAreaInsets.bottom
31 | }
32 | }
33 | screenEdgeInsets = UIEdgeInsets(top: topEdgeInset, left: 12, bottom: bottomEdgeInset, right: 12)
34 | logoImage = ImageManager.imageWithName("JXCaptain_icon_shield")!
35 | }
36 |
37 | public func show() {
38 | floatingWindow.isHidden = false
39 | }
40 |
41 | public func hide() {
42 | floatingWindow.rootViewController?.dismiss(animated: false, completion: nil)
43 | floatingWindow.isHidden = true
44 | }
45 |
46 | public func prepare() {
47 | soldiers.forEach { configSoldierClosure?($0) }
48 | soldiers.forEach { $0.prepare() }
49 | }
50 |
51 | public func enqueueSoldiers(_ soldiers: [Soldier]) {
52 | self.soldiers.append(contentsOf: soldiers)
53 | }
54 |
55 | public func removeAllSoldiers() {
56 | soldiers.removeAll()
57 | }
58 |
59 | public func dequeueSoldiers(soldierTypes: [Soldier.Type]) {
60 | for (index, soldier) in soldiers.enumerated() {
61 | let currentSoldierType = type(of: soldier)
62 | let isExisted = soldierTypes.contains { (type) -> Bool in
63 | if type == currentSoldierType {
64 | return true
65 | }else {
66 | return false
67 | }
68 | }
69 | if isExisted {
70 | soldiers.remove(at: index)
71 | break
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/CaptainDefines.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptainDefines.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/26.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Soldier {
12 | var name: String { get }
13 | var team: String { get }
14 | var icon: UIImage? { get }
15 | var contentView: UIView? { get }
16 | var hasNewEvent: Bool { get }
17 | func prepare()
18 | func willAppear()
19 | func action(naviController: UINavigationController)
20 | }
21 |
22 | //hasNewEvent、contentView添加默认实现,有需要的Soldier才添加。
23 | public extension Soldier {
24 | var hasNewEvent: Bool { return false }
25 | var contentView: UIView? { return nil }
26 | func willAppear() { }
27 | }
28 |
29 | protocol Monitor {
30 | associatedtype ValueType
31 | var valueDidUpdateClosure: ((ValueType) -> Void)? { set get }
32 | func start()
33 | func end()
34 | }
35 |
36 | enum MonitorType {
37 | case fps
38 | case memory
39 | case cpu
40 | case anr
41 | }
42 |
43 | public extension Notification.Name {
44 | static let JXCaptainSoldierNewEventDidChange = Notification.Name("JXCaptainSoldierNewEventDidChange")
45 | static let JXCaptainNetworkObserverSoldierNewFlowDidReceive = Notification.Name("JXCaptainNetworkObserverSoldierNewFlowDidReceive")
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/Manager/File/ANRFileManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ANRFileManager.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/28.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class ANRFileManager: SoldierFileManager {
12 | public static func directoryURL() -> URL? {
13 | let fileManager = FileManager.default
14 | guard let cacheURL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first else {
15 | return nil
16 | }
17 | let tempDirectoryURL = cacheURL.appendingPathComponent("com.JXCaptain.anr", isDirectory: true)
18 | if !fileManager.fileExists(atPath: tempDirectoryURL.path) {
19 | do {
20 | try fileManager.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil)
21 | }catch (let error){
22 | print("ANRFileManager create ANR directory error:\(error.localizedDescription)")
23 | return nil
24 | }
25 | }
26 | return tempDirectoryURL
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Manager/File/CrashFileManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CrashFileManager.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/21.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class CrashFileManager: SoldierFileManager {
12 | public static func directoryURL() -> URL? {
13 | let fileManager = FileManager.default
14 | guard let cacheURL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first else {
15 | return nil
16 | }
17 | let directoryURL = cacheURL.appendingPathComponent("com.JXCaptain.crash", isDirectory: true)
18 | if !fileManager.fileExists(atPath: directoryURL.path) {
19 | do {
20 | try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
21 | }catch (let error){
22 | print("CrashFileManager create crash directory error:\(error.localizedDescription)")
23 | return nil
24 | }
25 | }
26 | return directoryURL
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Manager/File/SoldierFileManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SoldierFileManager.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/29.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol SoldierFileManager {
12 | static func saveInfo(_ info: String, fileName: String?, fileNamePrefix: String?)
13 | static func directoryURL() -> URL?
14 | static func allFiles() -> [SanboxModel]
15 | static func deleteAllFiles()
16 | }
17 |
18 | public extension SoldierFileManager {
19 | static func saveInfo(_ info: String, fileName: String? = nil, fileNamePrefix: String? = nil) {
20 | guard !info.isEmpty else {
21 | return
22 | }
23 | var targetFileName: String?
24 | if fileName != nil {
25 | targetFileName = fileName
26 | }else {
27 | let dateFormatter = DateFormatter()
28 | dateFormatter.timeZone = TimeZone.current
29 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
30 | let dateString = dateFormatter.string(from: Date())
31 | if fileNamePrefix != nil {
32 | targetFileName = "\(fileNamePrefix!)-\(dateString)"
33 | }else {
34 | targetFileName = dateString
35 | }
36 | }
37 |
38 | guard let fileURL = directoryURL()?.appendingPathComponent("\(targetFileName!).txt") else {
39 | return
40 | }
41 | do {
42 | try info.write(to: fileURL, atomically: true, encoding: .utf8)
43 | } catch(let error) {
44 | print("write info error:\(error.localizedDescription)")
45 | }
46 | }
47 |
48 | static func directoryURL() -> URL? {
49 | return nil
50 | }
51 |
52 | static func allFiles() -> [SanboxModel] {
53 | guard let targetDirectory = directoryURL() else {
54 | return [SanboxModel]()
55 | }
56 | let fileManager = FileManager.default
57 | guard let fileURLs = try? fileManager.contentsOfDirectory(at: targetDirectory, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) else {
58 | return [SanboxModel]()
59 | }
60 | let sortedFileURLs = fileURLs.sorted { (first, second) -> Bool in
61 | let firstAttributes = try? fileManager.attributesOfItem(atPath: first.path)
62 | let secondAttributes = try? fileManager.attributesOfItem(atPath: second.path)
63 | let firstDate = firstAttributes?[FileAttributeKey.creationDate] as? Date
64 | let secondDate = secondAttributes?[FileAttributeKey.creationDate] as? Date
65 | if firstDate != nil && secondDate != nil {
66 | return firstDate! > secondDate!
67 | }
68 | return false
69 | }
70 | var sanboxInfos = [SanboxModel]()
71 | for fileURL in sortedFileURLs {
72 | let model = SanboxModel(fileURL: fileURL, name: fileURL.lastPathComponent)
73 | sanboxInfos.append(model)
74 | }
75 | return sanboxInfos
76 | }
77 |
78 | static func deleteAllFiles() {
79 | guard let targetDirectory = directoryURL() else {
80 | return
81 | }
82 | let fileManager = FileManager.default
83 | do {
84 | try fileManager.removeItem(at: targetDirectory)
85 | }catch (let error) {
86 | print("deleteAllFiles error:\(error.localizedDescription)")
87 | }
88 | }
89 | }
90 |
91 |
--------------------------------------------------------------------------------
/Sources/Manager/Image/ImageManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageManager.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/22.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class ImageManager {
12 | static func imageWithName(_ name: String) -> UIImage? {
13 | let frameworkBundle = Bundle(for: Captain.self)
14 | let resourceBundleURL = frameworkBundle.url(forResource: "Resource", withExtension: "bundle")
15 | let resourceBundle = Bundle(url: resourceBundleURL!)
16 | return UIImage(named: name, in: resourceBundle, compatibleWith: nil)
17 | }
18 | }
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Sources/Manager/NetworkManager/NetworkManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkManager.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/29.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class NetworkManager {
12 | static func httpBody(request: URLRequest) -> Data? {
13 | if request.httpBody != nil {
14 | return request.httpBody
15 | }else if request.httpMethod == "POST" {
16 | let buffer = UnsafeMutablePointer.allocate(capacity: 1024)
17 | let stream = request.httpBodyStream
18 | stream?.open()
19 | var data = Data()
20 | while stream?.hasBytesAvailable == true {
21 | let len = stream?.read(buffer, maxLength: 1024) ?? 0
22 | if len > 0 && stream?.streamError == nil {
23 | data.append(buffer, count: len)
24 | }
25 | }
26 | stream?.close()
27 | buffer.deallocate()
28 | return data
29 | }
30 | return nil
31 | }
32 |
33 | static func jsonString(from data: Data) -> String? {
34 | if data.isEmpty {
35 | return nil
36 | }
37 | guard let object = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) else {
38 | return String(data: data, encoding: .utf8)
39 | }
40 | if JSONSerialization.isValidJSONObject(object) {
41 | if let jsonData = try? JSONSerialization.data(withJSONObject: object, options: .prettyPrinted) {
42 | let string = String(data: jsonData, encoding: .utf8)
43 | return string?.replacingOccurrences(of: "\\/", with: "/")
44 | }
45 | }
46 | return nil
47 | }
48 |
49 | static func flowLengthString(_ length: Int) -> String {
50 | if length < 1024 {
51 | return String(format: "%dBytes", length)
52 | }else if length < 1024 * 1024 {
53 | return String(format: "%.1fKB", Double(length)/1204)
54 | }else {
55 | return String(format: "%.1fMB", Double(length)/1204/1024)
56 | }
57 | }
58 |
59 | static func requestFlowLength(_ request: URLRequest) -> Int {
60 | var headerFields = [String : String]()
61 | if request.allHTTPHeaderFields != nil {
62 | headerFields.merge(request.allHTTPHeaderFields!) { (current, _) -> String in
63 | return current
64 | }
65 | }
66 | if let url = request.url {
67 | let cookies = HTTPCookieStorage.shared.cookies(for: url)
68 | if cookies?.isEmpty == false {
69 | let cookiesHeader = HTTPCookie.requestHeaderFields(with: cookies!)
70 | headerFields.merge(cookiesHeader) { (current, _) -> String in
71 | return current
72 | }
73 | }
74 | }
75 | let headersLength = headerFieldsLength(headerFields)
76 | let bodyLength = httpBody(request: request)?.count ?? 0
77 | return headersLength + bodyLength
78 | }
79 |
80 | static func responseFlowLength(_ response: URLResponse, responseData: Data) -> Int {
81 | return responseData.count
82 | //FIXME: https://stackoverflow.com/questions/39256489/how-to-bridge-a-long-long-c-macro-to-swift
83 | /*
84 | guard let httpResponse = response as? HTTPURLResponse else {
85 | return responseData.count
86 | }
87 | let headersLength = headerFieldsLength(httpResponse.allHeaderFields)
88 | var contentLength: Int = 0
89 | if httpResponse.expectedContentLength != NSURLResponseUnknownLength {
90 | contentLength = httpResponse.expectedContentLength
91 | }else {
92 | contentLength = responseData.count
93 | }
94 | return headersLength + contentLength
95 | */
96 | }
97 |
98 | static func responseData(requestID: String) -> Data? {
99 | return NetworkObserverSoldier.shared.cache.object(forKey: requestID as AnyObject) as? Data
100 | }
101 |
102 | static func responseImage(requestID: String) -> UIImage? {
103 | let responseData = NetworkObserverSoldier.shared.cache.object(forKey: requestID as AnyObject) as? Data
104 | if responseData != nil {
105 | return UIImage(data: responseData!, scale: UIScreen.main.scale)
106 | }
107 | return nil
108 | }
109 |
110 | static func responseImages(requestID: String) -> [UIImage]? {
111 | let responseData = NetworkObserverSoldier.shared.cache.object(forKey: requestID as AnyObject) as? Data
112 | if responseData != nil, let imageSource = CGImageSourceCreateWithData(responseData! as CFData, nil) {
113 | let imagesCount = CGImageSourceGetCount(imageSource)
114 | var images = [UIImage]()
115 | for index in 0.. String? {
126 | let responseData = NetworkObserverSoldier.shared.cache.object(forKey: requestID as AnyObject) as? Data
127 | if responseData != nil {
128 | return NetworkManager.jsonString(from: responseData!)
129 | }
130 | return nil
131 | }
132 |
133 | //MARK: - Private
134 |
135 | private static func headerFieldsLength(_ headerFields: Any) -> Int {
136 | let data = try? JSONSerialization.data(withJSONObject: headerFields, options: .prettyPrinted)
137 | return data?.count ?? 0
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Sources/Soldiers/ANR/ANRMonitor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ANRMonitor.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/28.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class ANRMonitor: Monitor {
12 | typealias ValueType = Double
13 | var valueDidUpdateClosure: ((ValueType) -> Void)?
14 | var threshold: Double = 1
15 | private var thread: ANRPingThread?
16 |
17 | deinit {
18 | valueDidUpdateClosure = nil
19 | end()
20 | }
21 |
22 | func start() {
23 | end()
24 | thread = ANRPingThread(threshold: threshold, handler: {[weak self] (value) in
25 | self?.valueDidUpdateClosure?(value)
26 | })
27 | thread?.start()
28 | }
29 |
30 | func end() {
31 | thread?.cancel()
32 | thread = nil
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/Soldiers/ANR/ANRPingThread.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ANRPingThread.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/28.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public class ANRPingThread: Thread {
12 | let threshold: Double
13 | let handler: ((Double)->())?
14 | private var isApplicationInActive = true
15 | private let semaphore: DispatchSemaphore
16 | private var isMainThreadBlocked: Bool = false
17 |
18 | public init(threshold: Double, handler: ((Double)->())?) {
19 | self.threshold = threshold
20 | self.handler = handler
21 | semaphore = DispatchSemaphore(value: 0)
22 | super.init()
23 |
24 | NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
25 | NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
26 | }
27 |
28 | override public func main() {
29 | while !isCancelled {
30 | if isApplicationInActive {
31 | isMainThreadBlocked = true
32 | DispatchQueue.main.async {
33 | self.isMainThreadBlocked = false
34 | self.semaphore.signal()
35 | }
36 | Thread.sleep(forTimeInterval: self.threshold)
37 | if self.isMainThreadBlocked {
38 | handler?(self.threshold)
39 | }
40 | _ = semaphore.wait(timeout: .distantFuture)
41 | }else {
42 | Thread.sleep(forTimeInterval: self.threshold)
43 | }
44 | }
45 | }
46 |
47 | //MARK: - Private
48 | @objc private func applicationDidBecomeActive() {
49 | isApplicationInActive = true
50 | }
51 |
52 | @objc private func applicationDidEnterBackground() {
53 | isApplicationInActive = false
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/Soldiers/ANR/ANRSoldier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ANRSoldier.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/28.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import BSBacktraceLogger
11 |
12 | public class ANRSoldier: Soldier {
13 | public var name: String
14 | public var team: String
15 | public var icon: UIImage?
16 | public var hasNewEvent: Bool {
17 | set {
18 | if canCheckNewEvent {
19 | UserDefaults.standard.isANRSoldierHasNewEvent = newValue
20 | }
21 | }
22 | get {
23 | if canCheckNewEvent {
24 | return UserDefaults.standard.isANRSoldierHasNewEvent
25 | }else {
26 | return false
27 | }
28 | }
29 | }
30 | public var canCheckNewEvent: Bool = true
31 | public var threshold: Double = 1
32 | var isActive: Bool {
33 | set { UserDefaults.standard.isANRSoldierActive = newValue }
34 | get { UserDefaults.standard.isANRSoldierActive }
35 | }
36 | var monitorView: MonitorConsoleLabel?
37 | let monitor: ANRMonitor
38 |
39 | public init() {
40 | name = "卡顿"
41 | team = "性能检测"
42 | icon = ImageManager.imageWithName("JXCaptain_icon_anr")
43 | monitor = ANRMonitor()
44 | }
45 |
46 | public func prepare() {
47 | if isActive {
48 | start()
49 | }
50 | }
51 |
52 | public func action(naviController: UINavigationController) {
53 | naviController.pushViewController(ANRDashboardViewController(soldier: self), animated: true)
54 | }
55 |
56 | public func start() {
57 | monitor.threshold = threshold
58 | monitor.start()
59 | monitor.valueDidUpdateClosure = {[weak self] (value) in
60 | self?.dump()
61 | }
62 | isActive = true
63 | }
64 |
65 | public func end() {
66 | monitor.end()
67 | isActive = false
68 | }
69 |
70 | func dump() {
71 | guard let threadInfo = BSBacktraceLogger.bs_backtraceOfMainThread() else {
72 | return
73 | }
74 | hasNewEvent = true
75 | ANRFileManager.saveInfo(threadInfo)
76 | NotificationCenter.default.post(name: .JXCaptainSoldierNewEventDidChange, object: self)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Sources/Soldiers/AppInfo/AppInfoSoldier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppInfoSoldier.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/22.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class AppInfoSoldier: Soldier {
12 | public var name: String
13 | public var team: String
14 | public var icon: UIImage?
15 |
16 | public init() {
17 | name = "APP信息"
18 | team = "常用工具"
19 | icon = ImageManager.imageWithName("JXCaptain_icon_app_info")
20 | }
21 |
22 | public func prepare() {
23 | }
24 |
25 | public func action(naviController: UINavigationController) {
26 | naviController.pushViewController(AppInfoViewController(style: .grouped), animated: true)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Soldiers/CPU/CPUMonitor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CPUMonitor.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/28.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import MachO
11 |
12 | class CPUMonitor: Monitor {
13 | typealias ValueType = Double
14 | var valueDidUpdateClosure: ((ValueType) -> Void)?
15 | private var timer: Timer?
16 |
17 | deinit {
18 | timer?.invalidate()
19 | timer = nil
20 | valueDidUpdateClosure = nil
21 | }
22 |
23 | func start() {
24 | end()
25 | timer = Timer(timeInterval: 0.5, target: self, selector: #selector(processTimer), userInfo: nil, repeats: true)
26 | timer?.fire()
27 | RunLoop.current.add(timer!, forMode: .common)
28 | }
29 |
30 | func end() {
31 | timer?.invalidate()
32 | timer = nil
33 | }
34 |
35 | @objc func processTimer() {
36 | valueDidUpdateClosure?(cpuUsage())
37 | }
38 |
39 | private func cpuUsage() -> Double {
40 | var kr: kern_return_t
41 | var task_info_count: mach_msg_type_number_t
42 | task_info_count = mach_msg_type_number_t(TASK_INFO_MAX)
43 | var tinfo = [integer_t](repeating: 0, count: Int(task_info_count))
44 |
45 | kr = task_info(mach_task_self_, task_flavor_t(TASK_BASIC_INFO), &tinfo, &task_info_count)
46 | if kr != KERN_SUCCESS {
47 | return -1
48 | }
49 |
50 | var thread_list: thread_act_array_t? = UnsafeMutablePointer(mutating: [thread_act_t]())
51 |
52 | var thread_count: mach_msg_type_number_t = 0
53 | defer {
54 | if thread_list != nil {
55 | vm_deallocate(mach_task_self_, unsafeBitCast(thread_list, to: vm_address_t.self), vm_size_t(Int(thread_count) * MemoryLayout.stride) )
56 | }
57 | }
58 | kr = task_threads(mach_task_self_, &thread_list, &thread_count)
59 | if kr != KERN_SUCCESS {
60 | return -1
61 | }
62 |
63 | var tot_cpu: Double = 0
64 |
65 | if let thread_list = thread_list {
66 | for j in 0 ..< Int(thread_count) {
67 | var thread_info_count = mach_msg_type_number_t(THREAD_INFO_MAX)
68 | var thinfo = [integer_t](repeating: 0, count: Int(thread_info_count))
69 | kr = thread_info(thread_list[j], thread_flavor_t(THREAD_BASIC_INFO),
70 | &thinfo, &thread_info_count)
71 | if kr != KERN_SUCCESS {
72 | return -1
73 | }
74 | let threadBasicInfo = convertThreadInfoToThreadBasicInfo(thinfo)
75 | if threadBasicInfo.flags != TH_FLAGS_IDLE {
76 | tot_cpu += (Double(threadBasicInfo.cpu_usage) / Double(TH_USAGE_SCALE)) * 100.0
77 | }
78 | } // for each thread
79 | }
80 |
81 | return tot_cpu
82 | }
83 |
84 | fileprivate func convertThreadInfoToThreadBasicInfo(_ threadInfo: [integer_t]) -> thread_basic_info {
85 | var result = thread_basic_info()
86 | result.user_time = time_value_t(seconds: threadInfo[0], microseconds: threadInfo[1])
87 | result.system_time = time_value_t(seconds: threadInfo[2], microseconds: threadInfo[3])
88 | result.cpu_usage = threadInfo[4]
89 | result.policy = threadInfo[5]
90 | result.run_state = threadInfo[6]
91 | result.flags = threadInfo[7]
92 | result.suspend_count = threadInfo[8]
93 | result.sleep_time = threadInfo[9]
94 | return result
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Sources/Soldiers/CPU/CPUSoldier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CPUSoldier.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/26.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class CPUSoldier: Soldier {
12 | public var name: String
13 | public var team: String
14 | public var icon: UIImage?
15 | var isActive: Bool {
16 | set { UserDefaults.standard.isCPUSoldierActive = newValue }
17 | get { UserDefaults.standard.isCPUSoldierActive }
18 | }
19 | var monitorView: MonitorConsoleLabel?
20 | let monitor: CPUMonitor
21 |
22 | public init() {
23 | name = "CPU"
24 | team = "性能检测"
25 | icon = ImageManager.imageWithName("JXCaptain_icon_cpu")
26 | monitor = CPUMonitor()
27 | }
28 |
29 | public func prepare() {
30 | if isActive {
31 | start()
32 | }
33 | }
34 |
35 | public func action(naviController: UINavigationController) {
36 | naviController.pushViewController(CPUDashboardViewController(soldier: self), animated: true)
37 | }
38 |
39 | public func start() {
40 | monitor.start()
41 | monitorView = MonitorConsoleLabel()
42 | monitor.valueDidUpdateClosure = {[weak self] (value) in
43 | self?.monitorView?.update(type: .cpu, value: value)
44 | }
45 | MonitorListWindow.shared.enqueue(monitorView: monitorView!)
46 | isActive = true
47 | }
48 |
49 | public func end() {
50 | monitor.end()
51 | MonitorListWindow.shared.dequeue(monitorView: monitorView!)
52 | isActive = false
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/Soldiers/Crash/CrashSignalExceptionHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CrashSignalExceptionSoldier.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/21.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | typealias SignalHandler = (Int32, UnsafeMutablePointer<__siginfo>?, UnsafeMutableRawPointer?) -> Void
12 | private var previousABRTSignalHandler : SignalHandler?
13 | private var previousBUSSignalHandler : SignalHandler?
14 | private var previousFPESignalHandler : SignalHandler?
15 | private var previousILLSignalHandler : SignalHandler?
16 | private var previousPIPESignalHandler : SignalHandler?
17 | private var previousSEGVSignalHandler : SignalHandler?
18 | private var previousSYSSignalHandler : SignalHandler?
19 | private var previousTRAPSignalHandler : SignalHandler?
20 | private let preHandlers = [SIGABRT : previousABRTSignalHandler,
21 | SIGBUS : previousBUSSignalHandler,
22 | SIGFPE : previousFPESignalHandler,
23 | SIGILL : previousILLSignalHandler,
24 | SIGPIPE : previousPIPESignalHandler,
25 | SIGSEGV : previousSEGVSignalHandler,
26 | SIGSYS : previousSYSSignalHandler,
27 | SIGTRAP : previousTRAPSignalHandler]
28 |
29 | public class CrashSignalExceptionHandler {
30 | public static var exceptionReceiveClosure: ((Int32?, NSException?, String?)->())?
31 |
32 | public func prepare() {
33 | backupOriginalHandler()
34 | signalNewRegister()
35 | }
36 |
37 | func backupOriginalHandler() {
38 | for (signal, handler) in preHandlers {
39 | var tempHandler = handler
40 | backupSingleHandler(signal: signal, preHandler: &tempHandler)
41 | }
42 | }
43 |
44 | func backupSingleHandler(signal: Int32, preHandler: inout SignalHandler?) {
45 | let empty: UnsafeMutablePointer? = nil
46 | var old_action_abrt = sigaction()
47 | sigaction(signal, empty, &old_action_abrt)
48 | if old_action_abrt.__sigaction_u.__sa_sigaction != nil {
49 | preHandler = old_action_abrt.__sigaction_u.__sa_sigaction
50 | }
51 | }
52 |
53 | func signalNewRegister() {
54 | SoldierSignalRegister(signal: SIGABRT)
55 | SoldierSignalRegister(signal: SIGBUS)
56 | SoldierSignalRegister(signal: SIGFPE)
57 | SoldierSignalRegister(signal: SIGILL)
58 | SoldierSignalRegister(signal: SIGPIPE)
59 | SoldierSignalRegister(signal: SIGSEGV)
60 | SoldierSignalRegister(signal: SIGSYS)
61 | SoldierSignalRegister(signal: SIGTRAP)
62 | }
63 | }
64 |
65 | func SoldierSignalRegister(signal: Int32) {
66 | var action = sigaction()
67 | action.__sigaction_u.__sa_sigaction = SoldierSignalHandler
68 | action.sa_flags = SA_NODEFER | SA_SIGINFO
69 | sigemptyset(&action.sa_mask)
70 | let empty: UnsafeMutablePointer? = nil
71 | sigaction(signal, &action, empty)
72 | }
73 |
74 | func SoldierSignalHandler(signal: Int32, info: UnsafeMutablePointer<__siginfo>?, context: UnsafeMutableRawPointer?) {
75 | var exceptionInfo = "Signal Exception:\n"
76 | exceptionInfo.append("Signal \(SignalName(signal)) was raised.\n")
77 | exceptionInfo.append("threadInfo:\n")
78 | exceptionInfo.append("Call Stack:\n")
79 | let callStackSymbols = Thread.callStackSymbols
80 | for index in 0.. String {
94 | switch signal {
95 | case SIGABRT: return "SIGABRT"
96 | case SIGBUS: return "SIGBUS"
97 | case SIGFPE: return "SIGFPE"
98 | case SIGILL: return "SIGILL"
99 | case SIGPIPE: return "SIGPIPE"
100 | case SIGSEGV: return "SIGSEGV"
101 | case SIGSYS: return "SIGSYS"
102 | case SIGTRAP: return "SIGTRAP"
103 | default: return "None"
104 | }
105 | }
106 |
107 | func ClearSignalRigister() {
108 | signal(SIGSEGV,SIG_DFL);
109 | signal(SIGFPE,SIG_DFL);
110 | signal(SIGBUS,SIG_DFL);
111 | signal(SIGTRAP,SIG_DFL);
112 | signal(SIGABRT,SIG_DFL);
113 | signal(SIGILL,SIG_DFL);
114 | signal(SIGPIPE,SIG_DFL);
115 | signal(SIGSYS,SIG_DFL);
116 | }
117 |
118 |
--------------------------------------------------------------------------------
/Sources/Soldiers/Crash/CrashSoldier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CrashSoldier.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/22.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class CrashSoldier: Soldier {
12 | public var name: String
13 | public var team: String
14 | public var icon: UIImage?
15 | public var exceptionReceiveClosure: ((Int32?, NSException?, String?)->())?
16 | public var hasNewEvent: Bool {
17 | set {
18 | if canCheckNewEvent {
19 | UserDefaults.standard.isCrashSoldierHasNewEvent = newValue
20 | }
21 | }
22 | get {
23 | if canCheckNewEvent {
24 | return UserDefaults.standard.isCrashSoldierHasNewEvent
25 | }else {
26 | return false
27 | }
28 | }
29 | }
30 | public var canCheckNewEvent: Bool = true
31 | let uncaughtExceptionHandler: CrashUncaughtExceptionHandler
32 | let signalExceptionHandler: CrashSignalExceptionHandler
33 |
34 | public init() {
35 | name = "Crash日志"
36 | team = "常用工具"
37 | icon = ImageManager.imageWithName("JXCaptain_icon_crash")
38 | uncaughtExceptionHandler = CrashUncaughtExceptionHandler()
39 | signalExceptionHandler = CrashSignalExceptionHandler()
40 | CrashUncaughtExceptionHandler.exceptionReceiveClosure = {[weak self] (signal, exception, info) in
41 | self?.exceptionReceiveClosure?(signal, exception, info)
42 | self?.hasNewEvent = true
43 | DispatchQueue.main.async {
44 | NotificationCenter.default.post(name: .JXCaptainSoldierNewEventDidChange, object: self)
45 | }
46 | }
47 | CrashSignalExceptionHandler.exceptionReceiveClosure = {[weak self] (signal, exception, info) in
48 | self?.exceptionReceiveClosure?(signal, exception, info)
49 | self?.hasNewEvent = true
50 | DispatchQueue.main.async {
51 | NotificationCenter.default.post(name: .JXCaptainSoldierNewEventDidChange, object: self)
52 | }
53 | }
54 | }
55 |
56 | public func prepare() {
57 | uncaughtExceptionHandler.prepare()
58 | signalExceptionHandler.prepare()
59 | }
60 |
61 | public func action(naviController: UINavigationController) {
62 | naviController.pushViewController(CrashDashboardViewController(soldier: self), animated: true)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/Soldiers/Crash/CrashUncaughtExceptionHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CrashSoldier.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/20.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | private var preUncaughtExceptionHandler: NSUncaughtExceptionHandler?
12 |
13 | public class CrashUncaughtExceptionHandler {
14 | public static var exceptionReceiveClosure: ((Int32?, NSException?, String?)->())?
15 |
16 | public func prepare() {
17 | preUncaughtExceptionHandler = NSGetUncaughtExceptionHandler()
18 | NSSetUncaughtExceptionHandler(SoldierUncaughtExceptionHandler)
19 | }
20 | }
21 |
22 |
23 | func SoldierUncaughtExceptionHandler(exception: NSException) -> Void {
24 | let stackArray = exception.callStackSymbols
25 | let reason = exception.reason
26 | let name = exception.name.rawValue
27 | let stackInfo = stackArray.reduce("") { (result, item) -> String in
28 | return result + "\n\(item)"
29 | }
30 | let exceptionInfo = name + "\n" + (reason ?? "no reason") + "\n" + stackInfo
31 | CrashUncaughtExceptionHandler.exceptionReceiveClosure?(nil, exception, exceptionInfo)
32 | CrashFileManager.saveInfo(exceptionInfo, fileNamePrefix: "Uncaught:")
33 | preUncaughtExceptionHandler?(exception)
34 | kill(getpid(), SIGKILL)
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Soldiers/FPS/FPSMonitor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FPSMonitor.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/26.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class FPSMonitor: Monitor {
12 | typealias ValueType = Int
13 | var valueDidUpdateClosure: ((ValueType) -> Void)?
14 | var link: CADisplayLink?
15 | var periodCount: Int = 0
16 | var periodStartTime: CFTimeInterval = 0
17 |
18 |
19 | deinit {
20 | link?.invalidate()
21 | link = nil
22 | valueDidUpdateClosure = nil
23 | }
24 |
25 | func start() {
26 | link = CADisplayLink(target: self, selector: #selector(processLink))
27 | link?.add(to: RunLoop.main, forMode: .common)
28 | }
29 |
30 | func end() {
31 | link?.invalidate()
32 | link = nil
33 | }
34 |
35 | @objc func processLink() {
36 | if periodStartTime == 0 {
37 | periodStartTime = link!.timestamp
38 | return
39 | }
40 | periodCount += 1
41 | let duration = link!.timestamp - periodStartTime
42 | if duration < 1 {
43 | return
44 | }
45 | let fps = CFTimeInterval(periodCount)/duration
46 | valueDidUpdateClosure?(Int(fps + 0.5))
47 | periodStartTime = link!.timestamp
48 | periodCount = 0
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/Soldiers/FPS/FPSSoldier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FPSSoldier.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/26.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class FPSSoldier: Soldier {
12 | public var name: String
13 | public var team: String
14 | public var icon: UIImage?
15 | var isActive: Bool {
16 | set { UserDefaults.standard.isFPSSoldierActive = newValue }
17 | get { UserDefaults.standard.isFPSSoldierActive }
18 | }
19 | var monitorView: MonitorConsoleLabel?
20 | let monitor: FPSMonitor
21 |
22 | public init() {
23 | name = "FPS"
24 | team = "性能检测"
25 | icon = ImageManager.imageWithName("JXCaptain_icon_fps")
26 | monitor = FPSMonitor()
27 | }
28 |
29 | public func prepare() {
30 | if isActive {
31 | start()
32 | }
33 | }
34 |
35 | public func action(naviController: UINavigationController) {
36 | naviController.pushViewController(FPSDashboardViewController(soldier: self), animated: true)
37 | }
38 |
39 | public func start() {
40 | monitor.start()
41 | monitorView = MonitorConsoleLabel()
42 | monitor.valueDidUpdateClosure = {[weak self] (value) in
43 | self?.monitorView?.update(type: .fps, value: Double(value))
44 | }
45 | MonitorListWindow.shared.enqueue(monitorView: monitorView!)
46 | isActive = true
47 | }
48 |
49 | public func end() {
50 | monitor.end()
51 | MonitorListWindow.shared.dequeue(monitorView: monitorView!)
52 | isActive = false
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/Soldiers/Memory/MemoryMonitor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MemoryMonitor.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/26.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import MachO
11 |
12 | class MemoryMonitor: Monitor {
13 | typealias ValueType = Double
14 | var valueDidUpdateClosure: ((ValueType) -> Void)?
15 | private var timer: Timer?
16 |
17 | deinit {
18 | timer?.invalidate()
19 | timer = nil
20 | valueDidUpdateClosure = nil
21 | }
22 |
23 | func start() {
24 | end()
25 | timer = Timer(timeInterval: 0.5, target: self, selector: #selector(processTimer), userInfo: nil, repeats: true)
26 | timer?.fire()
27 | RunLoop.current.add(timer!, forMode: .common)
28 | }
29 |
30 | func end() {
31 | timer?.invalidate()
32 | timer = nil
33 | }
34 |
35 | @objc func processTimer() {
36 | valueDidUpdateClosure?(memory())
37 | }
38 |
39 | private func memory() -> Double {
40 | var vmInfo = task_vm_info_data_t()
41 | let TASK_VM_INFO_COUNT = MemoryLayout.stride/MemoryLayout.stride
42 | var count = mach_msg_type_number_t(TASK_VM_INFO_COUNT)
43 | let kerr = withUnsafeMutablePointer(to: &vmInfo) {
44 | $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
45 | task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
46 | }
47 | }
48 | if kerr == KERN_SUCCESS {
49 | return Double(vmInfo.phys_footprint)/1024/1024
50 | }else {
51 | return -1
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/Soldiers/Memory/MemorySoldier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MemorySoldier.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/26.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class MemorySoldier: Soldier {
12 | public var name: String
13 | public var team: String
14 | public var icon: UIImage?
15 | var isActive: Bool {
16 | set { UserDefaults.standard.isMemorySoldierActive = newValue }
17 | get { UserDefaults.standard.isMemorySoldierActive }
18 | }
19 | var monitorView: MonitorConsoleLabel?
20 | let monitor: MemoryMonitor
21 |
22 | public init() {
23 | name = "内存"
24 | team = "性能检测"
25 | icon = ImageManager.imageWithName("JXCaptain_icon_memory")
26 | monitor = MemoryMonitor()
27 | }
28 |
29 | public func prepare() {
30 | if isActive {
31 | start()
32 | }
33 | }
34 |
35 | public func action(naviController: UINavigationController) {
36 | naviController.pushViewController(MemoryDashboardViewController(soldier: self), animated: true)
37 | }
38 |
39 | public func start() {
40 | monitor.start()
41 | monitorView = MonitorConsoleLabel()
42 | monitor.valueDidUpdateClosure = {[weak self] (value) in
43 | self?.monitorView?.update(type: .memory, value: value)
44 | }
45 | MonitorListWindow.shared.enqueue(monitorView: monitorView!)
46 | isActive = true
47 | }
48 |
49 | public func end() {
50 | monitor.end()
51 | MonitorListWindow.shared.dequeue(monitorView: monitorView!)
52 | isActive = false
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/Soldiers/NetworkObserver/JXCaptainURLProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JXCaptainURLProtocol.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/29.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | private let KJXCaptainURLProtocolIdentifier = "KJXCaptainURLProtocolIdentifier"
12 |
13 | class JXCaptainURLProtocol: URLProtocol, URLSessionDataDelegate {
14 | var session: URLSession!
15 | var sessionTask: URLSessionTask?
16 | var receivedData: Data?
17 | var receivedResponse: URLResponse?
18 | var startDate: Date?
19 | var receivedError: Error?
20 |
21 | override init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
22 | super.init(request: request, cachedResponse: cachedResponse, client: client)
23 | session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
24 | }
25 |
26 | override class func canInit(with request: URLRequest) -> Bool {
27 | if URLProtocol.property(forKey: KJXCaptainURLProtocolIdentifier, in: request) != nil {
28 | return false
29 | }
30 | if request.url?.scheme != "http" && request.url?.scheme != "https" {
31 | return false
32 | }
33 | return true
34 | }
35 |
36 | override class func canonicalRequest(for request: URLRequest) -> URLRequest {
37 | guard let mutableRequest = ((request as NSURLRequest).mutableCopy() as? NSMutableURLRequest) else {
38 | return request
39 | }
40 | URLProtocol.setProperty("JXCaptain", forKey: KJXCaptainURLProtocolIdentifier, in: mutableRequest)
41 | return mutableRequest as URLRequest
42 | }
43 |
44 | override func startLoading() {
45 | startDate = Date()
46 | sessionTask = session.dataTask(with: request)
47 | sessionTask?.resume()
48 | }
49 |
50 | override func stopLoading() {
51 | sessionTask?.cancel()
52 | sessionTask = nil
53 | receivedData = nil
54 | receivedResponse = nil
55 | startDate = nil
56 | receivedError = nil
57 | }
58 |
59 | func recordRequest() {
60 | guard let receivedResponse = receivedResponse, let receivedData = receivedData, let startDate = startDate else {
61 | return
62 | }
63 | NetworkObserverSoldier.shared.recordRequest(request: request, response: receivedResponse, responseData: receivedData, error: receivedError as NSError?, startDate: startDate)
64 | }
65 |
66 | //MARK: - URLSessionDelegate
67 | func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
68 | guard let redirectRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else {
69 | return
70 | }
71 | // The new request was copied from our old request, so it has our magic property. We actually
72 | // have to remove that so that, when the client starts the new request, we see it. If we
73 | // don't do this then we never see the new request and thus don't get a chance to change
74 | // its caching behaviour.
75 | //
76 | // We also cancel our current connection because the client is going to start a new request for
77 | // us anyway.
78 | URLProtocol.removeProperty(forKey: KJXCaptainURLProtocolIdentifier, in: redirectRequest)
79 | // Tell the client about the redirect.
80 | client?.urlProtocol(self, wasRedirectedTo: redirectRequest as URLRequest, redirectResponse: response)
81 | // Stop our load. The CFNetwork infrastructure will create a new NSURLProtocol instance to run
82 | // the load of the redirect.
83 |
84 | // The following ends up calling -URLSession:task:didCompleteWithError: with NSURLErrorDomain / NSURLErrorCancelled,
85 | // which specificallys traps and ignores the error.
86 | sessionTask?.cancel()
87 | let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
88 | receivedError = error
89 | client?.urlProtocol(self, didFailWithError: error)
90 | }
91 |
92 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
93 | client?.urlProtocol(self, didReceive: URLAuthenticationChallenge(authenticationChallenge: challenge, sender: JXCaptainURLSessionChallengeSender(completionHandler: completionHandler)))
94 | }
95 |
96 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
97 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
98 | receivedData = Data()
99 | receivedResponse = response
100 | completionHandler(.allow)
101 | }
102 |
103 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) {
104 | completionHandler(proposedResponse)
105 | }
106 |
107 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
108 | receivedData?.append(data)
109 | client?.urlProtocol(self, didLoad: data)
110 | }
111 |
112 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
113 | if error == nil {
114 | recordRequest()
115 | client?.urlProtocolDidFinishLoading(self)
116 | }else if let localError = error as NSError? {
117 | receivedError = error
118 | if localError.domain == NSURLErrorDomain && localError.code == NSURLErrorCancelled {
119 | // Do nothing. This happens in two cases:
120 | //
121 | // o during a redirect, in which case the redirect code has already told the client about
122 | // the failure
123 | //
124 | // o if the request is cancelled by a call to -stopLoading, in which case the client doesn't
125 | // want to know about the failure
126 | }else {
127 | client?.urlProtocol(self, didFailWithError: error!)
128 | }
129 | }
130 | session.finishTasksAndInvalidate()
131 | }
132 | }
133 |
134 | extension URLSession {
135 |
136 | static let swizzleInit: Void = {
137 | let originalSelector = Selector(("initWithConfiguration:delegate:delegateQueue:"))
138 | let swizzledSelector = Selector(("initWithCaptainConfiguration:delegate:delegateQueue:"))
139 | let originalMethod = class_getInstanceMethod(URLSession.self, originalSelector)
140 | let swizzledMethod = class_getInstanceMethod(URLSession.self, swizzledSelector)
141 | guard originalMethod != nil, swizzledMethod != nil else {
142 | return
143 | }
144 | let didAddMethod = class_addMethod(URLSession.self, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))
145 | if didAddMethod {
146 | class_replaceMethod(URLSession.self, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
147 | } else {
148 | method_exchangeImplementations(originalMethod!, swizzledMethod!);
149 | }
150 | }()
151 |
152 | @objc convenience init(captainConfiguration: URLSessionConfiguration, delegate: URLSessionDelegate?, delegateQueue queue: OperationQueue?) {
153 | self.init(captainConfiguration: captainConfiguration, delegate: delegate, delegateQueue: queue)
154 |
155 | let result = captainConfiguration.protocolClasses?.contains(where: { (type) -> Bool in
156 | if type == JXCaptainURLProtocol.self {
157 | return true
158 | }else {
159 | return false
160 | }
161 | })
162 | if result != true {
163 | var protocols = captainConfiguration.protocolClasses
164 | protocols?.insert(JXCaptainURLProtocol.self, at: 0)
165 | captainConfiguration.protocolClasses = protocols
166 | }
167 | }
168 | }
169 |
170 | class JXCaptainURLSessionChallengeSender: NSObject, URLAuthenticationChallengeSender {
171 | let completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
172 |
173 | init(completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
174 | self.completionHandler = completionHandler
175 | super.init()
176 | }
177 |
178 | func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {
179 | completionHandler(.useCredential, credential)
180 | }
181 |
182 | func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {
183 | completionHandler(.useCredential, nil)
184 | }
185 |
186 | func cancel(_ challenge: URLAuthenticationChallenge) {
187 | completionHandler(.cancelAuthenticationChallenge, nil)
188 | }
189 |
190 | func performDefaultHandling(for challenge: URLAuthenticationChallenge) {
191 | completionHandler(.performDefaultHandling, nil)
192 | }
193 |
194 | func rejectProtectionSpaceAndContinue(with challenge: URLAuthenticationChallenge) {
195 | completionHandler(.rejectProtectionSpace, nil)
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/Sources/Soldiers/NetworkObserver/NetworkObserverSoldier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkObserverSoldier.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/29.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import WebKit
11 |
12 | public class NetworkObserverSoldier: Soldier {
13 | public static let shared = NetworkObserverSoldier()
14 | public var name: String
15 | public var team: String
16 | public var icon: UIImage?
17 | /// responseData数据缓存最大容量,默认:50MB
18 | public var responseCacheByteLimit: Int = 50 * 1024 * 1024 {
19 | didSet {
20 | cache.countLimit = responseCacheByteLimit
21 | }
22 | }
23 | /// 是否能拦截WKWebView的接口,默认为false。
24 | /// 拦截的原理参考文章:https://blog.moecoder.com/2016/10/26/support-nsurlprotocol-in-wkwebview/ 因为WKWebView是一个单独的进程,如果要通过自定义的协议进行拦截,就会导致进程间通信,降低性能。所以,需要拦截WKWebView时,需要自己设置为true。
25 | public var canInterceptWKWebView: Bool = false
26 | var isActive: Bool {
27 | set { UserDefaults.standard.isNetworkObserverSoldierActive = newValue }
28 | get { UserDefaults.standard.isNetworkObserverSoldierActive }
29 | }
30 | var monitorView: MonitorConsoleLabel?
31 | let monitor: ANRMonitor
32 | var flowModels = [NetworkFlowModel]()
33 | let cache = NSCache()
34 |
35 | public init() {
36 | name = "流量"
37 | team = "性能检测"
38 | icon = ImageManager.imageWithName("JXCaptain_icon_network")
39 | monitor = ANRMonitor()
40 | cache.countLimit = responseCacheByteLimit
41 | }
42 |
43 | public func prepare() {
44 | if isActive {
45 | start()
46 | }
47 | }
48 |
49 | public func action(naviController: UINavigationController) {
50 | naviController.pushViewController(NetworkObserverDashboardViewController(soldier: self), animated: true)
51 | }
52 |
53 | func start() {
54 | if canInterceptWKWebView {
55 | WKWebView.register(schemes: ["http", "https"])
56 | }
57 | URLProtocol.registerClass(JXCaptainURLProtocol.self)
58 | URLSession.swizzleInit
59 | isActive = true
60 | }
61 |
62 | func end() {
63 | if canInterceptWKWebView {
64 | WKWebView.unregister(schemes: ["http", "https"])
65 | }
66 | URLProtocol.unregisterClass(JXCaptainURLProtocol.self)
67 | isActive = false
68 | }
69 |
70 | func recordRequest(request: URLRequest, response: URLResponse?, responseData: Data?, error: NSError?, startDate: Date) {
71 | let flowModel = NetworkFlowModel(request: request, response: response, responseData: responseData, error: error, startDate: startDate)
72 | flowModels.insert(flowModel, at: 0)
73 | if let data = responseData {
74 | cache.setObject(data as AnyObject, forKey: flowModel.requestID as AnyObject, cost: data.count)
75 | }
76 | DispatchQueue.main.async {
77 | NotificationCenter.default.post(name: NSNotification.Name.JXCaptainNetworkObserverSoldierNewFlowDidReceive, object: flowModel)
78 | }
79 | }
80 | }
81 |
82 | extension String {
83 | public var base64Decode: String? {
84 | guard let data = Data(base64Encoded: self) else { return nil }
85 | return String(data: data, encoding: .utf8)
86 | }
87 | public var base64Encode: String {
88 | return Data(utf8).base64EncodedString()
89 | }
90 | }
91 |
92 | extension WKWebView {
93 | private class func browsing_contextController() -> (NSObject.Type)? {
94 | guard let str = "YnJvd3NpbmdDb250ZXh0Q29udHJvbGxlcg==".base64Decode else { assertionFailure(); return nil }
95 | // str: "browsingContextController"
96 | guard let obj = WKWebView().value(forKey: str) else { return nil }
97 | return type(of: obj) as? NSObject.Type
98 | }
99 |
100 | private class func perform_browsing_contextController(aSelector: Selector, schemes: Set) -> Bool {
101 | guard let obj = browsing_contextController(), obj.responds(to: aSelector), schemes.count > 0 else {
102 | assertionFailure(); return false
103 | }
104 | var result = schemes.count > 0
105 | schemes.forEach({ (scheme) in
106 | let ret = obj.perform(aSelector, with: scheme)
107 | result = result && (ret != nil)
108 | })
109 | return result
110 | }
111 | }
112 |
113 | extension WKWebView {
114 | @discardableResult public class func register(schemes: Set) -> Bool {
115 | guard let str = "cmVnaXN0ZXJTY2hlbWVGb3JDdXN0b21Qcm90b2NvbDo=".base64Decode else {
116 | assertionFailure(); return false
117 | }
118 | // str: "registerSchemeForCustomProtocol:"
119 | let register = NSSelectorFromString(str)
120 | return perform_browsing_contextController(aSelector: register, schemes: schemes)
121 | }
122 |
123 | @discardableResult public class func unregister(schemes: Set) -> Bool {
124 | guard let str = "dW5yZWdpc3RlclNjaGVtZUZvckN1c3RvbVByb3RvY29sOg==".base64Decode else {
125 | assertionFailure(); return false
126 | }
127 | //str: "unregisterSchemeForCustomProtocol:"
128 | let unregister = NSSelectorFromString(str)
129 | return perform_browsing_contextController(aSelector: unregister, schemes: schemes)
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/Sources/Soldiers/SanboxBrowser/SanboxBrowserSoldier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BundleBrowserSoldier.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/21.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class SanboxBrowserSoldier: Soldier {
12 | public var name: String
13 | public var team: String
14 | public var icon: UIImage?
15 |
16 | public init() {
17 | name = "沙盒浏览"
18 | team = "常用工具"
19 | icon = ImageManager.imageWithName("JXCaptain_icon_sanbox")
20 | }
21 |
22 | public func prepare() {
23 | }
24 |
25 | public func action(naviController: UINavigationController) {
26 | naviController.pushViewController(JXFileBrowserController(path: NSHomeDirectory()), animated: true)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Soldiers/UserDefaults/UserDefaultsSoldier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsSoldier.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/9/9.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class UserDefaultsSoldier: Soldier {
12 | public static let shared = UserDefaultsSoldier()
13 | public var name: String
14 | public var team: String
15 | public var icon: UIImage?
16 |
17 | public init() {
18 | name = "UserDefaults"
19 | team = "常用工具"
20 | icon = ImageManager.imageWithName("JXCaptain_icon_app_user_defaults")
21 | }
22 |
23 | public func prepare() { }
24 |
25 | public func action(naviController: UINavigationController) {
26 | naviController.pushViewController(UserDefaultsKeyValuesListViewController(defaults: UserDefaults.standard), animated: true)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Soldiers/WebsiteEntry/WebsiteEntrySoldier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WebsiteSoldier.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/23.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class WebsiteEntrySoldier: Soldier {
12 | public var name: String
13 | public var team: String
14 | public var icon: UIImage?
15 | public var defaultWebsite: String?
16 | public var webDetailControllerClosure: ((String)->(UIViewController))?
17 |
18 | deinit {
19 | webDetailControllerClosure = nil
20 | }
21 |
22 | public init() {
23 | name = "H5任意门"
24 | team = "常用工具"
25 | icon = ImageManager.imageWithName("JXCaptain_icon_h5")
26 | }
27 |
28 | public func prepare() {
29 | }
30 |
31 | public func action(naviController: UINavigationController) {
32 | naviController.pushViewController(WebsiteEntryViewController(soldier: self), animated: true)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/UI/ANR/ANRDashboardViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ANRDashboardViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/28.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ANRDashboardViewController: UITableViewController {
12 | let soldier: ANRSoldier
13 |
14 | init(soldier: ANRSoldier) {
15 | self.soldier = soldier
16 | super.init(style: .plain)
17 | }
18 |
19 | required init?(coder aDecoder: NSCoder) {
20 | fatalError("init(coder:) has not been implemented")
21 | }
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 |
26 | title = "卡顿监控"
27 | tableView.register(DashboardCell.self, forCellReuseIdentifier: "swithCell")
28 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
29 | tableView.tableFooterView = UIView()
30 |
31 | if soldier.hasNewEvent {
32 | soldier.hasNewEvent = false
33 | NotificationCenter.default.post(name: .JXCaptainSoldierNewEventDidChange, object: soldier)
34 | }
35 | }
36 |
37 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
38 | return 3
39 | }
40 |
41 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
42 | if indexPath.row == 0 {
43 | let cell = tableView.dequeueReusableCell(withIdentifier: "swithCell", for: indexPath) as! DashboardCell
44 | cell.textLabel?.text = "卡顿检测开关"
45 | cell.toggle.isOn = soldier.isActive
46 | cell.toggleValueDidChange = {[weak self] (isOn) in
47 | if isOn {
48 | self?.soldier.start()
49 | }else {
50 | self?.soldier.end()
51 | }
52 | }
53 | return cell
54 | }else if indexPath.row == 1 {
55 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
56 | cell.accessoryType = .disclosureIndicator
57 | cell.textLabel?.text = "查看卡顿日志"
58 | return cell
59 | }else {
60 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
61 | cell.accessoryType = .disclosureIndicator
62 | cell.textLabel?.text = "清理卡顿日志"
63 | return cell
64 | }
65 | }
66 |
67 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
68 | tableView.deselectRow(at: indexPath, animated: true)
69 | if indexPath.row == 1 {
70 | let vc = ANRListViewController(dataSource: ANRFileManager.allFiles())
71 | self.navigationController?.pushViewController(vc, animated: true)
72 | }else if indexPath.row == 2 {
73 | let alert = UIAlertController(title: "提示", message: "确认删除所有卡顿日志吗?", preferredStyle: .alert)
74 | alert.addAction(UIAlertAction(title: "取消", style: .cancel, handler: nil))
75 | alert.addAction(UIAlertAction(title: "确定", style: .destructive, handler: { (action) in
76 | ANRFileManager.deleteAllFiles()
77 | }))
78 | present(alert, animated: true, completion: nil)
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Sources/UI/ANR/ANRListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ANRListViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/28.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ANRListViewController: FileListViewController {
12 | override func viewDidLoad() {
13 | super.viewDidLoad()
14 |
15 | title = "ANR日志列表"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/UI/AppInfo/AppInfoViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppInfoViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/22.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class AppInfoViewController: UITableViewController {
12 | var dataSource = [AppInfoSectionModel]()
13 |
14 | override func viewDidLoad() {
15 | super.viewDidLoad()
16 |
17 | title = "APP信息"
18 |
19 | tableView.register(AppInfoCell.self, forCellReuseIdentifier: "cell")
20 | tableView.tableFooterView = UIView()
21 |
22 | let phoneCellModels = [AppInfoCellModel(title: "手机型号", info: AppInfoManager.iphoneName()),
23 | AppInfoCellModel(title: "系统版本", info: AppInfoManager.iOSVersion())]
24 | let phoneInfo = AppInfoSectionModel(title: "手机信息", cellModels: phoneCellModels)
25 | let appCellModels = [AppInfoCellModel(title: "BundleID", info: AppInfoManager.bundleID()),
26 | AppInfoCellModel(title: "Version", info: AppInfoManager.bundleVersion()),
27 | AppInfoCellModel(title: "VersionCode", info: AppInfoManager.bundleCode())]
28 | let appInfo = AppInfoSectionModel(title: "APP信息", cellModels: appCellModels)
29 | let authorityCellModels = [AppInfoCellModel(title: "地理位置权限", info: AppInfoManager.locationAuthority()),
30 | AppInfoCellModel(title: "网络权限", info: AppInfoManager.networkAuthority()),
31 | AppInfoCellModel(title: "通知权限", info: AppInfoManager.notificationAuthority()),
32 | AppInfoCellModel(title: "相机权限", info: AppInfoManager.cameraAuthority()),
33 | AppInfoCellModel(title: "麦克风权限", info: AppInfoManager.audioAuthority()),
34 | AppInfoCellModel(title: "相册权限", info: AppInfoManager.cameraAuthority()),
35 | AppInfoCellModel(title: "通讯录权限", info: AppInfoManager.contactsAuthority()),
36 | AppInfoCellModel(title: "日历权限", info: AppInfoManager.calendarAuthority()),
37 | AppInfoCellModel(title: "提醒事项权限", info: AppInfoManager.reminderAuthority())]
38 | let authorityInfo = AppInfoSectionModel(title: "权限信息", cellModels: authorityCellModels)
39 | dataSource.append(contentsOf: [phoneInfo, appInfo, authorityInfo])
40 | }
41 |
42 | override func numberOfSections(in tableView: UITableView) -> Int {
43 | return dataSource.count
44 | }
45 |
46 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
47 | return dataSource[section].cellModels.count
48 | }
49 |
50 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
51 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! AppInfoCell
52 | let cellModel = dataSource[indexPath.section].cellModels[indexPath.row]
53 | cell.titleLabel.text = cellModel.title
54 | cell.infoLabel.text = cellModel.info
55 | return cell
56 | }
57 |
58 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
59 | return dataSource[section].title
60 | }
61 | }
62 |
63 | struct AppInfoSectionModel {
64 | let title: String
65 | let cellModels: [AppInfoCellModel]
66 | }
67 |
68 | struct AppInfoCellModel {
69 | let title: String
70 | let info: String
71 | }
72 |
73 | class AppInfoCell: UITableViewCell {
74 | let titleLabel: UILabel
75 | let infoLabel: UILabel
76 |
77 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
78 | titleLabel = UILabel()
79 | infoLabel = UILabel()
80 | super.init(style: style, reuseIdentifier: reuseIdentifier)
81 |
82 | self.selectionStyle = .none
83 |
84 | titleLabel.font = .systemFont(ofSize: 17)
85 | titleLabel.translatesAutoresizingMaskIntoConstraints = false
86 | contentView.addSubview(titleLabel)
87 | titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12).isActive = true
88 | titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
89 |
90 | infoLabel.font = .systemFont(ofSize: 15)
91 | infoLabel.textColor = .gray
92 | infoLabel.translatesAutoresizingMaskIntoConstraints = false
93 | contentView.addSubview(infoLabel)
94 | infoLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12).isActive = true
95 | infoLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
96 | }
97 |
98 | required init?(coder aDecoder: NSCoder) {
99 | fatalError("init(coder:) has not been implemented")
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Sources/UI/CPU/CPUDashboardViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CPUDashboardViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/28.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class CPUDashboardViewController: UITableViewController {
12 | let soldier: CPUSoldier
13 |
14 | init(soldier: CPUSoldier) {
15 | self.soldier = soldier
16 | super.init(style: .plain)
17 | }
18 |
19 | required init?(coder aDecoder: NSCoder) {
20 | fatalError("init(coder:) has not been implemented")
21 | }
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 |
26 | title = "CPU"
27 | tableView.register(DashboardCell.self, forCellReuseIdentifier: "cell")
28 | tableView.tableFooterView = UIView()
29 | }
30 |
31 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
32 | return 1
33 | }
34 |
35 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
36 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! DashboardCell
37 | cell.textLabel?.text = "CPU检测开关"
38 | cell.toggle.isOn = soldier.isActive
39 | cell.toggleValueDidChange = {[weak self] (isOn) in
40 | if isOn {
41 | self?.soldier.start()
42 | }else {
43 | self?.soldier.end()
44 | }
45 | }
46 | return cell
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/UI/Common/DashboardCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DashboardCell.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/26.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class DashboardCell: UITableViewCell {
12 | let toggle: UISwitch
13 | var toggleValueDidChange: ((Bool)->())?
14 |
15 | deinit {
16 | toggleValueDidChange = nil
17 | }
18 |
19 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
20 | toggle = UISwitch()
21 | super.init(style: style, reuseIdentifier: reuseIdentifier)
22 |
23 | selectionStyle = .none
24 | textLabel?.backgroundColor = .clear
25 | toggle.translatesAutoresizingMaskIntoConstraints = false
26 | toggle.addTarget(self, action: #selector(toggleDidClick), for: .valueChanged)
27 | contentView.addSubview(toggle)
28 | toggle.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12).isActive = true
29 | toggle.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
30 | }
31 |
32 | required init?(coder aDecoder: NSCoder) {
33 | fatalError("init(coder:) has not been implemented")
34 | }
35 |
36 | @objc func toggleDidClick() {
37 | toggleValueDidChange?(toggle.isOn)
38 | }
39 |
40 | override func layoutSubviews() {
41 | super.layoutSubviews()
42 |
43 | contentView.bringSubviewToFront(toggle)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/UI/Common/FileListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileListViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/29.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class FileListViewController: UITableViewController {
12 | let dataSource: [SanboxModel]
13 |
14 | init(dataSource: [SanboxModel]) {
15 | self.dataSource = dataSource
16 | super.init(style: .plain)
17 | }
18 |
19 | required init?(coder aDecoder: NSCoder) {
20 | fatalError("init(coder:) has not been implemented")
21 | }
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 |
26 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
27 | tableView.tableFooterView = UIView()
28 | if dataSource.isEmpty {
29 | let emptyLabel = UILabel()
30 | emptyLabel.font = .systemFont(ofSize: 25)
31 | emptyLabel.text = "暂无日志文件"
32 | emptyLabel.textAlignment = .center
33 | tableView.backgroundView = emptyLabel
34 | }
35 | }
36 |
37 | //MARK: - UITableViewDataSource & UITableViewDelegate
38 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
39 | return dataSource.count
40 | }
41 |
42 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
43 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
44 | cell.accessoryType = .disclosureIndicator
45 | let model = dataSource[indexPath.row]
46 | cell.textLabel?.text = model.name
47 | return cell
48 | }
49 |
50 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
51 | tableView.deselectRow(at: indexPath, animated: true)
52 | let model = dataSource[indexPath.row]
53 | let sheet = UIAlertController(title: nil, message: "请选择操作方式", preferredStyle: .actionSheet)
54 | sheet.addAction(UIAlertAction(title: "本地预览", style: .default, handler: { (action) in
55 | let previewVC = JXFilePreviewViewController(filePath: model.fileURL.path)
56 | self.navigationController?.pushViewController(previewVC, animated: true)
57 | }))
58 | sheet.addAction(UIAlertAction(title: "分享", style: .default, handler: { (action) in
59 | let activityController = UIActivityViewController(activityItems: [model.fileURL], applicationActivities: nil)
60 | self.present(activityController, animated: true, completion: nil)
61 | }))
62 | sheet.addAction(UIAlertAction(title: "取消", style: .cancel, handler: nil))
63 | present(sheet, animated: true, completion: nil)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/UI/Common/MonitorConsoleLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MonitorView.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/26.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class MonitorConsoleLabel: UILabel {
12 |
13 | override init(frame: CGRect) {
14 | super.init(frame: frame)
15 |
16 | textAlignment = .center
17 | }
18 |
19 | required init?(coder aDecoder: NSCoder) {
20 | fatalError("init(coder:) has not been implemented")
21 | }
22 |
23 | func update(type: MonitorType, value: Double) {
24 | switch type {
25 | case .fps:
26 | let percent = CGFloat(value/60)
27 | let valueColor = UIColor(hue: 0.27 * (percent - 0.2), saturation: 1, brightness: 0.9, alpha: 1)
28 | let contentText = "\(round(value)) FPS"
29 | let attrText = NSMutableAttributedString(string: contentText, attributes: [.foregroundColor : valueColor, .font : UIFont.systemFont(ofSize: 16)])
30 | attrText.addAttribute(.foregroundColor, value: UIColor.white, range: NSString(string: contentText).range(of: "FPS"))
31 | attributedText = attrText
32 | case .memory:
33 | let percent = CGFloat(value/350)
34 | let valueColor = color(percent: percent)
35 | let contentText = String(format: "%.1f M", value)
36 | let attrText = NSMutableAttributedString(string: contentText, attributes: [.foregroundColor : valueColor, .font : UIFont.systemFont(ofSize: 16)])
37 | attrText.addAttribute(.foregroundColor, value: UIColor.white, range: NSString(string: contentText).range(of: "M"))
38 | attributedText = attrText
39 | case .cpu:
40 | let percent = CGFloat(value/100)
41 | let valueColor = color(percent: percent)
42 | let contentText = String(format: "%.lf%% CPU", round(value))
43 | let attrText = NSMutableAttributedString(string: contentText, attributes: [.foregroundColor : valueColor, .font : UIFont.systemFont(ofSize: 16)])
44 | attrText.addAttribute(.foregroundColor, value: UIColor.white, range: NSString(string: contentText).range(of: "CPU"))
45 | attributedText = attrText
46 | default: break
47 | }
48 | }
49 |
50 | private func color(percent: CGFloat) -> UIColor {
51 | var r: CGFloat = 0
52 | var g: CGFloat = 0
53 | let one: CGFloat = 255 + 255
54 | if percent < 0.5 {
55 | r = one * percent
56 | g = 255
57 | }else if percent > 0.5 {
58 | g = 255 - ((percent - 0.5) * one)
59 | r = 255
60 | }
61 | return UIColor(red: r/255, green: g/255, blue: 0, alpha: 1)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/UI/Common/MonitorListWindow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MonitorListView.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/26.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class MonitorListWindow: UIWindow {
12 | static let `shared` = MonitorListWindow(frame: CGRect.zero)
13 | private var monitorViews: [UIView] = [UIView]()
14 | private let contentHeight: CGFloat = 20
15 | private let containerView = UIView()
16 |
17 | override init(frame: CGRect) {
18 | super.init(frame: UIScreen.main.bounds)
19 |
20 | self.windowLevel = .alert
21 | if rootViewController == nil {
22 | let vc = UIViewController()
23 | vc.view.backgroundColor = UIColor.clear
24 | let contentBGView = UIView()
25 | contentBGView.backgroundColor = UIColor.black
26 | var y: CGFloat = 0
27 | if #available(iOS 11.0, *) {
28 | if safeAreaInsets.top > 20 {
29 | y = 30
30 | }
31 | }
32 | contentBGView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: y + contentHeight)
33 | vc.view.addSubview(contentBGView)
34 | containerView.frame = CGRect(x: 0, y: y, width: UIScreen.main.bounds.size.width, height: contentHeight)
35 | vc.view.addSubview(containerView)
36 | rootViewController = vc
37 | }
38 | }
39 |
40 | required init?(coder aDecoder: NSCoder) {
41 | fatalError("init(coder:) has not been implemented")
42 | }
43 |
44 | override func layoutSubviews() {
45 | super.layoutSubviews()
46 |
47 | backgroundColor = .clear
48 | }
49 |
50 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
51 | return nil
52 | }
53 |
54 | func show() {
55 | isHidden = false
56 | }
57 |
58 | func hide() {
59 | isHidden = true
60 | }
61 |
62 | func enqueue(monitorView: UIView) {
63 | show()
64 | containerView.addSubview(monitorView)
65 | monitorViews.append(monitorView)
66 | relayoutMonitorViews()
67 | }
68 |
69 | func dequeue(monitorView: UIView) {
70 | for index in 0.. Int {
38 | return 2
39 | }
40 |
41 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
42 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
43 | cell.accessoryType = .disclosureIndicator
44 | if indexPath.row == 0 {
45 | cell.textLabel?.text = "查看Crash日志"
46 | }else if indexPath.row == 1 {
47 | cell.textLabel?.text = "清理Crash日志"
48 | }
49 | return cell
50 | }
51 |
52 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
53 | tableView.deselectRow(at: indexPath, animated: true)
54 | if indexPath.row == 0 {
55 | let vc = CrashListViewController(dataSource: CrashFileManager.allFiles())
56 | self.navigationController?.pushViewController(vc, animated: true)
57 | }else if indexPath.row == 1 {
58 | let alert = UIAlertController(title: "提示", message: "确认删除所有Crash日志吗?", preferredStyle: .alert)
59 | alert.addAction(UIAlertAction(title: "取消", style: .cancel, handler: nil))
60 | alert.addAction(UIAlertAction(title: "确定", style: .destructive, handler: { (action) in
61 | CrashFileManager.deleteAllFiles()
62 | }))
63 | present(alert, animated: true, completion: nil)
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/UI/Crash/CrashListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CrashListViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/21.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class CrashListViewController: FileListViewController {
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 |
16 | title = "Crash日志列表"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/UI/FPS/FPSDashboardViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FPSDashboardViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/26.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class FPSDashboardViewController: UITableViewController {
12 | let soldier: FPSSoldier
13 |
14 | init(soldier: FPSSoldier) {
15 | self.soldier = soldier
16 | super.init(style: .plain)
17 | }
18 |
19 | required init?(coder aDecoder: NSCoder) {
20 | fatalError("init(coder:) has not been implemented")
21 | }
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 |
26 | title = "FPS"
27 | tableView.register(DashboardCell.self, forCellReuseIdentifier: "cell")
28 | tableView.tableFooterView = UIView()
29 | }
30 |
31 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
32 | return 1
33 | }
34 |
35 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
36 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! DashboardCell
37 | cell.textLabel?.text = "FPS检测开关"
38 | cell.toggle.isOn = soldier.isActive
39 | cell.toggleValueDidChange = {[weak self] (isOn) in
40 | if isOn {
41 | self?.soldier.start()
42 | }else {
43 | self?.soldier.end()
44 | }
45 | }
46 | return cell
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/UI/Main/BaseTableViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseTableViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/9/9.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class BaseTableViewController: UITableViewController {
12 | override func viewDidLoad() {
13 | super.viewDidLoad()
14 |
15 | view.backgroundColor = .white
16 | }
17 |
18 | override var preferredStatusBarStyle: UIStatusBarStyle {
19 | return .default
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/UI/Main/BaseViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/23.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class BaseViewController: UIViewController {
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 |
16 | view.backgroundColor = .white
17 | }
18 |
19 | override var preferredStatusBarStyle: UIStatusBarStyle {
20 | return .default
21 | }
22 | }
23 |
24 | class BaseNavigationController: UINavigationController {
25 | override var childForStatusBarStyle: UIViewController? {
26 | return topController()
27 | }
28 |
29 | override var childForStatusBarHidden: UIViewController? {
30 | return topController()
31 | }
32 | }
33 |
34 | extension UIViewController {
35 | func topController() -> UIViewController? {
36 | if let navi = self as? UINavigationController {
37 | return navi.topViewController?.topController()
38 | }else if let tabbar = self as? UITabBarController {
39 | return tabbar.selectedViewController?.topController()
40 | }else if presentedViewController != nil {
41 | return presentedViewController?.topController()
42 | }else {
43 | return self
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/UI/Main/CaptainFloatingViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptainFloatingViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/21.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class CaptainFloatingViewController: BaseViewController {
12 | var shieldButton: UIButton!
13 | let shieldWidth: CGFloat = 50
14 | var newEventView: UIView!
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 |
19 | view.backgroundColor = .clear
20 |
21 | shieldButton = UIButton(type: .custom)
22 | shieldButton.setImage(Captain.default.logoImage, for: .normal)
23 | shieldButton.layer.shadowOpacity = 0.6
24 | shieldButton.layer.shadowColor = UIColor.black.cgColor
25 | shieldButton.layer.shadowRadius = 3
26 | shieldButton.layer.shadowOffset = CGSize.zero
27 | let screenEdgeInsets = Captain.default.screenEdgeInsets
28 | let y = screenEdgeInsets.top + (view.bounds.size.height - screenEdgeInsets.top - screenEdgeInsets.bottom - shieldWidth)/2
29 | shieldButton.frame = CGRect(x: screenEdgeInsets.left, y: y, width: shieldWidth, height: shieldWidth)
30 | shieldButton.addTarget(self, action: #selector(shieldButtonDidClick), for: .touchUpInside)
31 | view.addSubview(shieldButton)
32 |
33 | newEventView = UIView()
34 | newEventView.isHidden = true
35 | newEventView.backgroundColor = UIColor.red
36 | newEventView.layer.cornerRadius = 4
37 | newEventView.frame = CGRect(x: shieldButton.bounds.size.width - 8, y: 0, width: 8, height: 8)
38 | shieldButton.addSubview(newEventView)
39 | refreshNewEventView()
40 |
41 | let pan = UIPanGestureRecognizer(target: self, action: #selector(processPan(_:)))
42 | shieldButton.addGestureRecognizer(pan)
43 |
44 | NotificationCenter.default.addObserver(self, selector: #selector(soldierNewEventDidChange), name: .JXCaptainSoldierNewEventDidChange, object: nil)
45 | //FIXME:如果要保证当前的状态栏跟目标APP当前页面一直,可以不断触发setNeedsStatusBarAppearanceUpdate方法,但是经过测试会带来额外的6%CPU开销。
46 | // let link = CADisplayLink(target: self, selector: #selector(processLink))
47 | // link.add(to: RunLoop.current, forMode: .common)
48 | }
49 |
50 | @objc func processLink() {
51 | setNeedsStatusBarAppearanceUpdate()
52 | }
53 |
54 | override var preferredStatusBarStyle: UIStatusBarStyle {
55 | if UIApplication.shared.keyWindow?.rootViewController?.topController() == self {
56 | return .default
57 | }else {
58 | return UIApplication.shared.keyWindow?.rootViewController?.topController()?.preferredStatusBarStyle ?? .default
59 | }
60 | }
61 |
62 | @objc func shieldButtonDidClick() {
63 | let navi = BaseNavigationController(rootViewController: SoldierListViewController())
64 | navi.modalPresentationStyle = .fullScreen
65 | present(navi, animated: true, completion: nil)
66 | }
67 |
68 | @objc func soldierNewEventDidChange() {
69 | DispatchQueue.main.async {
70 | self.refreshNewEventView()
71 | }
72 | }
73 |
74 | func refreshNewEventView() {
75 | newEventView.isHidden = true
76 | for soldier in Captain.default.soldiers {
77 | if soldier.hasNewEvent {
78 | newEventView.isHidden = false
79 | break
80 | }
81 | }
82 | }
83 |
84 | @objc func processPan(_ gesture: UIPanGestureRecognizer) {
85 | let point = gesture.location(in: view)
86 | let screenEdgeInsets = Captain.default.screenEdgeInsets
87 | let minCenterX = screenEdgeInsets.left + shieldWidth/2
88 | let maxCenterX = view.bounds.size.width - screenEdgeInsets.right - shieldWidth/2
89 | let minCenterY = screenEdgeInsets.top + shieldWidth/2
90 | let maxCenterY = view.bounds.size.height - screenEdgeInsets.bottom - shieldWidth/2
91 | let centerX = min(maxCenterX, max(minCenterX, point.x))
92 | let centerY = min(maxCenterY, max(minCenterY, point.y))
93 | if gesture.state == .began {
94 | UIView.animate(withDuration: 0.1) {
95 | self.shieldButton.center = point
96 | }
97 | }else if gesture.state == .changed {
98 | shieldButton.center = CGPoint(x:centerX , y: centerY)
99 | }else if gesture.state == .ended || gesture.state == .cancelled {
100 | UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: {
101 | var center = self.shieldButton.center
102 | if center.x > self.view.bounds.size.width/2 {
103 | center.x = maxCenterX
104 | }else {
105 | center.x = minCenterX
106 | }
107 | self.shieldButton.center = center
108 | }, completion: nil)
109 | }
110 | }
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/Sources/UI/Main/CaptainFloatingWindow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // floatingWindow.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/21.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class CaptainFloatingWindow: UIWindow {
12 | let floatingVC: CaptainFloatingViewController
13 |
14 | override init(frame: CGRect) {
15 | floatingVC = CaptainFloatingViewController()
16 | super.init(frame: UIScreen.main.bounds)
17 |
18 | windowLevel = UIWindow.Level(rawValue: UIWindow.Level.statusBar.rawValue - 1)
19 | rootViewController = floatingVC
20 | backgroundColor = .clear
21 | }
22 |
23 | required init?(coder aDecoder: NSCoder) {
24 | fatalError("init(coder:) has not been implemented")
25 | }
26 |
27 | override func layoutSubviews() {
28 | super.layoutSubviews()
29 |
30 | backgroundColor = .clear
31 | }
32 |
33 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
34 | let view = super.hitTest(point, with: event)
35 | if view == floatingVC.view {
36 | return nil
37 | }
38 | return view
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/UI/Memory/MemoryDashboardViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MemoryDashboardViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/26.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class MemoryDashboardViewController: UITableViewController {
12 | let soldier: MemorySoldier
13 |
14 | init(soldier: MemorySoldier) {
15 | self.soldier = soldier
16 | super.init(style: .plain)
17 | }
18 |
19 | required init?(coder aDecoder: NSCoder) {
20 | fatalError("init(coder:) has not been implemented")
21 | }
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 |
26 | title = "内存"
27 | tableView.register(DashboardCell.self, forCellReuseIdentifier: "cell")
28 | tableView.tableFooterView = UIView()
29 | }
30 |
31 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
32 | return 1
33 | }
34 |
35 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
36 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! DashboardCell
37 | cell.textLabel?.text = "内存检测开关"
38 | cell.toggle.isOn = soldier.isActive
39 | cell.toggleValueDidChange = {[weak self] (isOn) in
40 | if isOn {
41 | self?.soldier.start()
42 | }else {
43 | self?.soldier.end()
44 | }
45 | }
46 | return cell
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/UI/Model/NetworkFlowDetailCellModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkFlowDetailCellModel.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/9/2.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum NetworkFlowDetailCellType {
12 | case normal
13 | case requestBody
14 | case responseBody
15 | }
16 |
17 | struct NetworkFlowDetailCellModel {
18 | let type: NetworkFlowDetailCellType
19 | let text: NSAttributedString
20 |
21 | init(type: NetworkFlowDetailCellType = .normal, text: NSAttributedString) {
22 | self.type = type
23 | self.text = text
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/UI/Model/NetworkFlowDetailSectionModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkFlowDetailSectionModel.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/9/2.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct NetworkFlowDetailSectionModel {
12 | let title: String
13 | let items: [NetworkFlowDetailCellModel]
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/UI/Model/NetworkFlowModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkTransaction.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/29.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | //@property (nonatomic, copy) NSString *requestId;
12 | struct NetworkFlowModel {
13 | let requestID: String
14 | let request: URLRequest
15 | let response: URLResponse?
16 | let error: NSError?
17 |
18 | let statusCode: Int?
19 | let startDate: Date
20 | let endDate: Date
21 | let duration: TimeInterval //单位秒
22 |
23 | let requestBodyString: String
24 | let requestBodySize: String
25 | let urlString: String?
26 | let method: String
27 | let mimeType: String
28 | let uploadFlow: String
29 | let downFlow: String
30 | let durationString: String
31 | let startDateString: String
32 | let statusCodeString: String
33 | let errorString: String?
34 | let isStatusCodeError: Bool
35 | let isImageResponseData: Bool
36 | let isGif: Bool
37 | let mediaFileName: String
38 | let isVedio: Bool
39 | let isAudio: Bool
40 |
41 | init(request: URLRequest, response: URLResponse?, responseData: Data?, error: NSError?, startDate: Date) {
42 | self.requestID = UUID().uuidString
43 | self.request = request
44 | self.response = response
45 | self.startDate = startDate
46 | self.error = error
47 |
48 | let defaultString = "Unknown"
49 | requestBodyString = NetworkManager.jsonString(from: NetworkManager.httpBody(request: request) ?? Data()) ?? defaultString
50 | if response != nil && responseData != nil {
51 | mimeType = response!.mimeType ?? defaultString
52 | downFlow = NetworkManager.flowLengthString(NetworkManager.responseFlowLength(response!, responseData: responseData!))
53 | }else {
54 | mimeType = defaultString
55 | downFlow = defaultString
56 | }
57 | isImageResponseData = mimeType.hasPrefix("image/")
58 | isGif = mimeType.contains("gif")
59 | isVedio = mimeType.hasPrefix("video/")
60 | isAudio = mimeType.hasPrefix("audio/")
61 | if request.url?.pathExtension.isEmpty == false {
62 | mediaFileName = request.url?.pathComponents.last ?? "unknown"
63 | }else {
64 | if let disposition = (response as? HTTPURLResponse)?.allHeaderFields["Content-Disposition"] as? String {
65 | let scanner = Scanner(string: disposition)
66 | scanner.scanUpTo("filename=\"", into: nil)
67 | scanner.scanString("filename=\"", into: nil)
68 | var result: NSString?
69 | scanner.scanUpTo("\"", into: &result)
70 | if result != nil {
71 | mediaFileName = result! as String
72 | }else {
73 | mediaFileName = "unknown"
74 | }
75 | }else {
76 | mediaFileName = "unknown"
77 | }
78 | }
79 |
80 | errorString = error?.localizedDescription
81 | urlString = request.url?.absoluteString
82 | method = request.httpMethod ?? defaultString
83 | statusCode = (response as? HTTPURLResponse)?.statusCode
84 | if statusCode != nil {
85 | statusCodeString = "\(statusCode!) \(HTTPURLResponse.localizedString(forStatusCode: statusCode!))"
86 | let errorStatusCodes = IndexSet(integersIn: Range.init(NSRange(location: 400, length: 200))!)
87 | isStatusCodeError = errorStatusCodes.contains(statusCode!)
88 | }else {
89 | statusCodeString = defaultString
90 | isStatusCodeError = false
91 | }
92 | endDate = Date()
93 | duration = endDate.timeIntervalSince(startDate)
94 | if duration > 1 {
95 | durationString = String(format: "%.1fs", duration)
96 | }else {
97 | durationString = String(format: "%.fms", duration * 1000)
98 | }
99 | let dateFormatter = DateFormatter()
100 | dateFormatter.timeZone = TimeZone.current
101 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
102 | startDateString = dateFormatter.string(from: startDate)
103 | requestBodySize = NetworkManager.flowLengthString(NetworkManager.httpBody(request: request)?.count ?? 0)
104 | uploadFlow = NetworkManager.flowLengthString(NetworkManager.requestFlowLength(request))
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/UI/Model/SanboxModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SanboxModel.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/21.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct SanboxModel {
12 | public let fileURL: URL
13 | public let name: String
14 | public init(fileURL: URL, name: String) {
15 | self.fileURL = fileURL
16 | self.name = name
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/UI/Model/SoldierListSectionModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SoldierListCellModel.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/22.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct SoldierListSectionModel {
12 | let teamName: String
13 | let soldiers: [Soldier]
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/UI/Model/UserDefaultsKeyValueModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsKeyValueModel.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/9/9.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum UserDefaultsVaule {
12 | case string(String)
13 | case number(NSNumber)
14 | case date(Date)
15 | case data(Data)
16 | case array([Any])
17 | case dictionary([String:Any])
18 | }
19 |
20 | struct UserDefaultsKeyValueModel {
21 | let key: String
22 | let value: UserDefaultsVaule
23 |
24 | init(key: String, value: Any, userDefaults: UserDefaults) {
25 | self.key = key
26 | if let date = userDefaults.object(forKey: key) as? Date {
27 | self.value = .date(date)
28 | }else if let array = userDefaults.array(forKey: key) {
29 | self.value = .array(array)
30 | }else if let dict = userDefaults.dictionary(forKey: key) {
31 | self.value = .dictionary(dict)
32 | }else if let string = userDefaults.string(forKey: key) {
33 | self.value = .string(string)
34 | }else if let data = userDefaults.data(forKey: key) {
35 | self.value = .data(data)
36 | }else if let number = userDefaults.object(forKey: key) as? NSNumber {
37 | self.value = .number(number)
38 | }else {
39 | self.value = .string("unknown")
40 | }
41 | }
42 |
43 | func valueDescription() -> String {
44 | switch value {
45 | case .date(let date):
46 | return date.description
47 | case .array(let array):
48 | return array.description
49 | case .dictionary(let dict):
50 | return dict.description
51 | case .string(let string):
52 | return string
53 | case .data(let data):
54 | return data.description
55 | case .number(let number):
56 | return number.description
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/UI/NetworkObserver/NetworkFlowDetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkFlowDetailViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/9/2.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class NetworkFlowDetailViewController: UITableViewController {
12 | let flowModel: NetworkFlowModel
13 | var dataSource = [NetworkFlowDetailSectionModel]()
14 |
15 | init(flowModel: NetworkFlowModel) {
16 | self.flowModel = flowModel
17 | super.init(style: .grouped)
18 |
19 | let request = flowModel.request
20 | let response = flowModel.response
21 | var responseBodyCellModel: NetworkFlowDetailCellModel?
22 | if NetworkObserverSoldier.shared.cache.object(forKey: flowModel.requestID as AnyObject) == nil {
23 | responseBodyCellModel = cellModel(title: "Response Body", detail: "no in cache")
24 | }else {
25 | responseBodyCellModel = cellModel(type: .responseBody, title: "Response Body", detail: "tap to view")
26 | }
27 | var generalItems = [cellModel(title: "Request URL", detail: request.url?.absoluteString),
28 | cellModel(title: "Request Method", detail: request.httpMethod),
29 | cellModel(title: "Request Body Size", detail: flowModel.requestBodySize),
30 | cellModel(type: .requestBody, title: "Request Body", detail: "tap to view"),
31 | cellModel(title: "Status Code", detail: flowModel.statusCodeString),
32 | responseBodyCellModel!,
33 | cellModel(title: "Response Size", detail: flowModel.downFlow),
34 | cellModel(title: "MIME Type", detail: flowModel.mimeType),
35 | cellModel(title: "Start Time", detail: flowModel.startDateString),
36 | cellModel(title: "Total Duration", detail: flowModel.durationString)]
37 | if flowModel.errorString != nil {
38 | generalItems.insert(cellModel(title: "Error", detail: flowModel.errorString), at: 2)
39 | }
40 | let generalSection = NetworkFlowDetailSectionModel(title: "General", items: generalItems)
41 | dataSource.append(generalSection)
42 |
43 | if let headerFileds = request.allHTTPHeaderFields, !headerFileds.isEmpty {
44 | var requestItems = [NetworkFlowDetailCellModel]()
45 | for (key, value) in headerFileds {
46 | requestItems.append(cellModel(title: key, detail: value))
47 | }
48 | dataSource.append(NetworkFlowDetailSectionModel(title: "Request Headers", items: requestItems))
49 | }
50 |
51 | if let headerFileds = (response as? HTTPURLResponse)?.allHeaderFields, !headerFileds.isEmpty {
52 | var responseItems = [NetworkFlowDetailCellModel]()
53 | for (key, value) in headerFileds {
54 | guard let keyString = key as? String else {
55 | continue
56 | }
57 | if let valueString = value as? String {
58 | responseItems.append(cellModel(title: keyString, detail: valueString))
59 | }else if let valueArray = value as? [String] {
60 | let valueString = valueArray.reduce("") { (result, item) -> String in
61 | return "\(result),\(item)"
62 | }
63 | responseItems.append(cellModel(title: keyString, detail: valueString))
64 | }else if let valueDict = value as? [String : String] {
65 | let valueString = valueDict.reduce("") { (result, valueTuple) -> String in
66 | return "\(result),\(valueTuple.key)=\(valueTuple.value)"
67 | }
68 | responseItems.append(cellModel(title: keyString, detail: valueString))
69 | }
70 | }
71 | dataSource.append(NetworkFlowDetailSectionModel(title: "Response Headers", items: responseItems))
72 | }
73 | }
74 |
75 | required init?(coder aDecoder: NSCoder) {
76 | fatalError("init(coder:) has not been implemented")
77 | }
78 |
79 | override func viewDidLoad() {
80 | super.viewDidLoad()
81 |
82 | title = "请求详情"
83 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
84 | }
85 |
86 | func cellModel(type: NetworkFlowDetailCellType = .normal, title: String, detail: String?) -> NetworkFlowDetailCellModel {
87 | return NetworkFlowDetailCellModel(type: type, text: cellText(title: title, detail: detail))
88 | }
89 |
90 | func cellText(title: String, detail: String?) -> NSAttributedString {
91 | let wholeString = "\(title):\(detail ?? "unknown")"
92 | let text = NSMutableAttributedString(string: wholeString, attributes: [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 13), NSAttributedString.Key.foregroundColor : UIColor.black])
93 | text.addAttributes([NSAttributedString.Key.font : UIFont.systemFont(ofSize: 15, weight: .medium), NSAttributedString.Key.foregroundColor : UIColor.gray], range: NSString(string: wholeString).range(of: "\(title):"))
94 | return text
95 | }
96 |
97 | //MARK: - UITableViewDataSource & UITableViewDelegate
98 | override func numberOfSections(in tableView: UITableView) -> Int {
99 | return dataSource.count
100 | }
101 |
102 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
103 | return dataSource[section].title
104 | }
105 |
106 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
107 | return dataSource[section].items.count
108 | }
109 |
110 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
111 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
112 | let cellModel = dataSource[indexPath.section].items[indexPath.row]
113 | if cellModel.type == .normal {
114 | cell.accessoryType = .none
115 | cell.selectionStyle = .none
116 | }else {
117 | cell.accessoryType = .disclosureIndicator
118 | cell.selectionStyle = .default
119 | }
120 | cell.textLabel?.numberOfLines = 0
121 | cell.textLabel?.attributedText = cellModel.text
122 | return cell
123 | }
124 |
125 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
126 | tableView.deselectRow(at: indexPath, animated: true)
127 | UIMenuController.shared.setMenuVisible(false, animated: true)
128 | let cellModel = dataSource[indexPath.section].items[indexPath.row]
129 | if cellModel.type != .normal {
130 | let vc = NetworkFlowResponseDataDetailViewController(flowModel: flowModel, cellType: cellModel.type)
131 | navigationController?.pushViewController(vc, animated: true)
132 | }
133 | }
134 |
135 | override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
136 | return true
137 | }
138 |
139 | override func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
140 | if action == #selector(copy(_:)) {
141 | return true
142 | }else {
143 | return false
144 | }
145 | }
146 |
147 | override func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
148 | if action == #selector(copy(_:)) {
149 | let cellModel = dataSource[indexPath.section].items[indexPath.row]
150 | UIPasteboard.general.string = cellModel.text.string
151 | }
152 | }
153 |
154 | override func scrollViewDidScroll(_ scrollView: UIScrollView) {
155 | UIMenuController.shared.setMenuVisible(false, animated: true)
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/Sources/UI/NetworkObserver/NetworkFlowListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkFlowListViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/30.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class NetworkFlowListViewController: UITableViewController {
12 | let soldier: NetworkObserverSoldier
13 | var dataSource: [NetworkFlowModel]
14 | var searchController: UISearchController!
15 | var filteredDataSource = [NetworkFlowModel]()
16 |
17 | init(soldier: NetworkObserverSoldier) {
18 | self.soldier = soldier
19 | dataSource = soldier.flowModels
20 | super.init(style: .plain)
21 |
22 | }
23 |
24 | required init?(coder aDecoder: NSCoder) {
25 | fatalError("init(coder:) has not been implemented")
26 | }
27 |
28 | override func viewDidLoad() {
29 | super.viewDidLoad()
30 |
31 | title = "请求列表"
32 |
33 | searchController = UISearchController(searchResultsController: nil)
34 | searchController.searchBar.placeholder = "搜索URL"
35 | searchController.hidesNavigationBarDuringPresentation = false
36 | searchController.searchResultsUpdater = self
37 | searchController.dimsBackgroundDuringPresentation = false
38 |
39 | tableView.register(NetworkFlowListCell.self, forCellReuseIdentifier: "cell")
40 | tableView.tableHeaderView = searchController.searchBar
41 |
42 | NotificationCenter.default.addObserver(self, selector: #selector(newFlowDidReceive(noti:)), name: NSNotification.Name.JXCaptainNetworkObserverSoldierNewFlowDidReceive, object: nil)
43 | }
44 |
45 | @objc func newFlowDidReceive(noti: Notification) {
46 | guard let flowModel = noti.object as? NetworkFlowModel else {
47 | return
48 | }
49 | dataSource.insert(flowModel, at: 0)
50 | if searchController.isActive, let searchText = searchController.searchBar.text {
51 | if flowModel.request.url?.absoluteString.range(of: searchText, options: .caseInsensitive) != nil {
52 | filteredDataSource.insert(flowModel, at: 0)
53 | tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .top)
54 | }
55 | }else {
56 | tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .top)
57 | }
58 | }
59 |
60 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
61 | if searchController.isActive {
62 | return filteredDataSource.count
63 | }else {
64 | return dataSource.count
65 | }
66 | }
67 |
68 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
69 | return 55
70 | }
71 |
72 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
73 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! NetworkFlowListCell
74 | var flowModel: NetworkFlowModel!
75 | if searchController.isActive {
76 | flowModel = filteredDataSource[indexPath.row]
77 | }else {
78 | flowModel = dataSource[indexPath.row]
79 | }
80 | if flowModel.errorString != nil {
81 | cell.urlLabel.textColor = .red
82 | cell.infoLabel.text = "\(flowModel.method) · \(flowModel.downFlow) · \(flowModel.durationString) · \(flowModel.startDateString)"
83 | }else {
84 | cell.urlLabel.textColor = .black
85 | let infoString = "\(flowModel.method) · \(flowModel.statusCode ?? -1) · \(flowModel.downFlow) · \(flowModel.durationString) · \(flowModel.startDateString)"
86 | if flowModel.isStatusCodeError {
87 | let statusCodeString = "\(flowModel.statusCode ?? -1)"
88 | let infoText = NSMutableAttributedString(string: infoString, attributes: [NSAttributedString.Key.foregroundColor : UIColor.gray])
89 | infoText.addAttribute(.foregroundColor, value: UIColor.red, range: NSString(string: infoString).range(of: statusCodeString))
90 | cell.infoLabel.attributedText = infoText
91 | }else {
92 | cell.infoLabel.text = infoString
93 | }
94 | }
95 | cell.urlLabel.text = flowModel.urlString
96 | return cell
97 | }
98 |
99 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
100 | tableView.deselectRow(at: indexPath, animated: true)
101 | var flowModel: NetworkFlowModel!
102 | if searchController.isActive {
103 | flowModel = filteredDataSource[indexPath.row]
104 | }else {
105 | flowModel = dataSource[indexPath.row]
106 | }
107 | searchController.isActive = false
108 | let vc = NetworkFlowDetailViewController(flowModel: flowModel)
109 | navigationController?.pushViewController(vc, animated: true)
110 | }
111 | }
112 |
113 | extension NetworkFlowListViewController: UISearchResultsUpdating {
114 | func updateSearchResults(for searchController: UISearchController) {
115 | if searchController.searchBar.text?.isEmpty == false {
116 | filteredDataSource.removeAll()
117 | for flowModel in dataSource {
118 | if flowModel.urlString?.range(of: searchController.searchBar.text!, options: .caseInsensitive) != nil {
119 | filteredDataSource.append(flowModel)
120 | }
121 | }
122 | }else {
123 | filteredDataSource = dataSource
124 | }
125 | tableView.reloadData()
126 | }
127 | }
128 |
129 |
130 | class NetworkFlowListCell: UITableViewCell {
131 | let urlLabel: UILabel
132 | let infoLabel: UILabel
133 |
134 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
135 | urlLabel = UILabel()
136 | infoLabel = UILabel()
137 | super.init(style: style, reuseIdentifier: reuseIdentifier)
138 |
139 | accessoryType = .disclosureIndicator
140 |
141 | urlLabel.textColor = .black
142 | urlLabel.font = .systemFont(ofSize: 13)
143 | urlLabel.translatesAutoresizingMaskIntoConstraints = false
144 | urlLabel.numberOfLines = 2
145 | contentView.addSubview(urlLabel)
146 | urlLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5).isActive = true
147 | urlLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12).isActive = true
148 | urlLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12).isActive = true
149 |
150 | infoLabel.textColor = .gray
151 | infoLabel.font = .systemFont(ofSize: 10)
152 | infoLabel.translatesAutoresizingMaskIntoConstraints = false
153 | infoLabel.numberOfLines = 1
154 | contentView.addSubview(infoLabel)
155 | infoLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5).isActive = true
156 | infoLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12).isActive = true
157 | infoLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12).isActive = true
158 | }
159 |
160 | required init?(coder aDecoder: NSCoder) {
161 | fatalError("init(coder:) has not been implemented")
162 | }
163 |
164 | override func prepareForReuse() {
165 | super.prepareForReuse()
166 |
167 | infoLabel.attributedText = nil
168 | infoLabel.text = nil
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/Sources/UI/NetworkObserver/NetworkFlowResponseDataDetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkFlowDetailTextViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/9/2.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 | import AVKit
12 |
13 | class NetworkFlowResponseDataDetailViewController: BaseViewController, UIScrollViewDelegate {
14 | let flowModel: NetworkFlowModel
15 | let cellType: NetworkFlowDetailCellType
16 | var text: String?
17 | var image: UIImage?
18 | var mediaURL: URL?
19 | var previewImageView: UIImageView?
20 | var previewScrollView: UIScrollView?
21 | var previewTextView: UITextView?
22 | var previewPlayerController: AVPlayerViewController?
23 |
24 | init(flowModel: NetworkFlowModel, cellType: NetworkFlowDetailCellType) {
25 | self.flowModel = flowModel
26 | self.cellType = cellType
27 | self.text = nil
28 | self.image = nil
29 | self.mediaURL = nil
30 | if cellType == .requestBody {
31 | self.text = flowModel.requestBodyString
32 | }else if cellType == .responseBody {
33 | if flowModel.isImageResponseData {
34 | self.image = NetworkManager.responseImage(requestID: flowModel.requestID)
35 | }else if flowModel.isVedio || flowModel.isAudio {
36 | let mediaData = NetworkManager.responseData(requestID: flowModel.requestID)
37 | let tempDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
38 | let tempMediaURL = tempDirectoryURL.appendingPathComponent(flowModel.mediaFileName)
39 | if (try? mediaData?.write(to: tempMediaURL)) != nil {
40 | self.mediaURL = tempMediaURL
41 | }
42 | }else {
43 | self.text = NetworkManager.responseJSON(requestID: flowModel.requestID)
44 | }
45 | }
46 | super.init(nibName: nil, bundle: nil)
47 | }
48 |
49 | required init?(coder aDecoder: NSCoder) {
50 | fatalError("init(coder:) has not been implemented")
51 | }
52 |
53 | override func viewDidLoad() {
54 | super.viewDidLoad()
55 |
56 | if cellType == .requestBody {
57 | title = "Request"
58 | }else {
59 | title = "Response"
60 | }
61 | view.backgroundColor = .white
62 |
63 | if image != nil {
64 | previewScrollView = UIScrollView()
65 | previewScrollView?.delegate = self
66 | previewScrollView?.minimumZoomScale = 1
67 | var imageWidthScale: CGFloat = 1
68 | if view.bounds.size.width > 0 {
69 | imageWidthScale = (image?.size.width ?? 0)/view.bounds.size.width
70 | }
71 | previewScrollView?.maximumZoomScale = max(2, imageWidthScale)
72 | view.addSubview(previewScrollView!)
73 |
74 | previewImageView = UIImageView()
75 | if flowModel.isGif {
76 | previewImageView?.animationImages = NetworkManager.responseImages(requestID: flowModel.requestID)
77 | previewImageView?.startAnimating()
78 | }else {
79 | previewImageView?.image = image
80 | }
81 | previewImageView?.contentMode = .scaleAspectFit
82 | previewScrollView?.addSubview(previewImageView!)
83 |
84 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Copy", style: .plain, target: self, action: #selector(copyItemDidClick))
85 | }else if text != nil {
86 | initTextView(with: text!)
87 |
88 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Copy", style: .plain, target: self, action: #selector(copyItemDidClick))
89 | }else if mediaURL != nil {
90 | previewPlayerController = AVPlayerViewController()
91 | previewPlayerController?.player = AVPlayer(url: mediaURL!)
92 | addChild(previewPlayerController!)
93 | view.addSubview(previewPlayerController!.view)
94 | }
95 | }
96 |
97 | @objc func copyItemDidClick() {
98 | if text != nil {
99 | UIPasteboard.general.string = text
100 | }else if image != nil {
101 | UIPasteboard.general.image = image
102 | }
103 | }
104 |
105 | override func viewDidLayoutSubviews() {
106 | super.viewDidLayoutSubviews()
107 |
108 | previewTextView?.frame = view.bounds
109 | previewScrollView?.frame = view.bounds
110 | previewScrollView?.contentSize = CGSize(width: view.bounds.size.width, height: view.bounds.size.height)
111 | var imageWidth = previewImageView?.image?.size.width ?? 0
112 | var imageHeight = previewImageView?.image?.size.width ?? 0
113 | if previewImageView?.animationImages?.isEmpty == false {
114 | imageWidth = previewImageView?.animationImages?.first?.size.width ?? 0
115 | imageHeight = previewImageView?.animationImages?.first?.size.height ?? 0
116 | }
117 | let imageViewWidth = min(imageWidth, view.bounds.size.width)
118 | let imageViewHeight = min(imageHeight, view.bounds.size.height)
119 | previewImageView?.bounds = CGRect(x: 0, y: 0, width: imageViewWidth, height: imageViewHeight)
120 | previewImageView?.center = CGPoint(x: view.bounds.size.width/2, y: view.bounds.size.height/2)
121 | previewPlayerController?.view.frame = view.bounds
122 | }
123 |
124 | func initTextView(with text: String) {
125 | previewTextView = UITextView()
126 | previewTextView?.isEditable = false
127 | previewTextView?.font = .systemFont(ofSize: 12)
128 | previewTextView?.textColor = .black
129 | previewTextView?.backgroundColor = .white
130 | previewTextView?.isScrollEnabled = true
131 | previewTextView?.textAlignment = .left
132 | previewTextView?.text = text
133 | view.addSubview(previewTextView!)
134 | }
135 |
136 | //MARK: - UIScrollViewDelegate
137 | func viewForZooming(in scrollView: UIScrollView) -> UIView? {
138 | return previewImageView
139 | }
140 |
141 | func scrollViewDidZoom(_ scrollView: UIScrollView) {
142 | var center = CGPoint(x: view.bounds.size.width/2, y: view.bounds.size.height/2)
143 | if scrollView.contentSize.width > view.bounds.size.width {
144 | center.x = scrollView.contentSize.width/2
145 | }
146 | if scrollView.contentSize.height > view.bounds.size.height {
147 | center.y = scrollView.contentSize.height/2
148 | }
149 | previewImageView?.center = center
150 | }
151 |
152 | }
153 |
--------------------------------------------------------------------------------
/Sources/UI/NetworkObserver/NetworkObserverDashboardViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkObserverDashboardViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/29.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class NetworkObserverDashboardViewController: UITableViewController {
12 | let soldier: NetworkObserverSoldier
13 |
14 | init(soldier: NetworkObserverSoldier) {
15 | self.soldier = soldier
16 | super.init(style: .plain)
17 | }
18 |
19 | required init?(coder aDecoder: NSCoder) {
20 | fatalError("init(coder:) has not been implemented")
21 | }
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 |
26 | title = "流量"
27 | tableView.register(DashboardCell.self, forCellReuseIdentifier: "swithCell")
28 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
29 | tableView.tableFooterView = UIView()
30 | }
31 |
32 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
33 | return 3
34 | }
35 |
36 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
37 | if indexPath.row == 0 {
38 | let cell = tableView.dequeueReusableCell(withIdentifier: "swithCell", for: indexPath) as! DashboardCell
39 | cell.textLabel?.text = "流量检测开关"
40 | cell.toggle.isOn = soldier.isActive
41 | cell.toggleValueDidChange = {[weak self] (isOn) in
42 | if isOn {
43 | self?.soldier.start()
44 | let alert = UIAlertController(title: nil, message: "需要重新启动APP才能生效!", preferredStyle: .alert)
45 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil))
46 | self?.present(alert, animated: true, completion: nil)
47 | }else {
48 | self?.soldier.end()
49 | }
50 | }
51 | return cell
52 | }else if indexPath.row == 1 {
53 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
54 | cell.accessoryType = .disclosureIndicator
55 | cell.textLabel?.text = "查看接口请求列表"
56 | return cell
57 | }else {
58 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
59 | cell.accessoryType = .disclosureIndicator
60 | cell.textLabel?.text = "清理接口请求记录"
61 | return cell
62 | }
63 | }
64 |
65 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
66 | tableView.deselectRow(at: indexPath, animated: true)
67 | if indexPath.row == 1 {
68 | let vc = NetworkFlowListViewController(soldier: soldier)
69 | self.navigationController?.pushViewController(vc, animated: true)
70 | }else if indexPath.row == 2 {
71 | let alert = UIAlertController(title: "提示", message: "确认删除所有接口请求记录吗?", preferredStyle: .alert)
72 | alert.addAction(UIAlertAction(title: "取消", style: .cancel, handler: nil))
73 | alert.addAction(UIAlertAction(title: "确定", style: .destructive, handler: { (action) in
74 | NetworkObserverSoldier.shared.flowModels.removeAll()
75 | }))
76 | present(alert, animated: true, completion: nil)
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/UI/SanboxBrowser/JXDatabaseConnector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JXDatabaseConnector.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/9/20.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SQLite3
11 |
12 | public class JXDatabaseConnector {
13 | let databasePath: String
14 | var db: OpaquePointer?
15 |
16 | public init(path: String) {
17 | databasePath = path
18 | }
19 |
20 | @discardableResult
21 | public func open() -> Bool {
22 | if db != nil {
23 | return true
24 | }
25 | let resultCode = sqlite3_open(databasePath, &db)
26 | if resultCode == SQLITE_OK {
27 | return true
28 | }
29 | print("open \(databasePath) error:\(resultCode)")
30 | return false
31 | }
32 |
33 | @discardableResult
34 | func close() -> Bool {
35 | guard let db = db else {
36 | return true
37 | }
38 | var resultCode: Int32 = 0
39 | var retry = false
40 | var triedFinalizingOpenStatements = false
41 | repeat {
42 | retry = false
43 | resultCode = sqlite3_close(db)
44 | if SQLITE_BUSY == resultCode || SQLITE_LOCKED == resultCode {
45 | if !triedFinalizingOpenStatements {
46 | triedFinalizingOpenStatements = true
47 | var pStmt: OpaquePointer?
48 | pStmt = sqlite3_next_stmt(db, nil)
49 | while pStmt != nil {
50 | sqlite3_finalize(pStmt)
51 | retry = true
52 | pStmt = sqlite3_next_stmt(db, nil)
53 | }
54 | }else if SQLITE_OK != resultCode {
55 | print("close \(databasePath) error:\(resultCode)")
56 | }
57 | }
58 | } while retry
59 | self.db = nil
60 | return true
61 | }
62 |
63 | public func allTables() -> [String] {
64 | let queryResult = executeQuery(sql: "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
65 | let result = queryResult.compactMap { (dict) -> String? in
66 | return dict["name"] as? String
67 | }
68 | return result
69 | }
70 |
71 | public func allColumns(with tableName: String) -> [String] {
72 | let queryResult = executeQuery(sql: "PRAGMA table_info('\(tableName)')")
73 | let result = queryResult.compactMap { (dict) -> String? in
74 | return dict["name"] as? String
75 | }
76 | return result
77 | }
78 |
79 | public func allData(with tableName: String) -> [[String:Any]] {
80 | return executeQuery(sql: "SELECT * FROM \(tableName)")
81 | }
82 |
83 | func executeQuery(sql: String) -> [[String:Any]] {
84 | open()
85 | var resultArray = [[String:Any]]()
86 | var pstmt: OpaquePointer?
87 | if sqlite3_prepare_v2(db, sql, -1, &pstmt, nil) == SQLITE_OK, pstmt != nil {
88 | while sqlite3_step(pstmt) == SQLITE_ROW {
89 | let dataNumber = sqlite3_data_count(pstmt)
90 | guard dataNumber > 0 else {
91 | continue
92 | }
93 | var dict = [String:Any].init(minimumCapacity: Int(dataNumber))
94 | let columnCount = sqlite3_column_count(pstmt)
95 | for index in 0.. Any? {
110 | let type = sqlite3_column_type(stmt, columnIndex)
111 | var resultValue: Any?
112 | if type == SQLITE_INTEGER {
113 | resultValue = sqlite3_column_int64(stmt, columnIndex)
114 | }else if type == SQLITE_FLOAT {
115 | resultValue = sqlite3_column_double(stmt, columnIndex)
116 | }else if type == SQLITE_BLOB {
117 | resultValue = dataForColumnIndex(columnIndex, stmt: stmt)
118 | }else {
119 | resultValue = stringForColumnIndex(columnIndex, stmt: stmt)
120 | }
121 | return resultValue
122 | }
123 |
124 | func dataForColumnIndex(_ columnIndex: Int32, stmt: OpaquePointer) -> Data? {
125 | if sqlite3_column_type(stmt, columnIndex) == SQLITE_NULL || columnIndex < 0 {
126 | return nil
127 | }
128 | let dataBuffer = sqlite3_column_blob(stmt, columnIndex)
129 | let dataSize = sqlite3_column_bytes(stmt, columnIndex)
130 | if dataBuffer == nil {
131 | return nil
132 | }
133 | return Data(bytes: dataBuffer!, count: Int(dataSize))
134 | }
135 |
136 | func stringForColumnIndex(_ columnIndex: Int32, stmt: OpaquePointer) -> String? {
137 | if sqlite3_column_type(stmt, columnIndex) == SQLITE_NULL || columnIndex < 0 {
138 | return nil
139 | }
140 | let cString = sqlite3_column_text(stmt, columnIndex)
141 | if cString == nil {
142 | return nil
143 | }
144 | return String(cString: cString!)
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/Sources/UI/SanboxBrowser/JXFileBrowserController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JXFileBrowserController.swift
3 | // JXFileBrowserController
4 | //
5 | // Created by jiaxin on 2018/7/6.
6 | // Copyright © 2018年 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | open class JXFileBrowserController: UIViewController {
12 | public let path: String!
13 | public var tableView: UITableView!
14 | public var dataSource: [String]!
15 | private let mainBundleResourcePath = "mainBundleResourcePath"
16 | private let emptyTips = "There are no files here."
17 |
18 | init(path: String) {
19 | self.path = path
20 | super.init(nibName: nil, bundle: nil)
21 | }
22 |
23 | required public init?(coder aDecoder: NSCoder) {
24 | fatalError("init(coder:) has not been implemented")
25 | }
26 |
27 | override open func viewDidLoad() {
28 | super.viewDidLoad()
29 |
30 | if path == NSHomeDirectory() {
31 | self.title = "NSHomeDirectory"
32 | }else {
33 | self.title = URL(fileURLWithPath: path).lastPathComponent
34 | }
35 |
36 | dataSource = [String]()
37 | if path == NSHomeDirectory(), Bundle.main.resourcePath != nil {
38 | dataSource.append(mainBundleResourcePath)
39 | }
40 | do {
41 | let fileNames = try FileManager.default.contentsOfDirectory(atPath: path)
42 | dataSource.append(contentsOf: fileNames)
43 | } catch let error {
44 | print("The error of contentsOfDirectory: %@", error)
45 | }
46 |
47 | tableView = UITableView(frame: self.view.bounds, style: .plain)
48 | tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
49 | tableView.dataSource = self
50 | tableView.delegate = self
51 | tableView.tableFooterView = UIView()
52 | view.addSubview(tableView)
53 |
54 | if dataSource.isEmpty {
55 | let emptyLabel = UILabel()
56 | emptyLabel.font = .systemFont(ofSize: 25)
57 | emptyLabel.text = emptyTips
58 | emptyLabel.textAlignment = .center
59 | tableView.backgroundView = emptyLabel
60 | }
61 | }
62 |
63 | func isImagePathExtension(filePath: String) -> Bool {
64 | return ["jpg", "jpeg", "png", "gif", "tiff", "tif"].contains(URL(fileURLWithPath: filePath).pathExtension)
65 | }
66 | }
67 |
68 | extension JXFileBrowserController: UITableViewDataSource, UITableViewDelegate {
69 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
70 | return dataSource.count
71 | }
72 |
73 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
74 | var cell = tableView.dequeueReusableCell(withIdentifier: "cell")
75 | if cell == nil {
76 | cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
77 | }
78 | cell?.accessoryType = .disclosureIndicator
79 | cell?.detailTextLabel?.textColor = UIColor.lightGray
80 | let source = dataSource[indexPath.row]
81 | cell?.textLabel?.text = source
82 | var fullPath = path + "/" + source
83 | if source == mainBundleResourcePath {
84 | fullPath = Bundle.main.resourcePath!
85 | }
86 | do {
87 | let attributes = try FileManager.default.attributesOfItem(atPath: fullPath)
88 | if attributes[FileAttributeKey.type] as! String == FileAttributeType.typeDirectory.rawValue {
89 | let count = (try? FileManager.default.contentsOfDirectory(atPath: fullPath).count) ?? 0
90 | cell?.detailTextLabel?.text = String(format: "%d file%@", count, ((count > 1) ? "s" : ""))
91 | }else {
92 | let fileSize = attributes[FileAttributeKey.size] as! Double
93 | let fileSizeString = ByteCountFormatter.string(fromByteCount: Int64(fileSize), countStyle: ByteCountFormatter.CountStyle.file)
94 | cell?.detailTextLabel?.text = fileSizeString
95 |
96 | if isImagePathExtension(filePath: fullPath) {
97 | cell?.imageView?.contentMode = .scaleAspectFit
98 | cell?.imageView?.clipsToBounds = true
99 | cell?.imageView?.image = UIImage(contentsOfFile: fullPath)
100 | }else {
101 | cell?.imageView?.image = nil
102 | }
103 | }
104 | } catch let error {
105 | print("The error of attributesOfItem: %@", error)
106 | }
107 | return cell!
108 | }
109 |
110 | public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
111 | tableView.deselectRow(at: indexPath, animated: true)
112 | let source = dataSource[indexPath.row]
113 | var fullPath = path + "/" + source
114 | if source == mainBundleResourcePath {
115 | fullPath = Bundle.main.resourcePath!
116 | }
117 | do {
118 | let attributes = try FileManager.default.attributesOfItem(atPath: fullPath)
119 | if attributes[FileAttributeKey.type] as? String == FileAttributeType.typeDirectory.rawValue {
120 | let fileBrowserController = JXFileBrowserController(path: fullPath)
121 | self.navigationController?.pushViewController(fileBrowserController, animated: true)
122 | }else {
123 | let fileExtension = URL(fileURLWithPath: fullPath).pathExtension.lowercased()
124 | if JXTableListViewController.supportsExtension(fileExtension) {
125 | let vc = JXTableListViewController(filePath: fullPath)
126 | navigationController?.pushViewController(vc, animated: true)
127 | }else if JXFilePreviewViewController.supportsExtension(fileExtension) {
128 | let previewVC = JXFilePreviewViewController(filePath: fullPath)
129 | self.navigationController?.pushViewController(previewVC, animated: true)
130 | }else {
131 | let sheet = UIAlertController(title: nil, message: "Unsupport this file, you can share it.", preferredStyle: .actionSheet)
132 | sheet.addAction(UIAlertAction(title: "Share", style: .default, handler: { (action) in
133 | let activityController = UIActivityViewController(activityItems: [URL(fileURLWithPath: fullPath)], applicationActivities: nil)
134 | self.present(activityController, animated: true, completion: nil)
135 | }))
136 | sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
137 | present(sheet, animated: true, completion: nil)
138 | }
139 | }
140 | } catch let error {
141 | let alert = UIAlertController(title: "The error of 'FileManager.default.attributesOfItem'", message: error.localizedDescription, preferredStyle: .alert)
142 | let confirm = UIAlertAction(title: "I know", style: .cancel, handler: nil)
143 | alert.addAction(confirm)
144 | self.present(alert, animated: true, completion: nil)
145 | print("The error of attributesOfItem: %@", error)
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Sources/UI/SanboxBrowser/JXFilePreviewViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JXFilePreviewViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/23.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 | import AVKit
12 |
13 | class JXFilePreviewViewController: UIViewController, UIScrollViewDelegate {
14 | let filePath: String
15 | var previewImageView: UIImageView?
16 | var previewScrollView: UIScrollView?
17 | var previewTextView: UITextView?
18 | var previewPlayerController: AVPlayerViewController?
19 |
20 | init(filePath: String) {
21 | self.filePath = filePath
22 | super.init(nibName: nil, bundle: nil)
23 | }
24 |
25 | required init?(coder aDecoder: NSCoder) {
26 | fatalError("init(coder:) has not been implemented")
27 | }
28 |
29 | override func viewDidLoad() {
30 | super.viewDidLoad()
31 |
32 | title = "File Preview"
33 | view.backgroundColor = .white
34 |
35 | let fileExtension = URL(fileURLWithPath: filePath).pathExtension.lowercased()
36 | if ["png", "jpg", "jpeg", "gif"].contains(fileExtension) {
37 | let image = UIImage(contentsOfFile: filePath)
38 | previewScrollView = UIScrollView()
39 | previewScrollView?.delegate = self
40 | previewScrollView?.minimumZoomScale = 1
41 | var imageWidthScale: CGFloat = 1
42 | if view.bounds.size.width > 0 {
43 | imageWidthScale = (image?.size.width ?? 0)/view.bounds.size.width
44 | }
45 | previewScrollView?.maximumZoomScale = max(2, imageWidthScale)
46 | view.addSubview(previewScrollView!)
47 |
48 | previewImageView = UIImageView()
49 | if fileExtension == "gif" {
50 | let url = URL(fileURLWithPath: filePath)
51 | if let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) {
52 | let imageCount = CGImageSourceGetCount(imageSource)
53 | var images = [UIImage]()
54 | for index in 0.. String in
73 | return "\(result)\n\(item.key):\(item.value)"
74 | }
75 | initTextView(with: text)
76 | }else {
77 | initTextView(with: "该文件没有内容")
78 | }
79 | }else if "strings" == fileExtension {
80 | if let data = FileManager.default.contents(atPath: filePath), let keyValues = try? PropertyListSerialization.propertyList(from: data, options: .mutableContainersAndLeaves, format: nil) as? [String] {
81 | let text = keyValues.reduce("") { (result, item) -> String in
82 | return "\(result)\n\(item)"
83 | }
84 | initTextView(with: text)
85 | }else {
86 | initTextView(with: "该文件没有内容")
87 | }
88 | }else {
89 | if let data = FileManager.default.contents(atPath: filePath), let text = String.init(data: data, encoding: .utf8) {
90 | initTextView(with: text)
91 | }else {
92 | initTextView(with: "该文件没有内容")
93 | }
94 | }
95 | }else if ["mp4", "mov", "3gp", "m4v", "avi", "aac", "mp3", "m4a", "flac", "wav", "ac3", "aa", "aax"].contains(fileExtension) {
96 | let player = AVPlayer(url: URL(fileURLWithPath: filePath))
97 | previewPlayerController = AVPlayerViewController()
98 | previewPlayerController?.player = player
99 | addChild(previewPlayerController!)
100 | view.addSubview(previewPlayerController!.view)
101 | }else {
102 | initTextView(with: "不支持该文件类型\(URL(fileURLWithPath: filePath).pathExtension)")
103 | }
104 |
105 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Share", style: .plain, target: self, action: #selector(didNaviShareItemClick))
106 | }
107 |
108 | @objc func didNaviShareItemClick() {
109 | let activityController = UIActivityViewController(activityItems: [URL(fileURLWithPath: filePath)], applicationActivities: nil)
110 | self.present(activityController, animated: true, completion: nil)
111 | }
112 |
113 | class func supportsExtension(_ extension: String) -> Bool {
114 | let extensions = ["png", "jpg", "jpeg", "gif", "strings", "plist", "txt", "log", "csv", "mp4", "mov", "3gp", "m4v", "avi", "aac", "mp3", "m4a", "flac", "wav", "ac3", "aa", "aax"]
115 | return extensions.contains(`extension`)
116 | }
117 |
118 | override func viewDidDisappear(_ animated: Bool) {
119 | super.viewDidDisappear(animated)
120 |
121 | previewPlayerController?.player?.pause()
122 | }
123 |
124 | override func viewDidLayoutSubviews() {
125 | super.viewDidLayoutSubviews()
126 |
127 | previewTextView?.frame = view.bounds
128 | previewScrollView?.frame = view.bounds
129 | previewScrollView?.contentSize = CGSize(width: view.bounds.size.width, height: view.bounds.size.height)
130 | var imageWidth = previewImageView?.image?.size.width ?? 0
131 | var imageHeight = previewImageView?.image?.size.width ?? 0
132 | if previewImageView?.animationImages?.isEmpty == false {
133 | imageWidth = previewImageView?.animationImages?.first?.size.width ?? 0
134 | imageHeight = previewImageView?.animationImages?.first?.size.height ?? 0
135 | }
136 | let imageViewWidth = min(imageWidth, view.bounds.size.width)
137 | let imageViewHeight = min(imageHeight, view.bounds.size.height)
138 | previewImageView?.bounds = CGRect(x: 0, y: 0, width: imageViewWidth, height: imageViewHeight)
139 | previewImageView?.center = CGPoint(x: view.bounds.size.width/2, y: view.bounds.size.height/2)
140 | previewPlayerController?.view.frame = view.bounds
141 | }
142 |
143 | func initTextView(with text: String) {
144 | previewTextView = UITextView()
145 | previewTextView?.isEditable = false
146 | previewTextView?.font = .systemFont(ofSize: 12)
147 | previewTextView?.textColor = .black
148 | previewTextView?.backgroundColor = .white
149 | previewTextView?.isScrollEnabled = true
150 | previewTextView?.textAlignment = .left
151 | previewTextView?.text = text
152 | view.addSubview(previewTextView!)
153 | }
154 |
155 | //MARK: - UIScrollViewDelegate
156 | func viewForZooming(in scrollView: UIScrollView) -> UIView? {
157 | return previewImageView
158 | }
159 |
160 | func scrollViewDidZoom(_ scrollView: UIScrollView) {
161 | var center = CGPoint(x: view.bounds.size.width/2, y: view.bounds.size.height/2)
162 | if scrollView.contentSize.width > view.bounds.size.width {
163 | center.x = scrollView.contentSize.width/2
164 | }
165 | if scrollView.contentSize.height > view.bounds.size.height {
166 | center.y = scrollView.contentSize.height/2
167 | }
168 | previewImageView?.center = center
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/Sources/UI/SanboxBrowser/JXTableContentViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JXTableContentViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/9/20.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class JXTableContentViewController: UIViewController {
12 | let filePath: String
13 | let tableName: String
14 | let excelView: ExcelView
15 | let connector: JXDatabaseConnector
16 | let allColumns: [String]
17 | let allDatabaseData: [[String:Any]]
18 | let allRowStrings: [[String]]
19 |
20 | init(filePath: String, tableName: String) {
21 | self.filePath = filePath
22 | self.tableName = tableName
23 | connector = JXDatabaseConnector(path: filePath)
24 | allColumns = connector.allColumns(with: tableName)
25 | allDatabaseData = connector.allData(with: tableName)
26 | var tempAllRowStrings = [[String]]()
27 | for row in 0.. Int {
82 | return allDatabaseData.count
83 | }
84 |
85 | func numberOfColumns(in excelView: ExcelView) -> Int {
86 | return allColumns.count
87 | }
88 |
89 | func excelView(_ excelView: ExcelView, rowNameAt row: Int) -> String {
90 | return "\(row)"
91 | }
92 |
93 | func excelView(_ excelView: ExcelView, columnNameAt column: Int) -> String {
94 | return allColumns[column]
95 | }
96 |
97 | func excelView(_ excelView: ExcelView, rowDatasAt row: Int) -> [String] {
98 | return allRowStrings[row]
99 | }
100 |
101 | func excelView(_ excelView: ExcelView, rowHeightAt row: Int) -> CGFloat {
102 | return 40
103 | }
104 |
105 | func excelView(_ excelView: ExcelView, columnWidthAt column: Int) -> CGFloat {
106 | return 120
107 | }
108 |
109 | func widthOfLeftHeader(in excelView: ExcelView) -> CGFloat {
110 | return 40
111 | }
112 |
113 | func heightOfTopHeader(in excelView: ExcelView) -> CGFloat {
114 | return 40
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Sources/UI/SanboxBrowser/JXTableListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JXTableListViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/9/20.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class JXTableListViewController: UITableViewController {
12 | let filePath: String
13 | let tableNames: [String]
14 |
15 | init(filePath: String) {
16 | self.filePath = filePath
17 | let connector = JXDatabaseConnector(path: filePath)
18 | tableNames = connector.allTables()
19 | super.init(nibName: nil, bundle: nil)
20 | }
21 |
22 | required init?(coder aDecoder: NSCoder) {
23 | fatalError("init(coder:) has not been implemented")
24 | }
25 |
26 | override func viewDidLoad() {
27 | super.viewDidLoad()
28 |
29 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
30 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Share", style: .plain, target: self, action: #selector(didNaviShareItemClick))
31 | }
32 |
33 | class func supportsExtension(_ extension: String) -> Bool {
34 | let extensions = ["db", "sqlite", "sqlite3"]
35 | return extensions.contains(`extension`)
36 | }
37 |
38 | @objc func didNaviShareItemClick() {
39 | let activityController = UIActivityViewController(activityItems: [URL(fileURLWithPath: filePath)], applicationActivities: nil)
40 | self.present(activityController, animated: true, completion: nil)
41 | }
42 |
43 | //MARK: - UITableViewDataSource & UITableViewDelegate
44 |
45 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
46 | return tableNames.count
47 | }
48 |
49 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
50 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
51 | cell.textLabel?.text = tableNames[indexPath.row]
52 | return cell
53 | }
54 |
55 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
56 | let vc = JXTableContentViewController(filePath: filePath, tableName: tableNames[indexPath.row])
57 | navigationController?.pushViewController(vc, animated: true)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/UI/SanboxBrowser/JXTextPreviewViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JXTextPreviewViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/10/8.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class JXTextPreviewViewController: UIViewController {
12 | let text: String
13 | var textView: UITextView!
14 |
15 | init(text: String) {
16 | self.text = text
17 | super.init(nibName: nil, bundle: nil)
18 | }
19 |
20 | required init?(coder: NSCoder) {
21 | fatalError("init(coder:) has not been implemented")
22 | }
23 |
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 |
27 | view.backgroundColor = .white
28 |
29 | textView = UITextView()
30 | textView.isEditable = false
31 | textView.font = .systemFont(ofSize: 12)
32 | textView.textColor = .black
33 | textView.backgroundColor = .white
34 | textView.isScrollEnabled = true
35 | textView.textAlignment = .left
36 | textView.text = text
37 | view.addSubview(textView)
38 |
39 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Copy", style: .plain, target: self, action: #selector(didNaviCopyItemClick))
40 | }
41 |
42 | @objc func didNaviCopyItemClick() {
43 | UIPasteboard.general.string = text
44 | }
45 |
46 | override func viewDidLayoutSubviews() {
47 | super.viewDidLayoutSubviews()
48 |
49 | let margin: CGFloat = 12
50 | textView.frame = view.bounds.inset(by: UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin))
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/UI/UserDefaults/UserDefaultsKeyValueDetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsKeyValueDetailViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/9/9.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class UserDefaultsKeyValueDetailViewController: BaseViewController {
12 | let defaults: UserDefaults
13 | var keyValueModel: UserDefaultsKeyValueModel
14 | var tipsLabel: UILabel!
15 | var inputTextView: UITextView!
16 |
17 | init(defaults: UserDefaults, keyValueModel: UserDefaultsKeyValueModel) {
18 | self.defaults = defaults
19 | self.keyValueModel = keyValueModel
20 | super.init(nibName: nil, bundle: nil)
21 | }
22 |
23 | required init?(coder aDecoder: NSCoder) {
24 | fatalError("init(coder:) has not been implemented")
25 | }
26 |
27 | override func viewDidLoad() {
28 | super.viewDidLoad()
29 |
30 | title = "Edit Value"
31 |
32 | let copy = UIBarButtonItem(title: "Copy", style: .plain, target: self, action: #selector(copyValue))
33 | let update = UIBarButtonItem(title: "Update", style: .plain, target: self, action: #selector(updateValue))
34 |
35 | tipsLabel = UILabel()
36 | tipsLabel.text = "Key:\(keyValueModel.key)"
37 | view.addSubview(tipsLabel)
38 |
39 | inputTextView = UITextView()
40 | inputTextView.font = .systemFont(ofSize: 17)
41 | inputTextView.keyboardType = .URL
42 | inputTextView.layer.borderColor = UIColor.lightGray.cgColor
43 | inputTextView.layer.borderWidth = 1
44 | inputTextView.text = keyValueModel.valueDescription()
45 | view.addSubview(inputTextView)
46 |
47 | inputTextView.isEditable = true
48 | navigationItem.rightBarButtonItems = [update, copy]
49 | switch keyValueModel.value {
50 | case .string(_):
51 | inputTextView.keyboardType = .default
52 | case .number(_):
53 | inputTextView.keyboardType = .numbersAndPunctuation
54 | default:
55 | inputTextView.isEditable = false
56 | navigationItem.rightBarButtonItems = [copy]
57 | }
58 | }
59 |
60 | override func viewDidLayoutSubviews() {
61 | super.viewDidLayoutSubviews()
62 |
63 | tipsLabel.frame = CGRect(x: 12, y: 12, width: view.bounds.size.width - 12*2, height: 20)
64 | inputTextView.frame = CGRect(x: 12, y: tipsLabel.frame.maxY + 3, width: view.bounds.size.width - 12*2, height: 200)
65 | }
66 |
67 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
68 | inputTextView.endEditing(true)
69 | }
70 |
71 | @objc func copyValue() {
72 | UIPasteboard.general.string = keyValueModel.valueDescription()
73 | }
74 |
75 | @objc func updateValue() {
76 | if inputTextView.text.isEmpty {
77 | defaults.setValue(nil, forKey: keyValueModel.key)
78 | return
79 | }
80 | guard let newValueText = inputTextView.text else {
81 | return
82 | }
83 | defaults.set(newValueText, forKey: keyValueModel.key)
84 | if let newValue = defaults.value(forKey: keyValueModel.key) {
85 | keyValueModel = UserDefaultsKeyValueModel(key: keyValueModel.key, value: newValue, userDefaults: defaults)
86 | inputTextView.text = keyValueModel.valueDescription()
87 | }else {
88 | inputTextView.text = nil
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Sources/UI/UserDefaults/UserDefaultsKeyValuesListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsKeyValuesListViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/9/9.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class UserDefaultsKeyValuesListViewController: BaseTableViewController {
12 | let defaults: UserDefaults
13 | var dataSource = [UserDefaultsKeyValueModel]()
14 | var filteredDataSource = [UserDefaultsKeyValueModel]()
15 | var searchController: UISearchController!
16 |
17 | init(defaults: UserDefaults) {
18 | self.defaults = defaults
19 |
20 | var tempDataSource = [UserDefaultsKeyValueModel]()
21 | defaults.dictionaryRepresentation().forEach { (keyValue) in
22 | let model = UserDefaultsKeyValueModel(key: keyValue.key, value: keyValue.value, userDefaults: defaults)
23 | tempDataSource.append(model)
24 | }
25 | tempDataSource.sort { (model1, model2) -> Bool in
26 | return model1.key < model2.key
27 | }
28 | dataSource = tempDataSource
29 | super.init(style: .plain)
30 | }
31 |
32 | required init?(coder aDecoder: NSCoder) {
33 | fatalError("init(coder:) has not been implemented")
34 | }
35 |
36 | override func viewDidLoad() {
37 | super.viewDidLoad()
38 |
39 | title = "Key Value List"
40 |
41 | searchController = UISearchController(searchResultsController: nil)
42 | searchController.searchBar.placeholder = "key filter"
43 | searchController.hidesNavigationBarDuringPresentation = false
44 | searchController.searchResultsUpdater = self
45 | searchController.dimsBackgroundDuringPresentation = false
46 | tableView.tableHeaderView = searchController.searchBar
47 | tableView.register(UserDefaultsKeyValuesListCell.self, forCellReuseIdentifier: "cell")
48 |
49 | NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChangeNotification(_:)), name: UserDefaults.didChangeNotification, object: nil)
50 | }
51 |
52 | func refreshDataSource() {
53 | var tempDataSource = [UserDefaultsKeyValueModel]()
54 | defaults.dictionaryRepresentation().forEach { (keyValue) in
55 | let model = UserDefaultsKeyValueModel(key: keyValue.key, value: keyValue.value, userDefaults: defaults)
56 | tempDataSource.append(model)
57 | }
58 | tempDataSource.sort { (model1, model2) -> Bool in
59 | return model1.key < model2.key
60 | }
61 | dataSource = tempDataSource
62 | }
63 |
64 | @objc func userDefaultsDidChangeNotification(_ noti: Notification) {
65 | DispatchQueue.main.async {
66 | self.refreshDataSource()
67 | self.tableView.reloadData()
68 | }
69 | }
70 |
71 | //MARK: - UITableViewDataSource & UITableViewDelegate
72 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
73 | if searchController.isActive {
74 | return filteredDataSource.count
75 | }
76 | return dataSource.count
77 | }
78 |
79 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
80 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
81 | var model: UserDefaultsKeyValueModel!
82 | if searchController.isActive {
83 | model = filteredDataSource[indexPath.row]
84 | }else {
85 | model = dataSource[indexPath.row]
86 | }
87 | cell.textLabel?.text = model.key
88 | cell.detailTextLabel?.text = model.valueDescription()
89 | cell.accessoryType = .disclosureIndicator
90 | return cell
91 | }
92 |
93 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
94 | var model: UserDefaultsKeyValueModel!
95 | if searchController.isActive {
96 | model = filteredDataSource[indexPath.row]
97 | }else {
98 | model = dataSource[indexPath.row]
99 | }
100 | searchController.isActive = false
101 | let vc = UserDefaultsKeyValueDetailViewController(defaults: defaults, keyValueModel: model)
102 | navigationController?.pushViewController(vc, animated: true)
103 | }
104 | }
105 |
106 | extension UserDefaultsKeyValuesListViewController: UISearchResultsUpdating {
107 | func updateSearchResults(for searchController: UISearchController) {
108 | if searchController.searchBar.text?.isEmpty == false {
109 | filteredDataSource.removeAll()
110 | for model in dataSource {
111 | if model.key.range(of: searchController.searchBar.text!, options: .caseInsensitive) != nil {
112 | filteredDataSource.append(model)
113 | }
114 | }
115 | }else {
116 | filteredDataSource = dataSource
117 | }
118 | tableView.reloadData()
119 | }
120 | }
121 |
122 | class UserDefaultsKeyValuesListCell: UITableViewCell {
123 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
124 | super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
125 | }
126 |
127 | required init?(coder aDecoder: NSCoder) {
128 | fatalError("init(coder:) has not been implemented")
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/Sources/UI/WebsiteEntry/WebDetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WebDetailViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/23.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import WebKit
11 |
12 | class WebDetailViewController: BaseViewController, WKNavigationDelegate {
13 | let website: String
14 | let webView: WKWebView
15 | let progressLine: CALayer
16 |
17 | deinit {
18 | webView.removeObserver(self, forKeyPath: "estimatedProgress")
19 | }
20 |
21 | init(website: String) {
22 | if website.hasPrefix("http://") || website.hasPrefix("https://") {
23 | self.website = website
24 | }else {
25 | self.website = "https://" + website
26 | }
27 | webView = WKWebView(frame: CGRect.zero)
28 | progressLine = CALayer()
29 | super.init(nibName: nil, bundle: nil)
30 | }
31 |
32 | required init?(coder aDecoder: NSCoder) {
33 | fatalError("init(coder:) has not been implemented")
34 | }
35 |
36 | override func viewDidLoad() {
37 | super.viewDidLoad()
38 |
39 | webView.navigationDelegate = self
40 | webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
41 | webView.allowsBackForwardNavigationGestures = true
42 | view.addSubview(webView)
43 |
44 | progressLine.backgroundColor = UIColor.blue.withAlphaComponent(0.5).cgColor
45 | view.layer.addSublayer(progressLine)
46 |
47 | guard let url = URL(string: website) else {
48 | let alert = UIAlertController(title: nil, message: "网址无效!", preferredStyle: .alert)
49 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: { (action) in
50 | self.navigationController?.popViewController(animated: true)
51 | }))
52 | present(alert, animated: true, completion: nil)
53 | return
54 | }
55 | let request = URLRequest(url: url)
56 | webView.load(request)
57 | }
58 |
59 | override func viewDidLayoutSubviews() {
60 | webView.frame = view.bounds
61 | }
62 |
63 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
64 | if keyPath == "estimatedProgress" {
65 | if webView.estimatedProgress == 0 || webView.estimatedProgress == 1 {
66 | progressLine.frame = CGRect.zero
67 | progressLine.isHidden = true
68 | }else {
69 | progressLine.isHidden = false
70 | }
71 | progressLine.frame = CGRect(x: 0, y: 0, width: CGFloat(webView.estimatedProgress)*view.bounds.size.width, height: 5)
72 | }else {
73 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
74 | }
75 | }
76 |
77 | //MARK: - WKNavigationDelegate
78 | func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
79 | print("JXCaptain H5任意门 didFail with error:\(error.localizedDescription)")
80 | let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert)
81 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil))
82 | present(alert, animated: true, completion: nil)
83 | }
84 |
85 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
86 | webView.evaluateJavaScript("document.title") { (result, error) in
87 | if let title = result as? String {
88 | self.title = title
89 | }
90 | }
91 | }
92 |
93 | func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
94 | let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert)
95 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil))
96 | present(alert, animated: true, completion: nil)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/UI/WebsiteEntry/WebsiteEntryViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WebsiteEntryViewController.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/8/23.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class WebsiteEntryViewController: BaseViewController {
12 | let soldier: WebsiteEntrySoldier
13 | var tipsLabel: UILabel!
14 | var inputTextView: UITextView!
15 | var confirmButton: UIButton!
16 |
17 | init(soldier: WebsiteEntrySoldier) {
18 | self.soldier = soldier
19 | super.init(nibName: nil, bundle: nil)
20 | }
21 |
22 | required init?(coder aDecoder: NSCoder) {
23 | fatalError("init(coder:) has not been implemented")
24 | }
25 |
26 | override func viewDidLoad() {
27 | super.viewDidLoad()
28 |
29 | title = "H5任意门"
30 |
31 | tipsLabel = UILabel()
32 | tipsLabel.textColor = .lightGray
33 | tipsLabel.text = "请在下方输入网址:"
34 | view.addSubview(tipsLabel)
35 |
36 | inputTextView = UITextView()
37 | if soldier.defaultWebsite != nil {
38 | inputTextView.text = soldier.defaultWebsite
39 | }else {
40 | inputTextView.text = "https://"
41 | }
42 | inputTextView.font = .systemFont(ofSize: 17)
43 | inputTextView.keyboardType = .URL
44 | inputTextView.layer.borderColor = UIColor.lightGray.cgColor
45 | inputTextView.layer.borderWidth = 1
46 | view.addSubview(inputTextView)
47 |
48 | confirmButton = UIButton(type: .custom)
49 | confirmButton.setTitleColor(.black, for: .normal)
50 | confirmButton.setTitle("点击跳转", for: .normal)
51 | confirmButton.layer.cornerRadius = 5
52 | confirmButton.backgroundColor = UIColor.lightGray.withAlphaComponent(0.3)
53 | confirmButton.addTarget(self, action: #selector(confirmButtonDidClick), for: .touchUpInside)
54 | view.addSubview(confirmButton)
55 | }
56 |
57 | override func viewDidAppear(_ animated: Bool) {
58 | super.viewDidAppear(animated)
59 |
60 | inputTextView.becomeFirstResponder()
61 | }
62 |
63 | override func viewDidLayoutSubviews() {
64 | super.viewDidLayoutSubviews()
65 |
66 | tipsLabel.frame = CGRect(x: 12, y: 12, width: 200, height: 20)
67 | inputTextView.frame = CGRect(x: 12, y: tipsLabel.frame.maxY + 3, width: view.bounds.size.width - 12*2, height: 200)
68 | confirmButton.frame = CGRect(x: 12, y: inputTextView.frame.maxY + 10, width: inputTextView.bounds.size.width, height: 50)
69 | }
70 |
71 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
72 | inputTextView.endEditing(true)
73 | }
74 |
75 | @objc func confirmButtonDidClick() {
76 | guard !inputTextView.text.isEmpty else {
77 | let alert = UIAlertController(title: nil, message: "网址不能为空!", preferredStyle: .alert)
78 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil))
79 | present(alert, animated: true, completion: nil)
80 | return
81 | }
82 | inputTextView.endEditing(true)
83 | if soldier.webDetailControllerClosure == nil {
84 | navigationController?.pushViewController(WebDetailViewController(website: inputTextView.text), animated: true)
85 | }else {
86 | navigationController?.pushViewController(soldier.webDetailControllerClosure!(inputTextView.text), animated: true)
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/UserDefaults+Access.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaults+Access.swift
3 | // JXCaptain
4 | //
5 | // Created by jiaxin on 2019/12/13.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension UserDefaults {
12 | var isNetworkObserverSoldierActive: Bool {
13 | set { set(newValue, forKey: #function) }
14 | get { bool(forKey: #function) }
15 | }
16 | var isANRSoldierActive: Bool {
17 | set { set(newValue, forKey: #function) }
18 | get { bool(forKey: #function) }
19 | }
20 | var isANRSoldierHasNewEvent: Bool {
21 | set { set(newValue, forKey: #function) }
22 | get { bool(forKey: #function) }
23 | }
24 | var isCPUSoldierActive: Bool {
25 | set { set(newValue, forKey: #function) }
26 | get { bool(forKey: #function) }
27 | }
28 | var isMemorySoldierActive: Bool {
29 | set { set(newValue, forKey: #function) }
30 | get { bool(forKey: #function) }
31 | }
32 | var isFPSSoldierActive: Bool {
33 | set { set(newValue, forKey: #function) }
34 | get { bool(forKey: #function) }
35 | }
36 | var isCrashSoldierHasNewEvent: Bool {
37 | set { set(newValue, forKey: #function) }
38 | get { bool(forKey: #function) }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------