运行的时候,会报错:

java.lang.NullPointerException: Attempt to invoke virtual method 'void android.app.ActionBar.setSubtitle(int)' on a null object reference

解决办法:

发现时是 actionBar出错,解决方法如下:

1、在这个onCreate函数中天加 getWindow().requestFeature(Window.FEATURE_ACTION_BAR);

2、将actionBar.setSubtitle(resId)注释掉,这样就不会使用到actionbar了。

这个Demo最重要的是3个类:BluetoothChat;DeviceListActivity;BluetoothChatService

BluetoothChat源码:

/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ package com.example.android.BluetoothChat; import android.annotation.TargetApi;
import android.app.ActionBar;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.View.OnClickListener;
import android.view.inputmethod.EditorInfo;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast; /**
* This is the main Activity that displays the current chat session.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class BluetoothChat extends Activity {
// Debugging
private static final String TAG = "BluetoothChat";
private static final boolean D = true; // Message types sent from the BluetoothChatService Handler
public static final int MESSAGE_STATE_CHANGE = 1;
public static final int MESSAGE_READ = 2;
public static final int MESSAGE_WRITE = 3;
public static final int MESSAGE_DEVICE_NAME = 4;
public static final int MESSAGE_TOAST = 5; // Key names received from the BluetoothChatService Handler
public static final String DEVICE_NAME = "device_name";
public static final String TOAST = "toast"; // Intent request codes
private static final int REQUEST_CONNECT_DEVICE_SECURE = 1;
private static final int REQUEST_CONNECT_DEVICE_INSECURE = 2;
private static final int REQUEST_ENABLE_BT = 3; // Layout Views
private ListView mConversationView;
private EditText mOutEditText;
private Button mSendButton; // Name of the connected device
private String mConnectedDeviceName = null;
// Array adapter for the conversation thread
private ArrayAdapter<String> mConversationArrayAdapter;
// String buffer for outgoing messages
private StringBuffer mOutStringBuffer;
// Local Bluetooth adapter
private BluetoothAdapter mBluetoothAdapter = null;
// Member object for the chat services
private BluetoothChatService mChatService = null; @Override
public void onCreate(Bundle savedInstanceState) {
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
super.onCreate(savedInstanceState);
if(D) Log.e(TAG, "+++ ON CREATE +++"); // Set up the window layout
setContentView(R.layout.main); // Get local Bluetooth adapter
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); // If the adapter is null, then Bluetooth is not supported
if (mBluetoothAdapter == null) {
Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();
finish();
return;
}
} @Override
public void onStart() {
super.onStart();
if(D) Log.e(TAG, "++ ON START ++"); // If BT is not on, request that it be enabled.
// setupChat() will then be called during onActivityResult
if (!mBluetoothAdapter.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
// Otherwise, setup the chat session
} else {
if (mChatService == null) setupChat();
}
} @Override
public synchronized void onResume() {
super.onResume();
if(D) Log.e(TAG, "+ ON RESUME +"); // Performing this check in onResume() covers the case in which BT was
// not enabled during onStart(), so we were paused to enable it...
// onResume() will be called when ACTION_REQUEST_ENABLE activity returns.
if (mChatService != null) {
// Only if the state is STATE_NONE, do we know that we haven't started already
if (mChatService.getState() == BluetoothChatService.STATE_NONE) {
// Start the Bluetooth chat services
mChatService.start();
}
}
} private void setupChat() {
Log.d(TAG, "setupChat()"); // Initialize the array adapter for the conversation thread
mConversationArrayAdapter = new ArrayAdapter<String>(this, R.layout.message);
mConversationView = (ListView) findViewById(R.id.in);
mConversationView.setAdapter(mConversationArrayAdapter); // Initialize the compose field with a listener for the return key
mOutEditText = (EditText) findViewById(R.id.edit_text_out);
mOutEditText.setOnEditorActionListener(mWriteListener); // Initialize the send button with a listener that for click events
mSendButton = (Button) findViewById(R.id.button_send);
mSendButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
// Send a message using content of the edit text widget
TextView view = (TextView) findViewById(R.id.edit_text_out);
String message = view.getText().toString();
sendMessage(message);
}
}); // Initialize the BluetoothChatService to perform bluetooth connections
mChatService = new BluetoothChatService(this, mHandler); // Initialize the buffer for outgoing messages
mOutStringBuffer = new StringBuffer("");
} @Override
public synchronized void onPause() {
super.onPause();
if(D) Log.e(TAG, "- ON PAUSE -");
} @Override
public void onStop() {
super.onStop();
if(D) Log.e(TAG, "-- ON STOP --");
} @Override
public void onDestroy() {
super.onDestroy();
// Stop the Bluetooth chat services
if (mChatService != null) mChatService.stop();
if(D) Log.e(TAG, "--- ON DESTROY ---");
} private void ensureDiscoverable() {
if(D) Log.d(TAG, "ensure discoverable");
if (mBluetoothAdapter.getScanMode() !=
BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
}
} /**
* Sends a message.
* @param message A string of text to send.
*/
private void sendMessage(String message) {
// Check that we're actually connected before trying anything
if (mChatService.getState() != BluetoothChatService.STATE_CONNECTED) {
Toast.makeText(this, R.string.not_connected, Toast.LENGTH_SHORT).show();
return;
} // Check that there's actually something to send
if (message.length() > 0) {
// Get the message bytes and tell the BluetoothChatService to write
byte[] send = message.getBytes();
mChatService.write(send); // Reset out string buffer to zero and clear the edit text field
mOutStringBuffer.setLength(0);
mOutEditText.setText(mOutStringBuffer);
}
} // The action listener for the EditText widget, to listen for the return key
private TextView.OnEditorActionListener mWriteListener =
new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView view, int actionId, KeyEvent event) {
// If the action is a key-up event on the return key, send the message
if (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_UP) {
String message = view.getText().toString();
sendMessage(message);
}
if(D) Log.i(TAG, "END onEditorAction");
return true;
}
}; private final void setStatus(int resId) {
final ActionBar actionBar = getActionBar();
actionBar.setSubtitle(resId);
} private final void setStatus(CharSequence subTitle) {
final ActionBar actionBar = getActionBar();
actionBar.setSubtitle(subTitle);
} // The Handler that gets information back from the BluetoothChatService
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_STATE_CHANGE:
if(D) Log.i(TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1);
switch (msg.arg1) {
case BluetoothChatService.STATE_CONNECTED:
setStatus(getString(R.string.title_connected_to, mConnectedDeviceName));
mConversationArrayAdapter.clear();
break;
case BluetoothChatService.STATE_CONNECTING:
setStatus(R.string.title_connecting);
break;
case BluetoothChatService.STATE_LISTEN:
case BluetoothChatService.STATE_NONE:
setStatus(R.string.title_not_connected);
break;
}
break;
case MESSAGE_WRITE:
byte[] writeBuf = (byte[]) msg.obj;
// construct a string from the buffer
String writeMessage = new String(writeBuf);
mConversationArrayAdapter.add("Me: " + writeMessage);
break;
case MESSAGE_READ:
byte[] readBuf = (byte[]) msg.obj;
// construct a string from the valid bytes in the buffer
String readMessage = new String(readBuf, 0, msg.arg1);
mConversationArrayAdapter.add(mConnectedDeviceName+": " + readMessage);
break;
case MESSAGE_DEVICE_NAME:
// save the connected device's name
mConnectedDeviceName = msg.getData().getString(DEVICE_NAME);
Toast.makeText(getApplicationContext(), "Connected to "
+ mConnectedDeviceName, Toast.LENGTH_SHORT).show();
break;
case MESSAGE_TOAST:
Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST),
Toast.LENGTH_SHORT).show();
break;
}
}
}; public void onActivityResult(int requestCode, int resultCode, Intent data) {
if(D) Log.d(TAG, "onActivityResult " + resultCode);
switch (requestCode) {
case REQUEST_CONNECT_DEVICE_SECURE:
// When DeviceListActivity returns with a device to connect
if (resultCode == Activity.RESULT_OK) {
connectDevice(data, true);
}
break;
case REQUEST_CONNECT_DEVICE_INSECURE:
// When DeviceListActivity returns with a device to connect
if (resultCode == Activity.RESULT_OK) {
connectDevice(data, false);
}
break;
case REQUEST_ENABLE_BT:
// When the request to enable Bluetooth returns
if (resultCode == Activity.RESULT_OK) {
// Bluetooth is now enabled, so set up a chat session
setupChat();
} else {
// User did not enable Bluetooth or an error occurred
Log.d(TAG, "BT not enabled");
Toast.makeText(this, R.string.bt_not_enabled_leaving, Toast.LENGTH_SHORT).show();
finish();
}
}
} private void connectDevice(Intent data, boolean secure) {
// Get the device MAC address
String address = data.getExtras()
.getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
// Get the BluetoothDevice object
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
// Attempt to connect to the device
mChatService.connect(device, secure);
} @Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.option_menu, menu);
return true;
} @Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent serverIntent = null;
switch (item.getItemId()) {
case R.id.secure_connect_scan:
// Launch the DeviceListActivity to see devices and do scan
serverIntent = new Intent(this, DeviceListActivity.class);
startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_SECURE);
return true;
case R.id.insecure_connect_scan:
// Launch the DeviceListActivity to see devices and do scan
serverIntent = new Intent(this, DeviceListActivity.class);
startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_INSECURE);
return true;
case R.id.discoverable:
// Ensure this device is discoverable by others
ensureDiscoverable();
return true;
}
return false;
} }

其中,先执行了onCreate(),onStart(),onResume()方法

    @Override
public void onCreate(Bundle savedInstanceState) {
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
super.onCreate(savedInstanceState);
if(D) Log.e(TAG, "+++ ON CREATE +++"); // Set up the window layout
setContentView(R.layout.main); // Get local Bluetooth adapter
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); // If the adapter is null, then Bluetooth is not supported
if (mBluetoothAdapter == null) {
Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();
finish();
return;
}
} @Override
public void onStart() {
super.onStart();
if(D) Log.e(TAG, "++ ON START ++"); // If BT is not on, request that it be enabled.
// setupChat() will then be called during onActivityResult
if (!mBluetoothAdapter.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
// Otherwise, setup the chat session
} else {
if (mChatService == null) setupChat();
}
} @Override
public synchronized void onResume() {
super.onResume();
if(D) Log.e(TAG, "+ ON RESUME +"); // Performing this check in onResume() covers the case in which BT was
// not enabled during onStart(), so we were paused to enable it...
// onResume() will be called when ACTION_REQUEST_ENABLE activity returns.
if (mChatService != null) {
// Only if the state is STATE_NONE, do we know that we haven't started already
if (mChatService.getState() == BluetoothChatService.STATE_NONE) {
// Start the Bluetooth chat services
mChatService.start();
}
}
}

常量:

// 调试用的日志标志TAG与是否打印日志的标志D
private static final String TAG = "BluetoothChat";
private static final boolean D = true; // 从BluetoothChatService传回来交给Handler处理的消息类型
public static final int MESSAGE_STATE_CHANGE = 1; // 蓝牙socket状态改变,居然有4个状态监听、正在连接、已连接、无。具体可以看Handler的handMessage方法
public static final int MESSAGE_READ = 2; // 这个消息类型被发送回来是因为蓝牙服务socket在读别的设备发来的内容
public static final int MESSAGE_WRITE = 3; // 这个消息类型被发送回来是因为蓝牙socket在写要发送的内容
public static final int MESSAGE_DEVICE_NAME = 4; // 这个消息发送回来是因为连接到了一个设备,并且获得了对方的名字,好像是手机的型号
public static final int MESSAGE_TOAST = 5; // 这个消息发送回来是有要用Toast控件广播的内容 // 从BluetoothChatService的发送回来消息内容里的键值
public static final String DEVICE_NAME = "device_name"; // 在发回设备名(MESSAGE_DEVICE_NAME)消息时,获取设备名时的键值
public static final String TOAST = "toast"; // 同样是键值,指向的是要Toast控件广播的内容 // Intent的请求值,在startActivityForResult时使用
private static final int REQUEST_CONNECT_DEVICE = 1; // 在要请求去搜索设备的时候使用到
private static final int REQUEST_ENABLE_BT = 2; // 在请求要使蓝牙可用的时候用到

主要是Menu菜单触发事件:

   @Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.option_menu, menu);
return true;
} @Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent serverIntent = null;
switch (item.getItemId()) {
case R.id.secure_connect_scan:
// Launch the DeviceListActivity to see devices and do scan
serverIntent = new Intent(this, DeviceListActivity.class);
startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_SECURE);
return true;
case R.id.insecure_connect_scan:
// Launch the DeviceListActivity to see devices and do scan
serverIntent = new Intent(this, DeviceListActivity.class);
startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_INSECURE);
return true;
case R.id.discoverable:
// Ensure this device is discoverable by others
ensureDiscoverable();
return true;
}
return false;
}

额外:

然后是ListView从底下开始显示:需要XML里一句:

android:stackFromBottom="true" 

学到了EditText注册了一个监听器,然后实现了软键盘按回车return键发送消息(一般我们在EditText里按return键是换行)

// 用于监听EditText的一个return键事件
private TextView.OnEditorActionListener mWriteListener =
new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView view, int actionId, KeyEvent event) {
// If the action is a key-up event on the return key, send the message
if (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_UP) {
String message = view.getText().toString();
sendMessage(message);
}
if(D) Log.i(TAG, "END onEditorAction");
return true;
}
};

DeviceListActivity源码:

This Activity appears as a dialog. It lists any paired devices and devices detected in the area after discovery. When a device is chosen by the user, the MAC address of the device is sent back to the parent Activity in the result Intent.

这个Aty以对话框的形式显示。显示任何已经配对的设备和查找的设备。当用户选择一个设备的时候,将会返回MAC地址到上一个Aty。

/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ package com.example.android.BluetoothChat; import java.util.Set; import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.AdapterView.OnItemClickListener; /**
* This Activity appears as a dialog. It lists any paired devices and
* devices detected in the area after discovery. When a device is chosen
* by the user, the MAC address of the device is sent back to the parent
* Activity in the result Intent.
*/
public class DeviceListActivity extends Activity {
// Debugging
private static final String TAG = "DeviceListActivity";
private static final boolean D = true; // Return Intent extra
public static String EXTRA_DEVICE_ADDRESS = "device_address"; // Member fields
private BluetoothAdapter mBtAdapter;
private ArrayAdapter<String> mPairedDevicesArrayAdapter;
private ArrayAdapter<String> mNewDevicesArrayAdapter; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // Setup the window
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.device_list); // Set result CANCELED in case the user backs out
setResult(Activity.RESULT_CANCELED); // Initialize the button to perform device discovery
Button scanButton = (Button) findViewById(R.id.button_scan);
scanButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
doDiscovery();
v.setVisibility(View.GONE);
}
}); // Initialize array adapters. One for already paired devices and
// one for newly discovered devices
mPairedDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name);
mNewDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name); // Find and set up the ListView for paired devices
ListView pairedListView = (ListView) findViewById(R.id.paired_devices);
pairedListView.setAdapter(mPairedDevicesArrayAdapter);
pairedListView.setOnItemClickListener(mDeviceClickListener); // Find and set up the ListView for newly discovered devices
ListView newDevicesListView = (ListView) findViewById(R.id.new_devices);
newDevicesListView.setAdapter(mNewDevicesArrayAdapter);
newDevicesListView.setOnItemClickListener(mDeviceClickListener); // Register for broadcasts when a device is discovered
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
this.registerReceiver(mReceiver, filter); // Register for broadcasts when discovery has finished
filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
this.registerReceiver(mReceiver, filter); // Get the local Bluetooth adapter
mBtAdapter = BluetoothAdapter.getDefaultAdapter(); // Get a set of currently paired devices
Set<BluetoothDevice> pairedDevices = mBtAdapter.getBondedDevices(); // If there are paired devices, add each one to the ArrayAdapter
if (pairedDevices.size() > 0) {
findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE);
for (BluetoothDevice device : pairedDevices) {
mPairedDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
} else {
String noDevices = getResources().getText(R.string.none_paired).toString();
mPairedDevicesArrayAdapter.add(noDevices);
}
} @Override
protected void onDestroy() {
super.onDestroy(); // Make sure we're not doing discovery anymore
if (mBtAdapter != null) {
mBtAdapter.cancelDiscovery();
} // Unregister broadcast listeners
this.unregisterReceiver(mReceiver);
} /**
* Start device discover with the BluetoothAdapter
*/
private void doDiscovery() {
if (D) Log.d(TAG, "doDiscovery()"); // Indicate scanning in the title
setProgressBarIndeterminateVisibility(true);
setTitle(R.string.scanning); // Turn on sub-title for new devices
findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE); // If we're already discovering, stop it
if (mBtAdapter.isDiscovering()) {
mBtAdapter.cancelDiscovery();
} // Request discover from BluetoothAdapter
mBtAdapter.startDiscovery();
} // The on-click listener for all devices in the ListViews
private OnItemClickListener mDeviceClickListener = new OnItemClickListener() {
public void onItemClick(AdapterView<?> av, View v, int arg2, long arg3) {
// Cancel discovery because it's costly and we're about to connect
mBtAdapter.cancelDiscovery(); // Get the device MAC address, which is the last 17 chars in the View
String info = ((TextView) v).getText().toString();
String address = info.substring(info.length() - 17); // Create the result Intent and include the MAC address
Intent intent = new Intent();
intent.putExtra(EXTRA_DEVICE_ADDRESS, address); // Set result and finish this Activity
setResult(Activity.RESULT_OK, intent);
finish();
}
}; // The BroadcastReceiver that listens for discovered devices and
// changes the title when discovery is finished
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction(); // When discovery finds a device
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// Get the BluetoothDevice object from the Intent
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// If it's already paired, skip it, because it's been listed already
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
mNewDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
// When discovery is finished, change the Activity title
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
setProgressBarIndeterminateVisibility(false);
setTitle(R.string.select_device);
if (mNewDevicesArrayAdapter.getCount() == 0) {
String noDevices = getResources().getText(R.string.none_found).toString();
mNewDevicesArrayAdapter.add(noDevices);
}
}
}
}; }

这边,学习到了3点:

第一点:

在onCreate()方法中,设置:如果用户按下返回键,设置CANCELED

// Set result CANCELED in case the user backs out
setResult(Activity.RESULT_CANCELED);

第二点:

将aty以dialog形式显示:需要在AndroidManifest.xml配置

<activity android:name=".DeviceListActivity"
android:label="@string/select_device"
android:theme="@android:style/Theme.Holo.Dialog"
android:configChanges="orientation|keyboardHidden" />

第三点:

将result设置成public static的。

// Return Intent extra
public static String EXTRA_DEVICE_ADDRESS = "device_address";
 // Create the result Intent and include the MAC address
Intent intent = new Intent();
intent.putExtra(EXTRA_DEVICE_ADDRESS, address);

前一个aty:

// Get the device MAC address
String address = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);

BluetoothChatService源码:

/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ package com.example.android.BluetoothChat; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log; /**
* This class does all the work for setting up and managing Bluetooth
* connections with other devices. It has a thread that listens for
* incoming connections, a thread for connecting with a device, and a
* thread for performing data transmissions when connected.
*/
public class BluetoothChatService {
// Debugging
private static final String TAG = "BluetoothChatService";
private static final boolean D = true; // Name for the SDP record when creating server socket
private static final String NAME_SECURE = "BluetoothChatSecure";
private static final String NAME_INSECURE = "BluetoothChatInsecure"; // Unique UUID for this application
private static final UUID MY_UUID_SECURE =
UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66");
private static final UUID MY_UUID_INSECURE =
UUID.fromString("8ce255c0-200a-11e0-ac64-0800200c9a66"); // Member fields
private final BluetoothAdapter mAdapter;
private final Handler mHandler;
private AcceptThread mSecureAcceptThread;
private AcceptThread mInsecureAcceptThread;
private ConnectThread mConnectThread;
private ConnectedThread mConnectedThread;
private int mState; // Constants that indicate the current connection state
public static final int STATE_NONE = 0; // we're doing nothing
public static final int STATE_LISTEN = 1; // now listening for incoming connections
public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
public static final int STATE_CONNECTED = 3; // now connected to a remote device /**
* Constructor. Prepares a new BluetoothChat session.
* @param context The UI Activity Context
* @param handler A Handler to send messages back to the UI Activity
*/
public BluetoothChatService(Context context, Handler handler) {
mAdapter = BluetoothAdapter.getDefaultAdapter();
mState = STATE_NONE;
mHandler = handler;
} /**
* Set the current state of the chat connection
* @param state An integer defining the current connection state
*/
private synchronized void setState(int state) {
if (D) Log.d(TAG, "setState() " + mState + " -> " + state);
mState = state; // Give the new state to the Handler so the UI Activity can update
mHandler.obtainMessage(BluetoothChat.MESSAGE_STATE_CHANGE, state, -1).sendToTarget();
} /**
* Return the current connection state. */
public synchronized int getState() {
return mState;
} /**
* Start the chat service. Specifically start AcceptThread to begin a
* session in listening (server) mode. Called by the Activity onResume() */
public synchronized void start() {
if (D) Log.d(TAG, "start"); // Cancel any thread attempting to make a connection
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;} // Cancel any thread currently running a connection
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;} setState(STATE_LISTEN); // Start the thread to listen on a BluetoothServerSocket
if (mSecureAcceptThread == null) {
mSecureAcceptThread = new AcceptThread(true);
mSecureAcceptThread.start();
}
if (mInsecureAcceptThread == null) {
mInsecureAcceptThread = new AcceptThread(false);
mInsecureAcceptThread.start();
}
} /**
* Start the ConnectThread to initiate a connection to a remote device.
* @param device The BluetoothDevice to connect
* @param secure Socket Security type - Secure (true) , Insecure (false)
*/
public synchronized void connect(BluetoothDevice device, boolean secure) {
if (D) Log.d(TAG, "connect to: " + device); // Cancel any thread attempting to make a connection
if (mState == STATE_CONNECTING) {
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
} // Cancel any thread currently running a connection
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;} // Start the thread to connect with the given device
mConnectThread = new ConnectThread(device, secure);
mConnectThread.start();
setState(STATE_CONNECTING);
} /**
* Start the ConnectedThread to begin managing a Bluetooth connection
* @param socket The BluetoothSocket on which the connection was made
* @param device The BluetoothDevice that has been connected
*/
public synchronized void connected(BluetoothSocket socket, BluetoothDevice
device, final String socketType) {
if (D) Log.d(TAG, "connected, Socket Type:" + socketType); // Cancel the thread that completed the connection
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;} // Cancel any thread currently running a connection
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;} // Cancel the accept thread because we only want to connect to one device
if (mSecureAcceptThread != null) {
mSecureAcceptThread.cancel();
mSecureAcceptThread = null;
}
if (mInsecureAcceptThread != null) {
mInsecureAcceptThread.cancel();
mInsecureAcceptThread = null;
} // Start the thread to manage the connection and perform transmissions
mConnectedThread = new ConnectedThread(socket, socketType);
mConnectedThread.start(); // Send the name of the connected device back to the UI Activity
Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_DEVICE_NAME);
Bundle bundle = new Bundle();
bundle.putString(BluetoothChat.DEVICE_NAME, device.getName());
msg.setData(bundle);
mHandler.sendMessage(msg); setState(STATE_CONNECTED);
} /**
* Stop all threads
*/
public synchronized void stop() {
if (D) Log.d(TAG, "stop"); if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
} if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
} if (mSecureAcceptThread != null) {
mSecureAcceptThread.cancel();
mSecureAcceptThread = null;
} if (mInsecureAcceptThread != null) {
mInsecureAcceptThread.cancel();
mInsecureAcceptThread = null;
}
setState(STATE_NONE);
} /**
* Write to the ConnectedThread in an unsynchronized manner
* @param out The bytes to write
* @see ConnectedThread#write(byte[])
*/
public void write(byte[] out) {
// Create temporary object
ConnectedThread r;
// Synchronize a copy of the ConnectedThread
synchronized (this) {
if (mState != STATE_CONNECTED) return;
r = mConnectedThread;
}
// Perform the write unsynchronized
r.write(out);
} /**
* Indicate that the connection attempt failed and notify the UI Activity.
*/
private void connectionFailed() {
// Send a failure message back to the Activity
Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(BluetoothChat.TOAST, "Unable to connect device");
msg.setData(bundle);
mHandler.sendMessage(msg); // Start the service over to restart listening mode
BluetoothChatService.this.start();
} /**
* Indicate that the connection was lost and notify the UI Activity.
*/
private void connectionLost() {
// Send a failure message back to the Activity
Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(BluetoothChat.TOAST, "Device connection was lost");
msg.setData(bundle);
mHandler.sendMessage(msg); // Start the service over to restart listening mode
BluetoothChatService.this.start();
} /**
* This thread runs while listening for incoming connections. It behaves
* like a server-side client. It runs until a connection is accepted
* (or until cancelled).
*/
private class AcceptThread extends Thread {
// The local server socket
private final BluetoothServerSocket mmServerSocket;
private String mSocketType; public AcceptThread(boolean secure) {
BluetoothServerSocket tmp = null;
mSocketType = secure ? "Secure":"Insecure"; // Create a new listening server socket
try {
if (secure) {
tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME_SECURE,
MY_UUID_SECURE);
} else {
tmp = mAdapter.listenUsingInsecureRfcommWithServiceRecord(
NAME_INSECURE, MY_UUID_INSECURE);
}
} catch (IOException e) {
Log.e(TAG, "Socket Type: " + mSocketType + "listen() failed", e);
}
mmServerSocket = tmp;
} public void run() {
if (D) Log.d(TAG, "Socket Type: " + mSocketType +
"BEGIN mAcceptThread" + this);
setName("AcceptThread" + mSocketType); BluetoothSocket socket = null; // Listen to the server socket if we're not connected
while (mState != STATE_CONNECTED) {
try {
// This is a blocking call and will only return on a
// successful connection or an exception
socket = mmServerSocket.accept();
} catch (IOException e) {
Log.e(TAG, "Socket Type: " + mSocketType + "accept() failed", e);
break;
} // If a connection was accepted
if (socket != null) {
synchronized (BluetoothChatService.this) {
switch (mState) {
case STATE_LISTEN:
case STATE_CONNECTING:
// Situation normal. Start the connected thread.
connected(socket, socket.getRemoteDevice(),
mSocketType);
break;
case STATE_NONE:
case STATE_CONNECTED:
// Either not ready or already connected. Terminate new socket.
try {
socket.close();
} catch (IOException e) {
Log.e(TAG, "Could not close unwanted socket", e);
}
break;
}
}
}
}
if (D) Log.i(TAG, "END mAcceptThread, socket Type: " + mSocketType); } public void cancel() {
if (D) Log.d(TAG, "Socket Type" + mSocketType + "cancel " + this);
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "Socket Type" + mSocketType + "close() of server failed", e);
}
}
} /**
* This thread runs while attempting to make an outgoing connection
* with a device. It runs straight through; the connection either
* succeeds or fails.
*/
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
private String mSocketType; public ConnectThread(BluetoothDevice device, boolean secure) {
mmDevice = device;
BluetoothSocket tmp = null;
mSocketType = secure ? "Secure" : "Insecure"; // Get a BluetoothSocket for a connection with the
// given BluetoothDevice
try {
if (secure) {
tmp = device.createRfcommSocketToServiceRecord(
MY_UUID_SECURE);
} else {
tmp = device.createInsecureRfcommSocketToServiceRecord(
MY_UUID_INSECURE);
}
} catch (IOException e) {
Log.e(TAG, "Socket Type: " + mSocketType + "create() failed", e);
}
mmSocket = tmp;
} public void run() {
Log.i(TAG, "BEGIN mConnectThread SocketType:" + mSocketType);
setName("ConnectThread" + mSocketType); // Always cancel discovery because it will slow down a connection
mAdapter.cancelDiscovery(); // Make a connection to the BluetoothSocket
try {
// This is a blocking call and will only return on a
// successful connection or an exception
mmSocket.connect();
} catch (IOException e) {
// Close the socket
try {
mmSocket.close();
} catch (IOException e2) {
Log.e(TAG, "unable to close() " + mSocketType +
" socket during connection failure", e2);
}
connectionFailed();
return;
} // Reset the ConnectThread because we're done
synchronized (BluetoothChatService.this) {
mConnectThread = null;
} // Start the connected thread
connected(mmSocket, mmDevice, mSocketType);
} public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect " + mSocketType + " socket failed", e);
}
}
} /**
* This thread runs during a connection with a remote device.
* It handles all incoming and outgoing transmissions.
*/
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream; public ConnectedThread(BluetoothSocket socket, String socketType) {
Log.d(TAG, "create ConnectedThread: " + socketType);
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null; // Get the BluetoothSocket input and output streams
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
} mmInStream = tmpIn;
mmOutStream = tmpOut;
} public void run() {
Log.i(TAG, "BEGIN mConnectedThread");
byte[] buffer = new byte[1024];
int bytes; // Keep listening to the InputStream while connected
while (true) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer); // Send the obtained bytes to the UI Activity
mHandler.obtainMessage(BluetoothChat.MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "disconnected", e);
connectionLost();
// Start the service over to restart listening mode
BluetoothChatService.this.start();
break;
}
}
} /**
* Write to the connected OutStream.
* @param buffer The bytes to write
*/
public void write(byte[] buffer) {
try {
mmOutStream.write(buffer); // Share the sent message back to the UI Activity
mHandler.obtainMessage(BluetoothChat.MESSAGE_WRITE, -1, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "Exception during write", e);
}
} public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}
}

AcceptThread:蓝牙服务端socket监听线程.:

    private class AcceptThread extends Thread {
// 本地的服务端socket
private final BluetoothServerSocket mmServerSocket; public AcceptThread() {
BluetoothServerSocket tmp = null; // 创建一个用于监听的服务端socket,通过下面这个方法,NAME参数没关系,MY_UUID是确定唯一通道的标示符,用于连接的socket也要通过它产生
try {
tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) {
Log.e(TAG, "listen() failed", e);
}
mmServerSocket = tmp;
} public void run() {
if (D) Log.d(TAG, "BEGIN mAcceptThread" + this);
setName("AcceptThread");
BluetoothSocket socket = null; // 不断的监听,直到状态被改变成已连接状态
while (mState != STATE_CONNECTED) {
try {
// 这是一个会产生阻塞的方法accept,要不就是成功地建立一个连接,要不就是返回一个异常
socket = mmServerSocket.accept();
} catch (IOException e) {
Log.e(TAG, "accept() failed", e);
break;
} // 连接已经建立成功
if (socket != null) {
synchronized (BluetoothChatService.this) { // 同步块,同一时间,只有一个线程可以访问该区域
switch (mState) {
case STATE_LISTEN:
case STATE_CONNECTING:
// 状态正常,开始进行线程通信,实际就是开启通信线程ConnectedThread
connected(socket, socket.getRemoteDevice());
break;
case STATE_NONE:
case STATE_CONNECTED:
// 未准备或已连接状态. 关闭新建的这个socket.
try {
socket.close();
} catch (IOException e) {
Log.e(TAG, "Could not close unwanted socket", e);
}
break;
}
}
}
}
if (D) Log.i(TAG, "END mAcceptThread");
} public void cancel() { //关闭服务端的socket
if (D) Log.d(TAG, "cancel " + this);
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of server failed", e);
}
}
}

ConnectThread:蓝牙socket连接线程:

    private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice; public ConnectThread(BluetoothDevice device) {
mmDevice = device;
BluetoothSocket tmp = null; // 通过远程设备以及唯一的UUID创建一个用于连接的socket
try {
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
Log.e(TAG, "create() failed", e);
}
mmSocket = tmp;
} public void run() {
Log.i(TAG, "BEGIN mConnectThread");
setName("ConnectThread"); // 一定要停止扫描,不然会减慢连接速度
mAdapter.cancelDiscovery(); // 连接到服务端的socket
try {
// connect方法也会造成阻塞,直到成功连接,或返回一个异常
mmSocket.connect();
} catch (IOException e) {
connectionFailed(); //连接失败发送要Toast的消息
// 关闭socket
try {
mmSocket.close();
} catch (IOException e2) {
Log.e(TAG, "unable to close() socket during connection failure", e2);
}
// 连接失败了,把软件变成监听模式,可以让别的设备来连接
BluetoothChatService.this.start();
return;
} // 重置连接线程,因为我们已经完成了
synchronized (BluetoothChatService.this) {
mConnectThread = null;
} // 开始进行线程通信,实际就是开启通信线程ConnectedThread
connected(mmSocket, mmDevice);
} public void cancel() { // 关闭连接用的socket
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}

ConnectedThread:连接后的通信线程:

    private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream; public ConnectedThread(BluetoothSocket socket) {
Log.d(TAG, "create ConnectedThread");
mmSocket = socket; // 这个是之前的用于连接的socket
InputStream tmpIn = null;
OutputStream tmpOut = null; // 从连接的socket里获取InputStream和OutputStream
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
} mmInStream = tmpIn;
mmOutStream = tmpOut;
} public void run() {
Log.i(TAG, "BEGIN mConnectedThread");
byte[] buffer = new byte[1024];
int bytes; // 已经连接上以后持续从通道中监听输入流的情况
while (true) {
try {
// 从通道的输入流InputStream中读取数据到buffer数组中
bytes = mmInStream.read(buffer); // 将获取到数据的消息发送到UI界面,同时也把内容buffer发过去显示
mHandler.obtainMessage(BluetoothChat.MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "disconnected", e);
connectionLost(); // 连接异常断开的时候发送一个需要Toast的消息,让软件进行Toast
break;
}
}
} /**
* Write to the connected OutStream.
* @param buffer The bytes to write
*/
public void write(byte[] buffer) { // 这个方法用于把发送内容写到通道的OutputStream中,会在发信息是被调用
try {
mmOutStream.write(buffer); //将buffer内容写进通道 // 用于将自己发送给对方的内容也在UI界面显示
mHandler.obtainMessage(BluetoothChat.MESSAGE_WRITE, -1, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "Exception during write", e);
}
} public void cancel() { //关闭socket,即关闭通道
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}

参考:

http://www.aichengxu.com/view/4418179

http://blog.csdn.net/wyzxk888/article/details/7543364

Bluetooth篇 开发实例之十 官网的Bluetooth Chat sample app.的更多相关文章

  1. Bluetooth篇 开发实例之十一 官网的Bluetooth Chat sample的bug

    当没有匹配的设备和没有找到可用设备的时候. // If there are paired devices, add each one to the ArrayAdapter if (pairedDev ...

  2. Bluetooth篇 开发实例之九 和蓝牙模块通信

    首先,我们要去连接蓝牙模块,那么,我们只要写客户端的程序就好了,蓝牙模块就相当于服务端. 连接就需要UUID. #蓝牙串口服务SerialPortServiceClass_UUID = ‘{00001 ...

  3. Android Developer -- Bluetooth篇 开发实例之三 管理连接

    Managing a Connection When you have successfully connected two (or more) devices, each one will have ...

  4. Bluetooth篇 开发实例之八 匹配

    自己写的App匹配蓝牙设备,不需要通过系统设置去连接. 匹配和通信是两回事. 用过Android系统设置(Setting)的人都知道蓝牙搜索之后可以建立配对和解除配对,但是这两项功能的函数没有在SDK ...

  5. Android Developer -- Bluetooth篇 开发实例之四 API详解

    http://www.open-open.com/lib/view/open1390879771695.html 这篇文章将会详细解析BluetoothAdapter的详细api, 包括隐藏方法, 每 ...

  6. Bluetooth篇 开发实例之七 匹配&UUID

    匹配和通信是两回事. 1.用过Android系统设置(Setting)的人都知道蓝牙搜索之后可以建立配对和解除配对,但是这两项功能的函数没有在SDK中给出.但是可以通过反射来获取. 知道这两个API的 ...

  7. Android Developer -- Bluetooth篇 开发实例之一 扫描设备

    第一步:声明Bluetooth Permissions <!-- 设置蓝牙访问权限 --> <uses-permission android:name="android.p ...

  8. Bluetooth篇 开发实例之六 蓝牙RSSI计算距离

    计算公式: d = 10^((abs(RSSI) - A) / (10 * n)) 其中: d - 计算所得距离 RSSI - 接收信号强度(负值) A - 发射端和接收端相隔1米时的信号强度 n - ...

  9. Bluetooth篇 开发实例之五 为什么无线信号(RSSI)是负值?

    原文:http://www.cnblogs.com/lele/articles/2832885.html   为什么无线信号(RSSI)是负值 答:其实归根到底为什么接收的无线信号是负值,这样子是不是 ...

随机推荐

  1. linux socket c/s上传文件

    这是上传文件的一个示例,可以参照自行修改成下载或者其它功能. 在上传时,需要先将文件名传到服务器端,这是采用一个结构体,包含文件名及文件名长度(可以用于校验),防止文件名乱码. client #inc ...

  2. URAL 1684. Jack's Last Word ( KMP next函数应用 )

    题意:问第二行的串能不能恰好分割成几个串,使得这几个串都是第一行串的前缀.如果是,输出No, 并输出这几个串,否则输出Yes. 这题是Special Judge,把两个串连接起来,中间用一个未出现过的 ...

  3. 团队项目-第四次Scrum 会议

    时间:10.26 时长:30分钟 地点:线上 工作情况 团队成员 已完成任务 待完成任务 解小锐 编写project和projectGenerator类 编写下一步的规格说明 陈鑫 采用creator ...

  4. event.returnValue=false与event.preventDefault()

    event.preventDefault()方法是用于取消事件的默认行为,但此方法并不被ie支持,在ie下需要用window.event.returnValue = false; 来实现. funct ...

  5. Axios & Interceptors & Serialization & CORS & cookie

    Axios https://github.com/axios/axios#config-defaults Global axios defaults axios.defaults.baseURL = ...

  6. 【bzoj2694】Lcm 莫比乌斯反演+线性筛

    题目描述 求$\sum\limits_{i=1}^n\sum\limits_{j=1}^m|\mu(gcd(i,j))|lcm(i,j)$,即$gcd(i,j)$不存在平方因子的$lcm(i,j)$之 ...

  7. mplab xIde 编译成功,但不能生成Hex文件

    设置不对 如果还不行,在设置下面

  8. 【HDU 5961 传递】

    Time Limit: 12000/6000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Total Submission ...

  9. 如何得到一个接口所有的实现类(及子接口)?例如:Eclipse IDE

    (一)Eclipse IDE的做法 它会解析所有的Java文件.Class文件. 技巧:在Eclipse中,选中Interface,按下F4,就可以查看到所有的实现类及子接口. 例如: (二)自己怎么 ...

  10. 类复制 MemberwiseClone与Clone(深 浅 Clone)

    MemberwiseClone 方法创建一个浅表副本,具体来说就是创建一个新对象,然后将当前对象的非静态字段复制到该新对象.如果字段是值类型的,则对该字段执行逐位复制.如果字段是引用类型,则复制引用但 ...