Extending a Program with Loadable Bundles

这篇是个教程,讲怎么在程序中加载Bundle的

Extending a Program with Loadable Bundles

Objective-C provides numerous dynamic programming features. These features increase the power and flexibility of the language, and enable you to modify a program during execution. In Chapter 7, you discussed dynamic loading and briefly described how the Foundation Framework NSBundle class can be used to manage bundles. Now you are going to create a program that utilizes these featuresto extend a running program using a loadable bundle.


Approach

For this example, you’ll actually develop two projects. In one project, you’ll code the program that uses a loadable bundle, and in the other project, you’ll create the loadable bundle. You’ll proceed as follows:

  1. In Project 1, develop a protocol and a class that conforms to this protocol.
  2. In Project 2, develop a loadable bundle comprised of a different class conforming to this protocol.
  3. In Project 1, use the NSBundle class to find and load this bundle, create an object from a class in the bundle (developed in step 2), and then invoke a method on this object.

Sounds like fun, right? OK, let’s get started!



Step 1: Laying the Foundation

In Xcode, create a new project by selecting New image Project … from the Xcode File menu. In the New Project Assistant pane, create a command-line application. In the Project Options window, specifyDynaLoader for the Product Name, choose Foundation for the Project Type, and select ARC memory management by selecting the Use Automatic Reference Counting check box. Specify the location in your file system where you want the project to be created (if necessary, select New Folder and enter the name and location for the folder), uncheck the Source Control check box, and then click the Create button.

Now you’re going to create a protocol and a class that conforms to the protocol. You probably recall from Chapter 2 that a protocol declares methods and properties that can be implemented by any class. Combined with dynamic typing, this provides an idea mechanism for specifying a set of methods implemented by a class that is dynamically loaded from a bundle. Select New image File … from the Xcode File menu, select the Objective-C protocol template, name the protocol Greeter, select the DynaLoader folder for the files location and the DynaLoader project as the target, and then click the Create button. A header file named Greeter.h has been added to the Xcode project navigator pane. Click this file to display the Greeter protocol (see Listing 9-1).

Listing 9-1.  Greeter Protocol Template File

#import <Foundation/Foundation.h>

@protocol Greeter <NSObject>

@end

In the editor pane, update the Greeter.h file as shown in Listing 9-2.

Listing 9-2.  Greeter Protocol Updates

#import <Foundation/Foundation.h>

@protocol Greeter <NSObject>

- (NSString *) greeting:(NSString *)salutation;

@end

This protocol declares a method named greeting that takes an NSString pointer as its parameter and returns an NSString pointer. Now let’s implement a class that conforms to the Greeter protocol. Select New image File … from the Xcode File menu, select the Objective-C class template, name the class BasicGreeter (select NSObject in the Subclass of drop-down list), select the DynaLoaderfolder for the files location and the DynaLoader project as the target, and then click the Createbutton. Next, in the Xcode project navigator pane, select the resulting header file namedBasicGreeter.h and update the interface as shown in Listing 9-3.

Listing 9-3.  BasicGreeter Interface

#import <Foundation/Foundation.h>
#import "Greeter.h"

@interface BasicGreeter : NSObject <Greeter>

@end

The template BasicGreeter interface has been updated to adopt the Greeter protocol; now select the BasicGreeter.m file and update the implementation as shown in Listing 9-4.

Listing 9-4.  BasicGreeter Implementation

#import "BasicGreeter.h"

@implementation BasicGreeter

- (NSString *) greeting:(NSString *)salutation
{
  return [NSString stringWithFormat:@"%@, World!", salutation];
}

@end

The BasicGreeter implementation defines the greeting: method (and thus conforms to the Greeterprotocol), returning a text string that begins with the method’s input parameter. Now let’s test this class. Select the main.m file and update the main() function, as shown in Listing 9-5.

Listing 9-5.  BasicGreeter main() Function

#import <Foundation/Foundation.h>
#import "BasicGreeter.h"

int main(int argc, const char * argv[])
{
  @autoreleasepool
  {
    id greeter = [[BasicGreeter alloc] init];
    NSLog(@"%@", [greeter greeting:@"Hello"]);
  }
  return 0;
}

First, the function creates a BasicGreeter object and assigns it to a variable named greeter. This variable is declared to be of type id and conforms to the Greeter protocol. It then invokes thegreeting: method on the object, and logs the result in the output pane. By declaring the variable as

id<Greeter> greeter

it can be assigned to any Objective-C object that conforms to the Greeter protocol. If you compile and run the program now, the output pane displays “Hello, World!” (see Figure 9-1).

9781430250500_Fig09-01.jpg

Figure 9-1DynaLoader program output

Great! You have completed step 1. Now you will create another class that conforms to the Greeter protocol, this time packaged in a loadable bundle.



Step 2: Creating a Loadable Bundle

Xcode makes it easy to create a loadable bundle. It’s simply another type of project. First, create a new project by selecting New image Project … from the Xcode File menu. In the New Project Assistant pane under OS X, selectFramework & Library, and then select Bundle (see Figure 9-2).

9781430250500_Fig09-02.jpg

Figure 9-2Xcode New Project Assistant, selecting a Bundle project template

Next, in the Project Options window, specify CustomGreeter for the Product Name, an Organization Name (optional value), and a Company Identifier(the default value is fine, but any name will suffice). Select Cocoa for the Framework the bundle will link against, and select ARC memory management by checking the Use Automatic Reference Counting check box. Specify the location in your file system where you want the project to be created (if necessary, select New Folder and enter the name and location for the folder), uncheck the Source Control check box, do not add to any project or workspace, and then click the Create button. The workspace window shown inFigure 9-3 is displayed.

9781430250500_Fig09-03.jpg

Figure 9-3CustomGreeter bundle template

Cool. Xcode created an empty bundle. In the project navigator pane under the Products group, notice the CustomGreeter.bundle file. This is the bundle to which you’ll add your code and resources. Also note the CustomGreeter-Info.plist file. It is a required file that contains information about the bundle’s configuration. Now let’s add a class to this bundle that implements theGreeter protocol. First, you need to include the Greeter.h header file in the bundle. Fortunately, Xcode provides a simple mechanism for importing files into a project. Hold down the Ctrl key and (from the Xcode project navigator pane) select the CustomGreeter project. A drop-down window then displays a set of options. Select the Add Files to “CustomGreeter” … option (see Figure 9-4).

9781430250500_Fig09-04.jpg

Figure 9-4Adding files to CustomGreeter option

The Greeter.h file is located under the DynaLoader project folder that you created in step 1. Navigate to this project folder, select the file, and click theAdd button (see Figure 9-5) to add it to the CustomGreeter bundle.

9781430250500_Fig09-05.jpg

Figure 9-5Adding the Greeter.h file to the CustomGreeter bundle

Now add a new class to the bundle that conforms to this protocol. Select New imageFile … from the Xcode File menu, select the Objective-C class template, name the class CustomGreeter (select NSObject in the Subclass of drop-down list), select the CustomGreeter folder for the files location and the CustomGreeterproject as the target, and then click the Create button. In the Xcode project navigator pane, select the resulting header file named CustomGreeter.h and update the interface, as shown in Listing 9-6.

Listing 9-6.  CustomGreeter Interface

#import <Foundation/Foundation.h>
#import "Greeter.h"

@interface CustomGreeter : NSObject <Greeter>

@end

As with the BasicGreeter class, the template CustomGreeter interface has been updated to adopt the Greeter protocol. Now select the CustomGreeter.m file and update the implementation as shown in Listing 9-7.

Listing 9-7.  CustomGreeter Implementation

#import "CustomGreeter.h"

@implementation CustomGreeter

- (NSString *) greeting:(NSString *)salutation
{
  return [NSString stringWithFormat:@"%@, Universe!", salutation];
}

@end

The CustomGreeter implementation defines the greeting: method (and thus conforms to the Greeter protocol), returning a text string that begins with the method’s input parameter. Now compile the bundle by selecting Build from the Xcode Product menu. And that’s it—you’ve created a loadable bundle that includes a class (CustomGreeter) that conforms to the Greeter protocol. In the Xcode project navigator pane, note the CustomGreeter.bundle file in the Products group. Select this bundle file and note in the Xcode file inspector (on the right side of the Xcode workspace window) the full path shown in Figure 9-6This indicates the path to the CustomGreeter.bundle file that you’ll need when you load the bundle using the NSBundle class.

9781430250500_Fig09-06.jpg

Figure 9-6CustomGreeter.bundle full path




Step 3: Dynamically Loading the Bundle

Now that you have created a loadable bundle, let’s use it to dynamically create an object (from a bundle class) and invoke a method on it. In Xcode, return to the DynaLoader project, select the main.m file in the project navigator pane, and update the main() function as shown in Listing 9-8.

Listing 9-8.  Using NSBundle to Load a Bundle

#import <Foundation/Foundation.h>
#import "BasicGreeter.h"

int main(int argc, const char * argv[])
{
  @autoreleasepool
  {
    id<Greeter> greeter = [[BasicGreeter alloc] init];
    NSLog(@"%@", [greeter greeting:@"Hello"]);
     
    // Now create a bundle at the specified path (retrieved from input argument)
    NSString *bundlePath;
    if (argc != 2)
    {
      // No bundle path provided, exit
      NSLog(@"Please provide a path for the bundle");
    }
    else
    {
      bundlePath = [NSString stringWithUTF8String:argv[1]];
      NSBundle *greeterBundle = [NSBundle bundleWithPath:bundlePath];
      if (greeterBundle == nil)
      {
        NSLog(@"Bundle not found at path");
      }
      else
      {
        // Dynamically load bundle
        NSError *error;
        BOOL isLoaded = [greeterBundle loadAndReturnError:&error];
        if (!isLoaded)
        {
          NSLog(@"Error = %@", [error localizedDescription]);
        }
        else
        {
          // Bundle loaded, create an object using bundle and send it a message
          Class greeterClass = [greeterBundle classNamed:@"CustomGreeter"];
          greeter = [[greeterClass alloc] init];
          NSLog(@"%@", [greeter greeting:@"Hello"]);
           
          // Done with dynamically loaded bundle, now unload it
          // First release any objects whose class is defined in the bundle!
          greeter = nil;
          BOOL isUnloaded = [greeterBundle unload];
          if (!isUnloaded)
          {
            NSLog(@"Couldn't unload bundle");
          }
        }
      }
    }
  }
  return 0;
}

I know, you added a lot of code here, but don’t worry. You’ll take this one step at a time. Functionally, the updates implement the following logic:

  1. Retrieve the bundle path argument
  2. Create an NSBundle object
  3. Load the bundle
  4. Get a bundle class
  5. Unload the bundle
  6. Set the bundle path argument

Let’s discuss the code for each of these in turn.

Retrieving the Bundle Path Argument

As you’ve probably observed by now, an Objective-C main() function specifies two parameters: argc and argv. These parameters enable you to pass arguments to the program on execution.

int main(int argc, const char * argv[])

The parameter argc is the number of arguments to the program, and the space-separated argument values are stored in the argv array. The first argument in the argv array (argv[0]) is the name of the program executable, thus the value of the argc parameter is always one or more. Consider, for example, that theDynaLoader program was executed from the command line as follows:

> DynaLoader /CustomGreeter.bundle

The main() function variable argc would have a value of 2 and the argv array would have two elements: the value of the first would be DynaLoader and the second would be /CustomGreeter.bundle. The main() function has been updated to require that the bundle path be provided as an input argument to the program. The following code from Listing 9-8 verifies that the program has the correct number of arguments and then creates the path.

NSString *bundlePath;
if (argc != 2)
{
  // No bundle path provided, exit
  NSLog(@"Please provide a path for the bundle");
}
else
{
  bundlePath = [NSString stringWithUTF8String:argv[1]];

The conditional logic (if (argc != 2)) checks to make sure that the argc count is two (one for the program name and one for the bundle path). If the count is not equal to two, a message is logged to the output pane and the program exits; otherwise, the path is assigned to the proper input parameter (argv[1]) and the program continues.

Creating an NSBundle Object

The next set of statements creates an NSBundle object using the bundle path and verifies that the object was successfully created. The code uses anNSBundle convenience class method to create and initialize the class instance for the bundle at the specified path.

+ (NSBundle *) bundleWithPath:(NSString *) fullPath;

The code attempts to create a bundle object using the provided path; if it is not successful, it logs a message to the output pane.

bundlePath = [NSString stringWithUTF8String:argv[1]];
NSBundle *greeterBundle = [NSBundle bundleWithPath:bundlePath];
if (greeterBundle == nil)
{
  NSLog(@"Bundle not found at path");
}

Loading a Bundle

Now the code attempts to dynamically load the bundle. NSBundle provides several methods to load a bundle into the Objective-C runtime. The instance method used here loads a bundle and stores an error object in the parameter if any errors occurred.

- (BOOL)loadAndReturnError:(NSError **) error;

The method returns a Boolean value of YES if the bundle is successfully loaded or has already been loaded. Notice that the method takes a variable of typeNSError ** as its parameter; if the bundle is not successfully loaded, this enables the method to return an NSError object with error information (later in this book you’ll learn all about error handling using the Foundation Framework NSError class). The code loads the bundle and performs a conditional check on the return value, logging an error message to the output pane if the bundle was not loaded successfully.

NSError *error;
BOOL isLoaded = [greeterBundle loadAndReturnError:&error];
if (!isLoaded)
{
  NSLog(@"Error = %@", [error localizedDescription]);
}

Getting a Bundle Class

Next, the code gets a bundle class, creates an instance of the class, and invokes a method on the object, logging the result returned by the method to the output pane.

Class greeterClass = [greeterBundle principalClass];
greeter = [[greeterClass alloc] init];
NSLog(@"%@", [greeter greeting:@"Hello"]);

The NSBundle instance method principalClass is used to retrieve theprincipal class of the bundle. The principal class controls all other classes in a bundle, and its classname is specified in the bundle’s Info.plist file. If the name is not specified, the first class loaded in the bundle is the principalclass. Because CustomGreeter.bundle contains only one class (CustomGreeter), the method loads a CustomGreeter class object. Next, an instance of the class is created and initialized. Note that this instance is assigned to the existing variable named greeter that was declared to be of typeid<Greeter>. Because the CustomGreeter class also conforms to the Greeterprotocol, this assignment is valid and causes neither a compile-time nor a runtime error.

Unloading a Bundle

Once the bundle is no longer needed, it can be unloaded, thereby conserving system resources. This is done with the following code from Listing 9-8.

greeter = nil;
BOOL isUnloaded = [greeterBundle unload];
if (!isUnloaded)
{
  NSLog(@"Couldn't unload bundle");
}

The runtime system requires that, prior to unloading a bundle, any objects created by a bundle class must be released; hence the greeter object is set tonil. The NSBundle unload method is used to unload a previously loaded bundle. In the preceding code, the result returned from the method invocation is tested to verify that the bundle was unloaded successfully; if not, a message is logged to the console.

Retrieving the Bundle Path Argument

As mentioned, the DynaLoader program takes an argument that specifies the full path of the bundle to be dynamically loaded. So prior to running the program, you have to identify the full path to the CustomGreeter.bundle and then run the program with this path set as its input argument. In step 2, you indicated how to view the full path of the CustomGreeter bundle (refer to Figure 9-6). In Xcode, open the CustomGreeter project (if not already opened), selectCustomGreeter.bundle in the project navigator pane, and then note the bundle full path in the file inspector. Now add this path to the Xcode copy buffer by dragging and selecting it, and then selecting Copy from the Xcode Edit menu(see Figure 9-7).

9781430250500_Fig09-07.jpg

Figure 9-7Copying the CustomGreeter.bundle full path

In Xcode, open the DynaLoader project, select DynaLoader from the Schemebutton at the top of the toolbar, and then select Edit Scheme … from the scheme drop-down (see Figure 9-8).

9781430250500_Fig09-08.jpg

Figure 9-8Edit CustomGreeter project scheme

A scheme defines build and test settings for an Xcode project or workspace. The build configuration information includes the arguments passed when the target program is executed, so this is where you’ll specify the full path for theCustomGreeter bundle. In the Scheme editing dialog, select the Argumentstab to edit the arguments passed on program launch.

9781430250500_Fig09-09.jpg

Figure 9-9. Scheme editing dialog Arguments tab

Under the Arguments Passed on Launch section, select the + button to add an argument. Now click inside the corresponding field to select it, paste the bundle path you copied earlier into the field by selecting Paste from the Xcode Edit menu, and click the Return button to store this value in the argument field. Finally, click the OK button to complete the update (see Figure 9-10).

9781430250500_Fig09-10.jpg

Figure 9-10Adding bundle path using the Scheme editor

The file path argument will now be passed to the DynaLoader program on launch. When you compile and run the DynaLoader program, you should observe the messages in the output pane shown in Figure 9-11.

9781430250500_Fig09-11.jpg

Figure 9-11DynaLoader program output

As shown in the output pane, the greeting: method is first invoked onBasicGreeter object, and then it is invoked on a CustomGreeter object that is created from the dynamically loaded CustomGreeter bundle. This demonstrates how dynamic loading using the NSBundle class enables code and resources to be added to a running program.



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值