CSipSimple源码分析(一)之代码流程和用户注册流程
来源:互联网 发布:coc野蛮人之王升级数据 编辑:程序博客网 时间:2024/05/16 13:59
简介
CSipSimple是Android手机上支持Sip的网络电话软件 , 基于的是pjsip开源sip协议栈
需要了解pjsip , 可以下载PJSUA开发文档中文版看看
源码流程
代码的大致流程如下 :
中间肯定有SipService/PjsioService和DBProvider的交互 UI =================================== ^ | | | | V Intent SipService DBProvider | ^ | | V | PjsipService UAStateReciver | ^ | | V =================================== pjsip底层库
注册流程分析
UI界面 : public class BasePrefsWizard extends GenericPrefs{ /** * 保存账户并将账户保存或更新到数据库 * @param wizardId */ private void saveAccount(String wizardId) {//BASIC boolean needRestart = false; PreferencesWrapper prefs = new PreferencesWrapper(getApplicationContext()); account = wizard.buildAccount(account);//1 . 创建账户 , 策略设计模式 account.wizard = wizardId; if (account.id == SipProfile.INVALID_ID) {//2 . 创建新的账户时为true // This account does not exists yet prefs.startEditing(); wizard.setDefaultParams(prefs); prefs.endEditing(); applyNewAccountDefault(account); //3 . 重点 : 将用户信息插入到数据库 Uri uri = getContentResolver().insert(SipProfile.ACCOUNT_URI, account.getDbContentValues()); // After insert, add filters for this wizard account.id = ContentUris.parseId(uri); List<Filter> filters = wizard.getDefaultFilters(account);//null if (filters != null) { for (Filter filter : filters) { //4 . 如果在设置模块的过滤器进行过配置 , 会执行以下代码 // Ensure the correct id if not done by the wizard filter.account = (int) account.id; getContentResolver().insert(SipManager.FILTER_URI, filter.getDbContentValues()); } } // Check if we have to restart needRestart = wizard.needRestart(); } else {//2 . 更新账户信息时执行 // TODO : should not be done there but if not we should add an // option to re-apply default params prefs.startEditing(); wizard.setDefaultParams(prefs); prefs.endEditing(); //3 . 重点 : 将账户信息更新到数据库中 getContentResolver().update(ContentUris.withAppendedId(SipProfile.ACCOUNT_ID_URI_BASE, account.id), account.getDbContentValues(), null, null); } // Mainly if global preferences were changed, we have to restart sip stack if (needRestart) { //5 . 在创建账户或更新账户时 , 修改过一些设置 , 会执行以下代码 , 进行重启协议栈 // 协议栈会更新配置信息 Intent intent = new Intent(SipManager.ACTION_SIP_REQUEST_RESTART); sendBroadcast(intent); } } } /** * 在上面的[1 . 创建账户]中 , 会使用该类 * 因为在选择向导的时候选择的是Basic , 因此这里选择Basic类进行分析 */ public class Basic extends BaseImplementation { /** * 创建一个账户 */ public SipProfile buildAccount(SipProfile account) { Log.d(THIS_FILE, "begin of save ...."); account.display_name = accountDisplayName.getText().trim();//用户名 String[] serverParts = accountServer.getText().split(":");//服务器 account.acc_id = "<sip:" + SipUri.encodeUser(accountUserName.getText().trim()) + "@"+serverParts[0].trim()+">"; String regUri = "sip:" + accountServer.getText();// account.reg_uri = regUri; account.proxies = new String[] { regUri } ; account.realm = "*"; account.username = getText(accountUserName).trim(); account.data = getText(accountPassword); account.scheme = SipProfile.CRED_SCHEME_DIGEST; account.datatype = SipProfile.CRED_DATA_PLAIN_PASSWD; //By default auto transport account.transport = SipProfile.TRANSPORT_UDP; return account; } } /** * 在上面[3 . 重点 : 将用户信息插入到数据库] 和 * [3 . 重点 : 将账户信息更新到数据库中]可以知道 , * 最终的用户信息是保存到数据库中的 , 因此下面分析数据库 */ public class DBProvider extends ContentProvider { /** * 将用户信息插入到数据库 */ @Override public Uri insert(Uri uri, ContentValues initialValues){ ... //省略 代码太多 , 看下重点代码 case ACCOUNTS_STATUS_ID: long id = ContentUris.parseId(uri); synchronized (profilesStatus) { SipProfileState ps = new SipProfileState(); if (profilesStatus.containsKey(id)) { ContentValues currentValues = profilesStatus.get(id); ps.createFromContentValue(currentValues); } ps.createFromContentValue(initialValues); ContentValues cv = ps.getAsContentValue(); cv.put(SipProfileState.ACCOUNT_ID, id); profilesStatus.put(id, cv); Log.d(THIS_FILE, "Added " + cv); } // 1. 通知内容观察者有新的账户注册 getContext().getContentResolver().notifyChange(uri, null); ...// 省略 if (rowId >= 0) { // TODO : for inserted account register it here // 2. 重点 , 通知内容观察者有新的账户注册 , 并通过Uri携带注册账户的ID // 后续可以通过该ID值 , 获取账户的状态信息等 Uri retUri = ContentUris.withAppendedId(baseInsertedUri, rowId); getContext().getContentResolver().notifyChange(retUri, null); if (matched == ACCOUNTS || matched == ACCOUNTS_ID) { broadcastAccountChange(rowId);// 3 . 重点通过广播通知账户信息发生改变 } ... //省略 } } /** * 更新用户数据 */ @Override public int update(Uri uri, ContentValues values, String where,String[] whereArgs) { ...// 省略 // 1 . 通知内容观察者有用户的信息发生改变 getContext().getContentResolver().notifyChange(uri, null); long rowId = -1; if (matched == ACCOUNTS_ID || matched == ACCOUNTS_STATUS_ID) { rowId = ContentUris.parseId(uri); // 2. 获取账户的ID } if (rowId >= 0) { if (matched == ACCOUNTS_ID) { // Don't broadcast if we only changed wizard or only changed // priority boolean doBroadcast = true; if (values.size() == 1) { if (values.containsKey(SipProfile.FIELD_WIZARD)) { doBroadcast = false; } else if (values.containsKey(SipProfile.FIELD_PRIORITY)) { doBroadcast = false; } } if (doBroadcast) { broadcastAccountChange(rowId);// 2. 重点 通过广播通知账户信息发生改变 } } else if (matched == ACCOUNTS_STATUS_ID) { broadcastRegistrationChange(rowId); } ... // 省略 } } } /** * 通过广播通知账户信息发生改变 */ private void broadcastAccountChange(long accountId) { Intent publishIntent = new Intent(SipManager.ACTION_SIP_ACCOUNT_CHANGED);// 1. 重点 广播的动作 publishIntent.putExtra(SipProfile.FIELD_ID, accountId); getContext().sendBroadcast(publishIntent); BackupWrapper.getInstance(getContext()).dataChanged(); } } /** * 该内容观察者用来接收上面[1. 重点 广播的动作] 的广播 , * 该内容观察者的注册是在SipService类中的registerBroadcasts()方法中进行的 */ public class DynamicReceiver4 extends BroadcastReceiver { /** * 处理账户信息发生改变的广播 */ private void onReceiveInternal(Context context, Intent intent, boolean isSticky) throws SameThreadException { ... // 省略 else if (action.equals(SipManager.ACTION_SIP_ACCOUNT_CHANGED)) { // 1. 处理用户信息发生改变的广播 final long accountId = intent.getLongExtra(SipProfile.FIELD_ID, SipProfile.INVALID_ID); // Should that be threaded? if (accountId != SipProfile.INVALID_ID) { // 2. 通过广播携带的账户ID , 获取到账户的实例 final SipProfile account = service.getAccount(accountId); if (account != null) { Log.d(THIS_FILE, "Enqueue set account registration"); // 3 . 重点 进行账户的注册 , 该service就是SipService service.setAccountRegistration(account, account.active ? 1 : 0, true); } } } ... // 省略 } } /** * 重点 所有的有关通话/信息的操作都是通过该服务来实现的 */ public class SipService extends Service { /** * 注册账户的方法 */ public boolean setAccountRegistration(SipProfile account, int renew, boolean forceReAdd) throws SameThreadException { boolean status = false; if (pjService != null) { //1. 重点 实际注册账户是通过PjsipService来实现的 status = pjService.setAccountRegistration(account, renew,forceReAdd); } return status; } } /** * 重点 , 所有与pjsip库进行操作的方法 , 都是通过该类调用pjsua的native方法类实现的 */ public class PjSipService { /** * 进行账户向服务端注册的方法 */ public boolean setAccountRegistration(SipProfile account, int renew, boolean forceReAdd) throws SameThreadException { int status = -1; if (!created || account == null) { Log.e(THIS_FILE, "PJSIP is not started here, nothing can be done"); return false; } if (account.id == SipProfile.INVALID_ID) { Log.w(THIS_FILE, "Trying to set registration on a deleted account"); return false; } // 获取账户的状态信息 SipProfileState profileState = getProfileState(account); // If local account -- Ensure we are not deleting, because this would be // invalid if (profileState.getWizard().equalsIgnoreCase( WizardUtils.LOCAL_WIZARD_TAG)) { if (renew == 0) { return false; } } // In case of already added, we have to act finely // If it's local we can just consider that we have to re-add account // since it will actually just touch the account with a modify if (profileState != null && profileState.isAddedToStack() && !profileState.getWizard().equalsIgnoreCase(WizardUtils.LOCAL_WIZARD_TAG)) { // The account is already there in accounts list service.getContentResolver().delete(ContentUris.withAppendedId(SipProfile.ACCOUNT_STATUS_URI, account.id), null, null); Log.d(THIS_FILE,"Account already added to stack, remove and re-load or delete"); if (renew == 1) { if (forceReAdd) {// 先删除账户 , 然后重新进行添加账户 status = pjsua.acc_del(profileState.getPjsuaId()); addAccount(account); // 1. 重点 添加账户 } else { // 设置账户的状态为在线 , 并重新进行注册 pjsua.acc_set_online_status(profileState.getPjsuaId(),getOnlineForStatus(service.getPresence())); status = pjsua.acc_set_registration(profileState.getPjsuaId(), renew); } } else { // if(status == pjsuaConstants.PJ_SUCCESS && renew == 0) { Log.d(THIS_FILE, "Delete account !!"); status = pjsua.acc_del(profileState.getPjsuaId());// 删除账户 } } else { if (renew == 1) { addAccount(account); // 2 .重点 添加账户 } else { Log.w(THIS_FILE, "Ask to unregister an unexisting account !!" + account.id); } } // PJ_SUCCESS = 0 return status == 0; } /** * 重点 , 添加账户 */ public boolean addAccount(SipProfile profile) throws SameThreadException { int status = pjsuaConstants.PJ_FALSE; if (!created) { Log.e(THIS_FILE, "PJSIP is not started here, nothing can be done"); return status == pjsuaConstants.PJ_SUCCESS; } PjSipAccount account = new PjSipAccount(profile);// 包装成PjSip账户 account.applyExtraParams(service);// 设置额外的信息 //账户状态实例 , 包含账户是否登录成功的状态 SipProfileState currentAccountStatus = getProfileState(profile); account.cfg.setRegister_on_acc_add(pjsuaConstants.PJ_FALSE); if (currentAccountStatus.isAddedToStack()) {// 判断该账户是否曾经加载过 pjsua.csipsimple_set_acc_user_data(// 曾经加载过 currentAccountStatus.getPjsuaId(), account.css_cfg); status = pjsua.acc_modify(currentAccountStatus.getPjsuaId(), account.cfg);// 1. 重点 , 对账户的配置进行修改 ,并返回该账户的状态 beforeAccountRegistration(currentAccountStatus.getPjsuaId(), profile);// 先清除该账户在代理服务器的状态 ContentValues cv = new ContentValues(); cv.put(SipProfileState.ADDED_STATUS, status);// 2. 重点 , 设置该账户的状态 //3 . 重点 , 讲该账户的状态更新到数据库中 service.getContentResolver().update(ContentUris.withAppendedId( SipProfile.ACCOUNT_STATUS_ID_URI_BASE, profile.id),cv, null, null); // 判断通用向导的类型 , 如果不是LOCAL ,则继续向下执行 , 我们选择的是BASIC if (!account.wizard.equalsIgnoreCase(WizardUtils.LOCAL_WIZARD_TAG)) { // Re register //重新进行注册 if (status == pjsuaConstants.PJ_SUCCESS) {// 账户的配置修改成功 status = pjsua.acc_set_registration( currentAccountStatus.getPjsuaId(), 1);// 则对账户设置注册 if (status == pjsuaConstants.PJ_SUCCESS) {// 账户设置注册成功 pjsua.acc_set_online_status( currentAccountStatus.getPjsuaId(), 1);// 则设置该账户为在线状态 } } } } else {// 如果该账户没有注册过 int[] accId = new int[1]; if (account.wizard.equalsIgnoreCase(WizardUtils.LOCAL_WIZARD_TAG)) {// 设置向导选择的BASIC , 不是LOCAL , 不向下执行 // We already have local account by default // For now consider we are talking about UDP one // In the future local account should be set per transport switch (account.transport) { case SipProfile.TRANSPORT_UDP: accId[0] = prefsWrapper.useIPv6() ? localUdp6AccPjId : localUdpAccPjId; break; case SipProfile.TRANSPORT_TCP: accId[0] = prefsWrapper.useIPv6() ? localTcp6AccPjId : localTcpAccPjId; break; case SipProfile.TRANSPORT_TLS: accId[0] = prefsWrapper.useIPv6() ? localTls6AccPjId : localTlsAccPjId; break; default: // By default use UDP accId[0] = localUdpAccPjId; break; } pjsua.csipsimple_set_acc_user_data(accId[0], account.css_cfg); // TODO : use video cfg here // nCfg.setVid_in_auto_show(pjsuaConstants.PJ_TRUE); // nCfg.setVid_out_auto_transmit(pjsuaConstants.PJ_TRUE); // status = pjsua.acc_modify(accId[0], nCfg); } else { // TODO 执行此处的代码 // Cause of standard account different from local account :) status = pjsua.acc_add(account.cfg, pjsuaConstants.PJ_FALSE, accId);// 4. 进行账户的添加 pjsua.csipsimple_set_acc_user_data(accId[0], account.css_cfg);// 设置账户数据 beforeAccountRegistration(accId[0], profile);// 在对用户进行注册之前的操作,清除该账户的状态 pjsua.acc_set_registration(accId[0], 1);//重点 4. 对账户进行注册 } if (status == pjsuaConstants.PJ_SUCCESS) {//5. 账户注册成功 SipProfileState ps = new SipProfileState(profile);// 获取该账户的Sip配置信息 ps.setAddedStatus(status);// 6. 设置注册的状态 ps.setPjsuaId(accId[0]); service.getContentResolver().insert( ContentUris.withAppendedId( SipProfile.ACCOUNT_STATUS_ID_URI_BASE, account.id), ps.getAsContentValue());// 7 .注册成功,并通知 pjsua.acc_set_online_status(accId[0], 1);// 设置该账户为在线状态 } } return status == pjsuaConstants.PJ_SUCCESS; } } /** * UAStateReceiver , 该类重点 , 所有与pjsip交互的回调 , 都是通过此类完成的 , * 相关的api可以参考文档中的pjsua_callback , 这里只关注注册的回调 */ public class UAStateReceiver{ @Override public void on_reg_state(final int accountId) { lockCpu(); pjService.service.getExecutor().execute(new SipRunnable() { @Override public void doRun() throws SameThreadException { // Update java infos // 1 . 更新账户的状态 , 又回到PjsipService中 pjService.updateProfileStateFromService(accountId); } }); unlockCpu(); } } /** * 重点 , 所有与pjsip库进行操作的方法 , 都是通过该类调用pjsua的native方法类实现的 */ public class PjsipService{ /** * Synchronize content provider backend from pjsip stack * * @param pjsuaId the pjsua id of the account to synchronize * @throws SameThreadException */ public void updateProfileStateFromService(int pjsuaId) throws SameThreadException { if (!created) { return; } long accId = getAccountIdForPjsipId(service, pjsuaId);// 1. 获取注册的账号 Log.d(THIS_FILE, "Update profile from service for " + pjsuaId + " aka in db " + accId); if (accId != SipProfile.INVALID_ID) { //2. 判断accId int success = pjsuaConstants.PJ_FALSE; pjsua_acc_info pjAccountInfo; pjAccountInfo = new pjsua_acc_info(); success = pjsua.acc_get_info(pjsuaId, pjAccountInfo); // 3. 重点 , 从pjsip协议栈中获取账户信息 if (success == pjsuaConstants.PJ_SUCCESS && pjAccountInfo != null) { ContentValues cv = new ContentValues(); // 4 . 设置用户的状态 try { // Should be fine : status code are coherent with RFC // status codes cv.put(SipProfileState.STATUS_CODE, pjAccountInfo.getStatus().swigValue()); } catch (IllegalArgumentException e) { cv.put(SipProfileState.STATUS_CODE, SipCallSession.StatusCode.INTERNAL_SERVER_ERROR); } cv.put(SipProfileState.STATUS_TEXT, pjStrToString(pjAccountInfo.getStatus_text())); cv.put(SipProfileState.EXPIRES, pjAccountInfo.getExpires()); //5 . 更新用户的状态 service.getContentResolver().update( ContentUris.withAppendedId(SipProfile.ACCOUNT_STATUS_ID_URI_BASE, accId), cv, null, null); Log.d(THIS_FILE, "Profile state UP : " + cv); } } else { Log.e(THIS_FILE, "Trying to update not added account " + pjsuaId); } } /** * 1. 获取注册的账号 */ public static long getAccountIdForPjsipId(Context ctxt, int pjId) { long accId = SipProfile.INVALID_ID; //1 .设置accId // 2 . 查找账户 Cursor c = ctxt.getContentResolver().query(SipProfile.ACCOUNT_STATUS_URI, null, null, null, null); if (c != null) { try { c.moveToFirst(); do { int pjsuaId = c.getInt(c.getColumnIndex(SipProfileState.PJSUA_ID)); Log.d(THIS_FILE, "Found pjsua " + pjsuaId + " searching " + pjId); if (pjsuaId == pjId) { accId = c.getInt(c.getColumnIndex(SipProfileState.ACCOUNT_ID)); break; } } while (c.moveToNext()); } catch (Exception e) { Log.e(THIS_FILE, "Error on looping over sip profiles", e); } finally { c.close(); } } return accId; } } /** * 最后又回到DBProvider , 其实所有账户UI层面的状态 , 都是和DBProvider打交道 , * MVC模式 */ public class DBProvider{ @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; String finalWhere; int matched = URI_MATCHER.match(uri); List<String> possibles = getPossibleFieldsForType(matched); checkSelection(possibles, where); // 1. 进行账户的更新 , 到此用户的注册创建注册流程已经走完 // 接下来 , 我们看UI层面是怎么更新用户的状态的 switch (matched) { case ACCOUNTS: count = db.update(SipProfile.ACCOUNTS_TABLE_NAME, values, where, whereArgs); break; case ACCOUNTS_ID: finalWhere = DatabaseUtilsCompat.concatenateWhere(SipProfile.FIELD_ID + " = " + ContentUris.parseId(uri), where); count = db.update(SipProfile.ACCOUNTS_TABLE_NAME, values, finalWhere, whereArgs); break; // ... 只看和账户相关的更新 case ACCOUNTS_STATUS_ID: long id = ContentUris.parseId(uri); synchronized (profilesStatus){ SipProfileState ps = new SipProfileState(); if(profilesStatus.containsKey(id)) { ContentValues currentValues = profilesStatus.get(id); ps.createFromContentValue(currentValues); } ps.createFromContentValue(values); ContentValues cv = ps.getAsContentValue(); cv.put(SipProfileState.ACCOUNT_ID, id); profilesStatus.put(id, cv); Log.d(THIS_FILE, "Updated "+cv); } count = 1; break; default: throw new IllegalArgumentException(UNKNOWN_URI_LOG + uri); } //内容观察者 , 提示数据库发生改变 getContext().getContentResolver().notifyChange(uri, null); long rowId = -1; if (matched == ACCOUNTS_ID || matched == ACCOUNTS_STATUS_ID) { rowId = ContentUris.parseId(uri); //获取更新账户的id } if (rowId >= 0) { if (matched == ACCOUNTS_ID) { // Don't broadcast if we only changed wizard or only changed priority boolean doBroadcast = true; if(values.size() == 1) { if(values.containsKey(SipProfile.FIELD_WIZARD)) { doBroadcast = false; }else if(values.containsKey(SipProfile.FIELD_PRIORITY)) { doBroadcast = false; } } if(doBroadcast) { broadcastAccountChange(rowId); // 发送广播 , 告知用户信息发生改变 } } else if (matched == ACCOUNTS_STATUS_ID) { broadcastRegistrationChange(rowId); // 发送广播 , 告知用户注册信息发生改变 , 主要更新AppWidget } } return count; } } /** * 刚才说过 , UI账户状态的操作 , 都是和数据层面打交道 , * 因此 , 这里用的是ContentResolver内容观察者 */ public class AccountsEditListFragment{ @Override public void onResume() { super.onResume(); if(statusObserver == null) { statusObserver = new AccountStatusContentObserver(mHandler);// 1 . 账户状态观察者 getActivity().getContentResolver().registerContentObserver(SipProfile.ACCOUNT_STATUS_URI, true, statusObserver); } mAdapter.notifyDataSetChanged(); } class AccountStatusContentObserver extends ContentObserver { public AccountStatusContentObserver(Handler h) { super(h); } public void onChange(boolean selfChange) { Log.d(THIS_FILE, "Accounts status.onChange( " + selfChange + ")"); ((BaseAdapter) getListAdapter()).notifyDataSetChanged();// 2. 更新账户状态UI } } } /** * 更新UI */ public class AccountsEditListAdapter{ @Override public void bindView(View view, Context context, Cursor cursor) { super.bindView(view, context, cursor); AccountListItemViews tagView = (AccountListItemViews) view.getTag(); if (tagView == null) { tagView = tagRowView(view); } // Get the view object and account object for the row final SipProfile account = new SipProfile(cursor); AccountRowTag tagIndicator = new AccountRowTag(); tagIndicator.accountId = account.id; tagIndicator.activated = account.active; tagView.indicator.setTag(tagIndicator); tagView.indicator.setVisibility(draggable ? View.GONE : View.VISIBLE); tagView.grabber.setVisibility(draggable ? View.VISIBLE : View.GONE); // Get the status of this profile tagView.labelView.setText(account.display_name); // 1. 更新用户状态和颜色 , 到此用户的注册流程结束 // Update status label and color if (account.active) { AccountStatusDisplay accountStatusDisplay = AccountListUtils.getAccountDisplay(context, account.id); tagView.statusView.setText(accountStatusDisplay.statusLabel); tagView.labelView.setTextColor(accountStatusDisplay.statusColor); // Update checkbox selection tagView.activeCheckbox.setChecked(true); tagView.barOnOff.setImageResource(accountStatusDisplay.checkBoxIndicator); } else { tagView.statusView.setText(R.string.acct_inactive); tagView.labelView.setTextColor(mContext.getResources().getColor( R.color.account_inactive)); // Update checkbox selection tagView.activeCheckbox.setChecked(false); tagView.barOnOff.setImageResource(R.drawable.ic_indicator_off); } // Update account image final WizardInfo wizardInfos = WizardUtils.getWizardClass(account.wizard); if (wizardInfos != null) { tagView.activeCheckbox.setBackgroundResource(wizardInfos.icon); } } }
阅读全文
0 0
- CSipSimple源码分析(一)之代码流程和用户注册流程
- Android Gatt连接流程源码分析之ClientIf注册
- netty源码分析之-Channel注册流程详解(8)
- 基于N源码的广播注册和发送流程分析
- Linux Device和Driver注册过程的源码流程分析
- SIP用户注册流程分析(improving)
- Openfire注册流程代码分析(转)
- Github用户注册流程
- Activity启动流程源码分析之入门(一)
- Service启动流程源码分析之startService(一)
- ThinkPHP3.2.3源码分析一之系统流程
- Hadoop之wordcount源码分析和MapReduce流程分析
- struts2流程和源码分析
- Kubelet源码分析(一) 启动流程分析
- MapReduce之 WordCount 源码分析和操作流程
- WebRTC源码分析一:音频处理流程
- Elasticsearch源码分析(一)启动流程
- tomcat源码流程分析(一)
- 内涵段子视频怎么下载到本地?怎么下载高清内涵视频?
- 身份证排序
- 算法提高 日期计算
- Python yield 使用浅析
- 手机端页面的点击效果实现
- CSipSimple源码分析(一)之代码流程和用户注册流程
- Java成员变量和成员方法
- 大话数据结构 code 第四章 03链栈_LinkStack
- C++快速幂
- python下人脸检测
- Codeforces 835D Round #427 Div2D :回文串DP
- 设计模式-外观模式
- 20个非常有用的Java程序片段
- 读取配置文件